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:
@@ -0,0 +1,50 @@
|
||||
import { IsString, IsOptional, IsNumber, Min } from "class-validator";
|
||||
|
||||
export class CreatePackageDto {
|
||||
@IsString()
|
||||
vendorTracking!: string;
|
||||
|
||||
@IsString()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
store!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
declaredValue?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
declaredWeightLb?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
lengthCm?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
widthCm?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
heightCm?: number;
|
||||
|
||||
/** userId del cliente al que pertenece este paquete */
|
||||
@IsString()
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
export class UpdateStatusDto {
|
||||
@IsString()
|
||||
status!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards } from "@nestjs/common";
|
||||
import { PackagesService } from "./packages.service";
|
||||
import { CreatePackageDto, UpdateStatusDto } from "./dto/package.dto";
|
||||
import { JwtAuthGuard, RolesGuard, Roles } from "../auth/guards/auth.guard";
|
||||
import { CurrentUser } from "../auth/decorators/current-user.decorator";
|
||||
|
||||
@Controller("packages")
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class PackagesController {
|
||||
constructor(private svc: PackagesService) {}
|
||||
|
||||
@Get()
|
||||
findAll(@CurrentUser() user: any, @Query("status") status?: string, @Query("search") search?: string): Promise<any[]> {
|
||||
return this.svc.findAll(user, { status, search });
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
findOne(@Param("id") id: string, @CurrentUser() user: any): Promise<any> {
|
||||
return this.svc.findOne(id, user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles("OPERADOR_BODEGA", "ADMIN_EMPRESA", "SUPER_ADMIN")
|
||||
create(@Body() dto: CreatePackageDto, @CurrentUser() user: any): Promise<any> {
|
||||
return this.svc.create(dto, user.id, user.tenantId);
|
||||
}
|
||||
|
||||
@Patch(":id/status")
|
||||
@Roles("OPERADOR_BODEGA", "AGENTE_ADUANERO", "ADMIN_EMPRESA", "SUPER_ADMIN")
|
||||
updateStatus(@Param("id") id: string, @Body() dto: UpdateStatusDto, @CurrentUser() user: any): Promise<any> {
|
||||
return this.svc.updateStatus(id, dto, user.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PackagesController } from "./packages.controller";
|
||||
import { PackagesService } from "./packages.service";
|
||||
|
||||
@Module({
|
||||
controllers: [PackagesController],
|
||||
providers: [PackagesService],
|
||||
})
|
||||
export class PackagesModule {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user