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,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",
|
||||
|
||||
Reference in New Issue
Block a user