Registro de horas: estado de pago por partida (por pagar / pagada)
Permite separar las horas ya cobradas de las pendientes para que los totales no se
acumulen y poder emitir la nota por pagar al cliente y conservar el recibo de lo pagado.
- Modelo: RegistroHoras.estadoPago ("por_pagar" default | "pagada") + fechaPago
(se sella al marcar pagada, se limpia al revertir). Migración idempotente.
- API: PATCH /api/cotizaciones/[id]/horas/[registroId] para marcar por pagar/pagada.
- calculators: ESTADOS_PAGO_HORAS + resumenPagoHoras() que suma por separado
pendiente vs pagado.
- Panel: resumen "Por pagar" vs "Pagado" siempre visible (con IVA si aplica),
badge/toggle de estado por fila, filtro de estado (Por pagar/Pagadas/Todas) que
además define el documento: "Nota de horas por pagar", "Recibo de horas pagadas"
o "Estado de cuenta de horas".
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8b1e420d4a
commit
d83a80c85a
@@ -242,6 +242,50 @@ export function periodoAgrupacion(
|
||||
return { clave: `${y}-${p2(m + 1)}-${p2(d)}`, etiqueta: formatFechaRegistro(fecha) };
|
||||
}
|
||||
|
||||
// Estado de cobro de una partida de horas registrada. "por_pagar" es pendiente de
|
||||
// cobro (entra en la nota por pagar al cliente); "pagada" ya se cobro (se conserva
|
||||
// como recibo/historial y NO se suma al pendiente).
|
||||
export const ESTADOS_PAGO_HORAS = ["por_pagar", "pagada"] as const;
|
||||
export type EstadoPagoHoras = (typeof ESTADOS_PAGO_HORAS)[number];
|
||||
|
||||
export const ESTADOS_PAGO_HORAS_LABEL: Record<EstadoPagoHoras, string> = {
|
||||
por_pagar: "Por pagar",
|
||||
pagada: "Pagada",
|
||||
};
|
||||
|
||||
// Normaliza el estado de pago: cualquier valor distinto de "pagada" cuenta como pendiente.
|
||||
export function esPagada(estadoPago?: string | null): boolean {
|
||||
return estadoPago === "pagada";
|
||||
}
|
||||
|
||||
// Suma horas e importe de un conjunto de registros separando por estado de pago, para
|
||||
// que lo pagado y lo pendiente nunca se mezclen en un solo total.
|
||||
export function resumenPagoHoras(
|
||||
registros: Array<{ horas: number; tarifaHora: number; estadoPago?: string | null }>
|
||||
): {
|
||||
porPagar: { horas: number; importe: number; count: number };
|
||||
pagada: { horas: number; importe: number; count: number };
|
||||
total: { horas: number; importe: number; count: number };
|
||||
} {
|
||||
const acc = () => ({ horas: 0, importe: 0, count: 0 });
|
||||
const r = { porPagar: acc(), pagada: acc(), total: acc() };
|
||||
for (const x of registros) {
|
||||
const importe = (x.horas || 0) * (x.tarifaHora || 0);
|
||||
const bucket = esPagada(x.estadoPago) ? r.pagada : r.porPagar;
|
||||
bucket.horas += x.horas || 0;
|
||||
bucket.importe += importe;
|
||||
bucket.count += 1;
|
||||
r.total.horas += x.horas || 0;
|
||||
r.total.importe += importe;
|
||||
r.total.count += 1;
|
||||
}
|
||||
for (const k of ["porPagar", "pagada", "total"] as const) {
|
||||
r[k].horas = Math.round(r[k].horas * 100) / 100;
|
||||
r[k].importe = Math.round(r[k].importe * 100) / 100;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export function bucefaloPrecio(nivel: string): number {
|
||||
return PLANES_BUCEFALO.find((p) => p.nivel === nivel)?.precio ?? 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user