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