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[]; }