Skip to content

Commit a80f86b

Browse files
Thales-Chagasclaude
andcommitted
Contas: popup de detalhes ao tocar no logo da categoria
Toque no avatar da categoria (contas a pagar/receber) abre um popup com: descricao completa (sem truncar), categoria, tipo, vencimento com contagem de dias, e — para series recorrentes — repeticao, inicio e termino. Quando a serie tem data final, mostra "Parcela X de Y" e quantas faltam; series SEM termino nao exibem nada de parcelas nem "termina em". Botoes Editar e Paguei/Recebi direto no popup. Helper totalParcelas() conta as parcelas de uma serie com fim pela propria regra (laco limitado). infoParcelas() so retorna dados quando ha data final. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2b2ec3e commit a80f86b

1 file changed

Lines changed: 148 additions & 3 deletions

File tree

src/App.jsx

Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ function dataRecorrencia(regra, n) {
290290
const rotuloRecorrencia = (rec) =>
291291
rec.tipo === "dias" ? `A cada ${rec.cada} dias` : RECORRENCIAS[rec.tipo]?.rotulo || "Recorrente";
292292

293+
// Total de parcelas de uma série COM data final (null = sem término → não conta).
294+
function totalParcelas(regra) {
295+
if (!regra?.fim) return null;
296+
let n = 0;
297+
while (n < 3000) {
298+
const d = dataRecorrencia(regra, n);
299+
if (!d || d > regra.fim) break;
300+
n++;
301+
}
302+
return n || null;
303+
}
304+
293305
const menorData = (a, b) => (a && a < b ? a : b);
294306

295307
// Gera as ocorrências a partir da parcela `aPartirN`, copiando os campos do
@@ -2014,9 +2026,116 @@ function ModalExcluirConta({ transacao, onExcluir, onExcluirSerie, onFechar }) {
20142026
);
20152027
}
20162028

2029+
// Popup de detalhes de uma conta (abre ao tocar no logo da categoria):
2030+
// descrição completa, categoria, vencimento, dados da recorrência e — só
2031+
// quando a série tem data final — quantas parcelas faltam.
2032+
function ModalDetalheConta({ transacao, categoria, parcelas, onPagar, onEditar, onFechar }) {
2033+
const t = transacao;
2034+
const grad = categoria ? gradCat(categoria) : gradPorId("grafite");
2035+
const ehReceita = t.tipo === "receita";
2036+
const rec = t.recorrencia;
2037+
const nome = t.descricao || categoria?.nome || "Lançamento";
2038+
2039+
const hoje = hojeISO();
2040+
const dias = Math.round((new Date(t.data + "T12:00:00") - new Date(hoje + "T12:00:00")) / 86400000);
2041+
const vencTxt =
2042+
dias < 0 ? (dias === -1 ? "Venceu ontem" : `Venceu há ${-dias} dias`)
2043+
: dias === 0 ? "Vence hoje"
2044+
: `Faltam ${dias} dia${dias > 1 ? "s" : ""}`;
2045+
const vencCor = dias < 0 ? "text-red-500" : dias <= 7 ? "text-amber-500" : "text-slate-400";
2046+
2047+
const Info = ({ rotulo, children }) => (
2048+
<div className="flex items-start justify-between gap-3 py-2.5">
2049+
<span className="shrink-0 pt-0.5 text-xs font-medium uppercase tracking-wide text-slate-400">{rotulo}</span>
2050+
<span className="text-right text-sm font-medium text-slate-700 dark:text-slate-200">{children}</span>
2051+
</div>
2052+
);
2053+
2054+
return (
2055+
<Modal titulo="Detalhes da conta" onFechar={onFechar}>
2056+
<div className="space-y-4">
2057+
{/* cabeçalho: avatar grande + nome completo + valor */}
2058+
<div className="flex items-center gap-3">
2059+
<div
2060+
className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl text-xl font-bold text-white shadow-sm"
2061+
style={{ background: cssGrad(grad) }}
2062+
>
2063+
{(nome[0] || "?").toUpperCase()}
2064+
</div>
2065+
<div className="min-w-0">
2066+
<p className="break-words text-base font-bold leading-snug text-slate-800 dark:text-slate-100">
2067+
{nome}
2068+
</p>
2069+
<p
2070+
className={
2071+
"text-2xl font-bold tabular-nums " +
2072+
(ehReceita ? "text-emerald-600 dark:text-emerald-400" : "text-slate-800 dark:text-slate-100")
2073+
}
2074+
>
2075+
{fmtBRL(t.valor)}
2076+
</p>
2077+
</div>
2078+
</div>
2079+
2080+
<div className="divide-y divide-slate-100 dark:divide-slate-800">
2081+
<Info rotulo="Tipo">{ehReceita ? "A receber" : "A pagar"}</Info>
2082+
<Info rotulo="Categoria">
2083+
<span className="inline-flex items-center gap-1.5">
2084+
<span className="h-2.5 w-2.5 rounded-full" style={{ background: cssGrad(grad) }} />
2085+
{categoria?.nome || "—"}
2086+
</span>
2087+
</Info>
2088+
<Info rotulo="Vencimento">
2089+
<span className="flex flex-col items-end">
2090+
<span className="tabular-nums">{fmtData(t.data)}</span>
2091+
<span className={"text-xs font-semibold " + vencCor}>{vencTxt}</span>
2092+
</span>
2093+
</Info>
2094+
{rec && (
2095+
<Info rotulo="Repetição">
2096+
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400">
2097+
<Repeat size={11} /> {rotuloRecorrencia(rec)}
2098+
</span>
2099+
</Info>
2100+
)}
2101+
{rec?.inicio && <Info rotulo="Início da série"><span className="tabular-nums">{fmtData(rec.inicio)}</span></Info>}
2102+
{rec?.fim && <Info rotulo="Termina em"><span className="tabular-nums">{fmtData(rec.fim)}</span></Info>}
2103+
{/* parcelas: só quando a série tem data final (sem fim = nada aqui) */}
2104+
{parcelas && (
2105+
<Info rotulo="Parcelas">
2106+
<span className="flex flex-col items-end">
2107+
<span>Parcela {parcelas.posicao} de {parcelas.total}</span>
2108+
<span className="text-xs font-semibold text-emerald-600 dark:text-emerald-400">
2109+
{parcelas.faltam <= 1 ? "última parcela" : `faltam ${parcelas.faltam} parcelas`}
2110+
</span>
2111+
</span>
2112+
</Info>
2113+
)}
2114+
</div>
2115+
2116+
<div className="flex gap-2">
2117+
<button
2118+
onClick={() => { onEditar(); onFechar(); }}
2119+
className="flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-slate-200 py-2.5 text-sm font-medium text-slate-500 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800"
2120+
>
2121+
<Pencil size={15} /> Editar
2122+
</button>
2123+
<button
2124+
onClick={() => { onPagar(); onFechar(); }}
2125+
className="flex flex-1 items-center justify-center gap-1.5 rounded-xl bg-emerald-600 py-2.5 text-sm font-semibold text-white transition hover:bg-emerald-700"
2126+
>
2127+
<Check size={15} /> {ehReceita ? "Recebi" : "Paguei"}
2128+
</button>
2129+
</div>
2130+
</div>
2131+
</Modal>
2132+
);
2133+
}
2134+
20172135
function PaginaContas({ espaco, empresarial, acoes }) {
20182136
const [form, setForm] = useState(null); // null | {tipoNovo} | transacao p/ editar
20192137
const [excluindo, setExcluindo] = useState(null); // transacao aguardando confirmação
2138+
const [detalhe, setDetalhe] = useState(null); // transacao cujo popup de detalhes está aberto
20202139
const [filtro, setFiltro] = useState("todas");
20212140

20222141
const hoje = hojeISO();
@@ -2058,6 +2177,17 @@ function PaginaContas({ espaco, empresarial, acoes }) {
20582177
return { texto: "Vence em " + fmtData(t.data), cor: "text-slate-400" };
20592178
}
20602179

2180+
// Info de parcelas p/ o popup: só quando a série tem data final (senão null).
2181+
// posição = qual parcela é esta; faltam = quantas restam a partir desta.
2182+
function infoParcelas(t) {
2183+
const rec = t.recorrencia;
2184+
if (!rec?.fim) return null;
2185+
const total = totalParcelas(rec);
2186+
if (!total) return null;
2187+
const n = rec.n ?? 0;
2188+
return { posicao: n + 1, total, faltam: Math.max(1, total - n) };
2189+
}
2190+
20612191
function Linha({ t }) {
20622192
const cat = catDe(t.categoriaId);
20632193
const grad = cat ? gradCat(cat) : gradPorId("grafite");
@@ -2093,12 +2223,16 @@ function PaginaContas({ espaco, empresarial, acoes }) {
20932223
);
20942224
return (
20952225
<div className="group flex items-start gap-2.5 py-2.5 sm:items-center sm:gap-3">
2096-
<div
2097-
className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-sm font-bold text-white shadow-sm sm:mt-0"
2226+
<button
2227+
type="button"
2228+
onClick={() => setDetalhe(t)}
2229+
title="Ver detalhes"
2230+
aria-label={"Ver detalhes de " + nome}
2231+
className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-sm font-bold text-white shadow-sm outline-none ring-emerald-300 transition hover:brightness-110 focus-visible:ring-2 active:scale-95 sm:mt-0 dark:ring-emerald-700"
20982232
style={{ background: cssGrad(grad) }}
20992233
>
21002234
{(nome[0] || "?").toUpperCase()}
2101-
</div>
2235+
</button>
21022236
<div className="min-w-0 flex-1">
21032237
{/* celular: nome + valor lado a lado; desktop: só o nome (valor na direita) */}
21042238
<div className="flex items-baseline justify-between gap-2">
@@ -2298,6 +2432,17 @@ function PaginaContas({ espaco, empresarial, acoes }) {
22982432
onFechar={() => setExcluindo(null)}
22992433
/>
23002434
)}
2435+
2436+
{detalhe && (
2437+
<ModalDetalheConta
2438+
transacao={detalhe}
2439+
categoria={catDe(detalhe.categoriaId)}
2440+
parcelas={infoParcelas(detalhe)}
2441+
onPagar={() => acoes.marcarOk(detalhe.id)}
2442+
onEditar={() => setForm(detalhe)}
2443+
onFechar={() => setDetalhe(null)}
2444+
/>
2445+
)}
23012446
</div>
23022447
);
23032448
}

0 commit comments

Comments
 (0)