Files
mexus-app/src/app/api/presupuestos/[id]/partidas/[partidaId]/route.ts
Mexus 56e39af3ff 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>
2026-02-05 07:14:14 +00:00

229 lines
5.8 KiB
TypeScript

import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { partidaPresupuestoSchema } from "@/lib/validations";
import { z } from "zod";
const partidaUpdateSchema = partidaPresupuestoSchema.partial().extend({
apuId: z.string().optional().nullable(),
});
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string; partidaId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.empresaId) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
}
const { id, partidaId } = await params;
const partida = await prisma.partidaPresupuesto.findFirst({
where: {
id: partidaId,
presupuestoId: id,
presupuesto: {
obra: { empresaId: session.user.empresaId },
},
},
include: {
apu: {
include: {
insumos: {
include: {
material: true,
categoriaManoObra: true,
equipo: true,
},
},
},
},
},
});
if (!partida) {
return NextResponse.json(
{ error: "Partida no encontrada" },
{ status: 404 }
);
}
return NextResponse.json(partida);
} catch (error) {
console.error("Error fetching partida:", error);
return NextResponse.json(
{ error: "Error al obtener la partida" },
{ status: 500 }
);
}
}
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string; partidaId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.empresaId) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
}
const { id, partidaId } = await params;
const body = await request.json();
const validatedData = partidaUpdateSchema.parse(body);
// Verify partida exists and belongs to empresa
const existingPartida = await prisma.partidaPresupuesto.findFirst({
where: {
id: partidaId,
presupuestoId: id,
presupuesto: {
obra: { empresaId: session.user.empresaId },
},
},
});
if (!existingPartida) {
return NextResponse.json(
{ error: "Partida no encontrada" },
{ status: 404 }
);
}
// If APU is provided, get price from it
let precioUnitario = validatedData.precioUnitario ?? existingPartida.precioUnitario;
if (validatedData.apuId) {
const apu = await prisma.analisisPrecioUnitario.findFirst({
where: {
id: validatedData.apuId,
empresaId: session.user.empresaId,
},
});
if (apu) {
precioUnitario = apu.precioUnitario;
}
}
const cantidad = validatedData.cantidad ?? existingPartida.cantidad;
const total = cantidad * precioUnitario;
const partida = await prisma.partidaPresupuesto.update({
where: { id: partidaId },
data: {
codigo: validatedData.codigo,
descripcion: validatedData.descripcion,
unidad: validatedData.unidad,
cantidad: validatedData.cantidad,
precioUnitario,
total,
categoria: validatedData.categoria,
apuId: validatedData.apuId,
},
include: {
apu: {
select: {
id: true,
codigo: true,
descripcion: true,
precioUnitario: true,
},
},
},
});
// Update presupuesto total
await updatePresupuestoTotal(id);
return NextResponse.json(partida);
} catch (error) {
console.error("Error updating partida:", error);
return NextResponse.json(
{ error: "Error al actualizar la partida" },
{ status: 500 }
);
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string; partidaId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.empresaId) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
}
const { id, partidaId } = await params;
// Verify partida exists and belongs to empresa
const existingPartida = await prisma.partidaPresupuesto.findFirst({
where: {
id: partidaId,
presupuestoId: id,
presupuesto: {
obra: { empresaId: session.user.empresaId },
},
},
});
if (!existingPartida) {
return NextResponse.json(
{ error: "Partida no encontrada" },
{ status: 404 }
);
}
await prisma.partidaPresupuesto.delete({
where: { id: partidaId },
});
// Update presupuesto total
await updatePresupuestoTotal(id);
return NextResponse.json({ message: "Partida eliminada correctamente" });
} catch (error) {
console.error("Error deleting partida:", error);
return NextResponse.json(
{ error: "Error al eliminar la partida" },
{ status: 500 }
);
}
}
async function updatePresupuestoTotal(presupuestoId: string) {
const result = await prisma.partidaPresupuesto.aggregate({
where: { presupuestoId },
_sum: { total: true },
});
const total = result._sum.total || 0;
await prisma.presupuesto.update({
where: { id: presupuestoId },
data: { total },
});
// Also update obra's presupuestoTotal if this presupuesto is approved
const presupuesto = await prisma.presupuesto.findUnique({
where: { id: presupuestoId },
select: { obraId: true, aprobado: true },
});
if (presupuesto?.aprobado) {
const obraTotal = await prisma.presupuesto.aggregate({
where: { obraId: presupuesto.obraId, aprobado: true },
_sum: { total: true },
});
await prisma.obra.update({
where: { id: presupuesto.obraId },
data: { presupuestoTotal: obraTotal._sum.total || 0 },
});
}
}