feat(#37): módulo de cartão de crédito com faturas mensais

- Migration 011: closing_day/due_day em accounts + tabela credit_bills
- CreditBillService: cria/atualiza fatura corrente on-demand no GET /accounts
- Widget FATURA ATUAL no dashboard com total e botão PAGAR
- AccountsView: campos closing_day/due_day para contas credit + painel fatura
- Dashboard: current_bills lista faturas não pagas de todos os cartões

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-27 15:12:53 -03:00
co-authored by Claude Sonnet 4.6
parent ee6cd9f37e
commit e3b3433703
17 changed files with 530 additions and 29 deletions
+59 -2
View File
@@ -13,7 +13,7 @@ const typeLabels: Record<string, string> = {
credit: 'Cartão de Crédito',
}
const blank = (): AccountInput => ({ name: '', type: 'checking', initial_balance: 0, yield_type: 'none' })
const blank = (): AccountInput => ({ name: '', type: 'checking', initial_balance: 0, yield_type: 'none', closing_day: null, due_day: null })
const form = ref(blank())
const editId = ref<number | null>(null)
const initialRaw = ref('')
@@ -27,11 +27,21 @@ function startEdit(id: number) {
const a = store.accounts.find((x) => x.id === id)
if (!a) return
editId.value = id
form.value = { name: a.name, type: a.type, initial_balance: a.initial_balance, yield_type: a.yield_type ?? 'none' }
form.value = {
name: a.name, type: a.type, initial_balance: a.initial_balance,
yield_type: a.yield_type ?? 'none',
closing_day: a.closing_day ?? null,
due_day: a.due_day ?? null,
}
initialRaw.value = a.initial_balance.toLocaleString('pt-BR', { minimumFractionDigits: 2 })
formError.value = null
}
async function payBill(billId: number) {
if (!confirm('Marcar fatura como PAGA?')) return
await store.payBill(billId)
}
function cancelEdit() {
editId.value = null
form.value = blank()
@@ -90,6 +100,16 @@ function totalBalance() {
<option value="cdi">CDI automático</option>
<option value="variable">Renda variável</option>
</select>
<template v-if="form.type === 'credit'">
<div class="fc-label fc-acc-form__day-wrap">
Fechamento
<input type="number" v-model.number="form.closing_day" min="1" max="28" class="fc-input fc-acc-form__day" />
</div>
<div class="fc-label fc-acc-form__day-wrap">
Vencimento
<input type="number" v-model.number="form.due_day" min="1" max="28" class="fc-input fc-acc-form__day" />
</div>
</template>
<input
v-model="initialRaw"
placeholder="Saldo inicial (ex: 1.500,00)"
@@ -147,6 +167,32 @@ function totalBalance() {
<li v-if="store.accounts.length === 0" class="fc-acc-empty fc-mono"> nenhuma conta cadastrada </li>
</ul>
</NeonPanel>
<!-- Current bills for credit accounts -->
<template v-for="a in store.accounts.filter(x => x.type === 'credit' && x.current_bill)" :key="'bill-' + a.id">
<NeonPanel :title="`FATURA · ${a.name}`" :variant="a.current_bill!.paid ? undefined : 'danger'">
<div class="fc-bill">
<div class="fc-bill__row">
<span class="fc-mono fc-bill__label">Período</span>
<span class="fc-mono fc-bill__val">{{ a.current_bill!.period_start }} {{ a.current_bill!.period_end }}</span>
</div>
<div class="fc-bill__row">
<span class="fc-mono fc-bill__label">Vencimento</span>
<span class="fc-mono fc-bill__val fc-blink" style="color:var(--fc-red)">{{ a.current_bill!.due_date }}</span>
</div>
<div class="fc-bill__row">
<span class="fc-pixel fc-bill__label" style="font-size:8px">TOTAL FATURA</span>
<span class="fc-mono fc-bill__total" style="color:var(--fc-red); font-size:20px; font-weight:700">
{{ fmt(a.current_bill!.total) }}
</span>
</div>
<div v-if="!a.current_bill!.paid" class="fc-bill__actions">
<button class="fc-btn fc-btn--primary" @click="payBill(a.current_bill!.id)">MARCAR PAGA</button>
</div>
<div v-else class="fc-bill__paid fc-pixel">FATURA PAGA </div>
</div>
</NeonPanel>
</template>
</div>
</template>
@@ -257,6 +303,17 @@ function totalBalance() {
.fc-acc-badge--cdi { background: rgba(34,197,94,.2); color: var(--fc-green); border: 1px solid var(--fc-green); }
.fc-acc-badge--var { background: rgba(251,191,36,.2); color: var(--fc-gold); border: 1px solid var(--fc-gold); }
.fc-acc-form__day-wrap { width: 90px; flex-shrink: 0; }
.fc-acc-form__day { width: 100%; }
/* Credit bill widget */
.fc-bill { display: flex; flex-direction: column; gap: var(--fc-space-3); }
.fc-bill__row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--fc-space-2); }
.fc-bill__label { font-size: 10px; color: var(--fc-text-dim); }
.fc-bill__val { font-size: 12px; }
.fc-bill__actions { margin-top: var(--fc-space-2); }
.fc-bill__paid { font-size: 8px; color: var(--fc-green); margin-top: var(--fc-space-2); }
.fc-acc-item__right {
display: flex;
align-items: center;