diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2807b9c..828825d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -146,6 +146,9 @@ model User { pushSubscriptions PushSubscription[] notificaciones Notificacion[] actividades ActividadLog[] + avancesRegistrados AvancePartida[] @relation("AvancesRegistrados") + avancesAprobados AvancePartida[] @relation("AvancesAprobados") + importaciones ImportacionCatalogo[] @@index([empresaId]) @@index([email]) @@ -174,6 +177,7 @@ model Empresa { equiposMaquinaria EquipoMaquinaria[] apus AnalisisPrecioUnitario[] configuracionAPU ConfiguracionAPU? + importaciones ImportacionCatalogo[] } model Cliente { @@ -360,11 +364,40 @@ model PartidaPresupuesto { // Relations gastos Gasto[] + avances AvancePartida[] @@index([presupuestoId]) @@index([apuId]) } +model AvancePartida { + id String @id @default(cuid()) + fecha DateTime @default(now()) + cantidadEjecutada Float // Cantidad ejecutada en este registro + cantidadAcumulada Float // Cantidad acumulada hasta este registro + porcentajeAvance Float // Porcentaje de avance (0-100) + montoEjecutado Float // Monto correspondiente a este avance + montoAcumulado Float // Monto acumulado hasta este registro + notas String? + fotos String[] + + partidaId String + partida PartidaPresupuesto @relation(fields: [partidaId], references: [id], onDelete: Cascade) + registradoPorId String + registradoPor User @relation("AvancesRegistrados", fields: [registradoPorId], references: [id]) + aprobadoPorId String? + aprobadoPor User? @relation("AvancesAprobados", fields: [aprobadoPorId], references: [id]) + aprobado Boolean @default(false) + fechaAprobacion DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([partidaId]) + @@index([registradoPorId]) + @@index([fecha]) +} + model Gasto { id String @id @default(cuid()) concepto String @@ -1074,3 +1107,41 @@ model ConfiguracionAPU { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +// ============== IMPORTACIÓN DE CATÁLOGOS ============== + +enum TipoImportacion { + MATERIALES + MANO_OBRA + EQUIPOS +} + +enum EstadoImportacion { + PENDIENTE + PROCESANDO + COMPLETADA + ERROR +} + +model ImportacionCatalogo { + id String @id @default(cuid()) + tipo TipoImportacion + nombreArchivo String + estado EstadoImportacion @default(PENDIENTE) + registrosTotal Int @default(0) + registrosCreados Int @default(0) + registrosActualizados Int @default(0) + registrosError Int @default(0) + errores String? // JSON con detalles de errores + fuenteDatos String? // BIMSA, Proveedor, Manual, etc. + empresaId String + empresa Empresa @relation(fields: [empresaId], references: [id]) + creadoPorId String + creadoPor User @relation(fields: [creadoPorId], references: [id]) + createdAt DateTime @default(now()) + completadoAt DateTime? + + @@index([empresaId]) + @@index([tipo]) + @@index([estado]) +} diff --git a/src/app/(dashboard)/obras/[id]/obra-detail-client.tsx b/src/app/(dashboard)/obras/[id]/obra-detail-client.tsx index 7f37ef5..cfdacce 100644 --- a/src/app/(dashboard)/obras/[id]/obra-detail-client.tsx +++ b/src/app/(dashboard)/obras/[id]/obra-detail-client.tsx @@ -53,6 +53,7 @@ import { BitacoraPDF, } from "@/components/pdf"; import { PartidasManager, ExplosionInsumos } from "@/components/presupuesto"; +import { ControlAvancePartidas } from "@/components/avances"; // Componente de carga const LoadingSpinner = () => ( @@ -600,13 +601,27 @@ export function ObraDetailClient({ obra }: ObraDetailProps) { - + + + Partidas + Control de Avance + + + + + + + + ))} diff --git a/src/app/(dashboard)/obras/obras-client.tsx b/src/app/(dashboard)/obras/obras-client.tsx index b44b4f6..1c76982 100644 --- a/src/app/(dashboard)/obras/obras-client.tsx +++ b/src/app/(dashboard)/obras/obras-client.tsx @@ -44,8 +44,8 @@ import { import { formatCurrency, formatPercentage, - formatDateShort, } from "@/lib/utils"; +import { ClientDate } from "@/components/ui/client-date"; import { ESTADO_OBRA_LABELS, ESTADO_OBRA_COLORS, @@ -229,7 +229,7 @@ export function ObrasClient({ obras }: { obras: Obra[] }) { {obra.fechaInicio && (
- {formatDateShort(obra.fechaInicio)} +
)} {obra.supervisor && ( diff --git a/src/app/api/avances/[id]/route.ts b/src/app/api/avances/[id]/route.ts new file mode 100644 index 0000000..dffce47 --- /dev/null +++ b/src/app/api/avances/[id]/route.ts @@ -0,0 +1,209 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +// GET - Get avance by ID +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + if (!session?.user?.empresaId) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const { id } = await params; + + const avance = await prisma.avancePartida.findFirst({ + where: { + id, + partida: { + presupuesto: { + obra: { + empresaId: session.user.empresaId, + }, + }, + }, + }, + include: { + partida: { + select: { + id: true, + codigo: true, + descripcion: true, + cantidad: true, + precioUnitario: true, + total: true, + presupuesto: { + select: { + id: true, + nombre: true, + obra: { + select: { id: true, nombre: true }, + }, + }, + }, + }, + }, + registradoPor: { + select: { nombre: true, apellido: true }, + }, + aprobadoPor: { + select: { nombre: true, apellido: true }, + }, + }, + }); + + if (!avance) { + return NextResponse.json({ error: "Avance no encontrado" }, { status: 404 }); + } + + return NextResponse.json(avance); + } catch (error) { + console.error("Error fetching avance:", error); + return NextResponse.json( + { error: "Error al obtener avance" }, + { status: 500 } + ); + } +} + +// PUT - Approve avance +export async function PUT( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + if (!session?.user?.empresaId || !session?.user?.id) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const { id } = await params; + const body = await request.json(); + + // Verify access + const existing = await prisma.avancePartida.findFirst({ + where: { + id, + partida: { + presupuesto: { + obra: { + empresaId: session.user.empresaId, + }, + }, + }, + }, + }); + + if (!existing) { + return NextResponse.json({ error: "Avance no encontrado" }, { status: 404 }); + } + + // Only allow approving/unapproving + const avance = await prisma.avancePartida.update({ + where: { id }, + data: { + aprobado: body.aprobado ?? existing.aprobado, + aprobadoPorId: body.aprobado ? session.user.id : null, + fechaAprobacion: body.aprobado ? new Date() : null, + notas: body.notas ?? existing.notas, + }, + include: { + partida: { + select: { + codigo: true, + descripcion: true, + }, + }, + registradoPor: { + select: { nombre: true, apellido: true }, + }, + aprobadoPor: { + select: { nombre: true, apellido: true }, + }, + }, + }); + + return NextResponse.json(avance); + } catch (error) { + console.error("Error updating avance:", error); + return NextResponse.json( + { error: "Error al actualizar avance" }, + { status: 500 } + ); + } +} + +// DELETE - Delete avance (only the most recent one for a partida) +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + if (!session?.user?.empresaId) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const { id } = await params; + + // Verify access and check if it's the most recent + const avance = await prisma.avancePartida.findFirst({ + where: { + id, + partida: { + presupuesto: { + obra: { + empresaId: session.user.empresaId, + }, + }, + }, + }, + include: { + partida: { + include: { + avances: { + orderBy: { fecha: "desc" }, + take: 1, + }, + }, + }, + }, + }); + + if (!avance) { + return NextResponse.json({ error: "Avance no encontrado" }, { status: 404 }); + } + + // Check if it's the most recent avance + const mostRecent = avance.partida.avances[0]; + if (mostRecent && mostRecent.id !== id) { + return NextResponse.json( + { error: "Solo se puede eliminar el ultimo avance registrado" }, + { status: 400 } + ); + } + + // Check if it's approved + if (avance.aprobado) { + return NextResponse.json( + { error: "No se puede eliminar un avance aprobado" }, + { status: 400 } + ); + } + + await prisma.avancePartida.delete({ + where: { id }, + }); + + return NextResponse.json({ message: "Avance eliminado correctamente" }); + } catch (error) { + console.error("Error deleting avance:", error); + return NextResponse.json( + { error: "Error al eliminar avance" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/avances/resumen/route.ts b/src/app/api/avances/resumen/route.ts new file mode 100644 index 0000000..c363ad4 --- /dev/null +++ b/src/app/api/avances/resumen/route.ts @@ -0,0 +1,122 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +// GET - Get avance summary for a presupuesto +export async function GET(request: Request) { + try { + const session = await auth(); + if (!session?.user?.empresaId) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const presupuestoId = searchParams.get("presupuestoId"); + const obraId = searchParams.get("obraId"); + + if (!presupuestoId && !obraId) { + return NextResponse.json( + { error: "Se requiere presupuestoId u obraId" }, + { status: 400 } + ); + } + + // Get partidas with their latest avance + const partidas = await prisma.partidaPresupuesto.findMany({ + where: { + presupuesto: { + ...(presupuestoId && { id: presupuestoId }), + ...(obraId && { obraId }), + obra: { + empresaId: session.user.empresaId, + }, + }, + }, + include: { + apu: { + select: { codigo: true, descripcion: true }, + }, + avances: { + orderBy: { fecha: "desc" }, + take: 1, + include: { + registradoPor: { + select: { nombre: true, apellido: true }, + }, + }, + }, + presupuesto: { + select: { + id: true, + nombre: true, + total: true, + }, + }, + }, + orderBy: { codigo: "asc" }, + }); + + // Calculate summary + let totalPresupuestado = 0; + let totalEjecutado = 0; + let partidasConAvance = 0; + + const partidasConResumen = partidas.map((partida) => { + const ultimoAvance = partida.avances[0]; + const cantidadAcumulada = ultimoAvance?.cantidadAcumulada || 0; + const montoAcumulado = ultimoAvance?.montoAcumulado || 0; + const porcentajeAvance = ultimoAvance?.porcentajeAvance || 0; + + totalPresupuestado += partida.total; + totalEjecutado += montoAcumulado; + if (cantidadAcumulada > 0) partidasConAvance++; + + return { + id: partida.id, + codigo: partida.codigo, + descripcion: partida.descripcion, + unidad: partida.unidad, + cantidadPresupuestada: partida.cantidad, + cantidadEjecutada: cantidadAcumulada, + cantidadPendiente: partida.cantidad - cantidadAcumulada, + porcentajeAvance, + precioUnitario: partida.precioUnitario, + montoPresupuestado: partida.total, + montoEjecutado: montoAcumulado, + montoPendiente: partida.total - montoAcumulado, + apu: partida.apu, + ultimoAvance: ultimoAvance + ? { + fecha: ultimoAvance.fecha, + cantidadEjecutada: ultimoAvance.cantidadEjecutada, + registradoPor: ultimoAvance.registradoPor, + aprobado: ultimoAvance.aprobado, + } + : null, + }; + }); + + const porcentajeAvanceGlobal = totalPresupuestado > 0 + ? (totalEjecutado / totalPresupuestado) * 100 + : 0; + + return NextResponse.json({ + resumen: { + totalPartidas: partidas.length, + partidasConAvance, + partidasSinAvance: partidas.length - partidasConAvance, + totalPresupuestado, + totalEjecutado, + totalPendiente: totalPresupuestado - totalEjecutado, + porcentajeAvanceGlobal, + }, + partidas: partidasConResumen, + }); + } catch (error) { + console.error("Error fetching avance summary:", error); + return NextResponse.json( + { error: "Error al obtener resumen de avances" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/avances/route.ts b/src/app/api/avances/route.ts new file mode 100644 index 0000000..76f35c2 --- /dev/null +++ b/src/app/api/avances/route.ts @@ -0,0 +1,197 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { z } from "zod"; + +const avanceSchema = z.object({ + partidaId: z.string().min(1, "Partida requerida"), + cantidadEjecutada: z.number().min(0, "Cantidad debe ser positiva"), + notas: z.string().optional(), + fotos: z.array(z.string()).optional(), +}); + +// GET - List avances (optionally filtered by partida or presupuesto) +export async function GET(request: Request) { + try { + const session = await auth(); + if (!session?.user?.empresaId) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const partidaId = searchParams.get("partidaId"); + const presupuestoId = searchParams.get("presupuestoId"); + const obraId = searchParams.get("obraId"); + + // Build where clause + const where: any = { + partida: { + presupuesto: { + obra: { + empresaId: session.user.empresaId, + }, + }, + }, + }; + + if (partidaId) { + where.partidaId = partidaId; + } + + if (presupuestoId) { + where.partida = { + ...where.partida, + presupuestoId, + }; + } + + if (obraId) { + where.partida = { + ...where.partida, + presupuesto: { + ...where.partida.presupuesto, + obraId, + }, + }; + } + + const avances = await prisma.avancePartida.findMany({ + where, + orderBy: { fecha: "desc" }, + include: { + partida: { + select: { + id: true, + codigo: true, + descripcion: true, + cantidad: true, + precioUnitario: true, + total: true, + presupuesto: { + select: { + id: true, + nombre: true, + obra: { + select: { id: true, nombre: true }, + }, + }, + }, + }, + }, + registradoPor: { + select: { nombre: true, apellido: true }, + }, + aprobadoPor: { + select: { nombre: true, apellido: true }, + }, + }, + }); + + return NextResponse.json(avances); + } catch (error) { + console.error("Error fetching avances:", error); + return NextResponse.json( + { error: "Error al obtener avances" }, + { status: 500 } + ); + } +} + +// POST - Create new avance +export async function POST(request: Request) { + try { + const session = await auth(); + if (!session?.user?.empresaId || !session?.user?.id) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const body = await request.json(); + const validatedData = avanceSchema.parse(body); + + // Get partida and verify access + const partida = await prisma.partidaPresupuesto.findFirst({ + where: { + id: validatedData.partidaId, + presupuesto: { + obra: { + empresaId: session.user.empresaId, + }, + }, + }, + include: { + avances: { + orderBy: { fecha: "desc" }, + take: 1, + }, + }, + }); + + if (!partida) { + return NextResponse.json( + { error: "Partida no encontrada" }, + { status: 404 } + ); + } + + // Calculate accumulated values + const lastAvance = partida.avances[0]; + const previousAcumulado = lastAvance?.cantidadAcumulada || 0; + const cantidadAcumulada = previousAcumulado + validatedData.cantidadEjecutada; + + // Validate not exceeding total + if (cantidadAcumulada > partida.cantidad) { + return NextResponse.json( + { + error: `La cantidad acumulada (${cantidadAcumulada.toFixed(2)}) excede la cantidad presupuestada (${partida.cantidad.toFixed(2)})` + }, + { status: 400 } + ); + } + + const porcentajeAvance = (cantidadAcumulada / partida.cantidad) * 100; + const montoEjecutado = validatedData.cantidadEjecutada * partida.precioUnitario; + const montoAcumulado = cantidadAcumulada * partida.precioUnitario; + + const avance = await prisma.avancePartida.create({ + data: { + fecha: new Date(), + cantidadEjecutada: validatedData.cantidadEjecutada, + cantidadAcumulada, + porcentajeAvance, + montoEjecutado, + montoAcumulado, + notas: validatedData.notas, + fotos: validatedData.fotos || [], + partidaId: validatedData.partidaId, + registradoPorId: session.user.id, + }, + include: { + partida: { + select: { + codigo: true, + descripcion: true, + cantidad: true, + total: true, + }, + }, + registradoPor: { + select: { nombre: true, apellido: true }, + }, + }, + }); + + return NextResponse.json(avance); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0].message }, + { status: 400 } + ); + } + console.error("Error creating avance:", error); + return NextResponse.json( + { error: "Error al registrar avance" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/importar/route.ts b/src/app/api/importar/route.ts new file mode 100644 index 0000000..040d561 --- /dev/null +++ b/src/app/api/importar/route.ts @@ -0,0 +1,345 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import * as XLSX from "xlsx"; +import { TipoImportacion, UnidadMedida } from "@prisma/client"; + +// Map common unit names to UnidadMedida enum +const UNIDAD_MAP: Record = { + "PZA": "PIEZA", + "PIEZA": "PIEZA", + "PZ": "PIEZA", + "UN": "UNIDAD", + "UNIDAD": "UNIDAD", + "M": "METRO", + "ML": "METRO", + "METRO": "METRO", + "M2": "METRO_CUADRADO", + "M²": "METRO_CUADRADO", + "METRO CUADRADO": "METRO_CUADRADO", + "M3": "METRO_CUBICO", + "M³": "METRO_CUBICO", + "METRO CUBICO": "METRO_CUBICO", + "KG": "KILOGRAMO", + "KILOGRAMO": "KILOGRAMO", + "TON": "TONELADA", + "TONELADA": "TONELADA", + "LT": "LITRO", + "L": "LITRO", + "LITRO": "LITRO", + "BOLSA": "BOLSA", + "BLS": "BOLSA", + "ROLLO": "ROLLO", + "RLL": "ROLLO", + "CAJA": "CAJA", + "CJA": "CAJA", + "HORA": "HORA", + "HR": "HORA", + "JORNADA": "JORNADA", + "JOR": "JORNADA", + "VIAJE": "VIAJE", + "VJE": "VIAJE", + "LOTE": "LOTE", + "LTE": "LOTE", + "GLOBAL": "GLOBAL", + "GLB": "GLOBAL", +}; + +function parseUnidad(unidad: string): UnidadMedida { + const normalized = unidad.toUpperCase().trim(); + return UNIDAD_MAP[normalized] || "UNIDAD"; +} + +// GET - List import history +export async function GET(request: Request) { + try { + const session = await auth(); + if (!session?.user?.empresaId) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const tipo = searchParams.get("tipo") as TipoImportacion | null; + + const where: any = { empresaId: session.user.empresaId }; + if (tipo) { + where.tipo = tipo; + } + + const importaciones = await prisma.importacionCatalogo.findMany({ + where, + orderBy: { createdAt: "desc" }, + take: 50, + include: { + creadoPor: { + select: { nombre: true, apellido: true }, + }, + }, + }); + + return NextResponse.json(importaciones); + } catch (error) { + console.error("Error fetching imports:", error); + return NextResponse.json( + { error: "Error al obtener historial de importaciones" }, + { status: 500 } + ); + } +} + +// POST - Process import file +export async function POST(request: Request) { + try { + const session = await auth(); + if (!session?.user?.empresaId || !session?.user?.id) { + return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + } + + const formData = await request.formData(); + const file = formData.get("file") as File; + const tipo = formData.get("tipo") as TipoImportacion; + const fuente = formData.get("fuente") as string || "Manual"; + + if (!file) { + return NextResponse.json({ error: "Archivo requerido" }, { status: 400 }); + } + + if (!tipo || !["MATERIALES", "MANO_OBRA", "EQUIPOS"].includes(tipo)) { + return NextResponse.json({ error: "Tipo de importacion invalido" }, { status: 400 }); + } + + // Create import record + const importacion = await prisma.importacionCatalogo.create({ + data: { + tipo, + nombreArchivo: file.name, + estado: "PROCESANDO", + fuenteDatos: fuente, + empresaId: session.user.empresaId, + creadoPorId: session.user.id, + }, + }); + + try { + // Read file + const buffer = await file.arrayBuffer(); + const workbook = XLSX.read(buffer, { type: "array" }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const data = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); + + if (!data.length) { + throw new Error("El archivo esta vacio"); + } + + let created = 0; + let updated = 0; + let errors = 0; + const errorDetails: string[] = []; + + // Process based on type + if (tipo === "MATERIALES") { + for (let i = 0; i < data.length; i++) { + const row = data[i] as any; + try { + const codigo = String(row.codigo || row.Codigo || row.CODIGO || row.clave || row.Clave || "").trim(); + const nombre = String(row.nombre || row.Nombre || row.NOMBRE || row.descripcion || row.Descripcion || "").trim(); + const unidadStr = String(row.unidad || row.Unidad || row.UNIDAD || "UNIDAD").trim(); + const precio = parseFloat(row.precio || row.Precio || row.PRECIO || row.precioUnitario || row.PrecioUnitario || 0); + + if (!codigo || !nombre) { + errors++; + errorDetails.push(`Fila ${i + 2}: Codigo o nombre faltante`); + continue; + } + + const unidad = parseUnidad(unidadStr); + + // Check if exists + const existing = await prisma.material.findFirst({ + where: { codigo, empresaId: session.user.empresaId }, + }); + + if (existing) { + await prisma.material.update({ + where: { id: existing.id }, + data: { nombre, unidad, precioUnitario: precio || existing.precioUnitario }, + }); + updated++; + } else { + await prisma.material.create({ + data: { + codigo, + nombre, + unidad, + precioUnitario: precio || 0, + empresaId: session.user.empresaId, + }, + }); + created++; + } + } catch (err) { + errors++; + errorDetails.push(`Fila ${i + 2}: ${err instanceof Error ? err.message : "Error desconocido"}`); + } + } + } else if (tipo === "MANO_OBRA") { + for (let i = 0; i < data.length; i++) { + const row = data[i] as any; + try { + const codigo = String(row.codigo || row.Codigo || row.CODIGO || "").trim(); + const nombre = String(row.nombre || row.Nombre || row.NOMBRE || row.categoria || row.Categoria || "").trim(); + const salarioDiario = parseFloat(row.salario || row.Salario || row.salarioDiario || row.SalarioDiario || 0); + + if (!codigo || !nombre) { + errors++; + errorDetails.push(`Fila ${i + 2}: Codigo o nombre faltante`); + continue; + } + + // Check if exists + const existing = await prisma.categoriaTrabajoAPU.findFirst({ + where: { codigo, empresaId: session.user.empresaId }, + }); + + // Calculate FSR with defaults + const factorIMSS = 0.2675; + const factorINFONAVIT = 0.05; + const factorRetiro = 0.02; + const factorVacaciones = 0.0411; + const factorPrimaVac = 0.0103; + const factorAguinaldo = 0.0411; + const factorSalarioReal = 1 + factorIMSS + factorINFONAVIT + factorRetiro + factorVacaciones + factorPrimaVac + factorAguinaldo; + const salarioReal = salarioDiario * factorSalarioReal; + + if (existing) { + await prisma.categoriaTrabajoAPU.update({ + where: { id: existing.id }, + data: { + nombre, + salarioDiario: salarioDiario || existing.salarioDiario, + factorSalarioReal, + salarioReal: salarioDiario ? salarioReal : existing.salarioReal, + }, + }); + updated++; + } else { + await prisma.categoriaTrabajoAPU.create({ + data: { + codigo, + nombre, + categoria: "PEON", // Default + salarioDiario: salarioDiario || 0, + factorIMSS, + factorINFONAVIT, + factorRetiro, + factorVacaciones, + factorPrimaVac, + factorAguinaldo, + factorSalarioReal, + salarioReal, + empresaId: session.user.empresaId, + }, + }); + created++; + } + } catch (err) { + errors++; + errorDetails.push(`Fila ${i + 2}: ${err instanceof Error ? err.message : "Error desconocido"}`); + } + } + } else if (tipo === "EQUIPOS") { + for (let i = 0; i < data.length; i++) { + const row = data[i] as any; + try { + const codigo = String(row.codigo || row.Codigo || row.CODIGO || "").trim(); + const nombre = String(row.nombre || row.Nombre || row.NOMBRE || row.descripcion || row.Descripcion || "").trim(); + const costoHorario = parseFloat(row.costoHorario || row.CostoHorario || row.precio || row.Precio || 0); + + if (!codigo || !nombre) { + errors++; + errorDetails.push(`Fila ${i + 2}: Codigo o nombre faltante`); + continue; + } + + // Check if exists + const existing = await prisma.equipoMaquinaria.findFirst({ + where: { codigo, empresaId: session.user.empresaId }, + }); + + if (existing) { + await prisma.equipoMaquinaria.update({ + where: { id: existing.id }, + data: { + nombre, + costoHorario: costoHorario || existing.costoHorario, + }, + }); + updated++; + } else { + await prisma.equipoMaquinaria.create({ + data: { + codigo, + nombre, + tipo: "MAQUINARIA_LIGERA", // Default + valorAdquisicion: 0, + vidaUtilHoras: 10000, + costoHorario: costoHorario || 0, + empresaId: session.user.empresaId, + }, + }); + created++; + } + } catch (err) { + errors++; + errorDetails.push(`Fila ${i + 2}: ${err instanceof Error ? err.message : "Error desconocido"}`); + } + } + } + + // Update import record + await prisma.importacionCatalogo.update({ + where: { id: importacion.id }, + data: { + estado: errors > 0 && created === 0 && updated === 0 ? "ERROR" : "COMPLETADA", + registrosTotal: data.length, + registrosCreados: created, + registrosActualizados: updated, + registrosError: errors, + errores: errorDetails.length > 0 ? JSON.stringify(errorDetails.slice(0, 50)) : null, + completadoAt: new Date(), + }, + }); + + return NextResponse.json({ + success: true, + importacionId: importacion.id, + total: data.length, + creados: created, + actualizados: updated, + errores: errors, + detalleErrores: errorDetails.slice(0, 10), + }); + + } catch (err) { + // Update import record with error + await prisma.importacionCatalogo.update({ + where: { id: importacion.id }, + data: { + estado: "ERROR", + errores: JSON.stringify([err instanceof Error ? err.message : "Error desconocido"]), + completadoAt: new Date(), + }, + }); + + throw err; + } + + } catch (error) { + console.error("Error processing import:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Error al procesar importacion" }, + { status: 500 } + ); + } +} diff --git a/src/components/apu/apu-list.tsx b/src/components/apu/apu-list.tsx index 5bdb7bf..6a3b9d6 100644 --- a/src/components/apu/apu-list.tsx +++ b/src/components/apu/apu-list.tsx @@ -51,6 +51,7 @@ import { } from "lucide-react"; import { UNIDAD_MEDIDA_LABELS } from "@/types"; import { UnidadMedida } from "@prisma/client"; +import { ImportadorCatalogo } from "@/components/importar"; interface APU { id: string; @@ -193,6 +194,7 @@ export function APUList({ apus: initialApus }: APUListProps) { )} +