feat: Fase 1 — Auth JWT+MFA, portales CRUD, UI completa

API:
- AuthModule: register, login, refresh, logout, MFA/TOTP setup+verify
- JwtStrategy + JwtAuthGuard + RolesGuard + CurrentUser decorator
- PackagesModule: CRUD paquetes + historial de estados
- PreAlertsModule: pre-alertas por usuario
- UsersModule: gestión de usuarios + roles + activación
- B2BModule: solicitudes de carga pesada/cotización
- ValidationPipe global + CORS configurado

Web (Next.js 15):
- globals.css completo (design system + utility classes)
- Layout raíz con WhatsApp flotante
- /login + /registro funcionales con JWT y redirección por rol
- /portal: dashboard, mi-casillero, mis-paquetes, pre-alerta, calculadora, perfil
- /admin: dashboard, usuarios (gestión roles/activación), tarifas, reportes, auditoría
- /bodega: dashboard, paquetes (crear+actualizar estado), verificación, despacho
- /tracking: tracking real con progreso visual + historial
- /calculadora: calculadora interactiva real (API SENAE §15)
- /como-funciona, /tarifas, /quienes-somos, /casillero
- /carga-pesada + /carga-pesada/cotizacion (formulario B2B)
- lib/api.ts: cliente HTTP con auto-refresh de token

Roles sincronizados con schema: SUPER_ADMIN, ADMIN_EMPRESA,
OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE
This commit is contained in:
Lizandro Guarnizo
2026-06-01 09:42:38 -05:00
parent 93db76897d
commit 4872053fd1
57 changed files with 4330 additions and 192 deletions
+102
View File
@@ -0,0 +1,102 @@
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { generateTrackingId } from "../common/utils/tracking-id.util";
import { CreatePackageDto, UpdateStatusDto } from "./dto/package.dto";
@Injectable()
export class PackagesService {
constructor(private prisma: PrismaService) {}
async findAll(user: any, filters?: { status?: string; search?: string }): Promise<any[]> {
const where: any = { tenantId: user.tenantId };
if (user.role === "CLIENTE") {
where.userId = user.id;
}
if (filters?.status) where.status = filters.status;
if (filters?.search) {
where.OR = [
{ trackingId: { contains: filters.search, mode: "insensitive" } },
{ vendorTracking: { contains: filters.search, mode: "insensitive" } },
{ description: { contains: filters.search, mode: "insensitive" } },
];
}
return this.prisma.client.package.findMany({
where,
include: {
user: { select: { firstName: true, lastName: true, email: true } },
statusHistory: { orderBy: { createdAt: "desc" }, take: 1 },
},
orderBy: { createdAt: "desc" },
});
}
async findOne(id: string, user: any): Promise<any> {
const pkg = await this.prisma.client.package.findUnique({
where: { id },
include: {
user: { select: { firstName: true, lastName: true, email: true } },
statusHistory: { orderBy: { createdAt: "desc" } },
preAlert: true,
},
});
if (!pkg) throw new NotFoundException("Paquete no encontrado.");
if (user.role === "CLIENTE" && pkg.userId !== user.id) throw new ForbiddenException();
return pkg;
}
async create(dto: CreatePackageDto, operatorId: string, tenantId: string): Promise<any> {
const trackingId = generateTrackingId();
const pkg = await this.prisma.client.package.create({
data: {
trackingId,
tenantId,
userId: dto.userId,
description: dto.description,
store: dto.store,
vendorTracking: dto.vendorTracking,
declaredValue: dto.declaredValue ?? 0,
declaredWeight: dto.declaredWeightLb ?? null,
lengthCm: dto.lengthCm ?? null,
widthCm: dto.widthCm ?? null,
heightCm: dto.heightCm ?? null,
status: "REGISTRADO",
},
});
await this.prisma.client.packageStatusHistory.create({
data: {
packageId: pkg.id,
status: "REGISTRADO",
createdBy: operatorId,
note: "Paquete registrado al recibirse en bodega NJ",
},
});
return pkg;
}
async updateStatus(id: string, dto: UpdateStatusDto, operatorId: string): Promise<any> {
const pkg = await this.prisma.client.package.findUnique({ where: { id } });
if (!pkg) throw new NotFoundException("Paquete no encontrado.");
const updated = await this.prisma.client.package.update({
where: { id },
data: { status: dto.status as any },
});
await this.prisma.client.packageStatusHistory.create({
data: {
packageId: id,
status: dto.status as any,
createdBy: operatorId,
note: dto.note,
},
});
return updated;
}
}