Registro de horas: accion explicita para regresar una partida pagada a "Por pagar"

El badge de estado deja de ser un toggle (facil de disparar por error) y pasa a ser
un indicador. En su lugar, la columna de acciones muestra un boton claro segun el estado:
"Marcar como pagada" (pendientes) o "Regresar a Por pagar" (pagadas, con confirmacion).
Asi se corrigen los casos donde una partida se marca como pagada por error. El backend
ya limpiaba la fechaPago al regresar a por_pagar; solo cambia la UX del panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
urieljareth
2026-07-11 11:31:18 -06:00
co-authored by Claude Opus 4.8
parent 3824dd00be
commit 63b14fc8b8
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { Clock, Plus, Trash2, Pencil, Download, Eye, X, CheckCircle2, CircleDashed } from "lucide-react"; import { Clock, Plus, Trash2, Pencil, Download, Eye, X, CheckCircle2, CircleDashed, RotateCcw } from "lucide-react";
import { import {
formatCurrency, formatCurrency,
formatFechaRegistro, formatFechaRegistro,
@@ -230,9 +230,10 @@ export function RegistroHorasPanel({
} }
}; };
// Alterna el estado de cobro de una partida (por_pagar <-> pagada). Optimista. // Cambia el estado de cobro de una partida a un valor explícito. Optimista.
const handleToggleEstado = async (r: Registro) => { // Al regresar a "por_pagar" el backend limpia la fechaPago (deja de contar como cobrada).
const nuevo = esPagada(r.estadoPago) ? "por_pagar" : "pagada"; const cambiarEstado = async (r: Registro, nuevo: "por_pagar" | "pagada") => {
if (esPagada(r.estadoPago) === (nuevo === "pagada")) return; // ya está en ese estado
const prev = registros; const prev = registros;
setTogglingId(r.id); setTogglingId(r.id);
setRegistros((rs) => rs.map((x) => (x.id === r.id ? { ...x, estadoPago: nuevo } : x))); setRegistros((rs) => rs.map((x) => (x.id === r.id ? { ...x, estadoPago: nuevo } : x)));
@@ -252,6 +253,13 @@ export function RegistroHorasPanel({
} }
}; };
// Regresa una partida pagada por error a "Por pagar". Pide confirmación porque se
// pierde la marca/fecha de pago y la partida vuelve a entrar en la nota por cobrar.
const regresarPorPagar = (r: Registro) => {
if (!confirm('¿Regresar esta partida a "Por pagar"? Se quitará la marca de pagada.')) return;
cambiarEstado(r, "por_pagar");
};
// Congela el HTML de la nota (con branding) y abre el modal de previsualización. // Congela el HTML de la nota (con branding) y abre el modal de previsualización.
const abrirPreview = () => setPreviewHtml(construirHTMLNota()); const abrirPreview = () => setPreviewHtml(construirHTMLNota());
@@ -658,17 +666,27 @@ export function RegistroHorasPanel({
<td className="py-2 pr-3 text-right">{formatCurrency(r.tarifaHora)}</td> <td className="py-2 pr-3 text-right">{formatCurrency(r.tarifaHora)}</td>
<td className="py-2 pr-3 text-right">{formatCurrency(r.horas * r.tarifaHora)}</td> <td className="py-2 pr-3 text-right">{formatCurrency(r.horas * r.tarifaHora)}</td>
<td className="py-2 pr-3">{r.descripcion}</td> <td className="py-2 pr-3">{r.descripcion}</td>
<td className="py-2 pr-3 whitespace-nowrap"> <td className="py-2 pr-3 whitespace-nowrap">{badgeEstado(r.estadoPago)}</td>
<button
onClick={() => handleToggleEstado(r)}
disabled={togglingId === r.id}
title={esPagada(r.estadoPago) ? "Marcar como por pagar" : "Marcar como pagada"}
className="disabled:opacity-50"
>
{badgeEstado(r.estadoPago)}
</button>
</td>
<td className="py-2 text-right whitespace-nowrap"> <td className="py-2 text-right whitespace-nowrap">
{esPagada(r.estadoPago) ? (
<button
onClick={() => regresarPorPagar(r)}
disabled={togglingId === r.id}
className="p-1 text-muted hover:text-amber-600 disabled:opacity-50"
title="Regresar a Por pagar"
>
<RotateCcw className="w-4 h-4" />
</button>
) : (
<button
onClick={() => cambiarEstado(r, "pagada")}
disabled={togglingId === r.id}
className="p-1 text-muted hover:text-green-600 disabled:opacity-50"
title="Marcar como pagada"
>
<CheckCircle2 className="w-4 h-4" />
</button>
)}
<button <button
onClick={() => handleEdit(r)} onClick={() => handleEdit(r)}
className="p-1 text-muted hover:text-primary" className="p-1 text-muted hover:text-primary"