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:
2026-06-10 02:21:18 -06:00
co-authored by Claude Fable 5
parent c3f4449296
commit 69c74faabb
43 changed files with 3536 additions and 1139 deletions
+63 -4
View File
@@ -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}