added expense view mode
This commit is contained in:
parent
fed0f7908a
commit
e127b8ec45
25 changed files with 349 additions and 125 deletions
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
||||||
"@angular/material": "~21.1.4",
|
"@angular/material": "~21.1.4",
|
||||||
"@angular/platform-browser": "^21.1.0",
|
"@angular/platform-browser": "^21.1.0",
|
||||||
"@angular/router": "^21.1.0",
|
"@angular/router": "^21.1.0",
|
||||||
|
"@js-temporal/polyfill": "^0.5.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2073,6 +2074,18 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@listr2/prompt-adapter-inquirer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz",
|
||||||
|
|
@ -5891,6 +5904,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/jsdom": {
|
||||||
"version": "27.4.0",
|
"version": "27.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"@angular/material": "~21.1.4",
|
"@angular/material": "~21.1.4",
|
||||||
"@angular/platform-browser": "^21.1.0",
|
"@angular/platform-browser": "^21.1.0",
|
||||||
"@angular/router": "^21.1.0",
|
"@angular/router": "^21.1.0",
|
||||||
|
"@js-temporal/polyfill": "^0.5.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -43,4 +44,4 @@
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"vitest": "^4.0.8"
|
"vitest": "^4.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { Expenses } from './pages/expenses/expenses';
|
import { ExpensePage } from './pages/expenses/expense-page.component';
|
||||||
import { Home } from './pages/home/home';
|
import { HomePage } from './pages/home/home-page.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: Home
|
component: HomePage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'expenses',
|
path: 'expenses',
|
||||||
component: Expenses
|
component: ExpensePage
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,4 @@
|
||||||
@for (expense of expenses(); track expense.id) {
|
@for (expense of expenses(); track expense.id) {
|
||||||
<app-expense [expense]="expense" />
|
<app-expense [expense]="expense" />
|
||||||
}
|
}
|
||||||
<!-- <mat-card appearance="outlined" class="expense-table-card">-->
|
|
||||||
<!-- <mat-card-header>-->
|
|
||||||
<!-- <mat-card-title>Tracked Expenses</mat-card-title>-->
|
|
||||||
<!-- </mat-card-header>-->
|
|
||||||
|
|
||||||
<!-- <mat-card-content>-->
|
|
||||||
<!-- <table mat-table [dataSource]="expenses()" class="expense-table">-->
|
|
||||||
<!-- <ng-container matColumnDef="date">-->
|
|
||||||
<!-- <th mat-header-cell *matHeaderCellDef>Date</th>-->
|
|
||||||
<!-- <td mat-cell *matCellDef="let expense">{{ `${expense.year}/${expense.month}/${expense.day}` | date }}</td>-->
|
|
||||||
<!-- </ng-container>-->
|
|
||||||
|
|
||||||
<!-- <ng-container matColumnDef="amount">-->
|
|
||||||
<!-- <th mat-header-cell *matHeaderCellDef>Amount</th>-->
|
|
||||||
<!-- <td mat-cell *matCellDef="let expense">{{ (expense.cents / 100) | currency: 'USD' }}</td>-->
|
|
||||||
<!-- </ng-container>-->
|
|
||||||
|
|
||||||
<!-- <ng-container matColumnDef="category">-->
|
|
||||||
<!-- <th mat-header-cell *matHeaderCellDef>Category</th>-->
|
|
||||||
<!-- <td mat-cell *matCellDef="let expense">{{ expense.category.name }}</td>-->
|
|
||||||
<!-- </ng-container>-->
|
|
||||||
|
|
||||||
<!-- <ng-container matColumnDef="merchant">-->
|
|
||||||
<!-- <th mat-header-cell *matHeaderCellDef>Merchant</th>-->
|
|
||||||
<!-- <td mat-cell *matCellDef="let expense">{{ expense.merchant?.name ?? '--' }}</td>-->
|
|
||||||
<!-- </ng-container>-->
|
|
||||||
|
|
||||||
<!-- <tr mat-header-row *matHeaderRowDef="columns"></tr>-->
|
|
||||||
<!-- <tr mat-row *matRowDef="let rowData; columns: columns"></tr>-->
|
|
||||||
<!-- </table>-->
|
|
||||||
<!-- </mat-card-content>-->
|
|
||||||
<!-- </mat-card>-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
.expense-list-container {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
import { Component, computed } from '@angular/core';
|
import { Component, computed } from '@angular/core';
|
||||||
import { ExpenseService } from '../../services/expense.service';
|
import { ExpenseService } from '../../services/expense.service';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { CurrencyPipe, DatePipe } from '@angular/common';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import {ExpenseComponent} from '../expense/expense.component';
|
import { ExpenseComponent } from '../expense/expense.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-expense-list',
|
selector: 'app-expense-list',
|
||||||
imports: [MatTableModule, MatCardModule, DatePipe, CurrencyPipe, ExpenseComponent],
|
imports: [MatTableModule, MatCardModule, ExpenseComponent],
|
||||||
templateUrl: './expense-list.component.html',
|
templateUrl: './expense-list.component.html',
|
||||||
styleUrl: './expense-list.component.scss',
|
styleUrl: './expense-list.component.scss',
|
||||||
})
|
})
|
||||||
export class ExpenseListComponent {
|
export class ExpenseListComponent {
|
||||||
protected expenses = computed(() => this.expensesService.expenses())
|
protected expenses = computed(() => this.expensesService.expenses())
|
||||||
protected columns = ['date', 'amount', 'category', 'merchant'];
|
|
||||||
|
|
||||||
public constructor(private readonly expensesService: ExpenseService) { }
|
public constructor(private readonly expensesService: ExpenseService) { }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<p>expense-add works!</p>
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<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">-->
|
||||||
|
<!-- }-->
|
||||||
|
<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>
|
||||||
|
|
@ -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<boolean>(); // TODO: should this even exist?
|
||||||
|
public expense = input<Expense | undefined>(undefined);
|
||||||
|
public categories = input<Category[]>([]);
|
||||||
|
public merchants = input<Merchant[]>([]);
|
||||||
|
public tags = input<Tag[]>([]);
|
||||||
|
|
||||||
|
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();
|
||||||
|
// });
|
||||||
|
|
||||||
|
private lastSelectedDate = signal<Date | undefined>(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<ExpenseForm>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<p>expense-view works!</p>
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
<div class="expense-container">
|
<div class="expense-container">
|
||||||
<mat-card appearance="outlined">
|
<mat-card appearance="outlined">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
@if (state() === 'add') {
|
@if (state() === 'add' || state() === 'edit') {
|
||||||
<div class="expense-header">
|
<div class="expense-header">
|
||||||
<mat-card-title>Track new Expense</mat-card-title>
|
<mat-card-title>
|
||||||
|
@if (state() === 'add') {
|
||||||
|
Track new Expense
|
||||||
|
} @else {
|
||||||
|
Edit Expense
|
||||||
|
}
|
||||||
|
</mat-card-title>
|
||||||
<button matButton="tonal" [disabled]="!enableSaveButton()" (click)="saveClick()">Save</button>
|
<button matButton="tonal" [disabled]="!enableSaveButton()" (click)="saveClick()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (state() === 'view') {
|
@if (state() === 'view') {
|
||||||
<div class="expense-header">
|
<div class="expense-header">
|
||||||
<mat-card-title>{{ `${expense()?.year}-${expense()?.month}-${expense()?.day}` | date }}</mat-card-title>
|
<mat-card-title>
|
||||||
|
{{ expense()?.date?.toString() | date }}: {{ expense()?.cents! / 100 | currency}}
|
||||||
|
</mat-card-title>
|
||||||
|
|
||||||
<button matIconButton>
|
<button matIconButton (click)="editClick()">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,13 +27,18 @@
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
@if (state() === 'add') {
|
<!-- @if (state() === 'add' || state() === 'edit') {-->
|
||||||
<div class="expense-content">
|
<div class="expense-content">
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Date</mat-label>
|
<mat-label>Date</mat-label>
|
||||||
<input matInput [matDatepicker]="expenseDatePicker" [formField]="expenseForm.date">
|
@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-toggle matIconSuffix [for]="expenseDatePicker"></mat-datepicker-toggle>
|
||||||
<mat-datepicker #expenseDatePicker></mat-datepicker>
|
<mat-datepicker #expenseDatePicker></mat-datepicker>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
|
|
@ -67,30 +80,56 @@
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
}
|
<!-- }-->
|
||||||
@if (state() === 'view') {
|
<!-- @if (state() === 'view') {-->
|
||||||
<div class="expense-content">
|
<!-- <div class="expense-content">-->
|
||||||
<div>
|
<!-- <mat-form-field appearance="outline">-->
|
||||||
Amount: {{ expense()?.cents! / 100 | currency}}
|
<!-- <input matInput disabled="true" [matDatepicker]="expenseDatePicker" [placeholder]="expense()?.year! + '-' + expense()?.month! + '-' + expense()?.day!">-->
|
||||||
</div>
|
<!-- <mat-datepicker-toggle matIconSuffix [for]="expenseDatePicker"></mat-datepicker-toggle>-->
|
||||||
|
<!-- <mat-datepicker #expenseDatePicker></mat-datepicker>-->
|
||||||
|
<!-- </mat-form-field>-->
|
||||||
|
|
||||||
<div>
|
<!-- <mat-form-field appearance="outline">-->
|
||||||
Category: {{ expense()?.category!.name }}
|
<!-- <mat-label>Cents</mat-label>-->
|
||||||
</div>
|
<!-- <input type="number" matInput disabled="true" [value]="expense()?.cents">-->
|
||||||
|
<!-- </mat-form-field>-->
|
||||||
|
|
||||||
<div>
|
<!-- <mat-form-field appearance="outline">-->
|
||||||
Merchant: {{ expense()?.merchant?.name ?? '--' }}
|
<!-- <mat-label>Category</mat-label>-->
|
||||||
</div>
|
<!-- <input type="text" matInput [matAutocomplete]="categoryAuto" [value]="expense()?.category!.name" disabled="true">-->
|
||||||
</div>
|
<!-- <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>-->
|
||||||
|
|
||||||
<div>
|
<!-- <mat-form-field appearance="outline">-->
|
||||||
Note: {{ expense()?.note ?? '--' }}
|
<!-- <mat-label>Merchant</mat-label>-->
|
||||||
</div>
|
<!-- <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>-->
|
||||||
|
|
||||||
<div>
|
<!-- <mat-form-field appearance="outline" class="view-note">-->
|
||||||
Tags:
|
<!-- <mat-label>Note</mat-label>-->
|
||||||
</div>
|
<!-- <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>-->
|
||||||
|
<!-- }-->
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,37 @@ mat-form-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
@media (min-width: 550px) {
|
||||||
.expense-content {
|
.expense-content {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-note {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-tags {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.expense-content {
|
.expense-content {
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-note {
|
||||||
|
grid-column: 2 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-tags {
|
||||||
|
grid-column: 1 / span 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { provideNativeDateAdapter } from '@angular/material/core';
|
import { provideNativeDateAdapter } from '@angular/material/core';
|
||||||
import { CreateExpense, Expense, ExpenseService } from '../../services/expense.service';
|
import { CreateExpense, Expense, ExpenseService } from '../../services/expense.service';
|
||||||
import {CurrencyPipe, DatePipe} from '@angular/common';
|
import { CurrencyPipe, DatePipe } from '@angular/common';
|
||||||
import {MatIcon} from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
|
import { Temporal } from '@js-temporal/polyfill';
|
||||||
|
// import { MatChip } from '@angular/material/chips';
|
||||||
|
|
||||||
interface ExpenseForm {
|
interface ExpenseForm {
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
|
|
@ -26,7 +28,20 @@ interface ExpenseForm {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-expense',
|
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],
|
providers: [provideNativeDateAdapter(), DatePipe],
|
||||||
templateUrl: './expense.component.html',
|
templateUrl: './expense.component.html',
|
||||||
styleUrl: './expense.component.scss',
|
styleUrl: './expense.component.scss',
|
||||||
|
|
@ -49,14 +64,14 @@ export class ExpenseComponent {
|
||||||
return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving();
|
return dateValid && centsValid && categoryValid && merchantValid && noteValid && !this.saving();
|
||||||
});
|
});
|
||||||
|
|
||||||
private lastSelectedDate: Date | undefined;
|
// private lastSelectedDate: Date | undefined;
|
||||||
private expenseDate = computed(() => {
|
// private expenseDate = computed(() => {
|
||||||
return this.expense()
|
// return this.expense()
|
||||||
? new Date(`${this.expense()?.year}-${this.expense()?.month}-${this.expense()?.day}`)
|
// ? new Date(`${this.expense()?.year}-${this.expense()?.month}-${this.expense()?.day}`)
|
||||||
: this.lastSelectedDate ?? '';
|
// : this.lastSelectedDate ?? '';
|
||||||
});
|
// });
|
||||||
private defaultForm: ExpenseForm = {
|
private defaultForm: ExpenseForm = {
|
||||||
date: this.expenseDate(),
|
date: '',
|
||||||
cents: this.expense()?.cents ?? NaN,
|
cents: this.expense()?.cents ?? NaN,
|
||||||
category: this.expense()?.category ?? '',
|
category: this.expense()?.category ?? '',
|
||||||
merchant: this.expense()?.merchant ?? '',
|
merchant: this.expense()?.merchant ?? '',
|
||||||
|
|
@ -78,32 +93,36 @@ export class ExpenseComponent {
|
||||||
private readonly datePipe: DatePipe) { }
|
private readonly datePipe: DatePipe) { }
|
||||||
|
|
||||||
public async saveClick(): Promise<void> {
|
public async saveClick(): Promise<void> {
|
||||||
const saveExpenseModel = this.expenseModel();
|
// const saveExpenseModel = this.expenseModel();
|
||||||
const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? [];
|
// const date = this.datePipe.transform(saveExpenseModel.date, 'yyyy-MM-dd')?.split('-') ?? [];
|
||||||
const expense: CreateExpense = {
|
// const expense: CreateExpense = {
|
||||||
year: date[0],
|
// year: date[0],
|
||||||
month: date[1],
|
// month: date[1],
|
||||||
day: date[2],
|
// day: date[2],
|
||||||
cents: saveExpenseModel.cents,
|
// cents: saveExpenseModel.cents,
|
||||||
category: saveExpenseModel.category as Category,
|
// category: saveExpenseModel.category as Category,
|
||||||
merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined,
|
// merchant: saveExpenseModel.merchant ? saveExpenseModel.merchant as Merchant : undefined,
|
||||||
note: saveExpenseModel.note ? saveExpenseModel.note : undefined,
|
// note: saveExpenseModel.note ? saveExpenseModel.note : undefined,
|
||||||
tags: saveExpenseModel.tags
|
// 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);
|
public editClick(): void {
|
||||||
try {
|
this.expenseMode.set('edit');
|
||||||
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 autocompleteDisplay(value: Merchant | Category) {
|
public autocompleteDisplay(value: Merchant | Category) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="expenses-container">
|
<div class="expenses-container">
|
||||||
<app-expense />
|
<app-expense />
|
||||||
<app-expense-list />
|
<!-- <app-expense-list />-->
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -8,7 +8,7 @@ import { ExpenseComponent } from '../../components/expense/expense.component';
|
||||||
ExpenseListComponent,
|
ExpenseListComponent,
|
||||||
ExpenseComponent
|
ExpenseComponent
|
||||||
],
|
],
|
||||||
templateUrl: './expenses.html',
|
templateUrl: './expense-page.component.html',
|
||||||
styleUrl: './expenses.scss'
|
styleUrl: './expense-page.component.scss'
|
||||||
})
|
})
|
||||||
export class Expenses { }
|
export class ExpensePage { }
|
||||||
0
src/app/pages/home/home-page.component.scss
Normal file
0
src/app/pages/home/home-page.component.scss
Normal file
|
|
@ -6,7 +6,7 @@ import { RouterLink } from '@angular/router';
|
||||||
imports: [
|
imports: [
|
||||||
RouterLink
|
RouterLink
|
||||||
],
|
],
|
||||||
templateUrl: './home.html',
|
templateUrl: './home-page.component.html',
|
||||||
styleUrl: './home.scss',
|
styleUrl: './home-page.component.scss',
|
||||||
})
|
})
|
||||||
export class Home { }
|
export class HomePage { }
|
||||||
|
|
@ -3,6 +3,7 @@ import { Category } from './category.service';
|
||||||
import { Merchant } from './merchant.service';
|
import { Merchant } from './merchant.service';
|
||||||
import { Tag } from './tag.service';
|
import { Tag } from './tag.service';
|
||||||
import { HttpService } from './http.service';
|
import { HttpService } from './http.service';
|
||||||
|
import { Temporal } from '@js-temporal/polyfill';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
|
@ -21,18 +22,18 @@ export class ExpenseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async postExpense(expense: CreateExpense): Promise<Expense> {
|
public async postExpense(expense: CreateExpense): Promise<Expense> {
|
||||||
const createdExpense = await this.http.post<Expense>(this.expensePath, expense);
|
// const createdExpense = await this.http.post<Expense>(this.expensePath, expense);
|
||||||
|
console.log(expense);
|
||||||
await this.fetchExpenses();
|
await this.fetchExpenses();
|
||||||
|
|
||||||
return createdExpense;
|
// return createdExpense;
|
||||||
|
return { ...expense, id: '', category: { id: '', name: ''}, tags: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Expense {
|
export interface Expense {
|
||||||
id: string;
|
id: string;
|
||||||
year: string;
|
date: Temporal.PlainDate;
|
||||||
month: string;
|
|
||||||
day: string;
|
|
||||||
cents: number;
|
cents: number;
|
||||||
category: Category;
|
category: Category;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|
@ -41,12 +42,10 @@ export interface Expense {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateExpense {
|
export interface CreateExpense {
|
||||||
year: string;
|
date: Temporal.PlainDate;
|
||||||
month: string;
|
|
||||||
day: string;
|
|
||||||
cents: number;
|
cents: number;
|
||||||
category: Category;
|
categoryId: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
merchant?: Merchant;
|
merchantId?: string;
|
||||||
tags: Tag[];
|
tagIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue