feat: Implement complete APU (Análisis de Precios Unitarios) module

High priority features:
- APU CRUD with materials, labor, and equipment breakdown
- Labor catalog with FSR (Factor de Salario Real) calculation
- Equipment catalog with hourly cost calculation
- Link APU to budget line items (partidas)
- Explosion de insumos (consolidated materials list)

Additional features:
- Duplicate APU functionality
- Excel export for explosion de insumos
- Search and filters in APU list
- Price validation alerts for outdated prices
- PDF report export for APU

New components:
- APUForm, APUList, APUDetail
- ManoObraForm, EquipoForm
- ConfiguracionAPUForm
- VincularAPUDialog
- PartidasManager
- ExplosionInsumos
- APUPDF

New UI components:
- Alert component
- Tooltip component

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mexus
2026-02-05 07:14:14 +00:00
parent e1847597d6
commit 56e39af3ff
47 changed files with 7779 additions and 18 deletions

View File

@@ -0,0 +1,104 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export async function POST(
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;
// Get the original APU with all insumos
const original = await prisma.analisisPrecioUnitario.findFirst({
where: {
id,
empresaId: session.user.empresaId,
},
include: {
insumos: true,
},
});
if (!original) {
return NextResponse.json({ error: "APU no encontrado" }, { status: 404 });
}
// Generate a unique code for the copy
let newCodigo = `${original.codigo}-COPIA`;
let counter = 1;
while (true) {
const existing = await prisma.analisisPrecioUnitario.findFirst({
where: {
codigo: newCodigo,
empresaId: session.user.empresaId,
},
});
if (!existing) break;
counter++;
newCodigo = `${original.codigo}-COPIA${counter}`;
}
// Create the duplicate APU with all its insumos
const duplicate = await prisma.analisisPrecioUnitario.create({
data: {
codigo: newCodigo,
descripcion: `${original.descripcion} (Copia)`,
unidad: original.unidad,
rendimientoDiario: original.rendimientoDiario,
costoMateriales: original.costoMateriales,
costoManoObra: original.costoManoObra,
costoEquipo: original.costoEquipo,
costoHerramienta: original.costoHerramienta,
costoDirecto: original.costoDirecto,
porcentajeIndirectos: original.porcentajeIndirectos,
costoIndirectos: original.costoIndirectos,
porcentajeUtilidad: original.porcentajeUtilidad,
costoUtilidad: original.costoUtilidad,
precioUnitario: original.precioUnitario,
empresaId: session.user.empresaId,
insumos: {
create: original.insumos.map((insumo) => ({
tipo: insumo.tipo,
descripcion: insumo.descripcion,
unidad: insumo.unidad,
cantidad: insumo.cantidad,
desperdicio: insumo.desperdicio,
cantidadConDesperdicio: insumo.cantidadConDesperdicio,
rendimiento: insumo.rendimiento,
precioUnitario: insumo.precioUnitario,
importe: insumo.importe,
materialId: insumo.materialId,
categoriaManoObraId: insumo.categoriaManoObraId,
equipoId: insumo.equipoId,
})),
},
},
include: {
insumos: {
include: {
material: true,
categoriaManoObra: true,
equipo: true,
},
},
},
});
return NextResponse.json(duplicate);
} catch (error) {
console.error("Error duplicating APU:", error);
return NextResponse.json(
{ error: "Error al duplicar el APU" },
{ status: 500 }
);
}
}