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
+60
View File
@@ -1,5 +1,65 @@
export const IVA_RATE = 0.16;
// Tarifa por hora sugerida para partidas personalizadas (Hora Centinela). El asesor puede ajustarla por partida.
export const TARIFA_HORA_DEFAULT = 700;
export function calcularPrecioHoras(horas: number, tarifaHora: number): number {
return Math.round((horas || 0) * (tarifaHora || 0) * 100) / 100;
}
// Modelos de cobro de una partida. "retainer" = importe minimo mensual fijo +
// tarifa de horas adicionales (las horas extra se facturan aparte, no suman al total).
export const MODELOS_COBRO: Record<string, string> = {
fijo: "Precio fijo",
horas: "Por horas",
retainer: "Retainer (minimo + adicionales)",
};
// Texto descriptivo de un retainer, reutilizado por UI, PDF y Excel.
export function describirRetainer(
montoMinimo: number,
horasIncluidas: number,
tarifaHora: number
): string {
const partes = [`${formatCurrency(montoMinimo || 0)}/mes`];
if (horasIncluidas) partes.push(`incluye ${horasIncluidas} hr`);
if (tarifaHora) partes.push(`adicional ${formatCurrency(tarifaHora)}/hr (se factura aparte)`);
return partes.join(" · ");
}
// Doble propuesta: cada partida puede ir en la Opcion 1, la Opcion 2 o en ambas.
// "ambas" aparece (y suma) en las dos opciones. null = cotizacion normal (sin doble propuesta).
export const OPCIONES = ["1", "2", "ambas"] as const;
export type OpcionPropuesta = (typeof OPCIONES)[number];
export interface MetaOpcion {
titulo?: string;
descripcion?: string;
noIncluye?: string;
}
// Totales de una opcion: suma las partidas de esa opcion + las marcadas "ambas".
// Respeta la regla de retainer: el total usa `precio`, no `horas x tarifa`. Las horas
// son informativas para la tabla comparativa.
export function calcularTotalesOpcion(
servicios: Array<{ opcion?: string | null; tipoPago: string; precio: number; horas?: number | null }>,
opcion: "1" | "2"
): { totalUnico: number; totalMensual: number; horas: number } {
const rel = servicios.filter((s) => s.opcion === "ambas" || s.opcion === opcion);
const totalUnico = rel
.filter((s) => s.tipoPago === "unico")
.reduce((a, s) => a + (s.precio || 0), 0);
const totalMensual = rel
.filter((s) => s.tipoPago === "mensual")
.reduce((a, s) => a + (s.precio || 0), 0);
const horas = rel.reduce((a, s) => a + (s.horas || 0), 0);
return {
totalUnico: Math.round(totalUnico * 100) / 100,
totalMensual: Math.round(totalMensual * 100) / 100,
horas: Math.round(horas * 100) / 100,
};
}
export const FASES: Record<number, string> = {
0: "FASE 0 - Auditoria / Acompanamiento",
1: "FASE 1 - Setup e Infraestructura",