Restaurar funcionalidades desde stash + setup de despliegue Coolify
- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop - Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED) - docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_* - Endpoint público /api/health (verifica BD) para healthchecks - seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas) - .env.example, .dockerignore, uvicorn con --proxy-headers - Limpieza: .xlsx y __pycache__ fuera del repo Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { formatDate, formatCurrency, IVA_RATE } from "@/lib/calculators";
|
||||
import { formatDate, formatCurrency, calcularTotalesOpcion, IVA_RATE, type MetaOpcion } from "@/lib/calculators";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Pencil } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ExportExcelButtonSaved, ExportPDFButtonSaved } from "@/components/ExportButtons";
|
||||
import { ExportExcelButtonSaved, ExportPDFButtonSaved, PreviewPDFButtonSaved } from "@/components/ExportButtons";
|
||||
import { DeleteCotizacionButton } from "./DeleteButton";
|
||||
import { CambiarEstadoButtons } from "./CambiarEstadoButtons";
|
||||
import { PreciosEditables } from "./PreciosEditables";
|
||||
@@ -32,7 +32,7 @@ export default async function CotizacionDetailPage({
|
||||
.filter((s) => s.tipoPago === "unico" && s.seleccionado)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
nombre: s.servicioCatalogo?.nombre || "Servicio",
|
||||
nombre: s.servicioCatalogo?.nombre || s.nombre || "Servicio",
|
||||
precio: s.precio,
|
||||
}));
|
||||
|
||||
@@ -40,10 +40,18 @@ export default async function CotizacionDetailPage({
|
||||
.filter((s) => s.tipoPago === "mensual" && s.seleccionado)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
nombre: s.servicioCatalogo?.nombre || "Servicio",
|
||||
nombre: s.servicioCatalogo?.nombre || s.nombre || "Servicio",
|
||||
precio: s.precio,
|
||||
}));
|
||||
|
||||
const opcionesMeta = (cot.opcionesMetadata as { "1"?: MetaOpcion; "2"?: MetaOpcion } | null) ?? {};
|
||||
const totalesOpcion = cot.esDoble
|
||||
? {
|
||||
"1": calcularTotalesOpcion(cot.servicios, "1"),
|
||||
"2": calcularTotalesOpcion(cot.servicios, "2"),
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -62,6 +70,7 @@ export default async function CotizacionDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PreviewPDFButtonSaved cotizacionId={cot.id} />
|
||||
<ExportPDFButtonSaved cotizacionId={cot.id} />
|
||||
<ExportExcelButtonSaved cotizacionId={cot.id} />
|
||||
<Link
|
||||
@@ -115,6 +124,56 @@ export default async function CotizacionDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cot.esDoble && totalesOpcion && (
|
||||
<div className="bg-card-bg rounded-xl border border-border p-5 mb-6">
|
||||
<h2 className="font-semibold text-lg mb-4">
|
||||
Doble propuesta — comparativa
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{(["1", "2"] as const).map((op) => {
|
||||
const t = totalesOpcion[op];
|
||||
const meta = opcionesMeta[op];
|
||||
return (
|
||||
<div key={op} className="border border-primary/30 rounded-lg p-4 bg-primary-light/10">
|
||||
<h3 className="font-semibold text-primary">
|
||||
Opcion {op}
|
||||
{meta?.titulo ? `: ${meta.titulo}` : ""}
|
||||
</h3>
|
||||
{meta?.descripcion && (
|
||||
<p className="text-sm text-muted mt-1 whitespace-pre-wrap">{meta.descripcion}</p>
|
||||
)}
|
||||
{meta?.noIncluye && (
|
||||
<p className="text-xs text-muted mt-2">
|
||||
<span className="font-medium">No incluye:</span>{" "}
|
||||
<span className="whitespace-pre-wrap">{meta.noIncluye}</span>
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-3 pt-3 border-t border-border space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>Total unico (c/IVA)</span>
|
||||
<span className="font-medium">{formatCurrency(t.totalUnico * (1 + IVA_RATE))}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Total mensual (c/IVA)</span>
|
||||
<span className="font-medium">{formatCurrency(t.totalMensual * (1 + IVA_RATE))}</span>
|
||||
</div>
|
||||
{t.horas > 0 && (
|
||||
<div className="flex justify-between text-muted text-xs">
|
||||
<span>Horas estimadas</span>
|
||||
<span>{t.horas} h</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted mt-3">
|
||||
Las partidas marcadas <b>Ambas</b> suman en las dos opciones. Edita la asignacion de cada partida desde <b>Editar</b>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PreciosEditables
|
||||
cotizacionId={cot.id}
|
||||
serviciosUnicos={serviciosUnicos}
|
||||
|
||||
Reference in New Issue
Block a user