@@ -290,6 +290,18 @@ function dataRecorrencia(regra, n) {
290290const 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+
293305const 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+
20172135function 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