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:
228
src/app/api/presupuestos/[id]/partidas/[partidaId]/route.ts
Normal file
228
src/app/api/presupuestos/[id]/partidas/[partidaId]/route.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user