From 7bb21f8796e05aa96f3aa4cc808c1dc54412e2b6 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Thu, 12 Feb 2026 14:56:09 -0600 Subject: [PATCH 01/18] added services and interfaces for expense types --- src/app/app.config.ts | 1 - .../components/expense-list/expense-list.ts | 4 +- src/app/services/categories.ts | 19 +++++++++ src/app/services/expenses.ts | 26 ++++++++++-- src/app/services/http.ts | 40 +++++++++++++++++++ src/app/services/merchants.ts | 19 +++++++++ src/app/services/sub-categories.ts | 19 +++++++++ src/app/services/tags.ts | 19 +++++++++ 8 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/app/services/categories.ts create mode 100644 src/app/services/http.ts create mode 100644 src/app/services/merchants.ts create mode 100644 src/app/services/sub-categories.ts create mode 100644 src/app/services/tags.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index cb1270e..e60fc79 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,6 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; - import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { diff --git a/src/app/components/expense-list/expense-list.ts b/src/app/components/expense-list/expense-list.ts index 05427fb..1206cbb 100644 --- a/src/app/components/expense-list/expense-list.ts +++ b/src/app/components/expense-list/expense-list.ts @@ -11,6 +11,8 @@ export class ExpenseList implements OnInit { public constructor(private readonly expenses: Expenses) { } public ngOnInit() { - void this.expenses.getExpenses(); + this.expenses.getExpenses().then(expenses => { + console.log({ expenses }); + }); } } diff --git a/src/app/services/categories.ts b/src/app/services/categories.ts new file mode 100644 index 0000000..405b7e9 --- /dev/null +++ b/src/app/services/categories.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class Categories { + public static readonly BASE_URL = 'http://localhost:3000/common-cents/categories'; + + public async getCategories(): Promise { + console.log('getCategories called'); + + return []; + } +} + +export interface Category { + id: string; + name: string; +} diff --git a/src/app/services/expenses.ts b/src/app/services/expenses.ts index 5c51b87..7b8b65f 100644 --- a/src/app/services/expenses.ts +++ b/src/app/services/expenses.ts @@ -1,12 +1,32 @@ import { Injectable } from '@angular/core'; +import { SubCategory } from './sub-categories'; +import { Category } from './categories'; +import { Merchant } from './merchants'; +import { Tag } from './tags'; +import { Http } from './http'; @Injectable({ providedIn: 'root', }) export class Expenses { - public static readonly BASE_URL = 'http://localhost:3000/common-cents/expenses'; + public static readonly EXPENSES_URI = 'http://localhost:3000/common-cents/expenses'; - public async getExpenses(): Promise { - console.log('getExpenses called'); + public constructor(private readonly http: Http) { } + + public async getExpenses(): Promise { + return this.http.get(Expenses.EXPENSES_URI); } } + +export interface Expense { + id: string; + year: string; + month: string; + day: string; + cents: number; + description?: string; + category: Category; + subCategory?: SubCategory; + merchant?: Merchant; + tags: Tag[]; +} diff --git a/src/app/services/http.ts b/src/app/services/http.ts new file mode 100644 index 0000000..ccda4b4 --- /dev/null +++ b/src/app/services/http.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class Http { + public constructor(private httpClient: HttpClient) { } + + public async get(url: string): Promise { + return this.request(url, 'get'); + } + + public async post(url: string, body?: any): Promise { + return this.request(url, 'post', body); + } + + public async put(url: string, body?: any): Promise { + return this.request(url, 'put', body); + } + + public async delete(url: string): Promise { + return this.request(url, 'delete'); + } + + private async request(url: string, method: 'get' | 'post' | 'put' | 'delete', body?: any): Promise { + const headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' }; + switch (method) { + case 'post': + return firstValueFrom(this.httpClient.post(url, body, { headers })); + case 'put': + return firstValueFrom(this.httpClient.put(url, body, { headers })); + case 'delete': + return firstValueFrom(this.httpClient.delete(url, { headers })); + default: + return firstValueFrom(this.httpClient.get(url, { headers })); + } + } +} diff --git a/src/app/services/merchants.ts b/src/app/services/merchants.ts new file mode 100644 index 0000000..730c177 --- /dev/null +++ b/src/app/services/merchants.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class Merchants { + public static readonly BASE_URL = 'http://localhost:3000/common-cents/merchants'; + + public async getMerchants(): Promise { + console.log('getMerchants called'); + + return []; + } +} + +export interface Merchant { + id: string; + name: string; +} diff --git a/src/app/services/sub-categories.ts b/src/app/services/sub-categories.ts new file mode 100644 index 0000000..8615603 --- /dev/null +++ b/src/app/services/sub-categories.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class SubCategories { + public static readonly BASE_URL = 'http://localhost:3000/common-cents/sub-categories'; + + public async getSubCategories(): Promise { + console.log('getSubCategories called'); + + return []; + } +} + +export interface SubCategory { + id: string; + name: string; +} diff --git a/src/app/services/tags.ts b/src/app/services/tags.ts new file mode 100644 index 0000000..282b344 --- /dev/null +++ b/src/app/services/tags.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class Tags { + public static readonly BASE_URL = 'http://localhost:3000/common-cents/tags'; + + public async getTags(): Promise { + console.log('getTags called'); + + return []; + } +} + +export interface Tag { + id: string; + name: string; +} -- 2.47.3 From fded7f7c0944360024c9efa5d8f02b9d6c18c0d1 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Thu, 12 Feb 2026 23:41:54 -0600 Subject: [PATCH 02/18] scaffold expense list --- .../expense-list/expense-list.component.html | 7 +++ .../expense-list/expense-list.component.scss | 7 +++ .../expense-list/expense-list.component.ts | 46 +++++++++++++++++++ .../components/expense-list/expense-list.html | 3 -- .../components/expense-list/expense-list.scss | 0 .../components/expense-list/expense-list.ts | 18 -------- .../components/expense/expense.component.html | 37 +++++++++++++++ .../components/expense/expense.component.scss | 24 ++++++++++ .../components/expense/expense.component.ts | 16 +++++++ src/app/pages/expenses/expenses.ts | 4 +- src/app/pages/home/home.html | 2 +- src/app/services/categories.ts | 19 -------- src/app/services/category.service.ts | 20 ++++++++ src/app/services/expense.service.ts | 32 +++++++++++++ src/app/services/expenses.ts | 32 ------------- src/app/services/{http.ts => http.service.ts} | 2 +- src/app/services/merchant.service.ts | 20 ++++++++ src/app/services/merchants.ts | 19 -------- src/app/services/sub-categories.ts | 19 -------- src/app/services/sub-category.service.ts | 20 ++++++++ src/app/services/tag.service.ts | 20 ++++++++ src/app/services/tags.ts | 19 -------- 22 files changed, 253 insertions(+), 133 deletions(-) create mode 100644 src/app/components/expense-list/expense-list.component.html create mode 100644 src/app/components/expense-list/expense-list.component.scss create mode 100644 src/app/components/expense-list/expense-list.component.ts delete mode 100644 src/app/components/expense-list/expense-list.html delete mode 100644 src/app/components/expense-list/expense-list.scss delete mode 100644 src/app/components/expense-list/expense-list.ts create mode 100644 src/app/components/expense/expense.component.html create mode 100644 src/app/components/expense/expense.component.scss create mode 100644 src/app/components/expense/expense.component.ts delete mode 100644 src/app/services/categories.ts create mode 100644 src/app/services/category.service.ts create mode 100644 src/app/services/expense.service.ts delete mode 100644 src/app/services/expenses.ts rename src/app/services/{http.ts => http.service.ts} (97%) create mode 100644 src/app/services/merchant.service.ts delete mode 100644 src/app/services/merchants.ts delete mode 100644 src/app/services/sub-categories.ts create mode 100644 src/app/services/sub-category.service.ts create mode 100644 src/app/services/tag.service.ts delete mode 100644 src/app/services/tags.ts diff --git a/src/app/components/expense-list/expense-list.component.html b/src/app/components/expense-list/expense-list.component.html new file mode 100644 index 0000000..c5fb02f --- /dev/null +++ b/src/app/components/expense-list/expense-list.component.html @@ -0,0 +1,7 @@ +
+ @for (expense of expenses(); track expense.id) { +
+ +
+ } +
diff --git a/src/app/components/expense-list/expense-list.component.scss b/src/app/components/expense-list/expense-list.component.scss new file mode 100644 index 0000000..10dc123 --- /dev/null +++ b/src/app/components/expense-list/expense-list.component.scss @@ -0,0 +1,7 @@ +.expense-list-container { + padding: 1rem; +} + +.expense-item { + padding-bottom: 1rem; +} diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts new file mode 100644 index 0000000..d13b24c --- /dev/null +++ b/src/app/components/expense-list/expense-list.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { Expense, ExpenseService } from '../../services/expense.service'; +import { ExpenseComponent } from '../expense/expense.component'; +import { Category, CategoryService } from '../../services/category.service'; +import { SubCategory, SubCategoryService } from '../../services/sub-category.service'; +import { Merchant, MerchantService } from '../../services/merchant.service'; +import { Tag, TagService } from '../../services/tag.service'; + +@Component({ + selector: 'app-expense-list', + imports: [ + ExpenseComponent + ], + templateUrl: './expense-list.component.html', + styleUrl: './expense-list.component.scss', +}) +export class ExpenseListComponent implements OnInit { + protected expenses = signal([]); + protected categories = signal([]); + protected subCategories = signal([]); + protected merchants = signal([]); + protected tags = signal([]); + + public constructor(private readonly expensesService: ExpenseService, + private readonly categoryService: CategoryService, + private readonly subCategoryService: SubCategoryService, + private readonly merchantService: MerchantService, + private readonly tagService: TagService) { } + + public ngOnInit() { + Promise.all([ + this.expensesService.getExpenses(), + this.categoryService.getCategories(), + this.subCategoryService.getSubCategories(), + this.merchantService.getMerchants(), + this.tagService.getTags() + ]).then(([expenses, categories, subCategories, merchants, tags]) => { + console.log({ expenses, categories, subCategories, merchants, tags }); // TODO: Remove me + this.expenses.set(expenses); + this.categories.set(categories); + this.subCategories.set(subCategories); + this.merchants.set(merchants); + this.tags.set(tags); + }) + } +} diff --git a/src/app/components/expense-list/expense-list.html b/src/app/components/expense-list/expense-list.html deleted file mode 100644 index d316863..0000000 --- a/src/app/components/expense-list/expense-list.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

expense-list works!

-
diff --git a/src/app/components/expense-list/expense-list.scss b/src/app/components/expense-list/expense-list.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/components/expense-list/expense-list.ts b/src/app/components/expense-list/expense-list.ts deleted file mode 100644 index 1206cbb..0000000 --- a/src/app/components/expense-list/expense-list.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Expenses } from '../../services/expenses'; - -@Component({ - selector: 'app-expense-list', - imports: [], - templateUrl: './expense-list.html', - styleUrl: './expense-list.scss', -}) -export class ExpenseList implements OnInit { - public constructor(private readonly expenses: Expenses) { } - - public ngOnInit() { - this.expenses.getExpenses().then(expenses => { - console.log({ expenses }); - }); - } -} diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html new file mode 100644 index 0000000..fee8c5b --- /dev/null +++ b/src/app/components/expense/expense.component.html @@ -0,0 +1,37 @@ +
+
+
+ {{ `${expense().year}/${expense().month}/${expense().day}` | date }} +
+ +
+ {{ (expense().cents / 100) | currency: 'USD' }} +
+ + @if (expense().merchant) { +
@ {{ expense().merchant?.name }}
+ } +
+ +
+
+ Category: {{ expense().category.name }} + @if (expense().subCategory) { + / {{ expense().subCategory?.name }} + } +
+ +
+ Description: {{ expense().description }} +
+
+ + +
diff --git a/src/app/components/expense/expense.component.scss b/src/app/components/expense/expense.component.scss new file mode 100644 index 0000000..a49c419 --- /dev/null +++ b/src/app/components/expense/expense.component.scss @@ -0,0 +1,24 @@ +.expense-container { + border-radius: 5px; + padding: 1rem; + box-shadow: rgba(99, 99, 99, 0.2) 0 2px 8px 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.expense-header { + display: flex; + gap: 0.5rem; +} + +.expense-body { + +} + +.expense-footer { + .expense-tags { + display: flex; + gap: 0.5rem; + } +} diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts new file mode 100644 index 0000000..1b61a9a --- /dev/null +++ b/src/app/components/expense/expense.component.ts @@ -0,0 +1,16 @@ +import {Component, input} from '@angular/core'; +import { Expense } from '../../services/expense.service'; +import {CurrencyPipe, DatePipe} from '@angular/common'; + +@Component({ + selector: 'app-expense', + imports: [ + CurrencyPipe, + DatePipe + ], + templateUrl: './expense.component.html', + styleUrl: './expense.component.scss', +}) +export class ExpenseComponent { + public expense = input.required(); +} diff --git a/src/app/pages/expenses/expenses.ts b/src/app/pages/expenses/expenses.ts index 1fefb3e..44db40c 100644 --- a/src/app/pages/expenses/expenses.ts +++ b/src/app/pages/expenses/expenses.ts @@ -1,10 +1,10 @@ import { Component } from '@angular/core'; -import { ExpenseList } from '../../components/expense-list/expense-list'; +import { ExpenseListComponent } from '../../components/expense-list/expense-list.component'; @Component({ selector: 'app-expenses', imports: [ - ExpenseList + ExpenseListComponent ], templateUrl: './expenses.html', styleUrl: './expenses.scss' diff --git a/src/app/pages/home/home.html b/src/app/pages/home/home.html index e74d430..94c5c02 100644 --- a/src/app/pages/home/home.html +++ b/src/app/pages/home/home.html @@ -1,7 +1,7 @@ diff --git a/src/app/services/categories.ts b/src/app/services/categories.ts deleted file mode 100644 index 405b7e9..0000000 --- a/src/app/services/categories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class Categories { - public static readonly BASE_URL = 'http://localhost:3000/common-cents/categories'; - - public async getCategories(): Promise { - console.log('getCategories called'); - - return []; - } -} - -export interface Category { - id: string; - name: string; -} diff --git a/src/app/services/category.service.ts b/src/app/services/category.service.ts new file mode 100644 index 0000000..ee88ef3 --- /dev/null +++ b/src/app/services/category.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { HttpService } from './http.service'; + +@Injectable({ + providedIn: 'root', +}) +export class CategoryService { + public static readonly CATEGORY_PATH = 'http://localhost:3000/common-cents/categories'; + + public constructor(private readonly http: HttpService) { } + + public async getCategories(): Promise { + return this.http.get(CategoryService.CATEGORY_PATH); + } +} + +export interface Category { + id: string; + name: string; +} diff --git a/src/app/services/expense.service.ts b/src/app/services/expense.service.ts new file mode 100644 index 0000000..d795591 --- /dev/null +++ b/src/app/services/expense.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { SubCategory } from './sub-category.service'; +import { Category } from './category.service'; +import { Merchant } from './merchant.service'; +import { Tag } from './tag.service'; +import { HttpService } from './http.service'; + +@Injectable({ + providedIn: 'root', +}) +export class ExpenseService { + public static readonly EXPENSE_PATH = 'http://localhost:3000/common-cents/expenses'; + + public constructor(private readonly http: HttpService) { } + + public async getExpenses(): Promise { + return this.http.get(ExpenseService.EXPENSE_PATH); + } +} + +export interface Expense { + id: string; + year: string; + month: string; + day: string; + cents: number; + description?: string; + category: Category; + subCategory?: SubCategory; + merchant?: Merchant; + tags: Tag[]; +} diff --git a/src/app/services/expenses.ts b/src/app/services/expenses.ts deleted file mode 100644 index 7b8b65f..0000000 --- a/src/app/services/expenses.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@angular/core'; -import { SubCategory } from './sub-categories'; -import { Category } from './categories'; -import { Merchant } from './merchants'; -import { Tag } from './tags'; -import { Http } from './http'; - -@Injectable({ - providedIn: 'root', -}) -export class Expenses { - public static readonly EXPENSES_URI = 'http://localhost:3000/common-cents/expenses'; - - public constructor(private readonly http: Http) { } - - public async getExpenses(): Promise { - return this.http.get(Expenses.EXPENSES_URI); - } -} - -export interface Expense { - id: string; - year: string; - month: string; - day: string; - cents: number; - description?: string; - category: Category; - subCategory?: SubCategory; - merchant?: Merchant; - tags: Tag[]; -} diff --git a/src/app/services/http.ts b/src/app/services/http.service.ts similarity index 97% rename from src/app/services/http.ts rename to src/app/services/http.service.ts index ccda4b4..d9f3212 100644 --- a/src/app/services/http.ts +++ b/src/app/services/http.service.ts @@ -5,7 +5,7 @@ import { firstValueFrom } from 'rxjs'; @Injectable({ providedIn: 'root', }) -export class Http { +export class HttpService { public constructor(private httpClient: HttpClient) { } public async get(url: string): Promise { diff --git a/src/app/services/merchant.service.ts b/src/app/services/merchant.service.ts new file mode 100644 index 0000000..20a9d68 --- /dev/null +++ b/src/app/services/merchant.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { HttpService } from './http.service'; + +@Injectable({ + providedIn: 'root', +}) +export class MerchantService { + public static readonly MERCHANT_PATH = 'http://localhost:3000/common-cents/merchants'; + + public constructor(private readonly http: HttpService) { } + + public async getMerchants(): Promise { + return this.http.get(MerchantService.MERCHANT_PATH); + } +} + +export interface Merchant { + id: string; + name: string; +} diff --git a/src/app/services/merchants.ts b/src/app/services/merchants.ts deleted file mode 100644 index 730c177..0000000 --- a/src/app/services/merchants.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class Merchants { - public static readonly BASE_URL = 'http://localhost:3000/common-cents/merchants'; - - public async getMerchants(): Promise { - console.log('getMerchants called'); - - return []; - } -} - -export interface Merchant { - id: string; - name: string; -} diff --git a/src/app/services/sub-categories.ts b/src/app/services/sub-categories.ts deleted file mode 100644 index 8615603..0000000 --- a/src/app/services/sub-categories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class SubCategories { - public static readonly BASE_URL = 'http://localhost:3000/common-cents/sub-categories'; - - public async getSubCategories(): Promise { - console.log('getSubCategories called'); - - return []; - } -} - -export interface SubCategory { - id: string; - name: string; -} diff --git a/src/app/services/sub-category.service.ts b/src/app/services/sub-category.service.ts new file mode 100644 index 0000000..1b4e268 --- /dev/null +++ b/src/app/services/sub-category.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { HttpService } from './http.service'; + +@Injectable({ + providedIn: 'root', +}) +export class SubCategoryService { + public static readonly SUBCATEGORY_PATH = 'http://localhost:3000/common-cents/sub-categories'; + + public constructor(private readonly http: HttpService) { } + + public async getSubCategories(): Promise { + return this.http.get(SubCategoryService.SUBCATEGORY_PATH); + } +} + +export interface SubCategory { + id: string; + name: string; +} diff --git a/src/app/services/tag.service.ts b/src/app/services/tag.service.ts new file mode 100644 index 0000000..e5acad5 --- /dev/null +++ b/src/app/services/tag.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { HttpService } from './http.service'; + +@Injectable({ + providedIn: 'root', +}) +export class TagService { + public static readonly TAG_PATH = 'http://localhost:3000/common-cents/tags'; + + public constructor(private readonly http: HttpService) { } + + public async getTags(): Promise { + return this.http.get(TagService.TAG_PATH); + } +} + +export interface Tag { + id: string; + name: string; +} diff --git a/src/app/services/tags.ts b/src/app/services/tags.ts deleted file mode 100644 index 282b344..0000000 --- a/src/app/services/tags.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class Tags { - public static readonly BASE_URL = 'http://localhost:3000/common-cents/tags'; - - public async getTags(): Promise { - console.log('getTags called'); - - return []; - } -} - -export interface Tag { - id: string; - name: string; -} -- 2.47.3 From 6e6cced0c6398d5fac0ab70cee62367936cffc3f Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Fri, 13 Feb 2026 15:00:51 -0600 Subject: [PATCH 03/18] refactored services and expense list --- .../add-expense/add-expense.component.html | 19 +++++++++ .../add-expense/add-expense.component.scss | 5 +++ .../add-expense/add-expense.component.ts | 14 +++++++ src/app/components/card/card.component.html | 15 +++++++ src/app/components/card/card.component.scss | 8 ++++ src/app/components/card/card.component.ts | 12 ++++++ .../expense-list/expense-list.component.ts | 39 +++---------------- .../components/expense/expense.component.html | 17 ++------ src/app/pages/expenses/expenses.html | 1 + src/app/pages/expenses/expenses.ts | 4 +- src/app/services/category.service.ts | 16 +++++--- src/app/services/expense.service.ts | 18 +++++---- src/app/services/merchant.service.ts | 14 ++++--- src/app/services/sub-category.service.ts | 20 ---------- src/app/services/tag.service.ts | 14 ++++--- 15 files changed, 125 insertions(+), 91 deletions(-) create mode 100644 src/app/components/add-expense/add-expense.component.html create mode 100644 src/app/components/add-expense/add-expense.component.scss create mode 100644 src/app/components/add-expense/add-expense.component.ts create mode 100644 src/app/components/card/card.component.html create mode 100644 src/app/components/card/card.component.scss create mode 100644 src/app/components/card/card.component.ts delete mode 100644 src/app/services/sub-category.service.ts diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html new file mode 100644 index 0000000..6da878b --- /dev/null +++ b/src/app/components/add-expense/add-expense.component.html @@ -0,0 +1,19 @@ +
+ +
+
Date
+
Amount
+
Category
+
Merchant
+
Tags
+
+ +
+ Note +
+ + +
+
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/add-expense/add-expense.component.scss new file mode 100644 index 0000000..92fce1a --- /dev/null +++ b/src/app/components/add-expense/add-expense.component.scss @@ -0,0 +1,5 @@ +.add-expense-body { + display: flex; + justify-content: space-between; + gap: 0.5rem; +} diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts new file mode 100644 index 0000000..86c8279 --- /dev/null +++ b/src/app/components/add-expense/add-expense.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import {CardComponent} from '../card/card.component'; + +@Component({ + selector: 'app-add-expense', + imports: [ + CardComponent + ], + templateUrl: './add-expense.component.html', + styleUrl: './add-expense.component.scss', +}) +export class AddExpenseComponent { + +} diff --git a/src/app/components/card/card.component.html b/src/app/components/card/card.component.html new file mode 100644 index 0000000..7826017 --- /dev/null +++ b/src/app/components/card/card.component.html @@ -0,0 +1,15 @@ +
+ @if (header()) { +
{{ header() }}
+ } @else { + + } + + + + @if (footer()) { + + } @else { + + } +
diff --git a/src/app/components/card/card.component.scss b/src/app/components/card/card.component.scss new file mode 100644 index 0000000..888e043 --- /dev/null +++ b/src/app/components/card/card.component.scss @@ -0,0 +1,8 @@ +.card-container { + border-radius: 5px; + padding: 1rem; + box-shadow: rgba(99, 99, 99, 0.2) 0 2px 8px 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} diff --git a/src/app/components/card/card.component.ts b/src/app/components/card/card.component.ts new file mode 100644 index 0000000..3e45981 --- /dev/null +++ b/src/app/components/card/card.component.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-card', + imports: [], + templateUrl: './card.component.html', + styleUrl: './card.component.scss', +}) +export class CardComponent { + public header = input(''); + public footer = input(''); +} diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts index d13b24c..15cc613 100644 --- a/src/app/components/expense-list/expense-list.component.ts +++ b/src/app/components/expense-list/expense-list.component.ts @@ -1,10 +1,6 @@ -import { Component, OnInit, signal } from '@angular/core'; -import { Expense, ExpenseService } from '../../services/expense.service'; +import { Component, computed } from '@angular/core'; +import { ExpenseService } from '../../services/expense.service'; import { ExpenseComponent } from '../expense/expense.component'; -import { Category, CategoryService } from '../../services/category.service'; -import { SubCategory, SubCategoryService } from '../../services/sub-category.service'; -import { Merchant, MerchantService } from '../../services/merchant.service'; -import { Tag, TagService } from '../../services/tag.service'; @Component({ selector: 'app-expense-list', @@ -14,33 +10,8 @@ import { Tag, TagService } from '../../services/tag.service'; templateUrl: './expense-list.component.html', styleUrl: './expense-list.component.scss', }) -export class ExpenseListComponent implements OnInit { - protected expenses = signal([]); - protected categories = signal([]); - protected subCategories = signal([]); - protected merchants = signal([]); - protected tags = signal([]); +export class ExpenseListComponent { + protected expenses = computed(() => this.expensesService.expenses()) - public constructor(private readonly expensesService: ExpenseService, - private readonly categoryService: CategoryService, - private readonly subCategoryService: SubCategoryService, - private readonly merchantService: MerchantService, - private readonly tagService: TagService) { } - - public ngOnInit() { - Promise.all([ - this.expensesService.getExpenses(), - this.categoryService.getCategories(), - this.subCategoryService.getSubCategories(), - this.merchantService.getMerchants(), - this.tagService.getTags() - ]).then(([expenses, categories, subCategories, merchants, tags]) => { - console.log({ expenses, categories, subCategories, merchants, tags }); // TODO: Remove me - this.expenses.set(expenses); - this.categories.set(categories); - this.subCategories.set(subCategories); - this.merchants.set(merchants); - this.tags.set(tags); - }) - } + public constructor(private readonly expensesService: ExpenseService) { } } diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html index fee8c5b..93f5357 100644 --- a/src/app/components/expense/expense.component.html +++ b/src/app/components/expense/expense.component.html @@ -1,34 +1,25 @@
-
- {{ `${expense().year}/${expense().month}/${expense().day}` | date }} -
- -
- {{ (expense().cents / 100) | currency: 'USD' }} -
+ {{ `${expense().year}/${expense().month}/${expense().day}` | date }}: {{ (expense().cents / 100) | currency: 'USD' }} @if (expense().merchant) { -
@ {{ expense().merchant?.name }}
+ at {{ expense().merchant?.name }} }
Category: {{ expense().category.name }} - @if (expense().subCategory) { - / {{ expense().subCategory?.name }} - }
- Description: {{ expense().description }} + Note: {{ expense().note ?? 'N/A' }}
diff --git a/src/app/components/card/card.component.scss b/src/app/components/card/card.component.scss index 888e043..8256ece 100644 --- a/src/app/components/card/card.component.scss +++ b/src/app/components/card/card.component.scss @@ -6,3 +6,9 @@ flex-direction: column; gap: 0.5rem; } + +.card-header { + font-size: 1.25rem; + font-weight: 700; + font-family: sans-serif; +} diff --git a/src/app/components/expense-list/expense-list.component.scss b/src/app/components/expense-list/expense-list.component.scss index 10dc123..afbad75 100644 --- a/src/app/components/expense-list/expense-list.component.scss +++ b/src/app/components/expense-list/expense-list.component.scss @@ -1,7 +1,3 @@ -.expense-list-container { - padding: 1rem; -} - .expense-item { padding-bottom: 1rem; } diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html index 93f5357..45ab8ab 100644 --- a/src/app/components/expense/expense.component.html +++ b/src/app/components/expense/expense.component.html @@ -1,28 +1,27 @@
-
- {{ `${expense().year}/${expense().month}/${expense().day}` | date }}: {{ (expense().cents / 100) | currency: 'USD' }} - - @if (expense().merchant) { - at {{ expense().merchant?.name }} - } -
- -
-
- Category: {{ expense().category.name }} + +
+ {{ `${expense().year}/${expense().month}/${expense().day}` | date }}: {{ (expense().cents / 100) | currency: 'USD' }}
-
- Note: {{ expense().note ?? 'N/A' }} -
-
+
+
Merchant: {{ expense().merchant?.name ?? '--' }}
- +
diff --git a/src/app/components/expense/expense.component.scss b/src/app/components/expense/expense.component.scss index a49c419..e27f306 100644 --- a/src/app/components/expense/expense.component.scss +++ b/src/app/components/expense/expense.component.scss @@ -1,24 +1,26 @@ -.expense-container { - border-radius: 5px; - padding: 1rem; - box-shadow: rgba(99, 99, 99, 0.2) 0 2px 8px 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - .expense-header { - display: flex; - gap: 0.5rem; + font-family: sans-serif; + font-weight: bold; } .expense-body { - + display: flex; + flex-direction: column; + gap: 0.25rem; } .expense-footer { - .expense-tags { - display: flex; - gap: 0.5rem; - } + font-family: sans-serif; + font-size: 0.8rem; + font-weight: lighter; + display: flex; + align-items: center; + gap: 0.5rem; + color: #515151; +} + +.expense-tag { + border-radius: 0.5rem; + background-color: #f1f1f1; + padding: 0.1rem 0.5rem; } diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 1b61a9a..647ed96 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -1,12 +1,14 @@ -import {Component, input} from '@angular/core'; +import { Component, input } from '@angular/core'; import { Expense } from '../../services/expense.service'; -import {CurrencyPipe, DatePipe} from '@angular/common'; +import { CurrencyPipe, DatePipe } from '@angular/common'; +import { CardComponent } from '../card/card.component'; @Component({ selector: 'app-expense', imports: [ CurrencyPipe, - DatePipe + DatePipe, + CardComponent ], templateUrl: './expense.component.html', styleUrl: './expense.component.scss', diff --git a/src/app/pages/expenses/expenses.scss b/src/app/pages/expenses/expenses.scss index e69de29..b35508b 100644 --- a/src/app/pages/expenses/expenses.scss +++ b/src/app/pages/expenses/expenses.scss @@ -0,0 +1,5 @@ +.expenses-container { + display: flex; + flex-direction: column; + gap: 1rem; +} -- 2.47.3 From 8bf9875e8e714e8f8f6ac27d2def6d053613a474 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Fri, 13 Feb 2026 19:53:32 -0600 Subject: [PATCH 05/18] small tweak to add expense --- .../add-expense/add-expense.component.html | 38 ++++++++++++++++--- .../add-expense/add-expense.component.scss | 2 +- .../add-expense/add-expense.component.ts | 33 ++++++++++++++-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html index e810435..8c2bfde 100644 --- a/src/app/components/add-expense/add-expense.component.html +++ b/src/app/components/add-expense/add-expense.component.html @@ -1,18 +1,44 @@
-
Date
-
Amount
-
Category
-
Merchant
+
+ Date: +
+ +
+ Cents (required): + +
+ +
+ Category (required): + +
- Note + Merchant (optional): +
+
+ Note (optional): +
+ +
Tags
+
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/add-expense/add-expense.component.scss index 92fce1a..45d6dd6 100644 --- a/src/app/components/add-expense/add-expense.component.scss +++ b/src/app/components/add-expense/add-expense.component.scss @@ -1,5 +1,5 @@ .add-expense-body { display: flex; - justify-content: space-between; + justify-content: flex-start; gap: 0.5rem; } diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts index 86c8279..fc685de 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/add-expense/add-expense.component.ts @@ -1,14 +1,41 @@ -import { Component } from '@angular/core'; -import {CardComponent} from '../card/card.component'; +import {Component, computed, signal} from '@angular/core'; +import { CardComponent } from '../card/card.component'; +import {Category, CategoryService} from '../../services/category.service'; +import {Merchant, MerchantService} from '../../services/merchant.service'; +import { Tag } from '../../services/tag.service'; +import {form, FormField} from '@angular/forms/signals'; + +interface AddExpenseForm { + date: Date, + amount: number | string, + categoryId: string, + merchantId: string, + note: string, + tags: Tag[] +} @Component({ selector: 'app-add-expense', imports: [ - CardComponent + CardComponent, + FormField ], templateUrl: './add-expense.component.html', styleUrl: './add-expense.component.scss', }) export class AddExpenseComponent { + protected categories = computed(() => this.categoryService.categories()); + protected merchants = computed(() => this.merchantService.merchants()); + private addExpenseModel = signal({ + date: new Date(), + amount: '', + categoryId: '', + merchantId: '', + note: '', + tags: [] + }); + public expenseForm = form(this.addExpenseModel); + public constructor(private readonly categoryService: CategoryService, + private readonly merchantService: MerchantService) { } } -- 2.47.3 From 17531f7c292918afc264f324bbebcc059b6f9889 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Fri, 13 Feb 2026 22:41:01 -0600 Subject: [PATCH 06/18] added material design components --- package-lock.json | 39 +++++- package.json | 3 + .../add-expense/add-expense.component.html | 116 +++++++++++++----- .../add-expense/add-expense.component.scss | 10 +- .../add-expense/add-expense.component.ts | 9 +- src/app/components/card/card.component.html | 15 --- src/app/components/card/card.component.scss | 14 --- src/app/components/card/card.component.ts | 12 -- .../expense-list/expense-list.component.html | 37 +++++- .../expense-list/expense-list.component.scss | 3 - .../expense-list/expense-list.component.ts | 30 ++++- .../components/expense/expense.component.html | 27 ---- .../components/expense/expense.component.scss | 26 ---- .../components/expense/expense.component.ts | 18 --- src/index.html | 4 + src/styles.scss | 37 ++++++ 16 files changed, 236 insertions(+), 164 deletions(-) delete mode 100644 src/app/components/card/card.component.html delete mode 100644 src/app/components/card/card.component.scss delete mode 100644 src/app/components/card/card.component.ts delete mode 100644 src/app/components/expense/expense.component.html delete mode 100644 src/app/components/expense/expense.component.scss delete mode 100644 src/app/components/expense/expense.component.ts diff --git a/package-lock.json b/package-lock.json index 79f7ee8..37680ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,12 @@ "name": "common-cents-web", "version": "0.0.0", "dependencies": { + "@angular/cdk": "~21.1.4", "@angular/common": "^21.1.0", "@angular/compiler": "^21.1.0", "@angular/core": "^21.1.0", "@angular/forms": "^21.1.0", + "@angular/material": "~21.1.4", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", "rxjs": "~7.8.0", @@ -21,6 +23,7 @@ "@angular/build": "^21.1.3", "@angular/cli": "^21.1.3", "@angular/compiler-cli": "^21.1.0", + "@angular/material": "^21.1.4", "jsdom": "^27.1.0", "typescript": "~5.9.2", "vitest": "^4.0.8" @@ -422,6 +425,22 @@ } } }, + "node_modules/@angular/cdk": { + "version": "21.1.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.4.tgz", + "integrity": "sha512-PElA4Ww4TIa3+B/ND+fm8ZPDKONTIqc9a/s0qNxhcAD9IpDqjaBVi/fyg+ZWBtS+x0DQgJtKeCsSZ6sr2aFQaQ==", + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^21.0.0 || ^22.0.0", + "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/cli": { "version": "21.1.3", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.3.tgz", @@ -563,6 +582,24 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material": { + "version": "21.1.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-21.1.4.tgz", + "integrity": "sha512-Vte+4os5JexhtaPk7Duk5bIC+zU5SYF7ZHUnpmAJxW0u4D4C9lpTVepm2bfMVV2AxHjHVAnW+/anINIS401UsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "21.1.4", + "@angular/common": "^21.0.0 || ^22.0.0", + "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/forms": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/platform-browser": { "version": "21.1.3", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.1.3.tgz", @@ -6845,7 +6882,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -6899,7 +6935,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" diff --git a/package.json b/package.json index 6c41d2b..e3c47c6 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "private": true, "packageManager": "npm@10.9.2", "dependencies": { + "@angular/cdk": "~21.1.4", "@angular/common": "^21.1.0", "@angular/compiler": "^21.1.0", "@angular/core": "^21.1.0", "@angular/forms": "^21.1.0", + "@angular/material": "~21.1.4", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", "rxjs": "~7.8.0", @@ -36,6 +38,7 @@ "@angular/build": "^21.1.3", "@angular/cli": "^21.1.3", "@angular/compiler-cli": "^21.1.0", + "@angular/material": "^21.1.4", "jsdom": "^27.1.0", "typescript": "~5.9.2", "vitest": "^4.0.8" diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html index 8c2bfde..8d16c98 100644 --- a/src/app/components/add-expense/add-expense.component.html +++ b/src/app/components/add-expense/add-expense.component.html @@ -1,44 +1,96 @@
- -
-
- Date: + + + Track new Expense + + + + +
+
+ Date: +
+ +
+ Cents (required): + +
+ +
+ Category (required): + +
- Cents (required): - -
- -
- Category (required): - + + @for (merchant of merchants(); track merchant.id) { + }
-
-
- Merchant (optional): - -
+
+ Note (optional): +
-
- Note (optional): -
+
Tags
-
Tags
+ + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/add-expense/add-expense.component.scss index 45d6dd6..9bf429e 100644 --- a/src/app/components/add-expense/add-expense.component.scss +++ b/src/app/components/add-expense/add-expense.component.scss @@ -1,5 +1,5 @@ -.add-expense-body { - display: flex; - justify-content: flex-start; - gap: 0.5rem; -} +//.add-expense-body { +// display: flex; +// justify-content: flex-start; +// gap: 0.5rem; +//} diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts index fc685de..b0d0ccb 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/add-expense/add-expense.component.ts @@ -1,9 +1,9 @@ import {Component, computed, signal} from '@angular/core'; -import { CardComponent } from '../card/card.component'; import {Category, CategoryService} from '../../services/category.service'; import {Merchant, MerchantService} from '../../services/merchant.service'; import { Tag } from '../../services/tag.service'; import {form, FormField} from '@angular/forms/signals'; +import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; interface AddExpenseForm { date: Date, @@ -17,8 +17,11 @@ interface AddExpenseForm { @Component({ selector: 'app-add-expense', imports: [ - CardComponent, - FormField + FormField, + MatCard, + MatCardHeader, + MatCardTitle, + MatCardContent ], templateUrl: './add-expense.component.html', styleUrl: './add-expense.component.scss', diff --git a/src/app/components/card/card.component.html b/src/app/components/card/card.component.html deleted file mode 100644 index 7826017..0000000 --- a/src/app/components/card/card.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
- @if (header()) { -
{{ header() }}
- } @else { - - } - - - - @if (footer()) { - - } @else { - - } -
diff --git a/src/app/components/card/card.component.scss b/src/app/components/card/card.component.scss deleted file mode 100644 index 8256ece..0000000 --- a/src/app/components/card/card.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -.card-container { - border-radius: 5px; - padding: 1rem; - box-shadow: rgba(99, 99, 99, 0.2) 0 2px 8px 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.card-header { - font-size: 1.25rem; - font-weight: 700; - font-family: sans-serif; -} diff --git a/src/app/components/card/card.component.ts b/src/app/components/card/card.component.ts deleted file mode 100644 index 3e45981..0000000 --- a/src/app/components/card/card.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, input } from '@angular/core'; - -@Component({ - selector: 'app-card', - imports: [], - templateUrl: './card.component.html', - styleUrl: './card.component.scss', -}) -export class CardComponent { - public header = input(''); - public footer = input(''); -} diff --git a/src/app/components/expense-list/expense-list.component.html b/src/app/components/expense-list/expense-list.component.html index c5fb02f..bed23d2 100644 --- a/src/app/components/expense-list/expense-list.component.html +++ b/src/app/components/expense-list/expense-list.component.html @@ -1,7 +1,34 @@
- @for (expense of expenses(); track expense.id) { -
- -
- } + + + Tracked Expenses + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date{{ `${expense.year}/${expense.month}/${expense.day}` | date }}Amount{{ (expense.cents / 100) | currency: 'USD' }}Category{{ expense.category.name }}Merchant{{ expense.merchant?.name ?? '--' }}
+
+
diff --git a/src/app/components/expense-list/expense-list.component.scss b/src/app/components/expense-list/expense-list.component.scss index afbad75..e69de29 100644 --- a/src/app/components/expense-list/expense-list.component.scss +++ b/src/app/components/expense-list/expense-list.component.scss @@ -1,3 +0,0 @@ -.expense-item { - padding-bottom: 1rem; -} diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts index 15cc613..5a7d57d 100644 --- a/src/app/components/expense-list/expense-list.component.ts +++ b/src/app/components/expense-list/expense-list.component.ts @@ -1,17 +1,43 @@ import { Component, computed } from '@angular/core'; import { ExpenseService } from '../../services/expense.service'; -import { ExpenseComponent } from '../expense/expense.component'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, + MatTable +} from '@angular/material/table'; +import {CurrencyPipe, DatePipe} from '@angular/common'; +import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; @Component({ selector: 'app-expense-list', imports: [ - ExpenseComponent + MatTable, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + DatePipe, + MatCell, + MatCellDef, + CurrencyPipe, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatCard, + MatCardHeader, + MatCardTitle, + MatCardContent ], templateUrl: './expense-list.component.html', styleUrl: './expense-list.component.scss', }) export class ExpenseListComponent { protected expenses = computed(() => this.expensesService.expenses()) + protected columns = ['date', 'amount', 'category', 'merchant']; public constructor(private readonly expensesService: ExpenseService) { } } diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html deleted file mode 100644 index 45ab8ab..0000000 --- a/src/app/components/expense/expense.component.html +++ /dev/null @@ -1,27 +0,0 @@ -
- -
- {{ `${expense().year}/${expense().month}/${expense().day}` | date }}: {{ (expense().cents / 100) | currency: 'USD' }} -
- -
-
Merchant: {{ expense().merchant?.name ?? '--' }}
- -
Category: {{ expense().category.name }}
- -
Note: {{ expense().note ?? '--' }}
-
- - -
-
diff --git a/src/app/components/expense/expense.component.scss b/src/app/components/expense/expense.component.scss deleted file mode 100644 index e27f306..0000000 --- a/src/app/components/expense/expense.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -.expense-header { - font-family: sans-serif; - font-weight: bold; -} - -.expense-body { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.expense-footer { - font-family: sans-serif; - font-size: 0.8rem; - font-weight: lighter; - display: flex; - align-items: center; - gap: 0.5rem; - color: #515151; -} - -.expense-tag { - border-radius: 0.5rem; - background-color: #f1f1f1; - padding: 0.1rem 0.5rem; -} diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts deleted file mode 100644 index 647ed96..0000000 --- a/src/app/components/expense/expense.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Component, input } from '@angular/core'; -import { Expense } from '../../services/expense.service'; -import { CurrencyPipe, DatePipe } from '@angular/common'; -import { CardComponent } from '../card/card.component'; - -@Component({ - selector: 'app-expense', - imports: [ - CurrencyPipe, - DatePipe, - CardComponent - ], - templateUrl: './expense.component.html', - styleUrl: './expense.component.scss', -}) -export class ExpenseComponent { - public expense = input.required(); -} diff --git a/src/index.html b/src/index.html index 81d82d7..1da5b0f 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,10 @@ + + + + diff --git a/src/styles.scss b/src/styles.scss index 90d4ee0..cf6f56c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1 +1,38 @@ + +// Include theming for Angular Material with `mat.theme()`. +// This Sass mixin will define CSS variables that are used for styling Angular Material +// components according to the Material 3 design spec. +// Learn more about theming and how to use it for your application's +// custom components at https://material.angular.dev/guide/theming +@use '@angular/material' as mat; + +html { + height: 100%; + @include mat.theme(( + color: ( + primary: mat.$cyan-palette, + tertiary: mat.$orange-palette, + ), + typography: Roboto, + density: 0 + )); +} + +body { + // Default the application to a light color theme. This can be changed to + // `dark` to enable the dark color theme, or to `light dark` to defer to the + // user's system settings. + color-scheme: light; // TODO: choose 'light dark' + + // Set a default background, font and text colors for the application using + // Angular Material's system-level CSS variables. Learn more about these + // variables at https://material.angular.dev/guide/system-variables + background-color: var(--mat-sys-surface); + color: var(--mat-sys-on-surface); + font: var(--mat-sys-body-medium); + + // Reset the user agent margin. + margin: 0; + height: 100%; +} /* You can add global styles to this file, and also import other style files */ -- 2.47.3 From 2c0a8da60da3f541ae80e0cf951b31d884511a42 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Fri, 13 Feb 2026 23:50:37 -0600 Subject: [PATCH 07/18] rough migration to material design inputs --- .../add-expense/add-expense.component.html | 127 +++++++----------- .../add-expense/add-expense.component.scss | 28 +++- .../add-expense/add-expense.component.ts | 32 +++-- .../expense-list/expense-list.component.ts | 33 +---- 4 files changed, 93 insertions(+), 127 deletions(-) diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html index 8d16c98..00c2249 100644 --- a/src/app/components/add-expense/add-expense.component.html +++ b/src/app/components/add-expense/add-expense.component.html @@ -1,96 +1,65 @@
- + Track new Expense - -
-
- Date: -
+
+ + Date + + + + -
- Cents (required): - -
+ + Cents + + -
- Category (required): - + @for (category of categories(); track category.id) { - + {{ category.name }} } - -
+ + + + + Merchant + + + @for (merchant of merchants(); track merchant.id) { + {{ merchant.name }} + } + +
-
- Merchant (optional): - -
+
+ + Tags + + @for (tag of tags(); track tag.id) { + {{ tag.name }} + } + + -
- Note (optional): -
- -
Tags
- - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/add-expense/add-expense.component.scss index 9bf429e..a524941 100644 --- a/src/app/components/add-expense/add-expense.component.scss +++ b/src/app/components/add-expense/add-expense.component.scss @@ -1,5 +1,23 @@ -//.add-expense-body { -// display: flex; -// justify-content: flex-start; -// gap: 0.5rem; -//} +.add-expense-header { + padding-bottom: 1rem; +} + +.add-expense-footer { + padding: 0 0 1rem 0.5rem; +} + +.add-expense-dropdowns { + display: flex; + gap: 0.5rem; + justify-content: space-between; +} + +.add-expense-additional { + display: flex; + gap: 0.5rem; + justify-content: space-between; +} + +.add-expense-note { + width: 100%; +} diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts index b0d0ccb..0cde81e 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/add-expense/add-expense.component.ts @@ -1,9 +1,16 @@ -import {Component, computed, signal} from '@angular/core'; -import {Category, CategoryService} from '../../services/category.service'; -import {Merchant, MerchantService} from '../../services/merchant.service'; -import { Tag } from '../../services/tag.service'; -import {form, FormField} from '@angular/forms/signals'; -import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; +import { Component, computed, signal } from '@angular/core'; +import { CategoryService } from '../../services/category.service'; +import { MerchantService } from '../../services/merchant.service'; +import { Tag, TagService } from '../../services/tag.service'; +import { form, FormField } from '@angular/forms/signals'; +import { MatCardModule } from '@angular/material/card'; +import { MatInputModule } from '@angular/material/input'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatSelectModule } from '@angular/material/select'; +import { MatButtonModule } from '@angular/material/button'; +import { provideNativeDateAdapter } from '@angular/material/core'; interface AddExpenseForm { date: Date, @@ -16,19 +23,15 @@ interface AddExpenseForm { @Component({ selector: 'app-add-expense', - imports: [ - FormField, - MatCard, - MatCardHeader, - MatCardTitle, - MatCardContent - ], + imports: [MatDatepickerModule, MatFormFieldModule, MatInputModule, MatCardModule, MatAutocompleteModule, MatSelectModule, MatButtonModule, FormField], + providers: [provideNativeDateAdapter()], templateUrl: './add-expense.component.html', styleUrl: './add-expense.component.scss', }) export class AddExpenseComponent { protected categories = computed(() => this.categoryService.categories()); protected merchants = computed(() => this.merchantService.merchants()); + protected tags = computed(() => this.tagService.tags()); private addExpenseModel = signal({ date: new Date(), amount: '', @@ -40,5 +43,6 @@ export class AddExpenseComponent { public expenseForm = form(this.addExpenseModel); public constructor(private readonly categoryService: CategoryService, - private readonly merchantService: MerchantService) { } + private readonly merchantService: MerchantService, + private readonly tagService: TagService) { } } diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts index 5a7d57d..08864c8 100644 --- a/src/app/components/expense-list/expense-list.component.ts +++ b/src/app/components/expense-list/expense-list.component.ts @@ -1,37 +1,12 @@ import { Component, computed } from '@angular/core'; import { ExpenseService } from '../../services/expense.service'; -import { - MatCell, - MatCellDef, - MatColumnDef, - MatHeaderCell, - MatHeaderCellDef, - MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, - MatTable -} from '@angular/material/table'; -import {CurrencyPipe, DatePipe} from '@angular/common'; -import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; +import { MatTableModule } from '@angular/material/table'; +import { CurrencyPipe, DatePipe } from '@angular/common'; +import { MatCardModule } from '@angular/material/card'; @Component({ selector: 'app-expense-list', - imports: [ - MatTable, - MatColumnDef, - MatHeaderCell, - MatHeaderCellDef, - DatePipe, - MatCell, - MatCellDef, - CurrencyPipe, - MatHeaderRow, - MatHeaderRowDef, - MatRow, - MatRowDef, - MatCard, - MatCardHeader, - MatCardTitle, - MatCardContent - ], + imports: [MatTableModule, MatCardModule, DatePipe, CurrencyPipe], templateUrl: './expense-list.component.html', styleUrl: './expense-list.component.scss', }) -- 2.47.3 From 627bdcbd8037df1082bd6ac131c874246aac3010 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sat, 14 Feb 2026 10:20:23 -0600 Subject: [PATCH 08/18] created for validation and save button logic --- .../add-expense/add-expense.component.html | 10 ++-- .../add-expense/add-expense.component.scss | 4 +- .../add-expense/add-expense.component.ts | 51 ++++++++++++++----- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html index 00c2249..bcfe018 100644 --- a/src/app/components/add-expense/add-expense.component.html +++ b/src/app/components/add-expense/add-expense.component.html @@ -15,12 +15,12 @@ Cents - + Category - + @for (category of categories(); track category.id) { {{ category.name }} @@ -30,7 +30,7 @@ Merchant - + @for (merchant of merchants(); track merchant.id) { {{ merchant.name }} @@ -51,14 +51,14 @@ Note - +
- +
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/add-expense/add-expense.component.scss index a524941..74c3967 100644 --- a/src/app/components/add-expense/add-expense.component.scss +++ b/src/app/components/add-expense/add-expense.component.scss @@ -8,13 +8,13 @@ .add-expense-dropdowns { display: flex; - gap: 0.5rem; + gap: 1rem; justify-content: space-between; } .add-expense-additional { display: flex; - gap: 0.5rem; + gap: 1rem; justify-content: space-between; } diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts index 0cde81e..5931c7a 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/add-expense/add-expense.component.ts @@ -1,8 +1,8 @@ import { Component, computed, signal } from '@angular/core'; import { CategoryService } from '../../services/category.service'; import { MerchantService } from '../../services/merchant.service'; -import { Tag, TagService } from '../../services/tag.service'; -import { form, FormField } from '@angular/forms/signals'; +import { TagService } from '../../services/tag.service'; +import { form, FormField, min, required } from '@angular/forms/signals'; import { MatCardModule } from '@angular/material/card'; import { MatInputModule } from '@angular/material/input'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -12,13 +12,13 @@ import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { provideNativeDateAdapter } from '@angular/material/core'; -interface AddExpenseForm { - date: Date, - amount: number | string, - categoryId: string, - merchantId: string, - note: string, - tags: Tag[] +interface ExpenseForm { + date: Date; + cents: number; + category: string; + merchant: string; + note: string; + tags: string[]; } @Component({ @@ -32,17 +32,40 @@ export class AddExpenseComponent { protected categories = computed(() => this.categoryService.categories()); protected merchants = computed(() => this.merchantService.merchants()); protected tags = computed(() => this.tagService.tags()); - private addExpenseModel = signal({ + protected enableSaveButton = computed(() => { + const dateValid = this.expenseForm.date().valid(); + const centsValid = this.expenseForm.cents().valid(); + const categoryValid = this.expenseForm.category().valid(); + const merchantValid = this.expenseForm.merchant().valid(); + const noteValid = this.expenseForm.note().valid(); + + return dateValid && centsValid && categoryValid && merchantValid && noteValid; + }); + + private defaultFormState: ExpenseForm = { date: new Date(), - amount: '', - categoryId: '', - merchantId: '', + cents: NaN, + category: '', + merchant: '', note: '', tags: [] + }; + private expenseModel = signal(this.defaultFormState); + public expenseForm = form(this.expenseModel, (schema) => { + required(schema.date); + required(schema.cents); + min(schema.cents, 1); + required(schema.category); }); - public expenseForm = form(this.addExpenseModel); public constructor(private readonly categoryService: CategoryService, private readonly merchantService: MerchantService, private readonly tagService: TagService) { } + + public saveClick(): void { + const saveExpense = this.expenseModel(); + console.log(saveExpense); + this.expenseModel.set({ ...this.defaultFormState, date: saveExpense.date }); + this.expenseForm().reset(this.expenseModel()); + } } -- 2.47.3 From ca8814462924fb2f22a8ac793fcd48a2fd1c0b5a Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sat, 14 Feb 2026 20:15:31 -0600 Subject: [PATCH 09/18] ability to track expense --- .../add-expense/add-expense.component.html | 18 ++++-- .../add-expense/add-expense.component.ts | 58 ++++++++++++++----- src/app/services/expense.service.ts | 18 ++++++ 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html index bcfe018..afc5f76 100644 --- a/src/app/components/add-expense/add-expense.component.html +++ b/src/app/components/add-expense/add-expense.component.html @@ -20,10 +20,10 @@ Category - - + + @for (category of categories(); track category.id) { - {{ category.name }} + {{ category.name }} } @@ -31,9 +31,9 @@ Merchant - + @for (merchant of merchants(); track merchant.id) { - {{ merchant.name }} + {{ merchant.name }} } @@ -44,7 +44,7 @@ Tags @for (tag of tags(); track tag.id) { - {{ tag.name }} + {{ tag.name }} } @@ -60,6 +60,12 @@ + + @if (errorSaving()) { + + {{ errorSaving() }} + + }
diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts index 5931c7a..e3f0d4e 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/add-expense/add-expense.component.ts @@ -1,7 +1,7 @@ import { Component, computed, signal } from '@angular/core'; -import { CategoryService } from '../../services/category.service'; -import { MerchantService } from '../../services/merchant.service'; -import { TagService } from '../../services/tag.service'; +import { Category, CategoryService } from '../../services/category.service'; +import { Merchant, MerchantService } from '../../services/merchant.service'; +import { Tag, TagService } from '../../services/tag.service'; import { form, FormField, min, required } from '@angular/forms/signals'; import { MatCardModule } from '@angular/material/card'; import { MatInputModule } from '@angular/material/input'; @@ -11,20 +11,22 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { provideNativeDateAdapter } from '@angular/material/core'; +import { CreateExpense, ExpenseService } from '../../services/expense.service'; +import { DatePipe } from '@angular/common'; interface ExpenseForm { date: Date; cents: number; - category: string; - merchant: string; + category: Category | string; + merchant: Merchant | string; note: string; - tags: string[]; + tags: Tag[]; } @Component({ selector: 'app-add-expense', imports: [MatDatepickerModule, MatFormFieldModule, MatInputModule, MatCardModule, MatAutocompleteModule, MatSelectModule, MatButtonModule, FormField], - providers: [provideNativeDateAdapter()], + providers: [provideNativeDateAdapter(), DatePipe], templateUrl: './add-expense.component.html', styleUrl: './add-expense.component.scss', }) @@ -41,6 +43,8 @@ export class AddExpenseComponent { return dateValid && centsValid && categoryValid && merchantValid && noteValid; }); + protected saving = signal(false); + protected errorSaving = signal(''); private defaultFormState: ExpenseForm = { date: new Date(), @@ -60,12 +64,40 @@ export class AddExpenseComponent { public constructor(private readonly categoryService: CategoryService, private readonly merchantService: MerchantService, - private readonly tagService: TagService) { } + private readonly tagService: TagService, + private readonly expenseService: ExpenseService, + private readonly datePipe: DatePipe) { } - public saveClick(): void { - const saveExpense = this.expenseModel(); - console.log(saveExpense); - this.expenseModel.set({ ...this.defaultFormState, date: saveExpense.date }); - this.expenseForm().reset(this.expenseModel()); + public async saveClick(): Promise { + this.errorSaving.set(''); + const saveExpenseModel = this.expenseModel(); + const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; + const expense: CreateExpense = { + year: date[0], + month: date[1], + day: date[2], + cents: saveExpenseModel.cents, + category: saveExpenseModel.category as Category, + merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined, + note: saveExpenseModel.note ? saveExpenseModel.note : undefined, + tags: saveExpenseModel.tags + }; + + this.saving.set(true); + try { + await this.expenseService.postExpense(expense); + this.expenseModel.set({ ...this.defaultFormState, date: saveExpenseModel.date }); + this.expenseForm().reset(this.expenseModel()); + } + catch (error) { + this.errorSaving.set(`Error saving expense: ${error}`) + } + finally { + this.saving.set(false); + } + } + + public display(value: Merchant | Category | Tag) { + return value.name ?? null; } } diff --git a/src/app/services/expense.service.ts b/src/app/services/expense.service.ts index 993d225..2814357 100644 --- a/src/app/services/expense.service.ts +++ b/src/app/services/expense.service.ts @@ -19,6 +19,13 @@ export class ExpenseService { public async fetchExpenses(): Promise { this.internalExpenses.set(await this.http.get(this.expensePath)); } + + public async postExpense(expense: CreateExpense): Promise { + const createdExpense = await this.http.post(this.expensePath, expense); + await this.fetchExpenses(); + + return createdExpense; + } } export interface Expense { @@ -32,3 +39,14 @@ export interface Expense { merchant?: Merchant; tags: Tag[]; } + +export interface CreateExpense { + year: string; + month: string; + day: string; + cents: number; + category: Category; + note?: string; + merchant?: Merchant; + tags: Tag[]; +} -- 2.47.3 From 23132741a936863478342778dcc8f71423d2d2c8 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sat, 14 Feb 2026 21:51:33 -0600 Subject: [PATCH 10/18] add-expense-mobile-friendly --- src/app/app.scss | 2 +- .../add-expense/add-expense.component.html | 99 ++++++++----------- .../add-expense/add-expense.component.scss | 52 ++++++---- .../add-expense/add-expense.component.ts | 10 +- 4 files changed, 81 insertions(+), 82 deletions(-) diff --git a/src/app/app.scss b/src/app/app.scss index 29334f4..79354d0 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1,5 +1,5 @@ .app-content { - padding: 2rem; + padding: 0.5rem; } h1 { diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html index afc5f76..fb755ac 100644 --- a/src/app/components/add-expense/add-expense.component.html +++ b/src/app/components/add-expense/add-expense.component.html @@ -1,71 +1,56 @@
- + Track new Expense + -
- - Date - - - - + + Date + + + + - - Cents - - + + Cents + + - - Category - - - @for (category of categories(); track category.id) { - {{ category.name }} - } - - + + Category + + + @for (category of categories(); track category.id) { + {{ category.name }} + } + + - - Merchant - - - @for (merchant of merchants(); track merchant.id) { - {{ merchant.name }} - } - - -
+ + Merchant + + + @for (merchant of merchants(); track merchant.id) { + {{ merchant.name }} + } + + -
- - Tags - - @for (tag of tags(); track tag.id) { - {{ tag.name }} - } - - + + Tags + + @for (tag of tags(); track tag.id) { + {{ tag.name }} + } + + - - Note - - -
+ + Note + +
- - - - - - - @if (errorSaving()) { - - {{ errorSaving() }} - - } -
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/add-expense/add-expense.component.scss index 74c3967..790f538 100644 --- a/src/app/components/add-expense/add-expense.component.scss +++ b/src/app/components/add-expense/add-expense.component.scss @@ -1,23 +1,39 @@ -.add-expense-header { - padding-bottom: 1rem; -} - -.add-expense-footer { - padding: 0 0 1rem 0.5rem; -} - -.add-expense-dropdowns { +.add-expense-container { + width: 100%; display: flex; - gap: 1rem; - justify-content: space-between; + flex-direction: column; + align-items: center; } -.add-expense-additional { - display: flex; - gap: 1rem; - justify-content: space-between; -} - -.add-expense-note { +mat-card { + max-width: 800px; width: 100%; } + +mat-card-header { + padding-bottom: 1rem; + display: flex; + align-items: center; + justify-content: space-between; +} + +mat-card-content { + display: grid; +} + +mat-form-field { + width: 100%; +} + +@media (min-width: 550px) { + mat-card-content { + grid-template-columns: 1fr 1fr; + gap: 1rem; + } +} + +@media (min-width: 800px) { + mat-card-content { + grid-template-columns: 1fr 1fr 1fr; + } +} diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/add-expense/add-expense.component.ts index e3f0d4e..8bdbefc 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/add-expense/add-expense.component.ts @@ -15,7 +15,7 @@ import { CreateExpense, ExpenseService } from '../../services/expense.service'; import { DatePipe } from '@angular/common'; interface ExpenseForm { - date: Date; + date: Date | string; cents: number; category: Category | string; merchant: Merchant | string; @@ -44,10 +44,9 @@ export class AddExpenseComponent { return dateValid && centsValid && categoryValid && merchantValid && noteValid; }); protected saving = signal(false); - protected errorSaving = signal(''); private defaultFormState: ExpenseForm = { - date: new Date(), + date: '', cents: NaN, category: '', merchant: '', @@ -69,7 +68,6 @@ export class AddExpenseComponent { private readonly datePipe: DatePipe) { } public async saveClick(): Promise { - this.errorSaving.set(''); const saveExpenseModel = this.expenseModel(); const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; const expense: CreateExpense = { @@ -90,14 +88,14 @@ export class AddExpenseComponent { this.expenseForm().reset(this.expenseModel()); } catch (error) { - this.errorSaving.set(`Error saving expense: ${error}`) + console.error(error); } finally { this.saving.set(false); } } - public display(value: Merchant | Category | Tag) { + public autocompleteDisplay(value: Merchant | Category) { return value.name ?? null; } } -- 2.47.3 From fed0f7908a8752e716d70bac5ad0f67baec9e345 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sat, 14 Feb 2026 23:39:51 -0600 Subject: [PATCH 11/18] added expense state (view, edit, add) logic --- .../add-expense/add-expense.component.html | 56 ----------- .../expense-list/expense-list.component.html | 57 +++++------ .../expense-list/expense-list.component.scss | 4 + .../expense-list/expense-list.component.ts | 3 +- .../components/expense/expense.component.html | 96 +++++++++++++++++++ .../expense.component.scss} | 26 ++--- .../expense.component.ts} | 49 ++++++---- src/app/pages/expenses/expenses.html | 2 +- src/app/pages/expenses/expenses.ts | 4 +- 9 files changed, 180 insertions(+), 117 deletions(-) delete mode 100644 src/app/components/add-expense/add-expense.component.html create mode 100644 src/app/components/expense/expense.component.html rename src/app/components/{add-expense/add-expense.component.scss => expense/expense.component.scss} (57%) rename src/app/components/{add-expense/add-expense.component.ts => expense/expense.component.ts} (69%) diff --git a/src/app/components/add-expense/add-expense.component.html b/src/app/components/add-expense/add-expense.component.html deleted file mode 100644 index fb755ac..0000000 --- a/src/app/components/add-expense/add-expense.component.html +++ /dev/null @@ -1,56 +0,0 @@ -
- - - Track new Expense - - - - - - Date - - - - - - - Cents - - - - - Category - - - @for (category of categories(); track category.id) { - {{ category.name }} - } - - - - - Merchant - - - @for (merchant of merchants(); track merchant.id) { - {{ merchant.name }} - } - - - - - Tags - - @for (tag of tags(); track tag.id) { - {{ tag.name }} - } - - - - - Note - - - - -
diff --git a/src/app/components/expense-list/expense-list.component.html b/src/app/components/expense-list/expense-list.component.html index bed23d2..080cafc 100644 --- a/src/app/components/expense-list/expense-list.component.html +++ b/src/app/components/expense-list/expense-list.component.html @@ -1,34 +1,37 @@
- - - Tracked Expenses - + @for (expense of expenses(); track expense.id) { + + } + + + + - - - - - - + + + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - -
Date{{ `${expense.year}/${expense.month}/${expense.day}` | date }}Amount{{ (expense.cents / 100) | currency: 'USD' }}Category{{ expense.category.name }}Merchant{{ expense.merchant?.name ?? '--' }}
-
-
+ + + + +
diff --git a/src/app/components/expense-list/expense-list.component.scss b/src/app/components/expense-list/expense-list.component.scss index e69de29..818821c 100644 --- a/src/app/components/expense-list/expense-list.component.scss +++ b/src/app/components/expense-list/expense-list.component.scss @@ -0,0 +1,4 @@ +.expense-list-container { + display: grid; + gap: 1rem; +} diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts index 08864c8..6ebaf88 100644 --- a/src/app/components/expense-list/expense-list.component.ts +++ b/src/app/components/expense-list/expense-list.component.ts @@ -3,10 +3,11 @@ import { ExpenseService } from '../../services/expense.service'; import { MatTableModule } from '@angular/material/table'; import { CurrencyPipe, DatePipe } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; +import {ExpenseComponent} from '../expense/expense.component'; @Component({ selector: 'app-expense-list', - imports: [MatTableModule, MatCardModule, DatePipe, CurrencyPipe], + imports: [MatTableModule, MatCardModule, DatePipe, CurrencyPipe, ExpenseComponent], templateUrl: './expense-list.component.html', styleUrl: './expense-list.component.scss', }) diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html new file mode 100644 index 0000000..66f6e56 --- /dev/null +++ b/src/app/components/expense/expense.component.html @@ -0,0 +1,96 @@ +
+ + + @if (state() === 'add') { +
+ Track new Expense + +
+ } + @if (state() === 'view') { +
+ {{ `${expense()?.year}-${expense()?.month}-${expense()?.day}` | date }} + + +
+ } +
+ + + @if (state() === 'add') { +
+ + Date + + + + + + + Cents + + + + + Category + + + @for (category of categories(); track category.id) { + {{ category.name }} + } + + + + + Merchant + + + @for (merchant of merchants(); track merchant.id) { + {{ merchant.name }} + } + + + + + Note + + + + + Tags + + @for (tag of tags(); track tag.id) { + {{ tag.name }} + } + + +
+ } + @if (state() === 'view') { +
+
+ Amount: {{ expense()?.cents! / 100 | currency}} +
+ +
+ Category: {{ expense()?.category!.name }} +
+ +
+ Merchant: {{ expense()?.merchant?.name ?? '--' }} +
+
+ +
+ Note: {{ expense()?.note ?? '--' }} +
+ +
+ Tags: +
+ } +
+
+
diff --git a/src/app/components/add-expense/add-expense.component.scss b/src/app/components/expense/expense.component.scss similarity index 57% rename from src/app/components/add-expense/add-expense.component.scss rename to src/app/components/expense/expense.component.scss index 790f538..08a52af 100644 --- a/src/app/components/add-expense/add-expense.component.scss +++ b/src/app/components/expense/expense.component.scss @@ -1,23 +1,27 @@ -.add-expense-container { +.expense-container { width: 100%; display: flex; flex-direction: column; align-items: center; -} -mat-card { - max-width: 800px; - width: 100%; + mat-card { + max-width: 800px; + width: 100%; + } } mat-card-header { padding-bottom: 1rem; - display: flex; - align-items: center; - justify-content: space-between; + + .expense-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + } } -mat-card-content { +.expense-content { display: grid; } @@ -26,14 +30,14 @@ mat-form-field { } @media (min-width: 550px) { - mat-card-content { + .expense-content { grid-template-columns: 1fr 1fr; gap: 1rem; } } @media (min-width: 800px) { - mat-card-content { + .expense-content { grid-template-columns: 1fr 1fr 1fr; } } diff --git a/src/app/components/add-expense/add-expense.component.ts b/src/app/components/expense/expense.component.ts similarity index 69% rename from src/app/components/add-expense/add-expense.component.ts rename to src/app/components/expense/expense.component.ts index 8bdbefc..11dd70a 100644 --- a/src/app/components/add-expense/add-expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, signal } from '@angular/core'; +import { Component, computed, input, signal } from '@angular/core'; import { Category, CategoryService } from '../../services/category.service'; import { Merchant, MerchantService } from '../../services/merchant.service'; import { Tag, TagService } from '../../services/tag.service'; @@ -11,8 +11,9 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { provideNativeDateAdapter } from '@angular/material/core'; -import { CreateExpense, ExpenseService } from '../../services/expense.service'; -import { DatePipe } from '@angular/common'; +import { CreateExpense, Expense, ExpenseService } from '../../services/expense.service'; +import {CurrencyPipe, DatePipe} from '@angular/common'; +import {MatIcon} from '@angular/material/icon'; interface ExpenseForm { date: Date | string; @@ -24,13 +25,17 @@ interface ExpenseForm { } @Component({ - selector: 'app-add-expense', - imports: [MatDatepickerModule, MatFormFieldModule, MatInputModule, MatCardModule, MatAutocompleteModule, MatSelectModule, MatButtonModule, FormField], + selector: 'app-expense', + imports: [MatDatepickerModule, MatFormFieldModule, MatInputModule, MatCardModule, MatAutocompleteModule, MatSelectModule, MatButtonModule, FormField, DatePipe, MatIcon, CurrencyPipe], providers: [provideNativeDateAdapter(), DatePipe], - templateUrl: './add-expense.component.html', - styleUrl: './add-expense.component.scss', + templateUrl: './expense.component.html', + styleUrl: './expense.component.scss', }) -export class AddExpenseComponent { +export class ExpenseComponent { + public expense = input(); + protected expenseMode = signal<'view' | 'edit'>('view'); + protected state = computed<'add' | 'view' | 'edit'>(() => this.expense() ? this.expenseMode() : 'add'); + protected saving = signal(false); protected categories = computed(() => this.categoryService.categories()); protected merchants = computed(() => this.merchantService.merchants()); protected tags = computed(() => this.tagService.tags()); @@ -41,19 +46,24 @@ export class AddExpenseComponent { const merchantValid = this.expenseForm.merchant().valid(); const noteValid = this.expenseForm.note().valid(); - return dateValid && centsValid && categoryValid && merchantValid && noteValid; + return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving(); }); - protected saving = signal(false); - private defaultFormState: ExpenseForm = { - date: '', - cents: NaN, - category: '', - merchant: '', - note: '', - tags: [] + private lastSelectedDate: Date | undefined; + private expenseDate = computed(() => { + return this.expense() + ? new Date(`${this.expense()?.year}-${this.expense()?.month}-${this.expense()?.day}`) + : this.lastSelectedDate ?? ''; + }); + private defaultForm: ExpenseForm = { + date: this.expenseDate(), + cents: this.expense()?.cents ?? NaN, + category: this.expense()?.category ?? '', + merchant: this.expense()?.merchant ?? '', + note: this.expense()?.note ?? '', + tags: this.expense()?.tags ?? [] }; - private expenseModel = signal(this.defaultFormState); + private expenseModel = signal(this.defaultForm); public expenseForm = form(this.expenseModel, (schema) => { required(schema.date); required(schema.cents); @@ -84,7 +94,8 @@ export class AddExpenseComponent { this.saving.set(true); try { await this.expenseService.postExpense(expense); - this.expenseModel.set({ ...this.defaultFormState, date: saveExpenseModel.date }); + this.lastSelectedDate = saveExpenseModel.date ? saveExpenseModel.date as Date : undefined; + this.expenseModel.set({ ...this.defaultForm, date: saveExpenseModel.date }); this.expenseForm().reset(this.expenseModel()); } catch (error) { diff --git a/src/app/pages/expenses/expenses.html b/src/app/pages/expenses/expenses.html index 7d8dce9..2add09d 100644 --- a/src/app/pages/expenses/expenses.html +++ b/src/app/pages/expenses/expenses.html @@ -1,4 +1,4 @@
- +
diff --git a/src/app/pages/expenses/expenses.ts b/src/app/pages/expenses/expenses.ts index 90bd738..3132b93 100644 --- a/src/app/pages/expenses/expenses.ts +++ b/src/app/pages/expenses/expenses.ts @@ -1,12 +1,12 @@ import { Component } from '@angular/core'; import { ExpenseListComponent } from '../../components/expense-list/expense-list.component'; -import { AddExpenseComponent } from '../../components/add-expense/add-expense.component'; +import { ExpenseComponent } from '../../components/expense/expense.component'; @Component({ selector: 'app-expenses', imports: [ ExpenseListComponent, - AddExpenseComponent + ExpenseComponent ], templateUrl: './expenses.html', styleUrl: './expenses.scss' -- 2.47.3 From e127b8ec45acf28fb355a2ff19e595befc4bfe9c Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Mon, 16 Feb 2026 15:41:15 -0600 Subject: [PATCH 12/18] added expense view mode --- package-lock.json | 19 ++++ package.json | 3 +- src/app/app.routes.ts | 8 +- .../expense-list/expense-list.component.html | 32 ------- .../expense-list/expense-list.component.scss | 4 - .../expense-list/expense-list.component.ts | 6 +- .../expense-add/expense-add.component.html | 1 + .../expense-add/expense-add.component.scss} | 0 .../expense-add/expense-add.component.ts | 11 +++ .../expense-form/expense-form.component.html | 52 +++++++++++ .../expense-form/expense-form.component.scss | 0 .../expense-form/expense-form.component.ts | 87 +++++++++++++++++ .../expense-view/expense-view.component.html | 1 + .../expense-view/expense-view.component.scss | 0 .../expense-view/expense-view.component.ts | 11 +++ .../components/expense/expense.component.html | 93 +++++++++++++------ .../components/expense/expense.component.scss | 22 +++++ .../components/expense/expense.component.ts | 89 +++++++++++------- ...enses.html => expense-page.component.html} | 2 +- ...enses.scss => expense-page.component.scss} | 0 ...{expenses.ts => expense-page.component.ts} | 6 +- .../{home.html => home-page.component.html} | 0 src/app/pages/home/home-page.component.scss | 0 .../home/{home.ts => home-page.component.ts} | 6 +- src/app/services/expense.service.ts | 21 ++--- 25 files changed, 349 insertions(+), 125 deletions(-) create mode 100644 src/app/components/expense/expense-add/expense-add.component.html rename src/app/{pages/home/home.scss => components/expense/expense-add/expense-add.component.scss} (100%) create mode 100644 src/app/components/expense/expense-add/expense-add.component.ts create mode 100644 src/app/components/expense/expense-form/expense-form.component.html create mode 100644 src/app/components/expense/expense-form/expense-form.component.scss create mode 100644 src/app/components/expense/expense-form/expense-form.component.ts create mode 100644 src/app/components/expense/expense-view/expense-view.component.html create mode 100644 src/app/components/expense/expense-view/expense-view.component.scss create mode 100644 src/app/components/expense/expense-view/expense-view.component.ts rename src/app/pages/expenses/{expenses.html => expense-page.component.html} (65%) rename src/app/pages/expenses/{expenses.scss => expense-page.component.scss} (100%) rename src/app/pages/expenses/{expenses.ts => expense-page.component.ts} (72%) rename src/app/pages/home/{home.html => home-page.component.html} (100%) create mode 100644 src/app/pages/home/home-page.component.scss rename src/app/pages/home/{home.ts => home-page.component.ts} (59%) diff --git a/package-lock.json b/package-lock.json index 37680ab..907712b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@angular/material": "~21.1.4", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "@js-temporal/polyfill": "^0.5.1", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -2073,6 +2074,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-temporal/polyfill": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", + "license": "ISC", + "dependencies": { + "jsbi": "^4.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", @@ -5891,6 +5904,12 @@ "dev": true, "license": "MIT" }, + "node_modules/jsbi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", + "license": "Apache-2.0" + }, "node_modules/jsdom": { "version": "27.4.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", diff --git a/package.json b/package.json index e3c47c6..419698f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@angular/material": "~21.1.4", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "@js-temporal/polyfill": "^0.5.1", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -43,4 +44,4 @@ "typescript": "~5.9.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index a24f70e..5a89bba 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,14 +1,14 @@ import { Routes } from '@angular/router'; -import { Expenses } from './pages/expenses/expenses'; -import { Home } from './pages/home/home'; +import { ExpensePage } from './pages/expenses/expense-page.component'; +import { HomePage } from './pages/home/home-page.component'; export const routes: Routes = [ { path: '', - component: Home + component: HomePage }, { path: 'expenses', - component: Expenses + component: ExpensePage } ]; diff --git a/src/app/components/expense-list/expense-list.component.html b/src/app/components/expense-list/expense-list.component.html index 080cafc..ffd5211 100644 --- a/src/app/components/expense-list/expense-list.component.html +++ b/src/app/components/expense-list/expense-list.component.html @@ -2,36 +2,4 @@ @for (expense of expenses(); track expense.id) { } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/app/components/expense-list/expense-list.component.scss b/src/app/components/expense-list/expense-list.component.scss index 818821c..e69de29 100644 --- a/src/app/components/expense-list/expense-list.component.scss +++ b/src/app/components/expense-list/expense-list.component.scss @@ -1,4 +0,0 @@ -.expense-list-container { - display: grid; - gap: 1rem; -} diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts index 6ebaf88..88195b3 100644 --- a/src/app/components/expense-list/expense-list.component.ts +++ b/src/app/components/expense-list/expense-list.component.ts @@ -1,19 +1,17 @@ import { Component, computed } from '@angular/core'; import { ExpenseService } from '../../services/expense.service'; import { MatTableModule } from '@angular/material/table'; -import { CurrencyPipe, DatePipe } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; -import {ExpenseComponent} from '../expense/expense.component'; +import { ExpenseComponent } from '../expense/expense.component'; @Component({ selector: 'app-expense-list', - imports: [MatTableModule, MatCardModule, DatePipe, CurrencyPipe, ExpenseComponent], + imports: [MatTableModule, MatCardModule, ExpenseComponent], templateUrl: './expense-list.component.html', styleUrl: './expense-list.component.scss', }) export class ExpenseListComponent { protected expenses = computed(() => this.expensesService.expenses()) - protected columns = ['date', 'amount', 'category', 'merchant']; public constructor(private readonly expensesService: ExpenseService) { } } diff --git a/src/app/components/expense/expense-add/expense-add.component.html b/src/app/components/expense/expense-add/expense-add.component.html new file mode 100644 index 0000000..a881b02 --- /dev/null +++ b/src/app/components/expense/expense-add/expense-add.component.html @@ -0,0 +1 @@ +

expense-add works!

diff --git a/src/app/pages/home/home.scss b/src/app/components/expense/expense-add/expense-add.component.scss similarity index 100% rename from src/app/pages/home/home.scss rename to src/app/components/expense/expense-add/expense-add.component.scss diff --git a/src/app/components/expense/expense-add/expense-add.component.ts b/src/app/components/expense/expense-add/expense-add.component.ts new file mode 100644 index 0000000..5726791 --- /dev/null +++ b/src/app/components/expense/expense-add/expense-add.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-expense-add', + imports: [], + templateUrl: './expense-add.component.html', + styleUrl: './expense-add.component.scss', +}) +export class ExpenseAddComponent { + +} diff --git a/src/app/components/expense/expense-form/expense-form.component.html b/src/app/components/expense/expense-form/expense-form.component.html new file mode 100644 index 0000000..9e5b53c --- /dev/null +++ b/src/app/components/expense/expense-form/expense-form.component.html @@ -0,0 +1,52 @@ +
+ + Date + + + + + + + + + + + + Cents + + + + + Category + + + @for (category of categories(); track category.id) { + {{ category.name }} + } + + + + + Merchant + + + @for (merchant of merchants(); track merchant.id) { + {{ merchant.name }} + } + + + + + Note + + + + + Tags + + + + + + +
diff --git a/src/app/components/expense/expense-form/expense-form.component.scss b/src/app/components/expense/expense-form/expense-form.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/expense/expense-form/expense-form.component.ts b/src/app/components/expense/expense-form/expense-form.component.ts new file mode 100644 index 0000000..884758d --- /dev/null +++ b/src/app/components/expense/expense-form/expense-form.component.ts @@ -0,0 +1,87 @@ +import {Component, computed, input, output, signal} from '@angular/core'; +import { Expense } from '../../../services/expense.service'; +import { Category } from '../../../services/category.service'; +import { Merchant } from '../../../services/merchant.service'; +import { Tag } from '../../../services/tag.service'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import {form, min, required} from '@angular/forms/signals'; + +interface ExpenseForm { + date: Date | string; + cents: number; + category: Category | string; + merchant: Merchant | string; + note: string; + tags: Tag[]; +} + +@Component({ + selector: 'app-expense-form', + imports: [ + MatAutocompleteModule, + MatDatepickerModule, + MatInputModule, + MatSelectModule + ], + templateUrl: './expense-form.component.html', + styleUrl: './expense-form.component.scss', +}) +export class ExpenseFormComponent { + public disabled = input(); // TODO: should this even exist? + public expense = input(undefined); + public categories = input([]); + public merchants = input([]); + public tags = input([]); + + public valid = output(); + public value = output(); // TODO: form value or expense value? + + + // protected enableSaveButton = computed(() => { + // const dateValid = this.expenseForm.date().valid(); + // const centsValid = this.expenseForm.cents().valid(); + // const categoryValid = this.expenseForm.category().valid(); + // const merchantValid = this.expenseForm.merchant().valid(); + // const noteValid = this.expenseForm.note().valid(); + // + // return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving(); + // }); + + private lastSelectedDate = signal(undefined); + + private expenseDate = computed(() => { + const lastDate = this.lastSelectedDate(); + const expense = this.expense(); + + if (expense) { + + } + return this.expense() + ? new Date() + : this.lastSelectedDate ?? ''; + }); + + private defaultForm: ExpenseForm = { + date: new Date(), + cents: this.expense()?.cents ?? NaN, + category: this.expense()?.category ?? '', + merchant: this.expense()?.merchant ?? '', + note: this.expense()?.note ?? '', + tags: this.expense()?.tags ?? [] + }; + private expenseModel = signal(this.defaultForm); + public expenseForm = form(this.expenseModel, (schema) => { + required(schema.date); + required(schema.cents); + min(schema.cents, 1); + required(schema.category); + }); + + + public autocompleteDisplay(value: Merchant | Category) { + return value.name ?? null; + } +} diff --git a/src/app/components/expense/expense-view/expense-view.component.html b/src/app/components/expense/expense-view/expense-view.component.html new file mode 100644 index 0000000..fb4707d --- /dev/null +++ b/src/app/components/expense/expense-view/expense-view.component.html @@ -0,0 +1 @@ +

expense-view works!

diff --git a/src/app/components/expense/expense-view/expense-view.component.scss b/src/app/components/expense/expense-view/expense-view.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/expense/expense-view/expense-view.component.ts b/src/app/components/expense/expense-view/expense-view.component.ts new file mode 100644 index 0000000..3b626fd --- /dev/null +++ b/src/app/components/expense/expense-view/expense-view.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-expense-view', + imports: [], + templateUrl: './expense-view.component.html', + styleUrl: './expense-view.component.scss', +}) +export class ExpenseViewComponent { + +} diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html index 66f6e56..c16825a 100644 --- a/src/app/components/expense/expense.component.html +++ b/src/app/components/expense/expense.component.html @@ -1,17 +1,25 @@
- @if (state() === 'add') { + @if (state() === 'add' || state() === 'edit') {
- Track new Expense + + @if (state() === 'add') { + Track new Expense + } @else { + Edit Expense + } +
} @if (state() === 'view') {
- {{ `${expense()?.year}-${expense()?.month}-${expense()?.day}` | date }} + + {{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency}} + -
@@ -19,13 +27,18 @@
- @if (state() === 'add') { +
Date - + @if (state() === 'view') { + + + } @else { + + } - + @@ -67,30 +80,56 @@
- } - @if (state() === 'view') { -
-
- Amount: {{ expense()?.cents! / 100 | currency}} -
+ + + + + + + + -
- Category: {{ expense()?.category!.name }} -
+ + + + -
- Merchant: {{ expense()?.merchant?.name ?? '--' }} -
-
+ + + + + + + + + -
- Note: {{ expense()?.note ?? '--' }} -
+ + + + + + + + + -
- Tags: -
- } + + + + + + + + + + + + + + + +
diff --git a/src/app/components/expense/expense.component.scss b/src/app/components/expense/expense.component.scss index 08a52af..96c5a02 100644 --- a/src/app/components/expense/expense.component.scss +++ b/src/app/components/expense/expense.component.scss @@ -29,15 +29,37 @@ mat-form-field { width: 100%; } +.view-tags { + display: flex; + gap: 0.5rem; + align-items: center; +} + @media (min-width: 550px) { .expense-content { grid-template-columns: 1fr 1fr; gap: 1rem; } + + .view-note { + grid-column: 1 / span 2; + } + + .view-tags { + grid-column: 1 / span 2; + } } @media (min-width: 800px) { .expense-content { grid-template-columns: 1fr 1fr 1fr; } + + .view-note { + grid-column: 2 / span 2; + } + + .view-tags { + grid-column: 1 / span 3; + } } diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 11dd70a..0dc286b 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -12,8 +12,10 @@ import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { provideNativeDateAdapter } from '@angular/material/core'; import { CreateExpense, Expense, ExpenseService } from '../../services/expense.service'; -import {CurrencyPipe, DatePipe} from '@angular/common'; -import {MatIcon} from '@angular/material/icon'; +import { CurrencyPipe, DatePipe } from '@angular/common'; +import { MatIcon } from '@angular/material/icon'; +import { Temporal } from '@js-temporal/polyfill'; +// import { MatChip } from '@angular/material/chips'; interface ExpenseForm { date: Date | string; @@ -26,7 +28,20 @@ interface ExpenseForm { @Component({ selector: 'app-expense', - imports: [MatDatepickerModule, MatFormFieldModule, MatInputModule, MatCardModule, MatAutocompleteModule, MatSelectModule, MatButtonModule, FormField, DatePipe, MatIcon, CurrencyPipe], + imports: [ + MatDatepickerModule, + MatFormFieldModule, + MatInputModule, + MatCardModule, + MatAutocompleteModule, + MatSelectModule, + MatButtonModule, + FormField, + DatePipe, + MatIcon, + CurrencyPipe, + // MatChip + ], providers: [provideNativeDateAdapter(), DatePipe], templateUrl: './expense.component.html', styleUrl: './expense.component.scss', @@ -49,14 +64,14 @@ export class ExpenseComponent { return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving(); }); - private lastSelectedDate: Date | undefined; - private expenseDate = computed(() => { - return this.expense() - ? new Date(`${this.expense()?.year}-${this.expense()?.month}-${this.expense()?.day}`) - : this.lastSelectedDate ?? ''; - }); + // private lastSelectedDate: Date | undefined; + // private expenseDate = computed(() => { + // return this.expense() + // ? new Date(`${this.expense()?.year}-${this.expense()?.month}-${this.expense()?.day}`) + // : this.lastSelectedDate ?? ''; + // }); private defaultForm: ExpenseForm = { - date: this.expenseDate(), + date: '', cents: this.expense()?.cents ?? NaN, category: this.expense()?.category ?? '', merchant: this.expense()?.merchant ?? '', @@ -78,32 +93,36 @@ export class ExpenseComponent { private readonly datePipe: DatePipe) { } public async saveClick(): Promise { - const saveExpenseModel = this.expenseModel(); - const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; - const expense: CreateExpense = { - year: date[0], - month: date[1], - day: date[2], - cents: saveExpenseModel.cents, - category: saveExpenseModel.category as Category, - merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined, - note: saveExpenseModel.note ? saveExpenseModel.note : undefined, - tags: saveExpenseModel.tags - }; + // const saveExpenseModel = this.expenseModel(); + // const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; + // const expense: CreateExpense = { + // year: date[0], + // month: date[1], + // day: date[2], + // cents: saveExpenseModel.cents, + // category: saveExpenseModel.category as Category, + // merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined, + // note: saveExpenseModel.note ? saveExpenseModel.note : undefined, + // tags: saveExpenseModel.tags + // }; + // + // this.saving.set(true); + // try { + // await this.expenseService.postExpense(expense); + // this.lastSelectedDate = saveExpenseModel.date ? saveExpenseModel.date as Date : undefined; + // this.expenseModel.set({ ...this.defaultForm, date: saveExpenseModel.date }); + // this.expenseForm().reset(this.expenseModel()); + // } + // catch (error) { + // console.error(error); + // } + // finally { + // this.saving.set(false); + // } + } - this.saving.set(true); - try { - await this.expenseService.postExpense(expense); - this.lastSelectedDate = saveExpenseModel.date ? saveExpenseModel.date as Date : undefined; - this.expenseModel.set({ ...this.defaultForm, date: saveExpenseModel.date }); - this.expenseForm().reset(this.expenseModel()); - } - catch (error) { - console.error(error); - } - finally { - this.saving.set(false); - } + public editClick(): void { + this.expenseMode.set('edit'); } public autocompleteDisplay(value: Merchant | Category) { diff --git a/src/app/pages/expenses/expenses.html b/src/app/pages/expenses/expense-page.component.html similarity index 65% rename from src/app/pages/expenses/expenses.html rename to src/app/pages/expenses/expense-page.component.html index 2add09d..4491c8b 100644 --- a/src/app/pages/expenses/expenses.html +++ b/src/app/pages/expenses/expense-page.component.html @@ -1,4 +1,4 @@
- +
diff --git a/src/app/pages/expenses/expenses.scss b/src/app/pages/expenses/expense-page.component.scss similarity index 100% rename from src/app/pages/expenses/expenses.scss rename to src/app/pages/expenses/expense-page.component.scss diff --git a/src/app/pages/expenses/expenses.ts b/src/app/pages/expenses/expense-page.component.ts similarity index 72% rename from src/app/pages/expenses/expenses.ts rename to src/app/pages/expenses/expense-page.component.ts index 3132b93..18f24e0 100644 --- a/src/app/pages/expenses/expenses.ts +++ b/src/app/pages/expenses/expense-page.component.ts @@ -8,7 +8,7 @@ import { ExpenseComponent } from '../../components/expense/expense.component'; ExpenseListComponent, ExpenseComponent ], - templateUrl: './expenses.html', - styleUrl: './expenses.scss' + templateUrl: './expense-page.component.html', + styleUrl: './expense-page.component.scss' }) -export class Expenses { } +export class ExpensePage { } diff --git a/src/app/pages/home/home.html b/src/app/pages/home/home-page.component.html similarity index 100% rename from src/app/pages/home/home.html rename to src/app/pages/home/home-page.component.html diff --git a/src/app/pages/home/home-page.component.scss b/src/app/pages/home/home-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/home/home.ts b/src/app/pages/home/home-page.component.ts similarity index 59% rename from src/app/pages/home/home.ts rename to src/app/pages/home/home-page.component.ts index 7c1a5e2..724172f 100644 --- a/src/app/pages/home/home.ts +++ b/src/app/pages/home/home-page.component.ts @@ -6,7 +6,7 @@ import { RouterLink } from '@angular/router'; imports: [ RouterLink ], - templateUrl: './home.html', - styleUrl: './home.scss', + templateUrl: './home-page.component.html', + styleUrl: './home-page.component.scss', }) -export class Home { } +export class HomePage { } diff --git a/src/app/services/expense.service.ts b/src/app/services/expense.service.ts index 2814357..ac11b1e 100644 --- a/src/app/services/expense.service.ts +++ b/src/app/services/expense.service.ts @@ -3,6 +3,7 @@ import { Category } from './category.service'; import { Merchant } from './merchant.service'; import { Tag } from './tag.service'; import { HttpService } from './http.service'; +import { Temporal } from '@js-temporal/polyfill'; @Injectable({ providedIn: 'root', @@ -21,18 +22,18 @@ export class ExpenseService { } public async postExpense(expense: CreateExpense): Promise { - const createdExpense = await this.http.post(this.expensePath, expense); + // const createdExpense = await this.http.post(this.expensePath, expense); + console.log(expense); await this.fetchExpenses(); - return createdExpense; + // return createdExpense; + return { ...expense, id: '', category: { id: '', name: ''}, tags: [] }; } } export interface Expense { id: string; - year: string; - month: string; - day: string; + date: Temporal.PlainDate; cents: number; category: Category; note?: string; @@ -41,12 +42,10 @@ export interface Expense { } export interface CreateExpense { - year: string; - month: string; - day: string; + date: Temporal.PlainDate; cents: number; - category: Category; + categoryId: string; note?: string; - merchant?: Merchant; - tags: Tag[]; + merchantId?: string; + tagIds?: string[]; } -- 2.47.3 From 2ca674c3bb4410ffab7aa9c42dd132c26f35d59f Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Thu, 19 Feb 2026 14:19:25 -0600 Subject: [PATCH 13/18] break expense component into smaller pieces --- .../expense-list/expense-list.component.html | 1 + .../expense-list/expense-list.component.scss | 4 + .../expense-add/expense-add.component.html | 1 - .../expense-add/expense-add.component.scss | 0 .../expense-add/expense-add.component.ts | 11 -- .../expense-form/expense-form.component.html | 25 ++- .../expense-form/expense-form.component.scss | 20 +++ .../expense-form/expense-form.component.ts | 127 +++++++------- .../expense-view/expense-view.component.html | 1 - .../expense-view/expense-view.component.scss | 0 .../expense-view/expense-view.component.ts | 11 -- .../components/expense/expense.component.html | 146 +++------------ .../components/expense/expense.component.scss | 45 +---- .../components/expense/expense.component.ts | 166 ++++++------------ .../expenses/expense-page.component.html | 2 +- src/app/services/category.service.ts | 2 - src/app/services/expense.service.ts | 22 ++- src/styles.scss | 2 +- 18 files changed, 197 insertions(+), 389 deletions(-) delete mode 100644 src/app/components/expense/expense-add/expense-add.component.html delete mode 100644 src/app/components/expense/expense-add/expense-add.component.scss delete mode 100644 src/app/components/expense/expense-add/expense-add.component.ts delete mode 100644 src/app/components/expense/expense-view/expense-view.component.html delete mode 100644 src/app/components/expense/expense-view/expense-view.component.scss delete mode 100644 src/app/components/expense/expense-view/expense-view.component.ts diff --git a/src/app/components/expense-list/expense-list.component.html b/src/app/components/expense-list/expense-list.component.html index ffd5211..18a7201 100644 --- a/src/app/components/expense-list/expense-list.component.html +++ b/src/app/components/expense-list/expense-list.component.html @@ -1,4 +1,5 @@
+

Expenses

@for (expense of expenses(); track expense.id) { } diff --git a/src/app/components/expense-list/expense-list.component.scss b/src/app/components/expense-list/expense-list.component.scss index e69de29..818821c 100644 --- a/src/app/components/expense-list/expense-list.component.scss +++ b/src/app/components/expense-list/expense-list.component.scss @@ -0,0 +1,4 @@ +.expense-list-container { + display: grid; + gap: 1rem; +} diff --git a/src/app/components/expense/expense-add/expense-add.component.html b/src/app/components/expense/expense-add/expense-add.component.html deleted file mode 100644 index a881b02..0000000 --- a/src/app/components/expense/expense-add/expense-add.component.html +++ /dev/null @@ -1 +0,0 @@ -

expense-add works!

diff --git a/src/app/components/expense/expense-add/expense-add.component.scss b/src/app/components/expense/expense-add/expense-add.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/components/expense/expense-add/expense-add.component.ts b/src/app/components/expense/expense-add/expense-add.component.ts deleted file mode 100644 index 5726791..0000000 --- a/src/app/components/expense/expense-add/expense-add.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-expense-add', - imports: [], - templateUrl: './expense-add.component.html', - styleUrl: './expense-add.component.scss', -}) -export class ExpenseAddComponent { - -} diff --git a/src/app/components/expense/expense-form/expense-form.component.html b/src/app/components/expense/expense-form/expense-form.component.html index 9e5b53c..4822a8b 100644 --- a/src/app/components/expense/expense-form/expense-form.component.html +++ b/src/app/components/expense/expense-form/expense-form.component.html @@ -1,24 +1,19 @@
Date - - - - - - + Cents - + Category - + @for (category of categories(); track category.id) { {{ category.name }} @@ -28,7 +23,7 @@ Merchant - + @for (merchant of merchants(); track merchant.id) { {{ merchant.name }} @@ -38,15 +33,15 @@ Note - + Tags - - - - - + + @for (tag of tags(); track tag.id) { + {{ tag.name }} + } +
diff --git a/src/app/components/expense/expense-form/expense-form.component.scss b/src/app/components/expense/expense-form/expense-form.component.scss index e69de29..2e8b1c2 100644 --- a/src/app/components/expense/expense-form/expense-form.component.scss +++ b/src/app/components/expense/expense-form/expense-form.component.scss @@ -0,0 +1,20 @@ +.expense-form-container { + display: grid; + + mat-form-field { + width: 100%; + } +} + +@media (min-width: 550px) { + .expense-form-container { + grid-template-columns: 1fr 1fr; + gap: 1rem; + } +} + +@media (min-width: 800px) { + .expense-form-container { + grid-template-columns: 1fr 1fr 1fr; + } +} diff --git a/src/app/components/expense/expense-form/expense-form.component.ts b/src/app/components/expense/expense-form/expense-form.component.ts index 884758d..02b93c5 100644 --- a/src/app/components/expense/expense-form/expense-form.component.ts +++ b/src/app/components/expense/expense-form/expense-form.component.ts @@ -1,87 +1,96 @@ -import {Component, computed, input, output, signal} from '@angular/core'; +import { Component, computed, effect, input, OnInit, output, signal } from '@angular/core'; +import { min, required, disabled, form, FormField, Schema, schema } from '@angular/forms/signals'; import { Expense } from '../../../services/expense.service'; -import { Category } from '../../../services/category.service'; -import { Merchant } from '../../../services/merchant.service'; -import { Tag } from '../../../services/tag.service'; +import { Category, CategoryService } from '../../../services/category.service'; +import { Merchant, MerchantService } from '../../../services/merchant.service'; +import { Tag, TagService } from '../../../services/tag.service'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import {form, min, required} from '@angular/forms/signals'; +import { provideNativeDateAdapter } from '@angular/material/core'; interface ExpenseForm { - date: Date | string; + date: Date; cents: number; - category: Category | string; - merchant: Merchant | string; + category: Category; + merchant: Merchant; note: string; tags: Tag[]; } @Component({ selector: 'app-expense-form', - imports: [ - MatAutocompleteModule, - MatDatepickerModule, - MatInputModule, - MatSelectModule - ], + imports: [MatAutocompleteModule, MatDatepickerModule, MatInputModule, MatSelectModule, FormField], + providers: [provideNativeDateAdapter()], templateUrl: './expense-form.component.html', styleUrl: './expense-form.component.scss', }) -export class ExpenseFormComponent { - public disabled = input(); // TODO: should this even exist? - public expense = input(undefined); - public categories = input([]); - public merchants = input([]); - public tags = input([]); +export class ExpenseFormComponent implements OnInit { + public expense = input(); + public disabled = input(false); public valid = output(); - public value = output(); // TODO: form value or expense value? - - - // protected enableSaveButton = computed(() => { - // const dateValid = this.expenseForm.date().valid(); - // const centsValid = this.expenseForm.cents().valid(); - // const categoryValid = this.expenseForm.category().valid(); - // const merchantValid = this.expenseForm.merchant().valid(); - // const noteValid = this.expenseForm.note().valid(); - // - // return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving(); - // }); + public dirty = output() + public value = output(); + private formValid = computed(() => this.form().valid() && this.form().dirty()); + private formDirty = computed(() => this.form().dirty() && this.form().touched()); private lastSelectedDate = signal(undefined); + private formData = computed(() => this.buildForm(this.expense())); + private formModel = signal(this.formData()); - private expenseDate = computed(() => { - const lastDate = this.lastSelectedDate(); - const expense = this.expense(); + protected categories = computed(() => this.categoryService.categories()); + protected merchants = computed(() => this.merchantService.merchants()); + protected tags = computed(() => this.tagService.tags()); + protected form = form(this.formModel, this.buildFormOptions()); - if (expense) { - - } - return this.expense() - ? new Date() - : this.lastSelectedDate ?? ''; - }); - - private defaultForm: ExpenseForm = { - date: new Date(), - cents: this.expense()?.cents ?? NaN, - category: this.expense()?.category ?? '', - merchant: this.expense()?.merchant ?? '', - note: this.expense()?.note ?? '', - tags: this.expense()?.tags ?? [] - }; - private expenseModel = signal(this.defaultForm); - public expenseForm = form(this.expenseModel, (schema) => { - required(schema.date); - required(schema.cents); - min(schema.cents, 1); - required(schema.category); - }); + constructor(private readonly categoryService: CategoryService, + private readonly merchantService: MerchantService, + private readonly tagService: TagService) { + effect(() => { + this.valid.emit(this.formValid()); + this.dirty.emit(this.formDirty()); + }); + } + public ngOnInit(): void { + console.log({ expense: this.expense() }); + this.formModel.set(this.formData()); + this.form().reset(); + } public autocompleteDisplay(value: Merchant | Category) { - return value.name ?? null; + return value?.name ?? null; + } + + private buildForm(expense?: Expense): ExpenseForm { + let formData = { date: this.lastSelectedDate() ?? '', cents: '', category: '', merchant: '', note: '', tags: ''} as unknown as ExpenseForm + if (expense) { + formData = { + date: new Date(expense.date.toString()), + cents: expense.cents, + category: expense.category, + merchant: expense.merchant, + note: expense.note, + tags: expense.tags ?? [] + } as ExpenseForm; + } + return formData; + } + + private buildFormOptions(): Schema { + return schema((schema) => { + required(schema.date); + required(schema.cents); + required(schema.category); + min(schema.cents, 1); + disabled(schema.date, () => this.disabled()); + disabled(schema.cents, () => this.disabled()); + disabled(schema.category, () => this.disabled()); + disabled(schema.merchant, () => this.disabled()); + disabled(schema.note, () => this.disabled()); + disabled(schema.tags, () => this.disabled()); + }); } } diff --git a/src/app/components/expense/expense-view/expense-view.component.html b/src/app/components/expense/expense-view/expense-view.component.html deleted file mode 100644 index fb4707d..0000000 --- a/src/app/components/expense/expense-view/expense-view.component.html +++ /dev/null @@ -1 +0,0 @@ -

expense-view works!

diff --git a/src/app/components/expense/expense-view/expense-view.component.scss b/src/app/components/expense/expense-view/expense-view.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/components/expense/expense-view/expense-view.component.ts b/src/app/components/expense/expense-view/expense-view.component.ts deleted file mode 100644 index 3b626fd..0000000 --- a/src/app/components/expense/expense-view/expense-view.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-expense-view', - imports: [], - templateUrl: './expense-view.component.html', - styleUrl: './expense-view.component.scss', -}) -export class ExpenseViewComponent { - -} diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html index c16825a..b3916d4 100644 --- a/src/app/components/expense/expense.component.html +++ b/src/app/components/expense/expense.component.html @@ -1,135 +1,29 @@
- @if (state() === 'add' || state() === 'edit') { -
- - @if (state() === 'add') { - Track new Expense - } @else { - Edit Expense - } - - -
- } - @if (state() === 'view') { -
- - {{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency}} - - - -
- } +
+ @if (!expense()) { + Add new Expense + + + } @else { + @if (!editingExpense()) { + {{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency }} + + } @else { + Update Expense + + + } + } +
- -
- - Date - @if (state() === 'view') { - - - } @else { - - } - - - - - - Cents - - - - - Category - - - @for (category of categories(); track category.id) { - {{ category.name }} - } - - - - - Merchant - - - @for (merchant of merchants(); track merchant.id) { - {{ merchant.name }} - } - - - - - Note - - - - - Tags - - @for (tag of tags(); track tag.id) { - {{ tag.name }} - } - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
diff --git a/src/app/components/expense/expense.component.scss b/src/app/components/expense/expense.component.scss index 96c5a02..8a9e4cf 100644 --- a/src/app/components/expense/expense.component.scss +++ b/src/app/components/expense/expense.component.scss @@ -17,49 +17,6 @@ mat-card-header { width: 100%; display: flex; align-items: center; - justify-content: space-between; - } -} - -.expense-content { - display: grid; -} - -mat-form-field { - width: 100%; -} - -.view-tags { - display: flex; - gap: 0.5rem; - align-items: center; -} - -@media (min-width: 550px) { - .expense-content { - grid-template-columns: 1fr 1fr; - gap: 1rem; - } - - .view-note { - grid-column: 1 / span 2; - } - - .view-tags { - grid-column: 1 / span 2; - } -} - -@media (min-width: 800px) { - .expense-content { - grid-template-columns: 1fr 1fr 1fr; - } - - .view-note { - grid-column: 2 / span 2; - } - - .view-tags { - grid-column: 1 / span 3; + gap: 0.5rem; } } diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 0dc286b..52fa70a 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -1,131 +1,73 @@ -import { Component, computed, input, signal } from '@angular/core'; -import { Category, CategoryService } from '../../services/category.service'; -import { Merchant, MerchantService } from '../../services/merchant.service'; -import { Tag, TagService } from '../../services/tag.service'; -import { form, FormField, min, required } from '@angular/forms/signals'; +import { Component, input, model, signal } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; -import { MatInputModule } from '@angular/material/input'; -import { MatDatepickerModule } from '@angular/material/datepicker'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; -import { provideNativeDateAdapter } from '@angular/material/core'; -import { CreateExpense, Expense, ExpenseService } from '../../services/expense.service'; -import { CurrencyPipe, DatePipe } from '@angular/common'; +import { Expense, ExpenseService } from '../../services/expense.service'; import { MatIcon } from '@angular/material/icon'; -import { Temporal } from '@js-temporal/polyfill'; -// import { MatChip } from '@angular/material/chips'; - -interface ExpenseForm { - date: Date | string; - cents: number; - category: Category | string; - merchant: Merchant | string; - note: string; - tags: Tag[]; -} +import { ExpenseFormComponent } from './expense-form/expense-form.component'; +import { CurrencyPipe, DatePipe } from '@angular/common'; @Component({ selector: 'app-expense', - imports: [ - MatDatepickerModule, - MatFormFieldModule, - MatInputModule, - MatCardModule, - MatAutocompleteModule, - MatSelectModule, - MatButtonModule, - FormField, - DatePipe, - MatIcon, - CurrencyPipe, - // MatChip - ], - providers: [provideNativeDateAdapter(), DatePipe], + imports: [MatCardModule, MatButtonModule, MatIcon, ExpenseFormComponent, DatePipe, CurrencyPipe], + providers: [], templateUrl: './expense.component.html', styleUrl: './expense.component.scss', }) export class ExpenseComponent { public expense = input(); - protected expenseMode = signal<'view' | 'edit'>('view'); - protected state = computed<'add' | 'view' | 'edit'>(() => this.expense() ? this.expenseMode() : 'add'); - protected saving = signal(false); - protected categories = computed(() => this.categoryService.categories()); - protected merchants = computed(() => this.merchantService.merchants()); - protected tags = computed(() => this.tagService.tags()); - protected enableSaveButton = computed(() => { - const dateValid = this.expenseForm.date().valid(); - const centsValid = this.expenseForm.cents().valid(); - const categoryValid = this.expenseForm.category().valid(); - const merchantValid = this.expenseForm.merchant().valid(); - const noteValid = this.expenseForm.note().valid(); - return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving(); - }); + public editingExpense = signal(false); + public formValid = model(false); + public formDirty = model(false); - // private lastSelectedDate: Date | undefined; - // private expenseDate = computed(() => { - // return this.expense() - // ? new Date(`${this.expense()?.year}-${this.expense()?.month}-${this.expense()?.day}`) - // : this.lastSelectedDate ?? ''; - // }); - private defaultForm: ExpenseForm = { - date: '', - cents: this.expense()?.cents ?? NaN, - category: this.expense()?.category ?? '', - merchant: this.expense()?.merchant ?? '', - note: this.expense()?.note ?? '', - tags: this.expense()?.tags ?? [] - }; - private expenseModel = signal(this.defaultForm); - public expenseForm = form(this.expenseModel, (schema) => { - required(schema.date); - required(schema.cents); - min(schema.cents, 1); - required(schema.category); - }); - - public constructor(private readonly categoryService: CategoryService, - private readonly merchantService: MerchantService, - private readonly tagService: TagService, - private readonly expenseService: ExpenseService, - private readonly datePipe: DatePipe) { } - - public async saveClick(): Promise { - // const saveExpenseModel = this.expenseModel(); - // const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; - // const expense: CreateExpense = { - // year: date[0], - // month: date[1], - // day: date[2], - // cents: saveExpenseModel.cents, - // category: saveExpenseModel.category as Category, - // merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined, - // note: saveExpenseModel.note ? saveExpenseModel.note : undefined, - // tags: saveExpenseModel.tags - // }; - // - // this.saving.set(true); - // try { - // await this.expenseService.postExpense(expense); - // this.lastSelectedDate = saveExpenseModel.date ? saveExpenseModel.date as Date : undefined; - // this.expenseModel.set({ ...this.defaultForm, date: saveExpenseModel.date }); - // this.expenseForm().reset(this.expenseModel()); - // } - // catch (error) { - // console.error(error); - // } - // finally { - // this.saving.set(false); - // } - } + public constructor(private readonly expenseService: ExpenseService) { } public editClick(): void { - this.expenseMode.set('edit'); + this.editingExpense.set(true); } - public autocompleteDisplay(value: Merchant | Category) { - return value.name ?? null; + public async addClick(): Promise { + console.log('Adding new expense'); } + + public resetAddClick(): void { + console.log('Reset Add expense form'); + } + + public async updateClick(): Promise { + console.log('Updating expense') + this.editingExpense.set(false); + } + + public cancelUpdateClick(): void { + console.log('Canceling update'); + this.editingExpense.set(false); + } + + // const saveExpenseModel = this.expenseModel(); + // const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; + // const expense: CreateExpense = { + // year: date[0], + // month: date[1], + // day: date[2], + // cents: saveExpenseModel.cents, + // category: saveExpenseModel.category as Category, + // merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined, + // note: saveExpenseModel.note ? saveExpenseModel.note : undefined, + // tags: saveExpenseModel.tags + // }; + // + // this.saving.set(true); + // try { + // await this.expenseService.postExpense(expense); + // this.lastSelectedDate = saveExpenseModel.date ? saveExpenseModel.date as Date : undefined; + // this.expenseModel.set({ ...this.defaultForm, date: saveExpenseModel.date }); + // this.expenseForm().reset(this.expenseModel()); + // } + // catch (error) { + // console.error(error); + // } + // finally { + // this.saving.set(false); + // } } diff --git a/src/app/pages/expenses/expense-page.component.html b/src/app/pages/expenses/expense-page.component.html index 4491c8b..2add09d 100644 --- a/src/app/pages/expenses/expense-page.component.html +++ b/src/app/pages/expenses/expense-page.component.html @@ -1,4 +1,4 @@
- +
diff --git a/src/app/services/category.service.ts b/src/app/services/category.service.ts index d79dc47..211e6e8 100644 --- a/src/app/services/category.service.ts +++ b/src/app/services/category.service.ts @@ -9,8 +9,6 @@ export class CategoryService { public readonly categories = this.internalCategories.asReadonly(); public readonly categoryPath = 'http://localhost:3000/common-cents/categories'; - // public categories = signal([]); - public constructor(private readonly http: HttpService) { void this.fetchCategories(); } diff --git a/src/app/services/expense.service.ts b/src/app/services/expense.service.ts index ac11b1e..e782ae5 100644 --- a/src/app/services/expense.service.ts +++ b/src/app/services/expense.service.ts @@ -19,15 +19,23 @@ export class ExpenseService { public async fetchExpenses(): Promise { this.internalExpenses.set(await this.http.get(this.expensePath)); + console.log({ expenses: this.internalExpenses() }); // TODO: Remove } - public async postExpense(expense: CreateExpense): Promise { - // const createdExpense = await this.http.post(this.expensePath, expense); - console.log(expense); + public async postExpense(createExpense: CreateExpense): Promise { + const createdExpense = await this.http.post(this.expensePath, createExpense); await this.fetchExpenses(); + console.log({ createExpense, createdExpense}); // TODO: Remove - // return createdExpense; - return { ...expense, id: '', category: { id: '', name: ''}, tags: [] }; + return createdExpense; + } + + public async updateExpense(updateExpense: UpdateExpense): Promise { + const updatedExpense = await this.http.put(this.expensePath, updateExpense); + await this.fetchExpenses(); + console.log({ updateExpense, updatedExpense}); // TODO: Remove + + return updatedExpense; } } @@ -49,3 +57,7 @@ export interface CreateExpense { merchantId?: string; tagIds?: string[]; } + +export interface UpdateExpense extends CreateExpense { + id: string; +} diff --git a/src/styles.scss b/src/styles.scss index cf6f56c..ac47f6e 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -22,7 +22,7 @@ body { // Default the application to a light color theme. This can be changed to // `dark` to enable the dark color theme, or to `light dark` to defer to the // user's system settings. - color-scheme: light; // TODO: choose 'light dark' + color-scheme: light dark; // Set a default background, font and text colors for the application using // Angular Material's system-level CSS variables. Learn more about these -- 2.47.3 From 21e3741b2113f3184538717ab48b4b6e6b4882ab Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sun, 22 Feb 2026 15:26:10 -0600 Subject: [PATCH 14/18] added form resetting --- .../expense/expense-form/expense-form.component.ts | 14 ++++++++------ src/app/components/expense/expense.component.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/app/components/expense/expense-form/expense-form.component.ts b/src/app/components/expense/expense-form/expense-form.component.ts index 02b93c5..61d3fcb 100644 --- a/src/app/components/expense/expense-form/expense-form.component.ts +++ b/src/app/components/expense/expense-form/expense-form.component.ts @@ -29,14 +29,13 @@ interface ExpenseForm { export class ExpenseFormComponent implements OnInit { public expense = input(); public disabled = input(false); - + public lastDate = input(undefined); public valid = output(); public dirty = output() public value = output(); private formValid = computed(() => this.form().valid() && this.form().dirty()); private formDirty = computed(() => this.form().dirty() && this.form().touched()); - private lastSelectedDate = signal(undefined); private formData = computed(() => this.buildForm(this.expense())); private formModel = signal(this.formData()); @@ -46,8 +45,8 @@ export class ExpenseFormComponent implements OnInit { protected form = form(this.formModel, this.buildFormOptions()); constructor(private readonly categoryService: CategoryService, - private readonly merchantService: MerchantService, - private readonly tagService: TagService) { + private readonly merchantService: MerchantService, + private readonly tagService: TagService) { effect(() => { this.valid.emit(this.formValid()); this.dirty.emit(this.formDirty()); @@ -55,7 +54,10 @@ export class ExpenseFormComponent implements OnInit { } public ngOnInit(): void { - console.log({ expense: this.expense() }); + this.reset(); + } + + public reset(): void { this.formModel.set(this.formData()); this.form().reset(); } @@ -65,7 +67,7 @@ export class ExpenseFormComponent implements OnInit { } private buildForm(expense?: Expense): ExpenseForm { - let formData = { date: this.lastSelectedDate() ?? '', cents: '', category: '', merchant: '', note: '', tags: ''} as unknown as ExpenseForm + let formData = { date: this.lastDate() ?? '', cents: '', category: '', merchant: '', note: '', tags: ''} as unknown as ExpenseForm if (expense) { formData = { date: new Date(expense.date.toString()), diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 52fa70a..85506b4 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -1,4 +1,4 @@ -import { Component, input, model, signal } from '@angular/core'; +import { Component, input, model, signal, viewChild } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { Expense, ExpenseService } from '../../services/expense.service'; @@ -14,6 +14,7 @@ import { CurrencyPipe, DatePipe } from '@angular/common'; styleUrl: './expense.component.scss', }) export class ExpenseComponent { + private form = viewChild(ExpenseFormComponent); public expense = input(); public editingExpense = signal(false); @@ -31,7 +32,7 @@ export class ExpenseComponent { } public resetAddClick(): void { - console.log('Reset Add expense form'); + this.resetForm(); } public async updateClick(): Promise { @@ -40,8 +41,12 @@ export class ExpenseComponent { } public cancelUpdateClick(): void { - console.log('Canceling update'); this.editingExpense.set(false); + this.resetForm(); + } + + private resetForm(): void { + this.form()?.reset(); } // const saveExpenseModel = this.expenseModel(); -- 2.47.3 From ea4abe4fd69274f5617ec6fd35192668029b379f Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sun, 22 Feb 2026 21:03:32 -0600 Subject: [PATCH 15/18] logic for adding expense sucess --- .../expense-form/expense-form.component.ts | 28 +++++++---- .../components/expense/expense.component.html | 5 +- .../components/expense/expense.component.ts | 50 ++++++++++++++----- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/app/components/expense/expense-form/expense-form.component.ts b/src/app/components/expense/expense-form/expense-form.component.ts index 61d3fcb..07bc3a6 100644 --- a/src/app/components/expense/expense-form/expense-form.component.ts +++ b/src/app/components/expense/expense-form/expense-form.component.ts @@ -10,7 +10,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { provideNativeDateAdapter } from '@angular/material/core'; -interface ExpenseForm { +export interface ExpenseForm { date: Date; cents: number; category: Category; @@ -29,13 +29,13 @@ interface ExpenseForm { export class ExpenseFormComponent implements OnInit { public expense = input(); public disabled = input(false); - public lastDate = input(undefined); public valid = output(); public dirty = output() - public value = output(); + public value = output(); + private lastDate = signal(undefined); private formValid = computed(() => this.form().valid() && this.form().dirty()); - private formDirty = computed(() => this.form().dirty() && this.form().touched()); + private formDirty = computed(() => this.form().dirty() || this.form().touched()); private formData = computed(() => this.buildForm(this.expense())); private formModel = signal(this.formData()); @@ -48,8 +48,14 @@ export class ExpenseFormComponent implements OnInit { private readonly merchantService: MerchantService, private readonly tagService: TagService) { effect(() => { - this.valid.emit(this.formValid()); - this.dirty.emit(this.formDirty()); + const valid = this.formValid(); + const dirty = this.formDirty(); + const value = this.form().value(); + + this.valid.emit(valid); + this.dirty.emit(dirty); + this.value.emit(value); + this.lastDate.set(value.date); }); } @@ -57,9 +63,12 @@ export class ExpenseFormComponent implements OnInit { this.reset(); } - public reset(): void { + public reset(clearDate = false): void { + if (clearDate) { + this.lastDate.set(undefined); + this.form().reset(); + } this.formModel.set(this.formData()); - this.form().reset(); } public autocompleteDisplay(value: Merchant | Category) { @@ -67,7 +76,7 @@ export class ExpenseFormComponent implements OnInit { } private buildForm(expense?: Expense): ExpenseForm { - let formData = { date: this.lastDate() ?? '', cents: '', category: '', merchant: '', note: '', tags: ''} as unknown as ExpenseForm + let formData = { date: this.lastDate() ?? '', cents: NaN, category: {}, merchant: {}, note: '', tags: [] } as unknown as ExpenseForm if (expense) { formData = { date: new Date(expense.date.toString()), @@ -78,6 +87,7 @@ export class ExpenseFormComponent implements OnInit { tags: expense.tags ?? [] } as ExpenseForm; } + return formData; } diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html index b3916d4..e6c6cdd 100644 --- a/src/app/components/expense/expense.component.html +++ b/src/app/components/expense/expense.component.html @@ -21,9 +21,10 @@ + (dirty)="formDirty.set($event)" + (value)="formData.set($event)" />
diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 85506b4..32f28a8 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -1,10 +1,11 @@ import { Component, input, model, signal, viewChild } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; -import { Expense, ExpenseService } from '../../services/expense.service'; +import { CreateExpense, Expense, ExpenseService, UpdateExpense } from '../../services/expense.service'; import { MatIcon } from '@angular/material/icon'; -import { ExpenseFormComponent } from './expense-form/expense-form.component'; +import { ExpenseForm, ExpenseFormComponent } from './expense-form/expense-form.component'; import { CurrencyPipe, DatePipe } from '@angular/common'; +import { Temporal } from '@js-temporal/polyfill'; @Component({ selector: 'app-expense', @@ -18,8 +19,11 @@ export class ExpenseComponent { public expense = input(); public editingExpense = signal(false); + public savingExpense = signal(false); + public formValid = model(false); public formDirty = model(false); + public formData = model(); public constructor(private readonly expenseService: ExpenseService) { } @@ -28,25 +32,47 @@ export class ExpenseComponent { } public async addClick(): Promise { - console.log('Adding new expense'); + const form = this.formData()!; + const postExpense: CreateExpense = { + date: new Temporal.PlainDate(form.date.getFullYear(), form.date.getMonth(), form.date.getDate()), + cents: form.cents, + categoryId: form.category.id, + note: !!form.note ? form.note : undefined, + merchantId: !!form.merchant ? form.merchant.id : undefined, + tagIds: form.tags.map(tag => tag.id) + }; + await this.expenseService.postExpense(postExpense); + this.resetForm(); } public resetAddClick(): void { - this.resetForm(); + this.resetForm(true); } public async updateClick(): Promise { - console.log('Updating expense') - this.editingExpense.set(false); - } - - public cancelUpdateClick(): void { - this.editingExpense.set(false); + const form = this.formData()!; + const updateExpense: UpdateExpense = { + id: this.expense()!.id, + date: Temporal.PlainDate.from(form.date.toString()), + cents: form.cents, + categoryId: form.category.id, + note: !!form.note ? form.note : undefined, + merchantId: !!form.merchant ? form.merchant.id : undefined, + tagIds: form.tags.map(tag => tag.id) + }; + console.log('update:', updateExpense); + // post update dto + // set expense this.resetForm(); } - private resetForm(): void { - this.form()?.reset(); + public cancelUpdateClick(): void { + this.resetForm(); + } + + private resetForm(clearDate = false): void { + this.form()?.reset(clearDate); + this.editingExpense.set(false); } // const saveExpenseModel = this.expenseModel(); -- 2.47.3 From e78b24e5217c04739d20da5314d9574e562f5c8e Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sun, 22 Feb 2026 23:13:56 -0600 Subject: [PATCH 16/18] added in-place updating --- .../expense-list/expense-list.component.ts | 2 +- .../expense-form/expense-form.component.ts | 30 ++++++++++++++----- .../components/expense/expense.component.ts | 26 +++++++++------- src/app/services/expense.service.ts | 14 +++------ 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts index 88195b3..9846fb4 100644 --- a/src/app/components/expense-list/expense-list.component.ts +++ b/src/app/components/expense-list/expense-list.component.ts @@ -8,7 +8,7 @@ import { ExpenseComponent } from '../expense/expense.component'; selector: 'app-expense-list', imports: [MatTableModule, MatCardModule, ExpenseComponent], templateUrl: './expense-list.component.html', - styleUrl: './expense-list.component.scss', + styleUrl: './expense-list.component.scss' }) export class ExpenseListComponent { protected expenses = computed(() => this.expensesService.expenses()) diff --git a/src/app/components/expense/expense-form/expense-form.component.ts b/src/app/components/expense/expense-form/expense-form.component.ts index 07bc3a6..ceca49c 100644 --- a/src/app/components/expense/expense-form/expense-form.component.ts +++ b/src/app/components/expense/expense-form/expense-form.component.ts @@ -24,7 +24,7 @@ export interface ExpenseForm { imports: [MatAutocompleteModule, MatDatepickerModule, MatInputModule, MatSelectModule, FormField], providers: [provideNativeDateAdapter()], templateUrl: './expense-form.component.html', - styleUrl: './expense-form.component.scss', + styleUrl: './expense-form.component.scss' }) export class ExpenseFormComponent implements OnInit { public expense = input(); @@ -36,7 +36,7 @@ export class ExpenseFormComponent implements OnInit { private lastDate = signal(undefined); private formValid = computed(() => this.form().valid() && this.form().dirty()); private formDirty = computed(() => this.form().dirty() || this.form().touched()); - private formData = computed(() => this.buildForm(this.expense())); + private formData = signal(this.buildForm(this.expense())); private formModel = signal(this.formData()); protected categories = computed(() => this.categoryService.categories()); @@ -60,30 +60,46 @@ export class ExpenseFormComponent implements OnInit { } public ngOnInit(): void { - this.reset(); + this.initialize(); } public reset(clearDate = false): void { if (clearDate) { this.lastDate.set(undefined); - this.form().reset(); } + this.initialize(); + } + + public refresh(expense: Expense): void { + this.initialize(); + this.formData.set(this.buildForm(expense)); this.formModel.set(this.formData()); + this.form().reset(this.formModel()); } public autocompleteDisplay(value: Merchant | Category) { return value?.name ?? null; } + private initialize(): void { + this.formData.set(this.buildForm(this.expense())); + this.formModel.set(this.formData()); + this.form().reset(this.formModel()); + } + private buildForm(expense?: Expense): ExpenseForm { let formData = { date: this.lastDate() ?? '', cents: NaN, category: {}, merchant: {}, note: '', tags: [] } as unknown as ExpenseForm if (expense) { + const date = expense.date.toString().split('-'); + const day = Number(date[2]); + const month = Number(date[1]) - 1; + const year = Number(date[0]); formData = { - date: new Date(expense.date.toString()), + date: new Date(year, month, day), cents: expense.cents, category: expense.category, - merchant: expense.merchant, - note: expense.note, + merchant: expense.merchant ?? {}, + note: expense.note ?? '', tags: expense.tags ?? [] } as ExpenseForm; } diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 32f28a8..4aa4ee8 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -1,4 +1,4 @@ -import { Component, input, model, signal, viewChild } from '@angular/core'; +import { Component, model, signal, viewChild } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { CreateExpense, Expense, ExpenseService, UpdateExpense } from '../../services/expense.service'; @@ -12,15 +12,15 @@ import { Temporal } from '@js-temporal/polyfill'; imports: [MatCardModule, MatButtonModule, MatIcon, ExpenseFormComponent, DatePipe, CurrencyPipe], providers: [], templateUrl: './expense.component.html', - styleUrl: './expense.component.scss', + styleUrl: './expense.component.scss' }) export class ExpenseComponent { private form = viewChild(ExpenseFormComponent); - public expense = input(); public editingExpense = signal(false); - public savingExpense = signal(false); + public savingExpense = signal(false); // TODO: implement UI for saving... + public expense = model(); public formValid = model(false); public formDirty = model(false); public formData = model(); @@ -34,7 +34,7 @@ export class ExpenseComponent { public async addClick(): Promise { const form = this.formData()!; const postExpense: CreateExpense = { - date: new Temporal.PlainDate(form.date.getFullYear(), form.date.getMonth(), form.date.getDate()), + date: this.dateToPlainDate(form.date), cents: form.cents, categoryId: form.category.id, note: !!form.note ? form.note : undefined, @@ -51,19 +51,19 @@ export class ExpenseComponent { public async updateClick(): Promise { const form = this.formData()!; - const updateExpense: UpdateExpense = { + const putExpense: UpdateExpense = { id: this.expense()!.id, - date: Temporal.PlainDate.from(form.date.toString()), + date: this.dateToPlainDate(form.date), cents: form.cents, categoryId: form.category.id, note: !!form.note ? form.note : undefined, merchantId: !!form.merchant ? form.merchant.id : undefined, tagIds: form.tags.map(tag => tag.id) }; - console.log('update:', updateExpense); - // post update dto - // set expense - this.resetForm(); + const expense = await this.expenseService.updateExpense(putExpense); + this.expense.set(expense); + this.form()?.refresh(expense); + this.editingExpense.set(false); } public cancelUpdateClick(): void { @@ -75,6 +75,10 @@ export class ExpenseComponent { this.editingExpense.set(false); } + private dateToPlainDate(date: Date): Temporal.PlainDate { + return new Temporal.PlainDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); + } + // const saveExpenseModel = this.expenseModel(); // const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; // const expense: CreateExpense = { diff --git a/src/app/services/expense.service.ts b/src/app/services/expense.service.ts index e782ae5..243c53b 100644 --- a/src/app/services/expense.service.ts +++ b/src/app/services/expense.service.ts @@ -6,12 +6,12 @@ import { HttpService } from './http.service'; import { Temporal } from '@js-temporal/polyfill'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class ExpenseService { private internalExpenses = signal([]); public readonly expenses = this.internalExpenses.asReadonly(); - public readonly expensePath = 'http://localhost:3000/common-cents/expenses'; + public readonly expensePath = 'http://localhost:3000/common-cents/expenses'; // TODO: refactor public constructor(private readonly http: HttpService) { void this.fetchExpenses(); @@ -19,29 +19,23 @@ export class ExpenseService { public async fetchExpenses(): Promise { this.internalExpenses.set(await this.http.get(this.expensePath)); - console.log({ expenses: this.internalExpenses() }); // TODO: Remove } public async postExpense(createExpense: CreateExpense): Promise { const createdExpense = await this.http.post(this.expensePath, createExpense); await this.fetchExpenses(); - console.log({ createExpense, createdExpense}); // TODO: Remove return createdExpense; } public async updateExpense(updateExpense: UpdateExpense): Promise { - const updatedExpense = await this.http.put(this.expensePath, updateExpense); - await this.fetchExpenses(); - console.log({ updateExpense, updatedExpense}); // TODO: Remove - - return updatedExpense; + return await this.http.put(this.expensePath, updateExpense); } } export interface Expense { id: string; - date: Temporal.PlainDate; + date: Date; cents: number; category: Category; note?: string; -- 2.47.3 From 49af11d7b80722b81034aef39ee740de328fd19e Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Sun, 22 Feb 2026 23:28:10 -0600 Subject: [PATCH 17/18] added saving state --- .../components/expense/expense.component.html | 8 +-- .../components/expense/expense.component.ts | 59 ++++++++----------- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html index e6c6cdd..d24ce3a 100644 --- a/src/app/components/expense/expense.component.html +++ b/src/app/components/expense/expense.component.html @@ -4,16 +4,16 @@
@if (!expense()) { Add new Expense - - + + } @else { @if (!editingExpense()) { {{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency }} } @else { Update Expense - - + + } }
diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index 4aa4ee8..fe3c915 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -18,7 +18,7 @@ export class ExpenseComponent { private form = viewChild(ExpenseFormComponent); public editingExpense = signal(false); - public savingExpense = signal(false); // TODO: implement UI for saving... + public savingExpense = signal(false); public expense = model(); public formValid = model(false); @@ -41,8 +41,17 @@ export class ExpenseComponent { merchantId: !!form.merchant ? form.merchant.id : undefined, tagIds: form.tags.map(tag => tag.id) }; - await this.expenseService.postExpense(postExpense); - this.resetForm(); + this.savingExpense.set(true); + try { + await this.expenseService.postExpense(postExpense); + this.resetForm(); + } + catch (error) { + console.error(error); // TODO: Add error processing + } + finally { + this.savingExpense.set(false); + } } public resetAddClick(): void { @@ -60,10 +69,19 @@ export class ExpenseComponent { merchantId: !!form.merchant ? form.merchant.id : undefined, tagIds: form.tags.map(tag => tag.id) }; - const expense = await this.expenseService.updateExpense(putExpense); - this.expense.set(expense); - this.form()?.refresh(expense); - this.editingExpense.set(false); + this.savingExpense.set(true); + try { + const expense = await this.expenseService.updateExpense(putExpense); + this.expense.set(expense); + this.form()?.refresh(expense); + this.editingExpense.set(false); + } + catch (error) { + console.error(error); // TODO: Add error processing + } + finally { + this.savingExpense.set(false); + } } public cancelUpdateClick(): void { @@ -78,31 +96,4 @@ export class ExpenseComponent { private dateToPlainDate(date: Date): Temporal.PlainDate { return new Temporal.PlainDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); } - - // const saveExpenseModel = this.expenseModel(); - // const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? []; - // const expense: CreateExpense = { - // year: date[0], - // month: date[1], - // day: date[2], - // cents: saveExpenseModel.cents, - // category: saveExpenseModel.category as Category, - // merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined, - // note: saveExpenseModel.note ? saveExpenseModel.note : undefined, - // tags: saveExpenseModel.tags - // }; - // - // this.saving.set(true); - // try { - // await this.expenseService.postExpense(expense); - // this.lastSelectedDate = saveExpenseModel.date ? saveExpenseModel.date as Date : undefined; - // this.expenseModel.set({ ...this.defaultForm, date: saveExpenseModel.date }); - // this.expenseForm().reset(this.expenseModel()); - // } - // catch (error) { - // console.error(error); - // } - // finally { - // this.saving.set(false); - // } } -- 2.47.3 From eab674db3ff010ad3d93f722d92bd06b7bc74680 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Mon, 23 Feb 2026 00:19:30 -0600 Subject: [PATCH 18/18] added snackbar for status updates --- .../components/expense/expense.component.ts | 16 +++++-- src/app/services/category.service.ts | 2 +- src/app/services/http.service.ts | 2 +- src/app/services/merchant.service.ts | 2 +- src/app/services/snack-bar.ts | 44 +++++++++++++++++++ src/app/services/tag.service.ts | 2 +- 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 src/app/services/snack-bar.ts diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts index fe3c915..51cbdda 100644 --- a/src/app/components/expense/expense.component.ts +++ b/src/app/components/expense/expense.component.ts @@ -6,6 +6,7 @@ import { MatIcon } from '@angular/material/icon'; import { ExpenseForm, ExpenseFormComponent } from './expense-form/expense-form.component'; import { CurrencyPipe, DatePipe } from '@angular/common'; import { Temporal } from '@js-temporal/polyfill'; +import { SnackBarService } from '../../services/snack-bar'; @Component({ selector: 'app-expense', @@ -25,7 +26,8 @@ export class ExpenseComponent { public formDirty = model(false); public formData = model(); - public constructor(private readonly expenseService: ExpenseService) { } + public constructor(private readonly expenseService: ExpenseService, + private readonly snackBar: SnackBarService) { } public editClick(): void { this.editingExpense.set(true); @@ -42,12 +44,16 @@ export class ExpenseComponent { tagIds: form.tags.map(tag => tag.id) }; this.savingExpense.set(true); + const snackId = this.snackBar.staticBar('Tracking new expense...'); try { await this.expenseService.postExpense(postExpense); this.resetForm(); + this.snackBar.dismiss(snackId); + this.snackBar.autoBar('Expense tracked!'); } catch (error) { - console.error(error); // TODO: Add error processing + this.snackBar.dismiss(snackId); + this.snackBar.dismissableBar('Error: '); // TODO } finally { this.savingExpense.set(false); @@ -70,14 +76,18 @@ export class ExpenseComponent { tagIds: form.tags.map(tag => tag.id) }; this.savingExpense.set(true); + const snackId = this.snackBar.staticBar('Updating expense...'); try { const expense = await this.expenseService.updateExpense(putExpense); this.expense.set(expense); this.form()?.refresh(expense); this.editingExpense.set(false); + this.snackBar.dismiss(snackId); + this.snackBar.autoBar('Expense updated!'); } catch (error) { - console.error(error); // TODO: Add error processing + this.snackBar.dismiss(snackId); + this.snackBar.dismissableBar('Error: '); // TODO } finally { this.savingExpense.set(false); diff --git a/src/app/services/category.service.ts b/src/app/services/category.service.ts index 211e6e8..9c2da93 100644 --- a/src/app/services/category.service.ts +++ b/src/app/services/category.service.ts @@ -2,7 +2,7 @@ import { Injectable, signal } from '@angular/core'; import { HttpService } from './http.service'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class CategoryService { private internalCategories = signal([]); diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts index d9f3212..4ec3f04 100644 --- a/src/app/services/http.service.ts +++ b/src/app/services/http.service.ts @@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class HttpService { public constructor(private httpClient: HttpClient) { } diff --git a/src/app/services/merchant.service.ts b/src/app/services/merchant.service.ts index 6589120..8cca35b 100644 --- a/src/app/services/merchant.service.ts +++ b/src/app/services/merchant.service.ts @@ -2,7 +2,7 @@ import { Injectable, signal } from '@angular/core'; import { HttpService } from './http.service'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class MerchantService { private internalMerchants = signal([]); diff --git a/src/app/services/snack-bar.ts b/src/app/services/snack-bar.ts new file mode 100644 index 0000000..56f66fe --- /dev/null +++ b/src/app/services/snack-bar.ts @@ -0,0 +1,44 @@ +import { Injectable, signal } from '@angular/core'; +import { MatSnackBar, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; + +interface SnackBar { + id: string; + ref: MatSnackBarRef; +} + +@Injectable({ + providedIn: 'root' +}) +export class SnackBarService { + protected TIMEOUT = 3000; + + private snackBars = signal([]); + + public constructor(private snackBar: MatSnackBar) { } + + public autoBar(msg: string, milliseconds = this.TIMEOUT): void { + const timeout = milliseconds ?? this.TIMEOUT; + this.snackBar.open(msg, undefined, { duration: timeout }); + } + public dismissableBar(msg: string, dismissMsg = 'Dismiss'): void { + this.snackBar.open(msg, dismissMsg); + } + + public staticBar(msg: string): string { + const snackBar: SnackBar = { + id: crypto.randomUUID().toString(), + ref: this.snackBar.open(msg) + }; + this.snackBars.update((bars) => [...bars, snackBar]); + + return snackBar.id; + } + + public dismiss(id: string): void { + const snackBar = this.snackBars().find((snackBar) => snackBar.id === id); + if (snackBar) { + snackBar.ref.dismiss(); + this.snackBars.update((bars) => bars.filter((bar) => bar.id !== snackBar.id)); + } + } +} diff --git a/src/app/services/tag.service.ts b/src/app/services/tag.service.ts index dd3cb2e..06a373b 100644 --- a/src/app/services/tag.service.ts +++ b/src/app/services/tag.service.ts @@ -2,7 +2,7 @@ import { Injectable, signal } from '@angular/core'; import { HttpService } from './http.service'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class TagService { private internalTags = signal([]); -- 2.47.3