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>
229 lines
5.8 KiB
TypeScript
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 },
|
|
});
|
|
}
|
|
}
|