diff --git a/finnow-api/source/account/api.d b/finnow-api/source/account/api.d index 929c64a..c1c7173 100644 --- a/finnow-api/source/account/api.d +++ b/finnow-api/source/account/api.d @@ -11,6 +11,7 @@ import handy_http_handlers.path_handler; import profile.service; import account.model; import util.money; +import account.data; /// The data the API provides for an Account entity. struct AccountResponse { @@ -79,6 +80,25 @@ void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse r writeJsonBody(response, AccountResponse.of(account)); } +void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { + ulong accountId = request.getPathParamAs!ulong("accountId"); + AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request); + auto ds = getProfileDataSource(request); + AccountRepository repo = ds.getAccountRepository(); + auto account = repo.findById(accountId).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); + Account updated = repo.update(accountId, Account( + account.id, + account.createdAt, + account.archived, + AccountType.fromId(payload.type), + payload.numberSuffix, + payload.name, + Currency.ofCode(payload.currency), + payload.description + )); + writeJsonBody(response, AccountResponse.of(updated)); +} + void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); auto ds = getProfileDataSource(request); diff --git a/finnow-api/source/account/data_impl_sqlite.d b/finnow-api/source/account/data_impl_sqlite.d index 6334b0e..60a0723 100644 --- a/finnow-api/source/account/data_impl_sqlite.d +++ b/finnow-api/source/account/data_impl_sqlite.d @@ -70,7 +70,8 @@ SQL", newData.numberSuffix, newData.name, newData.currency.code, - newData.description + newData.description, + id ); return this.findById(id).orElseThrow("Account doesn't exist"); }); diff --git a/finnow-api/source/api_mapping.d b/finnow-api/source/api_mapping.d index 16be4d5..c47bb31 100644 --- a/finnow-api/source/api_mapping.d +++ b/finnow-api/source/api_mapping.d @@ -49,6 +49,7 @@ HttpRequestHandler mapApiHandlers() { a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts); a.map(HttpMethod.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount); a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleGetAccount); + a.map(HttpMethod.PUT, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleUpdateAccount); a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleDeleteAccount); import transaction.api; diff --git a/web-app/src/App.vue b/web-app/src/App.vue index 2cab29d..5d9fb7a 100644 --- a/web-app/src/App.vue +++ b/web-app/src/App.vue @@ -1,13 +1,10 @@ diff --git a/web-app/src/api/account.ts b/web-app/src/api/account.ts index 4b2b285..972986c 100644 --- a/web-app/src/api/account.ts +++ b/web-app/src/api/account.ts @@ -1,6 +1,43 @@ import { ApiClient } from './base' import type { Profile } from './profile' +export interface AccountType { + id: string + name: string + debitsPositive: boolean +} + +export abstract class AccountTypes { + public static readonly CHECKING: AccountType = { + id: 'CHECKING', + name: 'Checking', + debitsPositive: true, + } + public static readonly SAVINGS: AccountType = { + id: 'SAVINGS', + name: 'Savings', + debitsPositive: true, + } + public static readonly CREDIT_CARD: AccountType = { + id: 'CREDIT_CARD', + name: 'Credit Card', + debitsPositive: false, + } + public static readonly BROKERAGE: AccountType = { + id: 'BROKERAGE', + name: 'Brokerage', + debitsPositive: true, + } + + public static of(id: string): AccountType { + if (id === 'CHECKING') return AccountTypes.CHECKING + if (id === 'SAVINGS') return AccountTypes.SAVINGS + if (id === 'CREDIT_CARD') return AccountTypes.CREDIT_CARD + if (id === 'BROKERAGE') return AccountTypes.BROKERAGE + throw new Error('Unknown account type: ' + id) + } +} + export interface Account { id: number createdAt: string @@ -40,6 +77,10 @@ export class AccountApiClient extends ApiClient { return super.postJson(this.path, data) } + async updateAccount(id: number, data: AccountCreationPayload): Promise { + return super.putJson(this.path + '/' + id, data) + } + async deleteAccount(id: number): Promise { return super.delete(this.path + '/' + id) } diff --git a/web-app/src/assets/main.css b/web-app/src/assets/main.css index 3b56a92..35f6ab7 100644 --- a/web-app/src/assets/main.css +++ b/web-app/src/assets/main.css @@ -42,40 +42,9 @@ a:hover { text-decoration: underline; } -.app-page-container { - max-width: 600px; - margin-left: auto; - margin-right: auto; - padding: 0.5rem; - padding-bottom: 1rem; - background-color: var(--bg-page); - - border-bottom-left-radius: 2rem; - border-bottom-right-radius: 2rem; -} - -.app-page-title { - margin: 0; - font-size: 28px; - font-weight: 500; -} - .app-module-container { display: flex; flex-wrap: wrap; gap: 20px; padding: 1rem; } - -.app-module { - min-width: 300px; - min-height: 200px; - flex-grow: 1; - background-color: var(--bg-secondary); - border-radius: 0.5rem; - padding: 0.5rem; -} - -.app-module-header { - margin: 0; -} diff --git a/web-app/src/components/AppButton.vue b/web-app/src/components/AppButton.vue index 9de07da..249ae72 100644 --- a/web-app/src/components/AppButton.vue +++ b/web-app/src/components/AppButton.vue @@ -1,10 +1,16 @@ diff --git a/web-app/src/pages/home/ProfileModule.vue b/web-app/src/pages/home/ProfileModule.vue index b157388..52bb421 100644 --- a/web-app/src/pages/home/ProfileModule.vue +++ b/web-app/src/pages/home/ProfileModule.vue @@ -2,6 +2,7 @@ import { ProfileApiClient } from '@/api/profile'; import AppButton from '@/components/AppButton.vue'; import ConfirmModal from '@/components/ConfirmModal.vue'; +import HomeModule from '@/components/HomeModule.vue'; import { useProfileStore } from '@/stores/profile-store'; import { useTemplateRef } from 'vue'; import { useRouter } from 'vue-router'; @@ -23,20 +24,18 @@ async function deleteProfile() { } diff --git a/web-app/src/pages/home/TransactionsModule.vue b/web-app/src/pages/home/TransactionsModule.vue index 21feb83..169d532 100644 --- a/web-app/src/pages/home/TransactionsModule.vue +++ b/web-app/src/pages/home/TransactionsModule.vue @@ -1,6 +1,7 @@ diff --git a/web-app/src/router/index.ts b/web-app/src/router/index.ts index 955cd48..43ac9c6 100644 --- a/web-app/src/router/index.ts +++ b/web-app/src/router/index.ts @@ -6,6 +6,8 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vu import UserHomePage from '@/pages/UserHomePage.vue' import ProfilesPage from '@/pages/ProfilesPage.vue' import { useProfileStore } from '@/stores/profile-store' +import AccountPage from '@/pages/AccountPage.vue' +import EditAccountPage from '@/pages/forms/EditAccountPage.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -32,9 +34,30 @@ const router = createRouter({ meta: { title: 'Profiles' }, }, { - path: '/profiles/:name', - component: async () => ProfilePage, - meta: { title: (to: RouteLocationNormalized) => 'Profile ' + to.params.name }, + path: 'profiles/:name', + beforeEnter: profileSelected, + children: [ + { + path: '', + component: async () => ProfilePage, + meta: { title: (to: RouteLocationNormalized) => 'Profile ' + to.params.name }, + }, + { + path: 'accounts/:id', + component: async () => AccountPage, + meta: { title: 'Account' }, + }, + { + path: 'accounts/:id/edit', + component: async () => EditAccountPage, + meta: { title: 'Edit Account' }, + }, + { + path: 'add-account', + component: async () => EditAccountPage, + meta: { title: 'Add Account' }, + }, + ], }, ], }, diff --git a/web-app/src/util/alert.ts b/web-app/src/util/alert.ts index c46edd2..1ba78ac 100644 --- a/web-app/src/util/alert.ts +++ b/web-app/src/util/alert.ts @@ -1,13 +1,10 @@ export function showAlert(message: string): Promise { - const modal: HTMLDialogElement = document.getElementById( - 'global-alert-modal', - ) as HTMLDialogElement - const modalText: HTMLParagraphElement = document.getElementById( - 'global-alert-modal-text', - ) as HTMLParagraphElement - - modalText.innerText = message + getText().innerText = message + getOkButton().style.display = 'none' + getCancelButton().style.display = 'none' + getCloseButton().style.display = '' return new Promise((resolve) => { + const modal = getModal() modal.showModal() modal.addEventListener( 'close', @@ -18,3 +15,35 @@ export function showAlert(message: string): Promise { ) }) } + +export function showConfirm(message: string): Promise { + getText().innerText = message + getOkButton().style.display = '' + getCancelButton().style.display = '' + getCloseButton().style.display = 'none' + return new Promise((resolve) => { + const modal = getModal() + modal.showModal() + modal.addEventListener('close', () => resolve(modal.returnValue === 'ok'), { once: true }) + }) +} + +function getModal(): HTMLDialogElement { + return document.getElementById('global-alert-modal') as HTMLDialogElement +} + +function getText(): HTMLParagraphElement { + return document.getElementById('global-alert-modal-text') as HTMLParagraphElement +} + +function getOkButton(): HTMLButtonElement { + return document.getElementById('global-alert-modal-ok-button') as HTMLButtonElement +} + +function getCancelButton(): HTMLButtonElement { + return document.getElementById('global-alert-modal-cancel-button') as HTMLButtonElement +} + +function getCloseButton(): HTMLButtonElement { + return document.getElementById('global-alert-modal-close-button') as HTMLButtonElement +}