diff --git a/src/app/app.config.ts b/src/app/app.config.ts index cb1270e..e60fc79 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,6 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; - import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { diff --git a/src/app/components/expense-list/expense-list.component.html b/src/app/components/expense-list/expense-list.component.html new file mode 100644 index 0000000..c5fb02f --- /dev/null +++ b/src/app/components/expense-list/expense-list.component.html @@ -0,0 +1,7 @@ +
+ @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 new file mode 100644 index 0000000..10dc123 --- /dev/null +++ b/src/app/components/expense-list/expense-list.component.scss @@ -0,0 +1,7 @@ +.expense-list-container { + padding: 1rem; +} + +.expense-item { + padding-bottom: 1rem; +} diff --git a/src/app/components/expense-list/expense-list.component.ts b/src/app/components/expense-list/expense-list.component.ts new file mode 100644 index 0000000..b4dc0d1 --- /dev/null +++ b/src/app/components/expense-list/expense-list.component.ts @@ -0,0 +1,51 @@ +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([]); + protected categories = signal([]); + protected subCategories = signal([]); + protected merchants = signal([]); + protected tags = signal([]); + + public constructor(private readonly expensesService: ExpenseService, + private readonly categoryService: CategoryService, + private readonly subCategoryService: SubCategoryService, + private readonly merchantService: MerchantService, + private readonly tagService: TagService) { } + + public ngOnInit() { + // this.expensesService.getExpenses().then(expenses => { + // console.log({ expenses }); // TODO: remove me + // this.expenses.set(expenses); + // }); + + 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); + }) + } +} diff --git a/src/app/components/expense-list/expense-list.html b/src/app/components/expense-list/expense-list.html deleted file mode 100644 index d316863..0000000 --- a/src/app/components/expense-list/expense-list.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

expense-list works!

-
diff --git a/src/app/components/expense-list/expense-list.scss b/src/app/components/expense-list/expense-list.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/components/expense-list/expense-list.ts b/src/app/components/expense-list/expense-list.ts deleted file mode 100644 index 05427fb..0000000 --- a/src/app/components/expense-list/expense-list.ts +++ /dev/null @@ -1,16 +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() { - void this.expenses.getExpenses(); - } -} diff --git a/src/app/components/expense/expense.component.html b/src/app/components/expense/expense.component.html new file mode 100644 index 0000000..fee8c5b --- /dev/null +++ b/src/app/components/expense/expense.component.html @@ -0,0 +1,37 @@ +
+
+
+ {{ `${expense().year}/${expense().month}/${expense().day}` | date }} +
+ +
+ {{ (expense().cents / 100) | currency: 'USD' }} +
+ + @if (expense().merchant) { +
@ {{ expense().merchant?.name }}
+ } +
+ +
+
+ Category: {{ expense().category.name }} + @if (expense().subCategory) { + / {{ expense().subCategory?.name }} + } +
+ +
+ Description: {{ expense().description }} +
+
+ + +
diff --git a/src/app/components/expense/expense.component.scss b/src/app/components/expense/expense.component.scss new file mode 100644 index 0000000..a49c419 --- /dev/null +++ b/src/app/components/expense/expense.component.scss @@ -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; + } +} diff --git a/src/app/components/expense/expense.component.ts b/src/app/components/expense/expense.component.ts new file mode 100644 index 0000000..1b61a9a --- /dev/null +++ b/src/app/components/expense/expense.component.ts @@ -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(); +} diff --git a/src/app/pages/expenses/expenses.ts b/src/app/pages/expenses/expenses.ts index 1fefb3e..44db40c 100644 --- a/src/app/pages/expenses/expenses.ts +++ b/src/app/pages/expenses/expenses.ts @@ -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' diff --git a/src/app/pages/home/home.html b/src/app/pages/home/home.html index e74d430..94c5c02 100644 --- a/src/app/pages/home/home.html +++ b/src/app/pages/home/home.html @@ -1,7 +1,7 @@ diff --git a/src/app/services/category.service.ts b/src/app/services/category.service.ts new file mode 100644 index 0000000..ee88ef3 --- /dev/null +++ b/src/app/services/category.service.ts @@ -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 { + return this.http.get(CategoryService.CATEGORY_PATH); + } +} + +export interface Category { + id: string; + name: string; +} diff --git a/src/app/services/expense.service.ts b/src/app/services/expense.service.ts new file mode 100644 index 0000000..d795591 --- /dev/null +++ b/src/app/services/expense.service.ts @@ -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 { + return this.http.get(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[]; +} diff --git a/src/app/services/expenses.ts b/src/app/services/expenses.ts deleted file mode 100644 index 5c51b87..0000000 --- a/src/app/services/expenses.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class Expenses { - public static readonly BASE_URL = 'http://localhost:3000/common-cents/expenses'; - - public async getExpenses(): Promise { - console.log('getExpenses called'); - } -} diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts new file mode 100644 index 0000000..d9f3212 --- /dev/null +++ b/src/app/services/http.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class HttpService { + public constructor(private httpClient: HttpClient) { } + + public async get(url: string): Promise { + return this.request(url, 'get'); + } + + public async post(url: string, body?: any): Promise { + return this.request(url, 'post', body); + } + + public async put(url: string, body?: any): Promise { + return this.request(url, 'put', body); + } + + public async delete(url: string): Promise { + return this.request(url, 'delete'); + } + + private async request(url: string, method: 'get' | 'post' | 'put' | 'delete', body?: any): Promise { + const headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' }; + switch (method) { + case 'post': + return firstValueFrom(this.httpClient.post(url, body, { headers })); + case 'put': + return firstValueFrom(this.httpClient.put(url, body, { headers })); + case 'delete': + return firstValueFrom(this.httpClient.delete(url, { headers })); + default: + return firstValueFrom(this.httpClient.get(url, { headers })); + } + } +} diff --git a/src/app/services/merchant.service.ts b/src/app/services/merchant.service.ts new file mode 100644 index 0000000..20a9d68 --- /dev/null +++ b/src/app/services/merchant.service.ts @@ -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 { + return this.http.get(MerchantService.MERCHANT_PATH); + } +} + +export interface Merchant { + id: string; + name: string; +} diff --git a/src/app/services/sub-category.service.ts b/src/app/services/sub-category.service.ts new file mode 100644 index 0000000..1b4e268 --- /dev/null +++ b/src/app/services/sub-category.service.ts @@ -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 { + return this.http.get(SubCategoryService.SUBCATEGORY_PATH); + } +} + +export interface SubCategory { + id: string; + name: string; +} diff --git a/src/app/services/tag.service.ts b/src/app/services/tag.service.ts new file mode 100644 index 0000000..e5acad5 --- /dev/null +++ b/src/app/services/tag.service.ts @@ -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 { + return this.http.get(TagService.TAG_PATH); + } +} + +export interface Tag { + id: string; + name: string; +}