From 2b8524dcdecd252d670afe5004f3018818ceb868 Mon Sep 17 00:00:00 2001 From: Mlcarvalho1 Date: Thu, 28 May 2026 22:26:42 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20#44=20fatura=20de=20cart=C3=A3o=20com?= =?UTF-8?q?=20data=20futura=20fica=20pendente=20at=C3=A9=20confirma=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/api/cmd/server/main.go | 11 ++- apps/api/internal/handler/import.go | 34 +++++-- apps/api/internal/handler/pending_bill.go | 59 ++++++++++++ apps/api/internal/migration/migration.go | 5 +- .../sql/016_pending_bill_imports.sql | 9 ++ apps/api/internal/model/dashboard.go | 23 ++--- apps/api/internal/model/pending_bill.go | 10 ++ apps/api/internal/repository/pending_bill.go | 93 +++++++++++++++++++ apps/api/internal/service/dashboard.go | 19 ++-- apps/api/internal/service/dashboard_test.go | 31 ++++++- apps/api/internal/service/pending_bill.go | 73 +++++++++++++++ apps/web/src/stores/dashboard.ts | 9 ++ apps/web/src/stores/import.ts | 6 +- apps/web/src/stores/pending_bills.ts | 30 ++++++ apps/web/src/views/AccountsView.vue | 49 +++++++++- apps/web/src/views/ImportView.vue | 24 ++++- 16 files changed, 453 insertions(+), 32 deletions(-) create mode 100644 apps/api/internal/handler/pending_bill.go create mode 100644 apps/api/internal/migration/sql/016_pending_bill_imports.sql create mode 100644 apps/api/internal/model/pending_bill.go create mode 100644 apps/api/internal/repository/pending_bill.go create mode 100644 apps/api/internal/service/pending_bill.go create mode 100644 apps/web/src/stores/pending_bills.ts diff --git a/apps/api/cmd/server/main.go b/apps/api/cmd/server/main.go index bbe0932..0c62810 100644 --- a/apps/api/cmd/server/main.go +++ b/apps/api/cmd/server/main.go @@ -65,7 +65,10 @@ func main() { transactionRepo := repository.NewTransactionRepository(pool) importSvc := service.NewImportService(transactionRepo) - importHandler := handler.NewImportHandler(importSvc, gameSvc) + pendingBillRepo := repository.NewPendingBillRepository(pool) + pendingBillSvc := service.NewPendingBillService(pendingBillRepo, transactionRepo) + importHandler := handler.NewImportHandler(importSvc, gameSvc, pendingBillSvc) + pendingBillHandler := handler.NewPendingBillHandler(pendingBillSvc, gameSvc) manualTxRepo := repository.NewManualTransactionRepository(pool) txSvc := service.NewTransactionService(manualTxRepo) @@ -84,7 +87,7 @@ func main() { creditBillHandler := handler.NewCreditBillHandler(creditBillSvc) dashboardRepo := repository.NewDashboardRepository(pool) - dashboardSvc := service.NewDashboardService(dashboardRepo, recurringSvc, accountRepo, creditBillSvc) + dashboardSvc := service.NewDashboardService(dashboardRepo, recurringSvc, accountRepo, creditBillSvc, pendingBillSvc) dashboardHandler := handler.NewDashboardHandler(dashboardSvc) r.Get("/health", handler.Health) @@ -108,6 +111,10 @@ func main() { r.Post("/imports/preview", importHandler.Preview) r.Post("/imports/confirm", importHandler.Confirm) + r.Get("/pending-bills", pendingBillHandler.List) + r.Post("/pending-bills/{id}/confirm", pendingBillHandler.Confirm) + r.Delete("/pending-bills/{id}", pendingBillHandler.Discard) + r.Get("/transactions", txHandler.List) r.Post("/transactions", txHandler.Create) r.Put("/transactions/{id}", txHandler.Update) diff --git a/apps/api/internal/handler/import.go b/apps/api/internal/handler/import.go index acffc5c..e17ef2f 100644 --- a/apps/api/internal/handler/import.go +++ b/apps/api/internal/handler/import.go @@ -11,12 +11,13 @@ import ( const maxUploadSize = 10 << 20 // 10 MB type ImportHandler struct { - svc *service.ImportService - gameSvc *service.GameService + svc *service.ImportService + gameSvc *service.GameService + pendingSvc *service.PendingBillService } -func NewImportHandler(svc *service.ImportService, gameSvc *service.GameService) *ImportHandler { - return &ImportHandler{svc: svc, gameSvc: gameSvc} +func NewImportHandler(svc *service.ImportService, gameSvc *service.GameService, pendingSvc *service.PendingBillService) *ImportHandler { + return &ImportHandler{svc: svc, gameSvc: gameSvc, pendingSvc: pendingSvc} } // Preview parses the uploaded file and returns rows with duplicate flags. @@ -59,11 +60,14 @@ func (h *ImportHandler) Preview(w http.ResponseWriter, r *http.Request) { // Confirm saves the rows provided in the request body (after user review). // POST /api/imports/confirm +// When is_credit_card=true and payment_date is in the future, saves as pending instead of transactions. func (h *ImportHandler) Confirm(w http.ResponseWriter, r *http.Request) { var body struct { - Filename string `json:"filename"` + Filename string `json:"filename"` Rows []model.ImportRow `json:"rows"` - ParseErrCount int `json:"parse_error_count"` + ParseErrCount int `json:"parse_error_count"` + IsCreditCard bool `json:"is_credit_card"` + PaymentDate string `json:"payment_date"` // YYYY-MM-DD } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { respondError(w, http.StatusBadRequest, "invalid JSON") @@ -74,6 +78,24 @@ func (h *ImportHandler) Confirm(w http.ResponseWriter, r *http.Request) { return } + if body.IsCreditCard && body.PaymentDate != "" { + saved, pending, err := h.pendingSvc.MaybeSaveAsPending(r.Context(), body.Filename, body.PaymentDate, body.Rows) + if err != nil { + respondError(w, http.StatusInternalServerError, "failed to save pending bill") + return + } + if saved { + respondJSON(w, http.StatusOK, map[string]any{ + "pending": true, + "pending_bill": pending, + "imported": 0, + "duplicates": len(body.Rows) - len(pending.Rows), + "errors": body.ParseErrCount, + }) + return + } + } + result, err := h.svc.Confirm(r.Context(), body.Filename, body.Rows, body.ParseErrCount) if err != nil { respondError(w, http.StatusInternalServerError, "failed to save transactions") diff --git a/apps/api/internal/handler/pending_bill.go b/apps/api/internal/handler/pending_bill.go new file mode 100644 index 0000000..c43dc1d --- /dev/null +++ b/apps/api/internal/handler/pending_bill.go @@ -0,0 +1,59 @@ +package handler + +import ( + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + + "financeiro-carvalho/internal/service" +) + +type PendingBillHandler struct { + svc *service.PendingBillService + gameSvc *service.GameService +} + +func NewPendingBillHandler(svc *service.PendingBillService, gameSvc *service.GameService) *PendingBillHandler { + return &PendingBillHandler{svc: svc, gameSvc: gameSvc} +} + +func (h *PendingBillHandler) List(w http.ResponseWriter, r *http.Request) { + items, err := h.svc.List(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "failed to list pending bills") + return + } + respondJSON(w, http.StatusOK, items) +} + +func (h *PendingBillHandler) Confirm(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid id") + return + } + if err := h.svc.Confirm(r.Context(), id); err != nil { + if err == service.ErrPendingBillNotFound { + respondError(w, http.StatusNotFound, "pending bill not found") + return + } + respondError(w, http.StatusInternalServerError, "failed to confirm pending bill") + return + } + h.gameSvc.NotifyAction(r.Context(), "import_confirmed", map[string]any{"count": 1}) + w.WriteHeader(http.StatusNoContent) +} + +func (h *PendingBillHandler) Discard(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid id") + return + } + if err := h.svc.Discard(r.Context(), id); err != nil { + respondError(w, http.StatusInternalServerError, "failed to discard pending bill") + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/apps/api/internal/migration/migration.go b/apps/api/internal/migration/migration.go index 019a39b..708fbc4 100644 --- a/apps/api/internal/migration/migration.go +++ b/apps/api/internal/migration/migration.go @@ -53,6 +53,9 @@ var m014 string //go:embed sql/015_accounts_cdi_percentage.sql var m015 string +//go:embed sql/016_pending_bill_imports.sql +var m016 string + func Run(ctx context.Context, pool *pgxpool.Pool) error { // Bootstrap: ensure schema_migrations table exists before checking versions. if _, err := pool.Exec(ctx, ` @@ -64,7 +67,7 @@ func Run(ctx context.Context, pool *pgxpool.Pool) error { return fmt.Errorf("bootstrap schema_migrations: %w", err) } - migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015} + migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015, m016} for i, sql := range migrations { version := i + 1 var applied bool diff --git a/apps/api/internal/migration/sql/016_pending_bill_imports.sql b/apps/api/internal/migration/sql/016_pending_bill_imports.sql new file mode 100644 index 0000000..7af07d4 --- /dev/null +++ b/apps/api/internal/migration/sql/016_pending_bill_imports.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS pending_bill_imports ( + id SERIAL PRIMARY KEY, + profile_id INTEGER NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + payment_date DATE NOT NULL, + total NUMERIC(12,2) NOT NULL DEFAULT 0, + rows JSONB NOT NULL DEFAULT '[]', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/apps/api/internal/model/dashboard.go b/apps/api/internal/model/dashboard.go index 429b097..3e790a7 100644 --- a/apps/api/internal/model/dashboard.go +++ b/apps/api/internal/model/dashboard.go @@ -24,15 +24,16 @@ type RecentTransaction struct { } type DashboardData struct { - Month string `json:"month"` - TotalIncome float64 `json:"total_income"` - TotalExpenses float64 `json:"total_expenses"` - SavingsPct float64 `json:"savings_pct"` - TotalPatrimony float64 `json:"total_patrimony"` - ByCategory []CategoryTotal `json:"by_category"` - MonthlyEvolution []MonthEvolution `json:"monthly_evolution"` - RecentTransactions []RecentTransaction `json:"recent_transactions"` - PendingRecurring int `json:"pending_recurring"` - PendingIncomeRecurrings []PendingIncome `json:"pending_income_recurrings"` - CurrentBills []CreditBill `json:"current_bills"` + Month string `json:"month"` + TotalIncome float64 `json:"total_income"` + TotalExpenses float64 `json:"total_expenses"` + SavingsPct float64 `json:"savings_pct"` + TotalPatrimony float64 `json:"total_patrimony"` + ByCategory []CategoryTotal `json:"by_category"` + MonthlyEvolution []MonthEvolution `json:"monthly_evolution"` + RecentTransactions []RecentTransaction `json:"recent_transactions"` + PendingRecurring int `json:"pending_recurring"` + PendingIncomeRecurrings []PendingIncome `json:"pending_income_recurrings"` + CurrentBills []CreditBill `json:"current_bills"` + PendingBillImports []PendingBillImport `json:"pending_bill_imports"` } diff --git a/apps/api/internal/model/pending_bill.go b/apps/api/internal/model/pending_bill.go new file mode 100644 index 0000000..9f73c8f --- /dev/null +++ b/apps/api/internal/model/pending_bill.go @@ -0,0 +1,10 @@ +package model + +type PendingBillImport struct { + ID int `json:"id"` + Filename string `json:"filename"` + PaymentDate string `json:"payment_date"` + Total float64 `json:"total"` + Rows []ImportRow `json:"rows"` + CreatedAt string `json:"created_at"` +} diff --git a/apps/api/internal/repository/pending_bill.go b/apps/api/internal/repository/pending_bill.go new file mode 100644 index 0000000..065f77c --- /dev/null +++ b/apps/api/internal/repository/pending_bill.go @@ -0,0 +1,93 @@ +package repository + +import ( + "context" + "encoding/json" + + "github.com/jackc/pgx/v5/pgxpool" + + "financeiro-carvalho/internal/middleware" + "financeiro-carvalho/internal/model" +) + +type PendingBillRepository interface { + List(ctx context.Context) ([]model.PendingBillImport, error) + Create(ctx context.Context, filename, paymentDate string, total float64, rows []model.ImportRow) (*model.PendingBillImport, error) + GetByID(ctx context.Context, id int) (*model.PendingBillImport, error) + Delete(ctx context.Context, id int) error +} + +type pendingBillRepo struct{ pool *pgxpool.Pool } + +func NewPendingBillRepository(pool *pgxpool.Pool) PendingBillRepository { + return &pendingBillRepo{pool: pool} +} + +func (r *pendingBillRepo) List(ctx context.Context) ([]model.PendingBillImport, error) { + pid := middleware.ProfileIDFromCtx(ctx) + rows, err := r.pool.Query(ctx, ` + SELECT id, filename, payment_date::text, total, rows, created_at::text + FROM pending_bill_imports + WHERE profile_id = $1 + ORDER BY payment_date + `, pid) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []model.PendingBillImport + for rows.Next() { + var p model.PendingBillImport + var rowsJSON []byte + if err := rows.Scan(&p.ID, &p.Filename, &p.PaymentDate, &p.Total, &rowsJSON, &p.CreatedAt); err != nil { + return nil, err + } + _ = json.Unmarshal(rowsJSON, &p.Rows) + out = append(out, p) + } + return out, rows.Err() +} + +func (r *pendingBillRepo) Create(ctx context.Context, filename, paymentDate string, total float64, rows []model.ImportRow) (*model.PendingBillImport, error) { + pid := middleware.ProfileIDFromCtx(ctx) + rowsJSON, err := json.Marshal(rows) + if err != nil { + return nil, err + } + var p model.PendingBillImport + var rowsBack []byte + err = r.pool.QueryRow(ctx, ` + INSERT INTO pending_bill_imports (profile_id, filename, payment_date, total, rows) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, filename, payment_date::text, total, rows, created_at::text + `, pid, filename, paymentDate, total, rowsJSON). + Scan(&p.ID, &p.Filename, &p.PaymentDate, &p.Total, &rowsBack, &p.CreatedAt) + if err != nil { + return nil, err + } + _ = json.Unmarshal(rowsBack, &p.Rows) + return &p, nil +} + +func (r *pendingBillRepo) GetByID(ctx context.Context, id int) (*model.PendingBillImport, error) { + pid := middleware.ProfileIDFromCtx(ctx) + var p model.PendingBillImport + var rowsJSON []byte + err := r.pool.QueryRow(ctx, ` + SELECT id, filename, payment_date::text, total, rows, created_at::text + FROM pending_bill_imports + WHERE id = $1 AND profile_id = $2 + `, id, pid).Scan(&p.ID, &p.Filename, &p.PaymentDate, &p.Total, &rowsJSON, &p.CreatedAt) + if err != nil { + return nil, err + } + _ = json.Unmarshal(rowsJSON, &p.Rows) + return &p, nil +} + +func (r *pendingBillRepo) Delete(ctx context.Context, id int) error { + pid := middleware.ProfileIDFromCtx(ctx) + _, err := r.pool.Exec(ctx, `DELETE FROM pending_bill_imports WHERE id = $1 AND profile_id = $2`, id, pid) + return err +} diff --git a/apps/api/internal/service/dashboard.go b/apps/api/internal/service/dashboard.go index b0800ad..493eb7b 100644 --- a/apps/api/internal/service/dashboard.go +++ b/apps/api/internal/service/dashboard.go @@ -19,14 +19,15 @@ type PatrimonySource interface { } type DashboardService struct { - repo DashboardRepo - recurrSvc *RecurringService - patrimony PatrimonySource - billSvc *CreditBillService + repo DashboardRepo + recurrSvc *RecurringService + patrimony PatrimonySource + billSvc *CreditBillService + pendingBillSvc *PendingBillService } -func NewDashboardService(repo DashboardRepo, recurrSvc *RecurringService, patrimony PatrimonySource, billSvc *CreditBillService) *DashboardService { - return &DashboardService{repo: repo, recurrSvc: recurrSvc, patrimony: patrimony, billSvc: billSvc} +func NewDashboardService(repo DashboardRepo, recurrSvc *RecurringService, patrimony PatrimonySource, billSvc *CreditBillService, pendingBillSvc *PendingBillService) *DashboardService { + return &DashboardService{repo: repo, recurrSvc: recurrSvc, patrimony: patrimony, billSvc: billSvc, pendingBillSvc: pendingBillSvc} } func (s *DashboardService) Get(ctx context.Context, month string) (*model.DashboardData, error) { @@ -109,6 +110,11 @@ func (s *DashboardService) Get(ctx context.Context, month string) (*model.Dashbo currentBills = []model.CreditBill{} } + pendingBillImports, _ := s.pendingBillSvc.List(ctx) + if pendingBillImports == nil { + pendingBillImports = []model.PendingBillImport{} + } + return &model.DashboardData{ Month: month, TotalIncome: income, @@ -121,5 +127,6 @@ func (s *DashboardService) Get(ctx context.Context, month string) (*model.Dashbo PendingRecurring: pending, PendingIncomeRecurrings: pendingIncome, CurrentBills: currentBills, + PendingBillImports: pendingBillImports, }, nil } diff --git a/apps/api/internal/service/dashboard_test.go b/apps/api/internal/service/dashboard_test.go index 5689934..de6eb28 100644 --- a/apps/api/internal/service/dashboard_test.go +++ b/apps/api/internal/service/dashboard_test.go @@ -8,6 +8,21 @@ import ( "financeiro-carvalho/internal/service" ) +type mockImportTxRepo struct{} + +func (m *mockImportTxRepo) IsDuplicate(_ context.Context, _, _ string, _ float64) (bool, error) { + return false, nil +} +func (m *mockImportTxRepo) IsExternalIDKnown(_ context.Context, _ string) (bool, error) { + return false, nil +} +func (m *mockImportTxRepo) BulkInsert(_ context.Context, rows []model.ImportRow) (int, error) { + return len(rows), nil +} +func (m *mockImportTxRepo) SaveImportLog(_ context.Context, _, _ string, _, _, _ int) error { + return nil +} + type mockDashboardRepo struct { income float64 expenses float64 @@ -33,11 +48,25 @@ type mockPatrimony struct{} func (m *mockPatrimony) TotalPatrimony(_ context.Context) (float64, error) { return 0, nil } +type mockPendingBillRepo struct{} + +func (m *mockPendingBillRepo) List(_ context.Context) ([]model.PendingBillImport, error) { + return nil, nil +} +func (m *mockPendingBillRepo) Create(_ context.Context, _, _ string, _ float64, _ []model.ImportRow) (*model.PendingBillImport, error) { + return &model.PendingBillImport{}, nil +} +func (m *mockPendingBillRepo) GetByID(_ context.Context, _ int) (*model.PendingBillImport, error) { + return nil, nil +} +func (m *mockPendingBillRepo) Delete(_ context.Context, _ int) error { return nil } + func newDashboardSvc(income, expenses float64) *service.DashboardService { repo := &mockAccountRepo{} recurrSvc := service.NewRecurringService(newMockRecurring(nil), &mockTxRepo{}) billSvc := service.NewCreditBillService(&mockCreditBillRepo{}, repo) - return service.NewDashboardService(&mockDashboardRepo{income: income, expenses: expenses}, recurrSvc, &mockPatrimony{}, billSvc) + pendingBillSvc := service.NewPendingBillService(&mockPendingBillRepo{}, &mockImportTxRepo{}) + return service.NewDashboardService(&mockDashboardRepo{income: income, expenses: expenses}, recurrSvc, &mockPatrimony{}, billSvc, pendingBillSvc) } func TestDashboard_SavingsPct_40(t *testing.T) { diff --git a/apps/api/internal/service/pending_bill.go b/apps/api/internal/service/pending_bill.go new file mode 100644 index 0000000..8962335 --- /dev/null +++ b/apps/api/internal/service/pending_bill.go @@ -0,0 +1,73 @@ +package service + +import ( + "context" + "errors" + "time" + + "financeiro-carvalho/internal/model" + "financeiro-carvalho/internal/repository" +) + +var ErrPendingBillNotFound = errors.New("pending bill not found") + +type PendingBillService struct { + repo repository.PendingBillRepository + txRepo repository.TransactionRepository +} + +func NewPendingBillService(repo repository.PendingBillRepository, txRepo repository.TransactionRepository) *PendingBillService { + return &PendingBillService{repo: repo, txRepo: txRepo} +} + +func (s *PendingBillService) List(ctx context.Context) ([]model.PendingBillImport, error) { + items, err := s.repo.List(ctx) + if items == nil { + return []model.PendingBillImport{}, err + } + return items, err +} + +// Save stores a credit-card import as pending when payment_date is in the future. +// Returns (true, pendingBill, nil) if saved as pending; (false, nil, nil) if not applicable. +func (s *PendingBillService) MaybeSaveAsPending(ctx context.Context, filename, paymentDate string, rows []model.ImportRow) (bool, *model.PendingBillImport, error) { + if paymentDate == "" { + return false, nil, nil + } + t, err := time.Parse("2006-01-02", paymentDate) + if err != nil || !t.After(time.Now().Truncate(24*time.Hour)) { + return false, nil, nil + } + + var total float64 + newRows := make([]model.ImportRow, 0, len(rows)) + for _, r := range rows { + if !r.IsDuplicate { + total += r.Amount + newRows = append(newRows, r) + } + } + + p, err := s.repo.Create(ctx, filename, paymentDate, total, newRows) + if err != nil { + return true, nil, err + } + return true, p, nil +} + +// Confirm inserts the pending bill's rows as actual transactions and removes the pending record. +func (s *PendingBillService) Confirm(ctx context.Context, id int) error { + p, err := s.repo.GetByID(ctx, id) + if err != nil { + return ErrPendingBillNotFound + } + if _, err := s.txRepo.BulkInsert(ctx, p.Rows); err != nil { + return err + } + return s.repo.Delete(ctx, id) +} + +// Discard removes a pending bill import without creating transactions. +func (s *PendingBillService) Discard(ctx context.Context, id int) error { + return s.repo.Delete(ctx, id) +} diff --git a/apps/web/src/stores/dashboard.ts b/apps/web/src/stores/dashboard.ts index f0e562e..fd92501 100644 --- a/apps/web/src/stores/dashboard.ts +++ b/apps/web/src/stores/dashboard.ts @@ -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', () => { diff --git a/apps/web/src/stores/import.ts b/apps/web/src/stores/import.ts index 8f752e6..0749541 100644 --- a/apps/web/src/stores/import.ts +++ b/apps/web/src/stores/import.ts @@ -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() diff --git a/apps/web/src/stores/pending_bills.ts b/apps/web/src/stores/pending_bills.ts new file mode 100644 index 0000000..8952ab1 --- /dev/null +++ b/apps/web/src/stores/pending_bills.ts @@ -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([]) + const loading = ref(false) + + async function fetchAll() { + loading.value = true + try { + items.value = await api.get('/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 } +}) diff --git a/apps/web/src/views/AccountsView.vue b/apps/web/src/views/AccountsView.vue index 483e894..a6923aa 100644 --- a/apps/web/src/views/AccountsView.vue +++ b/apps/web/src/views/AccountsView.vue @@ -1,11 +1,13 @@