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
@@ -0,0 +1,23 @@
import { IsString, IsOptional, IsNumber, Min } from "class-validator";
export class CreatePreAlertDto {
@IsString()
store!: string;
@IsString()
description!: string;
@IsOptional()
@IsNumber()
@Min(0)
declaredValue?: number;
@IsOptional()
@IsString()
vendorTracking?: string;
}
export class UpdatePreAlertStatusDto {
@IsString()
status!: string;
}
@@ -0,0 +1,33 @@
import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards } from "@nestjs/common";
import { PreAlertsService } from "./pre-alerts.service";
import { CreatePreAlertDto, UpdatePreAlertStatusDto } from "./dto/pre-alert.dto";
import { JwtAuthGuard, RolesGuard, Roles } from "../auth/guards/auth.guard";
import { CurrentUser } from "../auth/decorators/current-user.decorator";
@Controller("pre-alerts")
@UseGuards(JwtAuthGuard, RolesGuard)
export class PreAlertsController {
constructor(private svc: PreAlertsService) {}
@Get()
findAll(@CurrentUser() user: any) {
return this.svc.findAll(user);
}
@Post()
@Roles("CLIENTE")
create(@Body() dto: CreatePreAlertDto, @CurrentUser() user: any) {
return this.svc.create(dto, user);
}
@Patch(":id/status")
@Roles("OPERADOR_BODEGA", "ADMIN_EMPRESA", "SUPER_ADMIN")
updateStatus(@Param("id") id: string, @Body() dto: UpdatePreAlertStatusDto): Promise<any> {
return this.svc.updateStatus(id, dto);
}
@Delete(":id")
remove(@Param("id") id: string, @CurrentUser() user: any) {
return this.svc.remove(id, user);
}
}
@@ -0,0 +1,6 @@
import { Module } from "@nestjs/common";
import { PreAlertsController } from "./pre-alerts.controller";
import { PreAlertsService } from "./pre-alerts.service";
@Module({ controllers: [PreAlertsController], providers: [PreAlertsService] })
export class PreAlertsModule {}
@@ -0,0 +1,45 @@
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreatePreAlertDto, UpdatePreAlertStatusDto } from "./dto/pre-alert.dto";
@Injectable()
export class PreAlertsService {
constructor(private prisma: PrismaService) {}
async findAll(user: any): Promise<any[]> {
const where: any = { tenantId: user.tenantId };
if (user.role === "CLIENTE") where.userId = user.id;
return this.prisma.client.preAlert.findMany({
where,
include: { user: { select: { firstName: true, lastName: true, email: true } } },
orderBy: { createdAt: "desc" },
});
}
async create(dto: CreatePreAlertDto, user: any): Promise<any> {
return this.prisma.client.preAlert.create({
data: {
tenantId: user.tenantId,
userId: user.id,
store: dto.store,
description: dto.description,
declaredValue: dto.declaredValue ?? 0,
vendorTracking: dto.vendorTracking,
status: "PENDIENTE",
},
});
}
async updateStatus(id: string, dto: UpdatePreAlertStatusDto): Promise<any> {
const alert = await this.prisma.client.preAlert.findUnique({ where: { id } });
if (!alert) throw new NotFoundException("Pre-alerta no encontrada.");
return this.prisma.client.preAlert.update({ where: { id }, data: { status: dto.status as any } });
}
async remove(id: string, user: any): Promise<any> {
const alert = await this.prisma.client.preAlert.findUnique({ where: { id } });
if (!alert) throw new NotFoundException("Pre-alerta no encontrada.");
if (user.role === "CLIENTE" && alert.userId !== user.id) throw new ForbiddenException();
return this.prisma.client.preAlert.delete({ where: { id } });
}
}