feat: Add estimaciones, dashboard comparativo, bulk pricing, and enhanced Gantt
- 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>
This commit is contained in:
438
src/app/api/estimaciones/[id]/route.ts
Normal file
438
src/app/api/estimaciones/[id]/route.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { auth } from "@/lib/auth";
|
||||
import {
|
||||
actualizarEstimacionSchema,
|
||||
cambiarEstadoEstimacionSchema,
|
||||
calcularTotalesEstimacion
|
||||
} from "@/lib/validations/estimaciones";
|
||||
|
||||
// GET - Obtener estimación por ID
|
||||
export async function GET(
|
||||
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;
|
||||
|
||||
const estimacion = await prisma.estimacion.findFirst({
|
||||
where: {
|
||||
id,
|
||||
presupuesto: {
|
||||
obra: {
|
||||
empresaId: session.user.empresaId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
presupuesto: {
|
||||
select: {
|
||||
id: true,
|
||||
nombre: true,
|
||||
total: true,
|
||||
obra: {
|
||||
select: {
|
||||
id: true,
|
||||
nombre: true,
|
||||
cliente: {
|
||||
select: {
|
||||
nombre: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partidas: {
|
||||
include: {
|
||||
partida: {
|
||||
select: {
|
||||
id: true,
|
||||
codigo: true,
|
||||
descripcion: true,
|
||||
unidad: true,
|
||||
cantidad: true,
|
||||
precioUnitario: true,
|
||||
total: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
partida: {
|
||||
codigo: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!estimacion) {
|
||||
return NextResponse.json(
|
||||
{ error: "Estimación no encontrada" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(estimacion);
|
||||
} catch (error) {
|
||||
console.error("Error al obtener estimación:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Error al obtener estimación" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Actualizar estimación
|
||||
export async function PUT(
|
||||
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;
|
||||
const body = await request.json();
|
||||
|
||||
// Verificar si es actualización de estado
|
||||
const estadoValidation = cambiarEstadoEstimacionSchema.safeParse(body);
|
||||
if (estadoValidation.success) {
|
||||
return await actualizarEstado(id, session.user.empresaId, estadoValidation.data);
|
||||
}
|
||||
|
||||
// Es actualización de datos
|
||||
const validation = actualizarEstimacionSchema.safeParse(body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{ error: "Datos inválidos", details: validation.error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = validation.data;
|
||||
|
||||
// Verificar que la estimación existe y pertenece a la empresa
|
||||
const estimacionExistente = await prisma.estimacion.findFirst({
|
||||
where: {
|
||||
id,
|
||||
presupuesto: {
|
||||
obra: {
|
||||
empresaId: session.user.empresaId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
presupuesto: {
|
||||
include: {
|
||||
partidas: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!estimacionExistente) {
|
||||
return NextResponse.json(
|
||||
{ error: "Estimación no encontrada" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Solo se puede editar si está en borrador
|
||||
if (estimacionExistente.estado !== "BORRADOR") {
|
||||
return NextResponse.json(
|
||||
{ error: "Solo se pueden editar estimaciones en borrador" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Si se actualizan partidas, recalcular todo
|
||||
if (data.partidas) {
|
||||
// Obtener acumulados anteriores
|
||||
const estimacionesAnteriores = await prisma.estimacionPartida.findMany({
|
||||
where: {
|
||||
estimacion: {
|
||||
presupuestoId: estimacionExistente.presupuestoId,
|
||||
id: { not: id },
|
||||
estado: { not: "RECHAZADA" },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
partidaId: true,
|
||||
cantidadEstimacion: true,
|
||||
importeEstimacion: true,
|
||||
},
|
||||
});
|
||||
|
||||
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 = estimacionExistente.presupuesto.partidas.find(
|
||||
(pp) => pp.id === p.partidaId
|
||||
);
|
||||
if (!partidaPresupuesto) {
|
||||
throw new Error(`Partida ${p.partidaId} no encontrada`);
|
||||
}
|
||||
|
||||
const acumulado = acumuladosPorPartida[p.partidaId] || { cantidad: 0, importe: 0 };
|
||||
|
||||
return {
|
||||
cantidadEstimacion: p.cantidadEstimacion,
|
||||
precioUnitario: partidaPresupuesto.precioUnitario,
|
||||
cantidadAnterior: acumulado.cantidad,
|
||||
cantidadContrato: partidaPresupuesto.cantidad,
|
||||
};
|
||||
});
|
||||
|
||||
const totales = calcularTotalesEstimacion(partidasParaCalculo, {
|
||||
porcentajeRetencion: data.porcentajeRetencion ?? estimacionExistente.porcentajeRetencion,
|
||||
porcentajeIVA: data.porcentajeIVA ?? estimacionExistente.porcentajeIVA,
|
||||
amortizacion: data.amortizacion ?? estimacionExistente.amortizacion,
|
||||
deduccionesVarias: data.deduccionesVarias ?? estimacionExistente.deduccionesVarias,
|
||||
});
|
||||
|
||||
// Eliminar partidas existentes y crear nuevas
|
||||
await prisma.estimacionPartida.deleteMany({
|
||||
where: { estimacionId: id },
|
||||
});
|
||||
|
||||
const estimacion = await prisma.estimacion.update({
|
||||
where: { id },
|
||||
data: {
|
||||
periodo: data.periodo,
|
||||
fechaInicio: data.fechaInicio ? new Date(data.fechaInicio) : undefined,
|
||||
fechaFin: data.fechaFin ? new Date(data.fechaFin) : undefined,
|
||||
porcentajeRetencion: data.porcentajeRetencion,
|
||||
porcentajeIVA: data.porcentajeIVA,
|
||||
amortizacion: data.amortizacion,
|
||||
deduccionesVarias: data.deduccionesVarias,
|
||||
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,
|
||||
partidas: {
|
||||
create: data.partidas.map((p) => {
|
||||
const partidaPresupuesto = estimacionExistente.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(estimacion);
|
||||
}
|
||||
|
||||
// Actualización simple sin partidas
|
||||
const estimacion = await prisma.estimacion.update({
|
||||
where: { id },
|
||||
data: {
|
||||
periodo: data.periodo,
|
||||
fechaInicio: data.fechaInicio ? new Date(data.fechaInicio) : undefined,
|
||||
fechaFin: data.fechaFin ? new Date(data.fechaFin) : undefined,
|
||||
porcentajeRetencion: data.porcentajeRetencion,
|
||||
porcentajeIVA: data.porcentajeIVA,
|
||||
amortizacion: data.amortizacion,
|
||||
deduccionesVarias: data.deduccionesVarias,
|
||||
observaciones: data.observaciones,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(estimacion);
|
||||
} catch (error) {
|
||||
console.error("Error al actualizar estimación:", error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Error al actualizar estimación" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper para actualizar estado
|
||||
async function actualizarEstado(
|
||||
id: string,
|
||||
empresaId: string,
|
||||
data: { estado: string; motivoRechazo?: string }
|
||||
) {
|
||||
const estimacion = await prisma.estimacion.findFirst({
|
||||
where: {
|
||||
id,
|
||||
presupuesto: {
|
||||
obra: {
|
||||
empresaId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!estimacion) {
|
||||
return NextResponse.json(
|
||||
{ error: "Estimación no encontrada" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validar transiciones de estado válidas
|
||||
const transicionesValidas: Record<string, string[]> = {
|
||||
BORRADOR: ["ENVIADA"],
|
||||
ENVIADA: ["EN_REVISION", "RECHAZADA"],
|
||||
EN_REVISION: ["APROBADA", "RECHAZADA"],
|
||||
APROBADA: ["PAGADA"],
|
||||
RECHAZADA: ["BORRADOR"],
|
||||
PAGADA: [],
|
||||
};
|
||||
|
||||
if (!transicionesValidas[estimacion.estado]?.includes(data.estado)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `No se puede cambiar de ${estimacion.estado} a ${data.estado}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
estado: data.estado,
|
||||
};
|
||||
|
||||
// Agregar fechas según el estado
|
||||
if (data.estado === "ENVIADA") {
|
||||
updateData.fechaEnvio = new Date();
|
||||
} else if (data.estado === "APROBADA") {
|
||||
updateData.fechaAprobacion = new Date();
|
||||
} else if (data.estado === "RECHAZADA") {
|
||||
updateData.motivoRechazo = data.motivoRechazo || "Sin motivo especificado";
|
||||
}
|
||||
|
||||
const estimacionActualizada = await prisma.estimacion.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
presupuesto: {
|
||||
select: {
|
||||
nombre: true,
|
||||
obra: {
|
||||
select: {
|
||||
nombre: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(estimacionActualizada);
|
||||
}
|
||||
|
||||
// DELETE - Eliminar estimación
|
||||
export async function DELETE(
|
||||
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;
|
||||
|
||||
// Verificar que la estimación existe
|
||||
const estimacion = await prisma.estimacion.findFirst({
|
||||
where: {
|
||||
id,
|
||||
presupuesto: {
|
||||
obra: {
|
||||
empresaId: session.user.empresaId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!estimacion) {
|
||||
return NextResponse.json(
|
||||
{ error: "Estimación no encontrada" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Solo se puede eliminar si está en borrador o rechazada
|
||||
if (!["BORRADOR", "RECHAZADA"].includes(estimacion.estado)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Solo se pueden eliminar estimaciones en borrador o rechazadas" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.estimacion.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error al eliminar estimación:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Error al eliminar estimación" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
263
src/app/api/estimaciones/route.ts
Normal file
263
src/app/api/estimaciones/route.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user