feat: #44 fatura de cartão com data futura fica pendente até confirmação
- Nova tabela pending_bill_imports (migration 016) - ImportHandler.Confirm: quando is_credit_card=true e payment_date > hoje, salva como pending em vez de inserir transações - Novos endpoints: GET /pending-bills, POST /pending-bills/:id/confirm, DELETE /pending-bills/:id - Dashboard inclui pending_bill_imports no payload - Frontend: resultado "fatura salva como pendente" no ImportView - AccountsView exibe widget de faturas pendentes com ações de confirmar/descartar - dashboard_test: mock de PendingBillRepo + TransactionRepository Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -34,6 +34,14 @@ export interface PendingIncome {
|
||||
late: boolean
|
||||
}
|
||||
|
||||
export interface PendingBillImport {
|
||||
id: number
|
||||
filename: string
|
||||
payment_date: string
|
||||
total: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
month: string
|
||||
total_income: number
|
||||
@@ -46,6 +54,7 @@ export interface DashboardData {
|
||||
pending_recurring: number
|
||||
pending_income_recurrings: PendingIncome[]
|
||||
current_bills: CreditBill[]
|
||||
pending_bill_imports: PendingBillImport[]
|
||||
}
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', () => {
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface ImportResult {
|
||||
imported: number
|
||||
duplicates: number
|
||||
errors: number
|
||||
pending?: boolean
|
||||
pending_bill?: { id: number; payment_date: string; total: number }
|
||||
}
|
||||
|
||||
export interface CSVMapping {
|
||||
@@ -63,7 +65,7 @@ export const useImportStore = defineStore('import', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
async function confirm(isCreditCard?: boolean, paymentDate?: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
@@ -74,6 +76,8 @@ export const useImportStore = defineStore('import', () => {
|
||||
filename: filename.value,
|
||||
rows: rows.value,
|
||||
parse_error_count: parseErrors.value.length,
|
||||
is_credit_card: isCreditCard ?? false,
|
||||
payment_date: paymentDate ?? '',
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/services/api'
|
||||
import type { PendingBillImport } from './dashboard'
|
||||
|
||||
export const usePendingBillsStore = defineStore('pending_bills', () => {
|
||||
const items = ref<PendingBillImport[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchAll() {
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = await api.get<PendingBillImport[]>('/pending-bills')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm(id: number) {
|
||||
await api.post(`/pending-bills/${id}/confirm`, {})
|
||||
items.value = items.value.filter((p) => p.id !== id)
|
||||
}
|
||||
|
||||
async function discard(id: number) {
|
||||
await api.delete(`/pending-bills/${id}`)
|
||||
items.value = items.value.filter((p) => p.id !== id)
|
||||
}
|
||||
|
||||
return { items, loading, fetchAll, confirm, discard }
|
||||
})
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAccountsStore, type AccountInput } from '@/stores/accounts'
|
||||
import { usePendingBillsStore } from '@/stores/pending_bills'
|
||||
import NeonPanel from '@/components/NeonPanel.vue'
|
||||
import CurrencyInput from '@/components/CurrencyInput.vue'
|
||||
|
||||
const store = useAccountsStore()
|
||||
onMounted(() => store.fetchAll())
|
||||
const pendingStore = usePendingBillsStore()
|
||||
onMounted(() => { store.fetchAll(); pendingStore.fetchAll() })
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
checking: 'Conta Corrente',
|
||||
@@ -173,6 +175,34 @@ function totalBalance() {
|
||||
</ul>
|
||||
</NeonPanel>
|
||||
|
||||
<!-- Pending bill imports -->
|
||||
<template v-if="pendingStore.items.length > 0">
|
||||
<NeonPanel title="FATURAS PENDENTES DE CONFIRMAÇÃO" variant="danger">
|
||||
<div class="fc-pending-list">
|
||||
<div
|
||||
v-for="p in pendingStore.items"
|
||||
:key="p.id"
|
||||
class="fc-pending-item"
|
||||
>
|
||||
<div class="fc-pending-item__info">
|
||||
<span class="fc-body fc-pending-item__name">{{ p.filename }}</span>
|
||||
<span class="fc-mono fc-pending-item__meta">
|
||||
Vencimento: {{ p.payment_date }} · {{ fmt(p.total) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="fc-pending-item__actions">
|
||||
<button class="fc-btn fc-btn--sm fc-btn--primary" @click="pendingStore.confirm(p.id)">
|
||||
CONFIRMAR PAGAMENTO
|
||||
</button>
|
||||
<button class="fc-btn fc-btn--sm fc-btn--ghost" @click="pendingStore.discard(p.id)">
|
||||
DESCARTAR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NeonPanel>
|
||||
</template>
|
||||
|
||||
<!-- 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'">
|
||||
@@ -327,4 +357,21 @@ function totalBalance() {
|
||||
|
||||
.fc-acc-item__balance { font-size: 16px; font-weight: 700; }
|
||||
.fc-acc-item__actions { display: flex; gap: var(--fc-space-1); }
|
||||
|
||||
/* Pending bill imports */
|
||||
.fc-pending-list { display: flex; flex-direction: column; gap: var(--fc-space-3); }
|
||||
.fc-pending-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--fc-space-3);
|
||||
padding: 10px var(--fc-space-3);
|
||||
border: 1px solid rgba(255,59,107,.3);
|
||||
border-radius: var(--fc-radius);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fc-pending-item__info { display: flex; flex-direction: column; gap: 4px; }
|
||||
.fc-pending-item__name { font-size: 13px; }
|
||||
.fc-pending-item__meta { font-size: 11px; color: var(--fc-text-dim); }
|
||||
.fc-pending-item__actions { display: flex; gap: var(--fc-space-2); }
|
||||
</style>
|
||||
|
||||
@@ -76,8 +76,26 @@ function fmt(amount: number) {
|
||||
<div class="fc-view">
|
||||
<span class="fc-pixel fc-view__title">:: IMPORTAR EXTRATO</span>
|
||||
|
||||
<!-- Result banner -->
|
||||
<NeonPanel v-if="store.result" variant="success" title="CONCLUÍDO">
|
||||
<!-- Result banner: pending bill -->
|
||||
<NeonPanel v-if="store.result?.pending" variant="success" title="FATURA SALVA COMO PENDENTE">
|
||||
<div class="fc-import-result">
|
||||
<span class="fc-mono fc-import-result__stat">
|
||||
Vencimento: <span class="fc-text-gold">{{ store.result.pending_bill?.payment_date }}</span>
|
||||
</span>
|
||||
<span class="fc-mono fc-import-result__stat fc-text-green">
|
||||
{{ fmt(store.result.pending_bill?.total ?? 0) }}
|
||||
</span>
|
||||
<span class="fc-mono fc-import-result__stat" style="color:var(--fc-text-dim);font-size:11px">
|
||||
Confirme o pagamento em Contas quando pagar a fatura
|
||||
</span>
|
||||
<button class="fc-btn fc-btn--ghost" @click="store.reset(); selectedFile = null">
|
||||
IMPORTAR OUTRO
|
||||
</button>
|
||||
</div>
|
||||
</NeonPanel>
|
||||
|
||||
<!-- Result banner: imported -->
|
||||
<NeonPanel v-else-if="store.result" variant="success" title="CONCLUÍDO">
|
||||
<div class="fc-import-result">
|
||||
<span class="fc-mono fc-import-result__stat">
|
||||
<span class="fc-text-green">{{ store.result.imported }}</span> importadas
|
||||
@@ -243,7 +261,7 @@ function fmt(amount: number) {
|
||||
<button
|
||||
class="fc-btn fc-btn--primary"
|
||||
:disabled="store.loading || newCount === 0"
|
||||
@click="store.confirm()"
|
||||
@click="store.confirm(csvMapping.all_expenses, csvMapping.all_expenses ? csvMapping.payment_date : '')"
|
||||
>
|
||||
{{ store.loading ? 'SALVANDO...' : `CONFIRMAR ${newCount} TRANSAÇÕES` }}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user