scaffold expense list

This commit is contained in:
Joe Arndt 2026-02-12 23:41:54 -06:00
parent 7bb21f8796
commit fded7f7c09
22 changed files with 253 additions and 133 deletions

View file

@ -0,0 +1,7 @@
<div class="expense-list-container">
@for (expense of expenses(); track expense.id) {
<div class="expense-item">
<app-expense [expense]="expense" />
</div>
}
</div>

View file

@ -0,0 +1,7 @@
.expense-list-container {
padding: 1rem;
}
.expense-item {
padding-bottom: 1rem;
}

View file

@ -0,0 +1,46 @@
import { Component, OnInit, signal } from '@angular/core';
import { Expense, ExpenseService } from '../../services/expense.service';
import { ExpenseComponent } from '../expense/expense.component';
import { Category, CategoryService } from '../../services/category.service';
import { SubCategory, SubCategoryService } from '../../services/sub-category.service';
import { Merchant, MerchantService } from '../../services/merchant.service';
import { Tag, TagService } from '../../services/tag.service';
@Component({
selector: 'app-expense-list',
imports: [
ExpenseComponent
],
templateUrl: './expense-list.component.html',
styleUrl: './expense-list.component.scss',
})
export class ExpenseListComponent implements OnInit {
protected expenses = signal<Expense[]>([]);
protected categories = signal<Category[]>([]);
protected subCategories = signal<SubCategory[]>([]);
protected merchants = signal<Merchant[]>([]);
protected tags = signal<Tag[]>([]);
public constructor(private readonly expensesService: ExpenseService,
private readonly categoryService: CategoryService,
private readonly subCategoryService: SubCategoryService,
private readonly merchantService: MerchantService,
private readonly tagService: TagService) { }
public ngOnInit() {
Promise.all([
this.expensesService.getExpenses(),
this.categoryService.getCategories(),
this.subCategoryService.getSubCategories(),
this.merchantService.getMerchants(),
this.tagService.getTags()
]).then(([expenses, categories, subCategories, merchants, tags]) => {
console.log({ expenses, categories, subCategories, merchants, tags }); // TODO: Remove me
this.expenses.set(expenses);
this.categories.set(categories);
this.subCategories.set(subCategories);
this.merchants.set(merchants);
this.tags.set(tags);
})
}
}

View file

@ -1,3 +0,0 @@
<div class="expense-list-container">
<p>expense-list works!</p>
</div>

View file

@ -1,18 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Expenses } from '../../services/expenses';
@Component({
selector: 'app-expense-list',
imports: [],
templateUrl: './expense-list.html',
styleUrl: './expense-list.scss',
})
export class ExpenseList implements OnInit {
public constructor(private readonly expenses: Expenses) { }
public ngOnInit() {
this.expenses.getExpenses().then(expenses => {
console.log({ expenses });
});
}
}

View file

@ -0,0 +1,37 @@
<div class="expense-container">
<div class="expense-header">
<div class="expense-date">
{{ `${expense().year}/${expense().month}/${expense().day}` | date }}
</div>
<div class="expense-amount">
{{ (expense().cents / 100) | currency: 'USD' }}
</div>
@if (expense().merchant) {
<div class="expense-merchant">&#64; {{ expense().merchant?.name }}</div>
}
</div>
<div class="expense-body">
<div>
Category: {{ expense().category.name }}
@if (expense().subCategory) {
/ {{ expense().subCategory?.name }}
}
</div>
<div>
Description: {{ expense().description }}
</div>
</div>
<div class="expense-footer">
<div class="expense-tags">
<div>Tags:</div>
@for (tag of expense().tags; track tag.id) {
<div>{{ tag.name }}</div>
}
</div>
</div>
</div>

View file

@ -0,0 +1,24 @@
.expense-container {
border-radius: 5px;
padding: 1rem;
box-shadow: rgba(99, 99, 99, 0.2) 0 2px 8px 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.expense-header {
display: flex;
gap: 0.5rem;
}
.expense-body {
}
.expense-footer {
.expense-tags {
display: flex;
gap: 0.5rem;
}
}

View file

@ -0,0 +1,16 @@
import {Component, input} from '@angular/core';
import { Expense } from '../../services/expense.service';
import {CurrencyPipe, DatePipe} from '@angular/common';
@Component({
selector: 'app-expense',
imports: [
CurrencyPipe,
DatePipe
],
templateUrl: './expense.component.html',
styleUrl: './expense.component.scss',
})
export class ExpenseComponent {
public expense = input.required<Expense>();
}

View file

@ -1,10 +1,10 @@
import { Component } from '@angular/core';
import { ExpenseList } from '../../components/expense-list/expense-list';
import { ExpenseListComponent } from '../../components/expense-list/expense-list.component';
@Component({
selector: 'app-expenses',
imports: [
ExpenseList
ExpenseListComponent
],
templateUrl: './expenses.html',
styleUrl: './expenses.scss'

View file

@ -1,7 +1,7 @@
<div class="home-container">
<ul>
<li>
<a routerLink="expenses">Expenses</a>
<a routerLink="expenses">ExpenseService</a>
</li>
</ul>
</div>

View file

@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class Categories {
public static readonly BASE_URL = 'http://localhost:3000/common-cents/categories';
public async getCategories(): Promise<Category[]> {
console.log('getCategories called');
return [];
}
}
export interface Category {
id: string;
name: string;
}

View file

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root',
})
export class CategoryService {
public static readonly CATEGORY_PATH = 'http://localhost:3000/common-cents/categories';
public constructor(private readonly http: HttpService) { }
public async getCategories(): Promise<Category[]> {
return this.http.get<Category[]>(CategoryService.CATEGORY_PATH);
}
}
export interface Category {
id: string;
name: string;
}

View file

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { SubCategory } from './sub-category.service';
import { Category } from './category.service';
import { Merchant } from './merchant.service';
import { Tag } from './tag.service';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root',
})
export class ExpenseService {
public static readonly EXPENSE_PATH = 'http://localhost:3000/common-cents/expenses';
public constructor(private readonly http: HttpService) { }
public async getExpenses(): Promise<Expense[]> {
return this.http.get<Expense[]>(ExpenseService.EXPENSE_PATH);
}
}
export interface Expense {
id: string;
year: string;
month: string;
day: string;
cents: number;
description?: string;
category: Category;
subCategory?: SubCategory;
merchant?: Merchant;
tags: Tag[];
}

View file

@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import { SubCategory } from './sub-categories';
import { Category } from './categories';
import { Merchant } from './merchants';
import { Tag } from './tags';
import { Http } from './http';
@Injectable({
providedIn: 'root',
})
export class Expenses {
public static readonly EXPENSES_URI = 'http://localhost:3000/common-cents/expenses';
public constructor(private readonly http: Http) { }
public async getExpenses(): Promise<Expense[]> {
return this.http.get<Expense[]>(Expenses.EXPENSES_URI);
}
}
export interface Expense {
id: string;
year: string;
month: string;
day: string;
cents: number;
description?: string;
category: Category;
subCategory?: SubCategory;
merchant?: Merchant;
tags: Tag[];
}

View file

@ -5,7 +5,7 @@ import { firstValueFrom } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class Http {
export class HttpService {
public constructor(private httpClient: HttpClient) { }
public async get<T>(url: string): Promise<T> {

View file

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root',
})
export class MerchantService {
public static readonly MERCHANT_PATH = 'http://localhost:3000/common-cents/merchants';
public constructor(private readonly http: HttpService) { }
public async getMerchants(): Promise<Merchant[]> {
return this.http.get<Merchant[]>(MerchantService.MERCHANT_PATH);
}
}
export interface Merchant {
id: string;
name: string;
}

View file

@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class Merchants {
public static readonly BASE_URL = 'http://localhost:3000/common-cents/merchants';
public async getMerchants(): Promise<Merchant[]> {
console.log('getMerchants called');
return [];
}
}
export interface Merchant {
id: string;
name: string;
}

View file

@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class SubCategories {
public static readonly BASE_URL = 'http://localhost:3000/common-cents/sub-categories';
public async getSubCategories(): Promise<SubCategory[]> {
console.log('getSubCategories called');
return [];
}
}
export interface SubCategory {
id: string;
name: string;
}

View file

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root',
})
export class SubCategoryService {
public static readonly SUBCATEGORY_PATH = 'http://localhost:3000/common-cents/sub-categories';
public constructor(private readonly http: HttpService) { }
public async getSubCategories(): Promise<SubCategory[]> {
return this.http.get<SubCategory[]>(SubCategoryService.SUBCATEGORY_PATH);
}
}
export interface SubCategory {
id: string;
name: string;
}

View file

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root',
})
export class TagService {
public static readonly TAG_PATH = 'http://localhost:3000/common-cents/tags';
public constructor(private readonly http: HttpService) { }
public async getTags(): Promise<Tag[]> {
return this.http.get<Tag[]>(TagService.TAG_PATH);
}
}
export interface Tag {
id: string;
name: string;
}

View file

@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class Tags {
public static readonly BASE_URL = 'http://localhost:3000/common-cents/tags';
public async getTags(): Promise<Tag[]> {
console.log('getTags called');
return [];
}
}
export interface Tag {
id: string;
name: string;
}