diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/ajax/backend.ts | 57 | ||||
| -rw-r--r-- | gui/src/components/app/App.tsx | 70 | ||||
| -rw-r--r-- | gui/src/components/histogram/Histogram.tsx | 44 | ||||
| -rw-r--r-- | gui/src/types/BalanceToBudgetRatio.spec.ts | 307 | ||||
| -rw-r--r-- | gui/src/types/account.ts | 26 | ||||
| -rw-r--r-- | gui/src/types/balanceMap.ts | 17 | ||||
| -rw-r--r-- | gui/src/types/balanceToBudgetRatio.ts | 83 | ||||
| -rw-r--r-- | gui/src/types/budget.ts | 12 | ||||
| -rw-r--r-- | gui/src/types/budgetMap.ts | 16 | ||||
| -rw-r--r-- | gui/src/types/category.ts | 51 | ||||
| -rw-r--r-- | gui/src/types/currency/gbp.ts | 10 | ||||
| -rw-r--r-- | gui/src/types/dtos/accountDto.ts | 8 |
12 files changed, 605 insertions, 96 deletions
diff --git a/gui/src/ajax/backend.ts b/gui/src/ajax/backend.ts index d58e513..90a7ea5 100644 --- a/gui/src/ajax/backend.ts +++ b/gui/src/ajax/backend.ts @@ -1,5 +1,58 @@ +import { BalanceMap, newCategoryBalanceMapFromDto } from "../types/balanceMap"; +import { BudgetMap, newBudgetMapFromDto } from "../types/budgetMap"; +import { Category, newCategoryFromDto } from "../types/category"; + const backendUrl = "http://localhost:8000"; -async function getAllAccounts(): Promise<Account[]> { - return; +function makeGetter<T>(params: FetcherMakerParams<T>): Getter<T> { + const parser = + "parser" in params + ? params.parser + : (dto: unknown[]) => <T>dto.map(params.elementParser); + + return async function () { + const response = await fetch(`${backendUrl}${params.path}`); + const dto = await response.json(); + + try { + return parser(dto); + } catch (err) { + console.error(`Error parsing DTO. ${err?.toString() ?? ""}`); + return null; + } + }; } + +export const getAllCategories: Getter<Category[]> = async () => { + const balances = await getAllCategoryBalances(); + const budgets = await getAllCurrentCategoryBudgets(); + return makeGetter<Category[]>({ + path: "/category", + elementParser: newCategoryFromDto(balances, budgets), + })(); +}; + +export const getAllCategoryBalances: Getter<BalanceMap<Category>> = makeGetter< + BalanceMap<Category> +>({ + path: "/balance/category", + parser: newCategoryBalanceMapFromDto, +}); + +export const getAllCurrentCategoryBudgets: Getter<BudgetMap<Category>> = + makeGetter<BudgetMap<Category>>({ + path: "/budget/category?current", + parser: newBudgetMapFromDto, + }); + +type FetcherMakerParams<T> = { + path: string; +} & AnyParser<T>; + +type Parser<T> = { parser: (dto: unknown) => T | null }; +type ElementParser<T> = { elementParser: (dto: unknown) => T | null }; +type AnyParser<T> = T extends any[] + ? Parser<T> | ElementParser<T[number]> + : Parser<T>; + +type Getter<T> = () => Promise<T | null>; diff --git a/gui/src/components/app/App.tsx b/gui/src/components/app/App.tsx index cd9f9ee..e9ef742 100644 --- a/gui/src/components/app/App.tsx +++ b/gui/src/components/app/App.tsx @@ -1,53 +1,33 @@ -import Category from '../../types/category'; -import Gbp from '../../types/currency/gbp'; +import { createResource, Match, Resource, Switch } from 'solid-js'; import Histogram from '../histogram/Histogram' import './App.css' +import { getAllCategories } from '../../ajax/backend'; +import { Category } from '../../types/category'; function App() { + const [categories] = createResource(getAllCategories); + return ( - <> - <Histogram categories={categories} /> - </> - ) + <Switch> + <Match when={categories.state === 'ready'}> + <Histogram categories={(categories as Extract<Resource<Category[]>, { state: 'ready' }>)()} /> + </Match> + <Match when={categories.state === 'pending'}> + <p>Loading...</p> + </Match> + <Match when={categories.state === 'unresolved'}> + <p>Unresolved!</p> + </Match> + <Match when={categories.state === 'errored'}> + <p>Error!</p> + <p>{JSON.stringify(categories.error, null, 2)}</p> + <p>{JSON.stringify(categories(), null, 2)}</p> + </Match> + <Match when={categories.state === 'refreshing'}> + <p>Refreshing!</p> + </Match> + </Switch> + ); } -const categories: Category[] = [ - { - id: 0, - name: 'Groceries', - currentBalance: new Gbp(71, 37, 1), - currentBudget: { - quantity: new Gbp(65, 0, 1), - period: 7, - }, - }, - { - id: 1, - name: 'Stationery', - currentBalance: new Gbp(10, 12, -1), - currentBudget: { - quantity: new Gbp(12, 0, 1), - period: 30, - }, - }, - { - id: 2, - name: 'Holidays', - currentBalance: new Gbp(325, 0, 1), - currentBudget: { - quantity: new Gbp(1000, 0, 1), - period: 365, - }, - }, - { - id: 3, - name: 'Wages', - currentBalance: new Gbp(1025, 60, -1), - currentBudget: { - quantity: new Gbp(2167, 7, -1), - period: 30.25, - }, - }, -].map(c => ({ ...c, balanceToBudgetRatio: c.currentBalance.valueOf() / c.currentBudget.quantity.valueOf() })); - export default App diff --git a/gui/src/components/histogram/Histogram.tsx b/gui/src/components/histogram/Histogram.tsx index ee4b7b0..3a8309b 100644 --- a/gui/src/components/histogram/Histogram.tsx +++ b/gui/src/components/histogram/Histogram.tsx @@ -1,10 +1,18 @@ -import Category from "../../types/category"; +import { BalanceToBudgetRatio } from "../../types/balanceToBudgetRatio"; +import { Category } from "../../types/category"; import './Histogram.scss'; function Histogram({ categories }: Props) { - const maxAbsBalanceToBudgetRatio = Math.max( - ...categories.map((c) => Math.abs(c.balanceToBudgetRatio)) - ); + if (categories === null) { + return ( + <p>No categories to show</p> + ); + } + + const balanceToBudgetRatioNormaliser = BalanceToBudgetRatio.makeNormaliser(...categories.map(c => c.balanceToBudgetRatio)); + for (const category of categories) { + category.balanceToBudgetRatio = balanceToBudgetRatioNormaliser(category.balanceToBudgetRatio); + } return ( <table> @@ -19,27 +27,24 @@ function Histogram({ categories }: Props) { <tbody> { categories - .sort((c1, c2) => c1.balanceToBudgetRatio - c2.balanceToBudgetRatio) - .map((category) => Row({ category, maxAbsBalanceToBudgetRatio })) + .sort((c1, c2) => BalanceToBudgetRatio.compare(c1.balanceToBudgetRatio, c2.balanceToBudgetRatio)) + .map((category) => Row({ category })) } </tbody> </table> ); } -function Row({ category, maxAbsBalanceToBudgetRatio }: RowProps) { - const normalisedRatio = category.balanceToBudgetRatio / maxAbsBalanceToBudgetRatio; - const variant = getRowVariant(new Number(normalisedRatio).valueOf()); - +function Row({ category }: RowProps) { return ( <tr> <td class="name">{category.name}</td> <td - class={`ratio ratio--${variant}`} - style={`--normal-abs-ratio: ${Math.abs(normalisedRatio)};`} + class={`ratio ratio--${category.balanceToBudgetRatio?.recommendedAction ?? ''}`} + style={`--normal-abs-ratio: ${Math.abs(category.balanceToBudgetRatio?.normalisedRatio ?? 0)};`} > <span class="ratio__text"> - {category.balanceToBudgetRatio.toFixed(2)} + {category.balanceToBudgetRatio?.toString() ?? ''} </span> <span class="ratio__bar"></span> </td> @@ -48,23 +53,12 @@ function Row({ category, maxAbsBalanceToBudgetRatio }: RowProps) { ); } -function getRowVariant(balanceToBudgetRatio: number) { - if (balanceToBudgetRatio < 0) { - return 'arrears'; - } - if (balanceToBudgetRatio >= 1) { - return 'spend'; - } - return 'save'; -} - type Props = { - categories: Category[], + categories: Category[] | null, }; type RowProps = { category: Category, - maxAbsBalanceToBudgetRatio: number, }; export default Histogram; diff --git a/gui/src/types/BalanceToBudgetRatio.spec.ts b/gui/src/types/BalanceToBudgetRatio.spec.ts new file mode 100644 index 0000000..3a956ea --- /dev/null +++ b/gui/src/types/BalanceToBudgetRatio.spec.ts @@ -0,0 +1,307 @@ +import { describe, expect, it } from "vitest"; +import { BalanceToBudgetRatio } from "./balanceToBudgetRatio"; +import { Gbp } from "./currency/gbp"; + +describe("balanceToBudgetRatio", () => { + it("new() returns null when budget is null", () => { + const result = BalanceToBudgetRatio.new(new Gbp(1, 0, 1), null); + expect(result).toBeNull(); + }); + + it("toString() rounds to 2 decimal places", () => { + const mockCurrencyPi = { valueOf: () => Math.PI }; + const mockCurrencyUnit = { valueOf: () => 1 }; + const sut = BalanceToBudgetRatio.new(mockCurrencyPi, { + quantity: mockCurrencyUnit, + period: 42, + }); + + const result = sut?.toString(); + + expect(result).toBe("3.14"); + }); + + it("new() sets normalisedRatio to 1 when balance and budget are equal and positive", () => { + const twoFifty = new Gbp(2, 50, 1); + const result = BalanceToBudgetRatio.new(twoFifty, { + quantity: twoFifty, + period: 42, + }); + expect(result?.normalisedRatio).toBeCloseTo(1.0); + }); + + it("new() sets normalisedRatio to 2 when balance is twice budget", () => { + const twoFifty = new Gbp(2, 50, 1); + const oneTwentyFive = new Gbp(1, 25, 1); + const result = BalanceToBudgetRatio.new(twoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + expect(result?.normalisedRatio).toBeCloseTo(2.0); + }); + + it("new() sets normalisedRatio to -2 when a negative balance is -1 * twice budget", () => { + const minusTwoFifty = new Gbp(2, 50, -1); + const oneTwentyFive = new Gbp(1, 25, 1); + const result = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + expect(result?.normalisedRatio).toBeCloseTo(-2.0); + }); + + it("new() sets normalisedRatio to -0.5 when balance is -1 * half a negative budget", () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const minusTwoFifty = new Gbp(2, 50, -1); + const result = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: minusTwoFifty, + period: 42, + }); + expect(result?.normalisedRatio).toBeCloseTo(-0.5); + }); + + it("new() sets normalisedRatio to 1 when balance and budget are equal and negative", () => { + const minusTwoFifty = new Gbp(2, 50, -1); + const result = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: minusTwoFifty, + period: 42, + }); + expect(result?.normalisedRatio).toBeCloseTo(1.0); + }); + + it('new() sets recommended action to "arrears" when ratio is less than 0', () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const minusTwoFifty = new Gbp(2, 50, -1); + + const result = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: minusTwoFifty, + period: 42, + }); + + expect(result?.recommendedAction).toBe("arrears"); + }); + + it('new() sets recommended action to "save" when ratio is between 0 and 1', () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const twoFifty = new Gbp(2, 50, 1); + + const result = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + + expect(result?.recommendedAction).toBe("save"); + }); + + it('new() sets recommended action to "spend" when ratio is larger than 1', () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const twoFifty = new Gbp(2, 50, 1); + + const result = BalanceToBudgetRatio.new(twoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + + expect(result?.recommendedAction).toBe("spend"); + }); + + it("normalises to a factor of 2 when all ratios are positive", () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const twoFifty = new Gbp(2, 50, 1); + const twoOverOne = BalanceToBudgetRatio.new(twoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + const twoOverTwo = BalanceToBudgetRatio.new(twoFifty, { + quantity: twoFifty, + period: 42, + }); + const oneOverOne = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: oneTwentyFive, + period: 42, + }); + const oneOverTwo = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + + const normaliser = BalanceToBudgetRatio.makeNormaliser( + twoOverOne, + twoOverTwo, + oneOverOne, + oneOverTwo, + ); + + expect(normaliser(twoOverOne).normalisedRatio).toBeCloseTo(1.0); + expect(normaliser(twoOverTwo).normalisedRatio).toBeCloseTo(0.5); + expect(normaliser(oneOverOne).normalisedRatio).toBeCloseTo(0.5); + expect(normaliser(oneOverTwo).normalisedRatio).toBeCloseTo(0.25); + }); + + it("normalises to a factor of 2 when all ratios are negative", () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const twoFifty = new Gbp(2, 50, 1); + const minusOneTwentyFive = new Gbp(1, 25, -1); + const minusTwoFifty = new Gbp(2, 50, -1); + const minusTwoOverOne = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + const minusTwoOverTwo = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: twoFifty, + period: 42, + }); + const minusOneOverOne = BalanceToBudgetRatio.new(minusOneTwentyFive, { + quantity: oneTwentyFive, + period: 42, + }); + const minusOneOverTwo = BalanceToBudgetRatio.new(minusOneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + + const normaliser = BalanceToBudgetRatio.makeNormaliser( + minusTwoOverOne, + minusTwoOverTwo, + minusOneOverOne, + minusOneOverTwo, + ); + + expect(normaliser(minusTwoOverOne).normalisedRatio).toBeCloseTo(-1.0); + expect(normaliser(minusTwoOverTwo).normalisedRatio).toBeCloseTo(-0.5); + expect(normaliser(minusOneOverOne).normalisedRatio).toBeCloseTo(-0.5); + expect(normaliser(minusOneOverTwo).normalisedRatio).toBeCloseTo(-0.25); + }); + + it("normalises to a factor of 2 when ratios are a mix of positive and negative", () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const twoFifty = new Gbp(2, 50, 1); + const minusOneTwentyFive = new Gbp(1, 25, -1); + const minusTwoFifty = new Gbp(2, 50, -1); + const twoOverOne = BalanceToBudgetRatio.new(twoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + const twoOverTwo = BalanceToBudgetRatio.new(twoFifty, { + quantity: twoFifty, + period: 42, + }); + const oneOverOne = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: oneTwentyFive, + period: 42, + }); + const oneOverTwo = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + const minusTwoOverOne = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: oneTwentyFive, + period: 42, + }); + const minusTwoOverTwo = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: twoFifty, + period: 42, + }); + const minusOneOverOne = BalanceToBudgetRatio.new(minusOneTwentyFive, { + quantity: oneTwentyFive, + period: 42, + }); + const minusOneOverTwo = BalanceToBudgetRatio.new(minusOneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + + const normaliser = BalanceToBudgetRatio.makeNormaliser( + twoOverOne, + twoOverTwo, + oneOverOne, + oneOverTwo, + minusTwoOverOne, + minusTwoOverTwo, + minusOneOverOne, + minusOneOverTwo, + ); + + expect(normaliser(twoOverOne).normalisedRatio).toBeCloseTo(1.0); + expect(normaliser(twoOverTwo).normalisedRatio).toBeCloseTo(0.5); + expect(normaliser(oneOverOne).normalisedRatio).toBeCloseTo(0.5); + expect(normaliser(oneOverTwo).normalisedRatio).toBeCloseTo(0.25); + expect(normaliser(minusTwoOverOne).normalisedRatio).toBeCloseTo(-1.0); + expect(normaliser(minusTwoOverTwo).normalisedRatio).toBeCloseTo(-0.5); + expect(normaliser(minusOneOverOne).normalisedRatio).toBeCloseTo(-0.5); + expect(normaliser(minusOneOverTwo).normalisedRatio).toBeCloseTo(-0.25); + }); + + it("sorts large positive ratios before small positive ratios", () => { + const oneTwentyFive = new Gbp(1, 25, 1); + const twoFifty = new Gbp(2, 50, 1); + const twoOverTwo = BalanceToBudgetRatio.new(twoFifty, { + quantity: twoFifty, + period: 42, + }); + const oneOverTwo = BalanceToBudgetRatio.new(oneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + + const resultLeftways = BalanceToBudgetRatio.compare(twoOverTwo, oneOverTwo); + const resultRightways = BalanceToBudgetRatio.compare( + oneOverTwo, + twoOverTwo, + ); + + expect(resultLeftways).toBeCloseTo(-resultRightways); + expect(resultLeftways).toBeLessThan(0); + }); + + it("sorts small negative ratios before large negative ratios", () => { + const twoFifty = new Gbp(2, 50, 1); + const minusOneTwentyFive = new Gbp(1, 25, -1); + const minusTwoFifty = new Gbp(2, 50, -1); + const minusTwoOverTwo = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: twoFifty, + period: 42, + }); + const minusOneOverTwo = BalanceToBudgetRatio.new(minusOneTwentyFive, { + quantity: twoFifty, + period: 42, + }); + + const resultLeftways = BalanceToBudgetRatio.compare( + minusOneOverTwo, + minusTwoOverTwo, + ); + const resultRightways = BalanceToBudgetRatio.compare( + minusTwoOverTwo, + minusOneOverTwo, + ); + + expect(resultLeftways).toBeCloseTo(-resultRightways); + expect(resultLeftways).toBeLessThan(0); + }); + + it("sorts positive ratios before negative ratios", () => { + const twoFifty = new Gbp(2, 50, 1); + const minusTwoFifty = new Gbp(2, 50, -1); + const twoOverTwo = BalanceToBudgetRatio.new(twoFifty, { + quantity: twoFifty, + period: 42, + }); + const minusTwoOverTwo = BalanceToBudgetRatio.new(minusTwoFifty, { + quantity: twoFifty, + period: 42, + }); + + const resultLeftways = BalanceToBudgetRatio.compare( + twoOverTwo, + minusTwoOverTwo, + ); + const resultRightways = BalanceToBudgetRatio.compare( + minusTwoOverTwo, + twoOverTwo, + ); + + expect(resultLeftways).toBeCloseTo(-resultRightways); + expect(resultLeftways).toBeLessThan(0); + }); +}); diff --git a/gui/src/types/account.ts b/gui/src/types/account.ts index 4a3626c..a463304 100644 --- a/gui/src/types/account.ts +++ b/gui/src/types/account.ts @@ -1,5 +1,27 @@ -import { Moment } from "moment"; +import moment, { Moment } from "moment"; import Currency from "./currency/currency"; +import Gbp from "./currency/gbp"; + +function newAccountFromDto(dto: unknown): Account { + const dtoDangerous = dto as AccountDto; + return { + id: dtoDangerous.id, + name: dtoDangerous.name, + openingBalance: new Gbp( + Math.floor(Math.abs(dtoDangerous.opening_balance) / 100), + Math.abs(dtoDangerous.opening_balance) % 100, + dtoDangerous.opening_balance < 0 ? -1 : 1, + ), + openingDate: moment(dtoDangerous.opening_date), + }; +} + +interface AccountDto { + readonly id: number; + readonly name: string; + readonly opening_balance: number; + readonly opening_date: string; +} interface Account { readonly id: number; @@ -8,4 +30,4 @@ interface Account { readonly openingDate: Moment; } -export default Account; +export { type Account, newAccountFromDto }; diff --git a/gui/src/types/balanceMap.ts b/gui/src/types/balanceMap.ts new file mode 100644 index 0000000..760843f --- /dev/null +++ b/gui/src/types/balanceMap.ts @@ -0,0 +1,17 @@ +import { Category } from "./category"; +import Currency from "./currency/currency"; +import { newGbpFromDto } from "./currency/gbp"; + +type BalanceMap<T extends { id: any }> = { + [id in T["id"]]: Currency; +}; + +function newCategoryBalanceMapFromDto(dto: unknown): BalanceMap<Category> { + return Object.fromEntries( + (dto as { category_id: Category["id"]; balance: number }[]).map( + ({ category_id, balance }) => [category_id, newGbpFromDto(balance)], + ), + ); +} + +export { type BalanceMap, newCategoryBalanceMapFromDto }; diff --git a/gui/src/types/balanceToBudgetRatio.ts b/gui/src/types/balanceToBudgetRatio.ts new file mode 100644 index 0000000..2f8000c --- /dev/null +++ b/gui/src/types/balanceToBudgetRatio.ts @@ -0,0 +1,83 @@ +import { Budget } from "./budget"; +import Currency from "./currency/currency"; + +class BalanceToBudgetRatio { + toString(): string { + return this._ratio?.toFixed(2) ?? ""; + } + + public readonly recommendedAction: "arrears" | "save" | "spend" | null; + public readonly normalisedRatio: number | null; + + private constructor( + private readonly _ratio: number | null, + private readonly _normalisationFactor: number, + ) { + if (this._ratio === null) { + this.normalisedRatio = null; + this.recommendedAction = null; + return; + } + + this.normalisedRatio = this._ratio * this._normalisationFactor; + + if (this._ratio < 0) { + this.recommendedAction = "arrears"; + } else if (this._ratio >= 1) { + this.recommendedAction = "spend"; + } else { + this.recommendedAction = "save"; + } + } + + static new(balance: Currency, budget: Budget | null) { + if (budget === null || budget.quantity === 0 || budget.quantity === -0) { + return null; + } + + return new BalanceToBudgetRatio( + balance.valueOf() / budget.quantity.valueOf(), + 1, + ); + } + + static makeNormaliser( + ...balanceToBudgetRatios: (BalanceToBudgetRatio | null)[] + ): (r: BalanceToBudgetRatio | null) => BalanceToBudgetRatio { + const validRatios = balanceToBudgetRatios.filter( + (r) => + r && + r._ratio && + !isNaN(r._ratio) && + Infinity > r._ratio && + -Infinity < r._ratio, + ); + + const maxAbsRatio = Math.max( + ...validRatios.map((r) => Math.abs(r?._ratio ?? 0)), + ); + + const normalisationFactor = 1 / maxAbsRatio; + return (r) => + new BalanceToBudgetRatio(r?._ratio ?? null, normalisationFactor); + } + + static compare( + lhs: BalanceToBudgetRatio | null, + rhs: BalanceToBudgetRatio | null, + ): number { + if (lhs === null || lhs._ratio === null) { + if (rhs === null || rhs._ratio === null) { + return 0; + } + return 1; + } + if (rhs === null || rhs._ratio === null) { + return -1; + } + + return rhs._ratio - lhs._ratio; + } +} + +export { BalanceToBudgetRatio }; diff --git a/gui/src/types/budget.ts b/gui/src/types/budget.ts index 86bae2c..5a19a1b 100644 --- a/gui/src/types/budget.ts +++ b/gui/src/types/budget.ts @@ -1,8 +1,12 @@ import Currency from "./currency/currency"; interface Budget { - quantity: Currency, - period: number, -}; + quantity: Currency; + period: number; +} -export default Budget; +function newBudgetFromDto(dto: unknown) { + return dto as Budget; +} + +export { type Budget, newBudgetFromDto }; diff --git a/gui/src/types/budgetMap.ts b/gui/src/types/budgetMap.ts new file mode 100644 index 0000000..5fa09f2 --- /dev/null +++ b/gui/src/types/budgetMap.ts @@ -0,0 +1,16 @@ +import { Budget, newBudgetFromDto } from "./budget"; +import { Category } from "./category"; + +type BudgetMap<T extends { id: any }> = { + [id in T["id"]]: Budget | null; +}; + +function newCategoryBudgetMapFromDto(dto: unknown): BudgetMap<Category> { + return Object.fromEntries( + (dto as { category_id: number; budget: unknown }[]).map( + ({ category_id, budget }) => [category_id, newBudgetFromDto(budget)], + ), + ); +} + +export { type BudgetMap, newCategoryBudgetMapFromDto as newBudgetMapFromDto }; diff --git a/gui/src/types/category.ts b/gui/src/types/category.ts index af4526b..005c041 100644 --- a/gui/src/types/category.ts +++ b/gui/src/types/category.ts @@ -1,12 +1,45 @@ -import Budget from "./budget"; +import { BalanceMap } from "./balanceMap"; +import { BalanceToBudgetRatio } from "./balanceToBudgetRatio"; +import { Budget } from "./budget"; +import { BudgetMap } from "./budgetMap"; import Currency from "./currency/currency"; interface Category { - id: number, - name: string, - currentBalance: Currency, - currentBudget: Budget, - balanceToBudgetRatio: number, -}; - -export default Category; + id: number; + name: string; + currentBalance: Currency; + currentBudget: Budget | null; + balanceToBudgetRatio: BalanceToBudgetRatio | null; +} + +interface CategoryDto { + id: number; + name: string; +} + +function newCategoryFromDto( + balances: BalanceMap<Category> | null, + budgets: BudgetMap<Category> | null, +): (dto: unknown) => Category | null { + if (balances === null || budgets === null) { + return () => null; + } + + return function (dto: unknown): Category { + const dtoDangerous = dto as CategoryDto; + const currentBalance = balances[dtoDangerous.id]; + const currentBudget = budgets[dtoDangerous.id]; + return { + id: dtoDangerous.id, + name: dtoDangerous.name, + currentBalance, + currentBudget, + balanceToBudgetRatio: BalanceToBudgetRatio.new( + currentBalance, + currentBudget, + ), + }; + }; +} + +export { type Category, newCategoryFromDto }; diff --git a/gui/src/types/currency/gbp.ts b/gui/src/types/currency/gbp.ts index ce24e48..b88aae3 100644 --- a/gui/src/types/currency/gbp.ts +++ b/gui/src/types/currency/gbp.ts @@ -43,4 +43,12 @@ class Gbp implements Currency { } } -export default Gbp; +function newGbpFromDto(dto: unknown): Gbp { + const signedPence = dto as number; + const pounds = Math.floor(Math.abs(signedPence) / 100); + const pence = Math.abs(signedPence) % 100; + const sign = signedPence < 0 ? -1 : 1; + return new Gbp(pounds, pence, sign); +} + +export { Gbp, newGbpFromDto }; diff --git a/gui/src/types/dtos/accountDto.ts b/gui/src/types/dtos/accountDto.ts deleted file mode 100644 index 0ed7622..0000000 --- a/gui/src/types/dtos/accountDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface AccountDto { - readonly id: number; - readonly name: string; - readonly openingBalance: number; - readonly openingDate: string; -} - -export default AccountDto; |
