- Implement complete Estimaciones module with CRUD operations - Create/edit/view estimaciones with partidas selection - Automatic calculation of accumulated amounts - State workflow (BORRADOR → ENVIADA → APROBADA → PAGADA) - Integration with presupuestos and partidas - Add dashboard comparativo presupuesto vs ejecutado - Summary cards with totals and variance - Category breakdown table with progress - Per-obra comparison table with filters - Integrated as tab in Reportes page - Implement bulk price update functionality - Support for MATERIAL, MANO_OBRA, EQUIPO types - Percentage or fixed value methods - Optional cascade recalculation of APUs - UI dialog in APU list - Enhance Gantt chart with API integration - New /api/obras/[id]/programacion endpoint - Drag & drop to change task dates (persisted) - Progress bar drag to update completion - Auto-fetch complete scheduling data - View mode options and refresh button - Add order creation from materials explosion - Material selection with checkboxes - Create purchase order dialog - Integration with existing ordenes system - Create missing UI components (Tabs, Checkbox, Form) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
8.0 KiB
TypeScript
264 lines
8.0 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { auth } from "@/lib/auth";
|
|
import { crearEstimacionSchema, calcularTotalesEstimacion } from "@/lib/validations/estimaciones";
|
|
|
|
// GET - Listar estimaciones (por 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 whereClause: Record<string, unknown> = {
|
|
presupuesto: {
|
|
obra: {
|
|
empresaId: session.user.empresaId,
|
|
},
|
|
},
|
|
};
|
|
|
|
if (presupuestoId) {
|
|
whereClause.presupuestoId = presupuestoId;
|
|
}
|
|
|
|
const estimaciones = await prisma.estimacion.findMany({
|
|
where: whereClause,
|
|
include: {
|
|
presupuesto: {
|
|
select: {
|
|
id: true,
|
|
nombre: true,
|
|
obra: {
|
|
select: {
|
|
id: true,
|
|
nombre: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partidas: {
|
|
include: {
|
|
partida: {
|
|
select: {
|
|
codigo: true,
|
|
descripcion: true,
|
|
unidad: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
_count: {
|
|
select: {
|
|
partidas: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [{ presupuestoId: "asc" }, { numero: "desc" }],
|
|
});
|
|
|
|
return NextResponse.json(estimaciones);
|
|
} catch (error) {
|
|
console.error("Error al obtener estimaciones:", error);
|
|
return NextResponse.json(
|
|
{ error: "Error al obtener estimaciones" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// POST - Crear nueva estimación
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.empresaId) {
|
|
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
|
}
|
|
|
|
const body = await request.json();
|
|
const validation = crearEstimacionSchema.safeParse(body);
|
|
|
|
if (!validation.success) {
|
|
return NextResponse.json(
|
|
{ error: "Datos inválidos", details: validation.error.errors },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const data = validation.data;
|
|
|
|
// Verificar que el presupuesto existe y pertenece a la empresa
|
|
const presupuesto = await prisma.presupuesto.findFirst({
|
|
where: {
|
|
id: data.presupuestoId,
|
|
obra: {
|
|
empresaId: session.user.empresaId,
|
|
},
|
|
},
|
|
include: {
|
|
partidas: true,
|
|
},
|
|
});
|
|
|
|
if (!presupuesto) {
|
|
return NextResponse.json(
|
|
{ error: "Presupuesto no encontrado" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Obtener el último número de estimación para este presupuesto
|
|
const ultimaEstimacion = await prisma.estimacion.findFirst({
|
|
where: { presupuestoId: data.presupuestoId },
|
|
orderBy: { numero: "desc" },
|
|
select: { numero: true },
|
|
});
|
|
|
|
const nuevoNumero = (ultimaEstimacion?.numero || 0) + 1;
|
|
|
|
// Obtener estimaciones anteriores para calcular acumulados
|
|
const estimacionesAnteriores = await prisma.estimacionPartida.findMany({
|
|
where: {
|
|
estimacion: {
|
|
presupuestoId: data.presupuestoId,
|
|
estado: { not: "RECHAZADA" },
|
|
},
|
|
},
|
|
select: {
|
|
partidaId: true,
|
|
cantidadEstimacion: true,
|
|
importeEstimacion: true,
|
|
},
|
|
});
|
|
|
|
// Calcular acumulados por partida
|
|
const acumuladosPorPartida: Record<string, { cantidad: number; importe: number }> = {};
|
|
for (const ep of estimacionesAnteriores) {
|
|
if (!acumuladosPorPartida[ep.partidaId]) {
|
|
acumuladosPorPartida[ep.partidaId] = { cantidad: 0, importe: 0 };
|
|
}
|
|
acumuladosPorPartida[ep.partidaId].cantidad += ep.cantidadEstimacion;
|
|
acumuladosPorPartida[ep.partidaId].importe += ep.importeEstimacion;
|
|
}
|
|
|
|
// Preparar partidas para cálculo
|
|
const partidasParaCalculo = data.partidas.map((p) => {
|
|
const partidaPresupuesto = presupuesto.partidas.find(
|
|
(pp) => pp.id === p.partidaId
|
|
);
|
|
if (!partidaPresupuesto) {
|
|
throw new Error(`Partida ${p.partidaId} no encontrada en el presupuesto`);
|
|
}
|
|
|
|
const acumulado = acumuladosPorPartida[p.partidaId] || { cantidad: 0, importe: 0 };
|
|
|
|
return {
|
|
cantidadEstimacion: p.cantidadEstimacion,
|
|
precioUnitario: partidaPresupuesto.precioUnitario,
|
|
cantidadAnterior: acumulado.cantidad,
|
|
cantidadContrato: partidaPresupuesto.cantidad,
|
|
};
|
|
});
|
|
|
|
// Calcular totales
|
|
const totales = calcularTotalesEstimacion(partidasParaCalculo, {
|
|
porcentajeRetencion: data.porcentajeRetencion || 5,
|
|
porcentajeIVA: data.porcentajeIVA || 16,
|
|
amortizacion: data.amortizacion || 0,
|
|
deduccionesVarias: data.deduccionesVarias || 0,
|
|
});
|
|
|
|
// Crear estimación con partidas
|
|
const estimacion = await prisma.estimacion.create({
|
|
data: {
|
|
numero: nuevoNumero,
|
|
periodo: data.periodo,
|
|
fechaInicio: new Date(data.fechaInicio),
|
|
fechaFin: new Date(data.fechaFin),
|
|
porcentajeRetencion: data.porcentajeRetencion || 5,
|
|
porcentajeIVA: data.porcentajeIVA || 16,
|
|
amortizacion: data.amortizacion || 0,
|
|
deduccionesVarias: data.deduccionesVarias || 0,
|
|
observaciones: data.observaciones,
|
|
importeEjecutado: totales.importeEjecutado,
|
|
importeAnterior: totales.importeAnterior,
|
|
importeAcumulado: totales.importeAcumulado,
|
|
retencion: totales.retencion,
|
|
subtotal: totales.subtotal,
|
|
iva: totales.iva,
|
|
total: totales.total,
|
|
importeNeto: totales.importeNeto,
|
|
presupuestoId: data.presupuestoId,
|
|
partidas: {
|
|
create: data.partidas.map((p) => {
|
|
const partidaPresupuesto = presupuesto.partidas.find(
|
|
(pp) => pp.id === p.partidaId
|
|
)!;
|
|
const acumulado = acumuladosPorPartida[p.partidaId] || { cantidad: 0, importe: 0 };
|
|
|
|
const cantidadAcumulada = acumulado.cantidad + p.cantidadEstimacion;
|
|
const importeEstimacion = p.cantidadEstimacion * partidaPresupuesto.precioUnitario;
|
|
const importeAcumulado = acumulado.importe + importeEstimacion;
|
|
|
|
const porcentajeAnterior = (acumulado.cantidad / partidaPresupuesto.cantidad) * 100;
|
|
const porcentajeEstimacion = (p.cantidadEstimacion / partidaPresupuesto.cantidad) * 100;
|
|
const porcentajeAcumulado = (cantidadAcumulada / partidaPresupuesto.cantidad) * 100;
|
|
|
|
return {
|
|
partidaId: p.partidaId,
|
|
cantidadContrato: partidaPresupuesto.cantidad,
|
|
cantidadAnterior: acumulado.cantidad,
|
|
cantidadEstimacion: p.cantidadEstimacion,
|
|
cantidadAcumulada,
|
|
cantidadPendiente: partidaPresupuesto.cantidad - cantidadAcumulada,
|
|
precioUnitario: partidaPresupuesto.precioUnitario,
|
|
importeAnterior: acumulado.importe,
|
|
importeEstimacion,
|
|
importeAcumulado,
|
|
porcentajeAnterior,
|
|
porcentajeEstimacion,
|
|
porcentajeAcumulado,
|
|
notas: p.notas,
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
include: {
|
|
partidas: {
|
|
include: {
|
|
partida: {
|
|
select: {
|
|
codigo: true,
|
|
descripcion: true,
|
|
unidad: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
presupuesto: {
|
|
select: {
|
|
nombre: true,
|
|
obra: {
|
|
select: {
|
|
nombre: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return NextResponse.json(estimacion, { status: 201 });
|
|
} catch (error) {
|
|
console.error("Error al crear estimación:", error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : "Error al crear estimación" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|