Basic API consumption support and refactored UI to use material #1
18 changed files with 197 additions and 389 deletions
|
|
@ -1,4 +1,5 @@
|
|||
<div class="expense-list-container">
|
||||
<h2>Expenses</h2>
|
||||
@for (expense of expenses(); track expense.id) {
|
||||
<app-expense [expense]="expense" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
.expense-list-container {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<p>expense-add works!</p>
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -1,24 +1,19 @@
|
|||
<div class="expense-form-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Date</mat-label>
|
||||
<!-- @if (state() === 'view') {-->
|
||||
<!-- <!– <input matInput disabled="true" [matDatepicker]="expenseDatePicker" [placeholder]="expense()?.year! + '-' + expense()?.month! + '-' + expense()?.day!">–>-->
|
||||
<!-- <input matInput disabled="true" [matDatepicker]="expenseDatePicker" [placeholder]="'2026-12-03'" [value]="expenseForm.date().value()">-->
|
||||
<!-- } @else {-->
|
||||
<!-- <input matInput [matDatepicker]="expenseDatePicker" [formField]="expenseForm.date">-->
|
||||
<!-- }-->
|
||||
<input matInput [matDatepicker]="expenseDatePicker" [formField]="form.date">
|
||||
<mat-datepicker-toggle matIconSuffix [for]="expenseDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #expenseDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Cents</mat-label>
|
||||
<!-- <input type="number" matInput [formField]="expenseForm.cents">-->
|
||||
<input type="number" matInput [formField]="form.cents">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Category</mat-label>
|
||||
<!-- <input type="text" matInput [matAutocomplete]="categoryAuto" [formField]="expenseForm.category" [value]="expenseForm.category()">-->
|
||||
<input type="text" matInput [matAutocomplete]="categoryAuto" [formField]="form.category">
|
||||
<mat-autocomplete [displayWith]="autocompleteDisplay" autoActiveFirstOption #categoryAuto="matAutocomplete">
|
||||
@for (category of categories(); track category.id) {
|
||||
<mat-option [value]="category">{{ category.name }}</mat-option>
|
||||
|
|
@ -28,7 +23,7 @@
|
|||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Merchant</mat-label>
|
||||
<!-- <input type="text" matInput [matAutocomplete]="merchantAuto" [formField]="expenseForm.merchant">-->
|
||||
<input type="text" matInput [matAutocomplete]="merchantAuto" [formField]="form.merchant">
|
||||
<mat-autocomplete [displayWith]="autocompleteDisplay" autoActiveFirstOption #merchantAuto="matAutocomplete">
|
||||
@for (merchant of merchants(); track merchant.id) {
|
||||
<mat-option [value]="merchant">{{ merchant.name }}</mat-option>
|
||||
|
|
@ -38,15 +33,15 @@
|
|||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Note</mat-label>
|
||||
<!-- <textarea rows="1" matInput [formField]="expenseForm.note"></textarea>-->
|
||||
<textarea rows="1" matInput [formField]="form.note"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tags</mat-label>
|
||||
<!-- <mat-select disableRipple multiple [formField]="expenseForm.tags">-->
|
||||
<!-- @for (tag of tags(); track tag.id) {-->
|
||||
<!-- <mat-option [value]="tag">{{ tag.name }}</mat-option>-->
|
||||
<!-- }-->
|
||||
<!-- </mat-select>-->
|
||||
<mat-select disableRipple multiple [formField]="form.tags">
|
||||
@for (tag of tags(); track tag.id) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<boolean>(); // TODO: should this even exist?
|
||||
public expense = input<Expense | undefined>(undefined);
|
||||
public categories = input<Category[]>([]);
|
||||
public merchants = input<Merchant[]>([]);
|
||||
public tags = input<Tag[]>([]);
|
||||
export class ExpenseFormComponent implements OnInit {
|
||||
public expense = input<Expense>();
|
||||
public disabled = input<boolean>(false);
|
||||
|
||||
public valid = output<boolean>();
|
||||
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<boolean>()
|
||||
public value = output();
|
||||
|
||||
private formValid = computed(() => this.form().valid() && this.form().dirty());
|
||||
private formDirty = computed(() => this.form().dirty() && this.form().touched());
|
||||
private lastSelectedDate = signal<Date | undefined>(undefined);
|
||||
private formData = computed(() => this.buildForm(this.expense()));
|
||||
private formModel = signal<ExpenseForm>(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<ExpenseForm>(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<ExpenseForm> {
|
||||
return schema<ExpenseForm>((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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<p>expense-view works!</p>
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -1,135 +1,29 @@
|
|||
<div class="expense-container">
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-header>
|
||||
@if (state() === 'add' || state() === 'edit') {
|
||||
<div class="expense-header">
|
||||
<mat-card-title>
|
||||
@if (state() === 'add') {
|
||||
Track new Expense
|
||||
} @else {
|
||||
Edit Expense
|
||||
}
|
||||
</mat-card-title>
|
||||
<button matButton="tonal" [disabled]="!enableSaveButton()" (click)="saveClick()">Save</button>
|
||||
</div>
|
||||
}
|
||||
@if (state() === 'view') {
|
||||
<div class="expense-header">
|
||||
<mat-card-title>
|
||||
{{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency}}
|
||||
</mat-card-title>
|
||||
|
||||
<button matIconButton (click)="editClick()">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<div class="expense-header">
|
||||
@if (!expense()) {
|
||||
<mat-card-title>Add new Expense</mat-card-title>
|
||||
<button matIconButton [disabled]="!formDirty()" (click)="resetAddClick()"><mat-icon>replay</mat-icon></button>
|
||||
<button matIconButton [disabled]="!formValid()" (click)="addClick()"><mat-icon>add</mat-icon></button>
|
||||
} @else {
|
||||
@if (!editingExpense()) {
|
||||
<mat-card-title>{{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency }}</mat-card-title>
|
||||
<button matIconButton (click)="editClick()"><mat-icon>edit</mat-icon></button>
|
||||
} @else {
|
||||
<mat-card-title>Update Expense</mat-card-title>
|
||||
<button matIconButton (click)="cancelUpdateClick()"><mat-icon>undo</mat-icon></button>
|
||||
<button matIconButton [disabled]="!formValid()" (click)="updateClick()"><mat-icon>save</mat-icon></button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<!-- @if (state() === 'add' || state() === 'edit') {-->
|
||||
<div class="expense-content">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Date</mat-label>
|
||||
@if (state() === 'view') {
|
||||
<!-- <input matInput disabled="true" [matDatepicker]="expenseDatePicker" [placeholder]="expense()?.year! + '-' + expense()?.month! + '-' + expense()?.day!">-->
|
||||
<input matInput disabled="true" [matDatepicker]="expenseDatePicker" [placeholder]="'2026-12-03'" [value]="expenseForm.date().value()">
|
||||
} @else {
|
||||
<input matInput [matDatepicker]="expenseDatePicker" [formField]="expenseForm.date">
|
||||
}
|
||||
<mat-datepicker-toggle matIconSuffix [for]="expenseDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #expenseDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Cents</mat-label>
|
||||
<input type="number" matInput [formField]="expenseForm.cents">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Category</mat-label>
|
||||
<input type="text" matInput [matAutocomplete]="categoryAuto" [formField]="expenseForm.category" [value]="expenseForm.category()">
|
||||
<mat-autocomplete [displayWith]="autocompleteDisplay" autoActiveFirstOption #categoryAuto="matAutocomplete">
|
||||
@for (category of categories(); track category.id) {
|
||||
<mat-option [value]="category">{{ category.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Merchant</mat-label>
|
||||
<input type="text" matInput [matAutocomplete]="merchantAuto" [formField]="expenseForm.merchant">
|
||||
<mat-autocomplete [displayWith]="autocompleteDisplay" autoActiveFirstOption #merchantAuto="matAutocomplete">
|
||||
@for (merchant of merchants(); track merchant.id) {
|
||||
<mat-option [value]="merchant">{{ merchant.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Note</mat-label>
|
||||
<textarea rows="1" matInput [formField]="expenseForm.note"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-select disableRipple multiple [formField]="expenseForm.tags">
|
||||
@for (tag of tags(); track tag.id) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- }-->
|
||||
<!-- @if (state() === 'view') {-->
|
||||
<!-- <div class="expense-content">-->
|
||||
<!-- <mat-form-field appearance="outline">-->
|
||||
<!-- <input matInput disabled="true" [matDatepicker]="expenseDatePicker" [placeholder]="expense()?.year! + '-' + expense()?.month! + '-' + expense()?.day!">-->
|
||||
<!-- <mat-datepicker-toggle matIconSuffix [for]="expenseDatePicker"></mat-datepicker-toggle>-->
|
||||
<!-- <mat-datepicker #expenseDatePicker></mat-datepicker>-->
|
||||
<!-- </mat-form-field>-->
|
||||
|
||||
<!-- <mat-form-field appearance="outline">-->
|
||||
<!-- <mat-label>Cents</mat-label>-->
|
||||
<!-- <input type="number" matInput disabled="true" [value]="expense()?.cents">-->
|
||||
<!-- </mat-form-field>-->
|
||||
|
||||
<!-- <mat-form-field appearance="outline">-->
|
||||
<!-- <mat-label>Category</mat-label>-->
|
||||
<!-- <input type="text" matInput [matAutocomplete]="categoryAuto" [value]="expense()?.category!.name" disabled="true">-->
|
||||
<!-- <mat-autocomplete [displayWith]="autocompleteDisplay" autoActiveFirstOption #categoryAuto="matAutocomplete">-->
|
||||
<!-- @for (category of categories(); track category.id) {-->
|
||||
<!-- <mat-option [value]="category">{{ category.name }}</mat-option>-->
|
||||
<!-- }-->
|
||||
<!-- </mat-autocomplete>-->
|
||||
<!-- </mat-form-field>-->
|
||||
|
||||
<!-- <mat-form-field appearance="outline">-->
|
||||
<!-- <mat-label>Merchant</mat-label>-->
|
||||
<!-- <input type="text" matInput [matAutocomplete]="merchantAuto" [value]="expense()?.merchant?.name ?? 'N/A'" disabled="true">-->
|
||||
<!-- <mat-autocomplete [displayWith]="autocompleteDisplay" autoActiveFirstOption #merchantAuto="matAutocomplete">-->
|
||||
<!-- @for (merchant of merchants(); track merchant.id) {-->
|
||||
<!-- <mat-option [value]="merchant">{{ merchant.name }}</mat-option>-->
|
||||
<!-- }-->
|
||||
<!-- </mat-autocomplete>-->
|
||||
<!-- </mat-form-field>-->
|
||||
|
||||
<!-- <mat-form-field appearance="outline" class="view-note">-->
|
||||
<!-- <mat-label>Note</mat-label>-->
|
||||
<!-- <textarea rows="1" matInput disabled="true" [value]="expense()?.note ?? 'N/A'"></textarea>-->
|
||||
<!-- </mat-form-field>-->
|
||||
|
||||
<!-- <div class="view-tags">-->
|
||||
<!-- <mat-card-subtitle>Tags:</mat-card-subtitle>-->
|
||||
<!-- @if (!expense()?.tags?.length) {-->
|
||||
<!-- N/A-->
|
||||
<!-- }-->
|
||||
<!-- @for (tag of expense()?.tags; track tag.id) {-->
|
||||
<!-- <mat-chip>{{ tag.name }}</mat-chip>-->
|
||||
<!-- }-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- }-->
|
||||
<app-expense-form [expense]="expense()"
|
||||
[disabled]="!!expense() && !editingExpense()"
|
||||
(valid)="formValid.set($event)"
|
||||
(dirty)="formDirty.set($event)" />
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Expense>();
|
||||
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<ExpenseForm>(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<void> {
|
||||
// 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<void> {
|
||||
console.log('Adding new expense');
|
||||
}
|
||||
|
||||
public resetAddClick(): void {
|
||||
console.log('Reset Add expense form');
|
||||
}
|
||||
|
||||
public async updateClick(): Promise<void> {
|
||||
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);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="expenses-container">
|
||||
<app-expense />
|
||||
<!-- <app-expense-list />-->
|
||||
<app-expense-list />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<Category[]>([]);
|
||||
|
||||
public constructor(private readonly http: HttpService) {
|
||||
void this.fetchCategories();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,23 @@ export class ExpenseService {
|
|||
|
||||
public async fetchExpenses(): Promise<void> {
|
||||
this.internalExpenses.set(await this.http.get(this.expensePath));
|
||||
console.log({ expenses: this.internalExpenses() }); // TODO: Remove
|
||||
}
|
||||
|
||||
public async postExpense(expense: CreateExpense): Promise<Expense> {
|
||||
// const createdExpense = await this.http.post<Expense>(this.expensePath, expense);
|
||||
console.log(expense);
|
||||
public async postExpense(createExpense: CreateExpense): Promise<Expense> {
|
||||
const createdExpense = await this.http.post<Expense>(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<Expense> {
|
||||
const updatedExpense = await this.http.put<Expense>(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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue