feat: excluir transações de extrato + delete em lote por mês
Remove restrição que bloqueava delete de transações importadas. Adiciona DELETE /transactions?month=YYYY-MM para exclusão em lote e botão "EXCLUIR MÊS" na view de transações. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -101,6 +101,7 @@ func main() {
|
|||||||
r.Post("/transactions", txHandler.Create)
|
r.Post("/transactions", txHandler.Create)
|
||||||
r.Put("/transactions/{id}", txHandler.Update)
|
r.Put("/transactions/{id}", txHandler.Update)
|
||||||
r.Delete("/transactions/{id}", txHandler.Delete)
|
r.Delete("/transactions/{id}", txHandler.Delete)
|
||||||
|
r.Delete("/transactions", txHandler.DeleteByMonth)
|
||||||
|
|
||||||
r.Get("/recurring", recurringHandler.List)
|
r.Get("/recurring", recurringHandler.List)
|
||||||
r.Post("/recurring", recurringHandler.Create)
|
r.Post("/recurring", recurringHandler.Create)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (h *TransactionHandler) Update(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
t.ID = id
|
t.ID = id
|
||||||
out, err := h.svc.Update(r.Context(), t)
|
out, err := h.svc.Update(r.Context(), t)
|
||||||
if errors.Is(err, repository.ErrNotFound) || errors.Is(err, service.ErrCannotEditImported) {
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
respondError(w, http.StatusNotFound, err.Error())
|
respondError(w, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,3 +83,17 @@ func (h *TransactionHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *TransactionHandler) DeleteByMonth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
month := r.URL.Query().Get("month")
|
||||||
|
if len(month) != 7 {
|
||||||
|
respondError(w, http.StatusBadRequest, "month required (YYYY-MM)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count, err := h.svc.DeleteByMonth(r.Context(), month)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, map[string]int{"deleted": count})
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type ManualTransactionRepository interface {
|
|||||||
Create(ctx context.Context, t model.Transaction) (*model.Transaction, error)
|
Create(ctx context.Context, t model.Transaction) (*model.Transaction, error)
|
||||||
Update(ctx context.Context, t model.Transaction) (*model.Transaction, error)
|
Update(ctx context.Context, t model.Transaction) (*model.Transaction, error)
|
||||||
Delete(ctx context.Context, id int) error
|
Delete(ctx context.Context, id int) error
|
||||||
|
DeleteByMonth(ctx context.Context, month string) (int, error)
|
||||||
// HasMatchingTransaction checks if a category has a transaction of the given type in the given month.
|
// HasMatchingTransaction checks if a category has a transaction of the given type in the given month.
|
||||||
HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64, txType string) (bool, error)
|
HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64, txType string) (bool, error)
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ func (r *manualTxRepo) Update(ctx context.Context, t model.Transaction) (*model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *manualTxRepo) Delete(ctx context.Context, id int) error {
|
func (r *manualTxRepo) Delete(ctx context.Context, id int) error {
|
||||||
tag, err := r.db.Exec(ctx, `DELETE FROM transactions WHERE id = $1 AND source = 'manual'`, id)
|
tag, err := r.db.Exec(ctx, `DELETE FROM transactions WHERE id = $1`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -103,6 +104,14 @@ func (r *manualTxRepo) Delete(ctx context.Context, id int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *manualTxRepo) DeleteByMonth(ctx context.Context, month string) (int, error) {
|
||||||
|
tag, err := r.db.Exec(ctx, `DELETE FROM transactions WHERE TO_CHAR(date, 'YYYY-MM') = $1`, month)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(tag.RowsAffected()), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *manualTxRepo) HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64, txType string) (bool, error) {
|
func (r *manualTxRepo) HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64, txType string) (bool, error) {
|
||||||
if categoryID == nil {
|
if categoryID == nil {
|
||||||
// No category set — cannot auto-match, always report as uncovered
|
// No category set — cannot auto-match, always report as uncovered
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidTransactionType = errors.New("type must be 'income' or 'expense'")
|
ErrInvalidTransactionType = errors.New("type must be 'income' or 'expense'")
|
||||||
ErrCannotEditImported = errors.New("imported transactions cannot be edited or deleted")
|
|
||||||
ErrTransactionEmptyDesc = errors.New("description is required")
|
ErrTransactionEmptyDesc = errors.New("description is required")
|
||||||
ErrTransactionInvalidAmount = errors.New("amount must be greater than zero")
|
ErrTransactionInvalidAmount = errors.New("amount must be greater than zero")
|
||||||
)
|
)
|
||||||
@@ -55,11 +54,15 @@ func (s *TransactionService) Update(ctx context.Context, t model.Transaction) (*
|
|||||||
func (s *TransactionService) Delete(ctx context.Context, id int) error {
|
func (s *TransactionService) Delete(ctx context.Context, id int) error {
|
||||||
err := s.repo.Delete(ctx, id)
|
err := s.repo.Delete(ctx, id)
|
||||||
if errors.Is(err, repository.ErrNotFound) {
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
return ErrCannotEditImported
|
return errors.New("transaction not found")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TransactionService) DeleteByMonth(ctx context.Context, month string) (int, error) {
|
||||||
|
return s.repo.DeleteByMonth(ctx, month)
|
||||||
|
}
|
||||||
|
|
||||||
func validateTransaction(t model.Transaction) error {
|
func validateTransaction(t model.Transaction) error {
|
||||||
if strings.TrimSpace(t.Description) == "" {
|
if strings.TrimSpace(t.Description) == "" {
|
||||||
return ErrTransactionEmptyDesc
|
return ErrTransactionEmptyDesc
|
||||||
|
|||||||
@@ -60,5 +60,12 @@ export const useTransactionsStore = defineStore('transactions', () => {
|
|||||||
transactions.value = transactions.value.filter((x) => x.id !== id)
|
transactions.value = transactions.value.filter((x) => x.id !== id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { transactions, loading, error, fetchAll, create, update, remove }
|
async function removeByMonth(month: string) {
|
||||||
|
await api.delete(`/transactions?month=${month}`)
|
||||||
|
transactions.value = transactions.value.filter(
|
||||||
|
(x) => x.date.slice(0, 7) !== month,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { transactions, loading, error, fetchAll, create, update, remove, removeByMonth }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ async function remove(id: number) {
|
|||||||
try { await store.remove(id) } catch (e: any) { alert(e.message) }
|
try { await store.remove(id) } catch (e: any) { alert(e.message) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeMonth() {
|
||||||
|
const label = monthLabel(currentMonth.value)
|
||||||
|
if (!confirm(`Excluir TODAS as transações de ${label}?`)) return
|
||||||
|
try { await store.removeByMonth(currentMonth.value) } catch (e: any) { alert(e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
function fmt(v: number) {
|
function fmt(v: number) {
|
||||||
return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||||
}
|
}
|
||||||
@@ -98,6 +104,11 @@ function monthLabel(ym: string) {
|
|||||||
@prev="changeMonth(-1)"
|
@prev="changeMonth(-1)"
|
||||||
@next="changeMonth(1)"
|
@next="changeMonth(1)"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="store.transactions.length > 0"
|
||||||
|
class="fc-btn fc-btn--sm fc-btn--danger"
|
||||||
|
@click="removeMonth"
|
||||||
|
>EXCLUIR MÊS</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
|
|||||||
Reference in New Issue
Block a user