From f5ff03260b27780b7d146f21d73611b8f25c8801 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Fri, 29 May 2026 18:56:14 -0700 Subject: [PATCH] feat(expense): add monthly reconciliation statement page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Task 16 — MonthlyStatementPageComponent with Kendo Grid list (year filter), create/edit dialog (server-computed totals preview), and finalize action that locks the statement. Co-Authored-By: Claude Sonnet 4.6 --- .../monthly-statement-page.component.html | 143 ++++++++++++++++++ .../monthly-statement-page.component.scss | 25 +++ .../monthly-statement-page.component.ts | 103 +++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.html create mode 100644 APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.scss create mode 100644 APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.ts diff --git a/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.html b/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.html new file mode 100644 index 0000000..acaae81 --- /dev/null +++ b/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.html @@ -0,0 +1,143 @@ +
+ + + +
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + {{ dataItem.difference | currency:'USD':'symbol':'1.2-2' }} + + + + + + + + {{ dataItem.isFinalized ? 'Yes' : 'No' }} + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+
Computed Summary
+ Total Giving + {{ preview.totalGiving | currency:'USD':'symbol':'1.2-2' }} + Total Expenses + {{ preview.totalExpenses | currency:'USD':'symbol':'1.2-2' }} + Calc. Closing + {{ preview.calculatedClosingBalance | currency:'USD':'symbol':'1.2-2' }} + Difference + + {{ preview.difference | currency:'USD':'symbol':'1.2-2' }} + +
+ +
+ + + + + +
+ +
diff --git a/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.scss b/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.scss new file mode 100644 index 0000000..6dd9a28 --- /dev/null +++ b/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.scss @@ -0,0 +1,25 @@ +// Tailwind handles most layout; minimal local overrides only. + +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + line-height: 1.4; +} + +.badge-yes { + background: #dcfce7; + color: #166534; +} + +.badge-no { + background: #f1f5f9; + color: #475569; +} + +// Ensure Tailwind text-red-600 works even with preflight off +.text-red-600 { + color: #dc2626; +} diff --git a/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.ts b/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.ts new file mode 100644 index 0000000..c14ed4c --- /dev/null +++ b/APP/src/app/features/expense/pages/monthly-statement-page/monthly-statement-page.component.ts @@ -0,0 +1,103 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { GridModule } from '@progress/kendo-angular-grid'; +import { ButtonsModule } from '@progress/kendo-angular-buttons'; +import { DialogsModule } from '@progress/kendo-angular-dialog'; +import { InputsModule } from '@progress/kendo-angular-inputs'; +import { MonthlyStatementApiService } from '../../services/monthly-statement-api.service'; +import { MonthlyStatementDto } from '../../models/expense.model'; + +@Component({ + selector: 'app-monthly-statement-page', + standalone: true, + imports: [CommonModule, FormsModule, GridModule, ButtonsModule, DialogsModule, InputsModule], + templateUrl: './monthly-statement-page.component.html', + styleUrls: ['./monthly-statement-page.component.scss'], +}) +export class MonthlyStatementPageComponent implements OnInit { + rows: MonthlyStatementDto[] = []; + loading = false; + yearFilter: number | null = null; + + dialogOpen = false; + editing: MonthlyStatementDto | null = null; + form = { + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + openingBalance: 0, + totalOtherIncome: 0, + bankStatementBalance: 0, + notes: '', + }; + preview: MonthlyStatementDto | null = null; + + constructor(private api: MonthlyStatementApiService) {} + + ngOnInit(): void { this.load(); } + + load(): void { + this.loading = true; + this.api.getAll(this.yearFilter ?? undefined).subscribe({ + next: r => { this.rows = r; this.loading = false; }, + error: () => { this.loading = false; }, + }); + } + + openNew(): void { + this.editing = null; + this.preview = null; + this.form = { + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + openingBalance: 0, + totalOtherIncome: 0, + bankStatementBalance: 0, + notes: '', + }; + this.dialogOpen = true; + } + + openEdit(row: MonthlyStatementDto): void { + this.editing = row; + this.preview = row; + this.form = { + year: row.year, + month: row.month, + openingBalance: row.openingBalance, + totalOtherIncome: row.totalOtherIncome, + bankStatementBalance: row.bankStatementBalance, + notes: row.notes ?? '', + }; + this.dialogOpen = true; + } + + save(): void { + const done = () => { this.dialogOpen = false; this.load(); }; + if (this.editing == null) { + this.api.create({ + year: this.form.year, + month: this.form.month, + openingBalance: this.form.openingBalance, + totalOtherIncome: this.form.totalOtherIncome, + bankStatementBalance: this.form.bankStatementBalance, + notes: this.form.notes || null, + }).subscribe({ + next: created => this.api.getById(created.id).subscribe(s => { this.preview = s; done(); }), + error: err => alert(err?.error?.message ?? 'Create failed (a statement for that month may already exist).'), + }); + } else { + this.api.update(this.editing.id, { + openingBalance: this.form.openingBalance, + totalOtherIncome: this.form.totalOtherIncome, + bankStatementBalance: this.form.bankStatementBalance, + notes: this.form.notes || null, + }).subscribe(done); + } + } + + finalize(row: MonthlyStatementDto): void { + if (!confirm(`Finalize ${row.year}-${String(row.month).padStart(2, '0')}? This locks the statement.`)) return; + this.api.finalize(row.id).subscribe(() => this.load()); + } +}