diff --git a/finnow-api/source/profile/data_impl_sqlite.d b/finnow-api/source/profile/data_impl_sqlite.d index 9eb0405..5fe6550 100644 --- a/finnow-api/source/profile/data_impl_sqlite.d +++ b/finnow-api/source/profile/data_impl_sqlite.d @@ -152,6 +152,7 @@ class SqliteProfileDataSource : ProfileDataSource { import std.file : exists; bool needsInit = !exists(path); this.db = Database(path); + db.run("PRAGMA foreign_keys = ON"); if (needsInit) { infoF!"Initializing database: %s"(dbPath); db.run(SCHEMA); diff --git a/finnow-api/source/transaction/service.d b/finnow-api/source/transaction/service.d index 975dcb2..d69157b 100644 --- a/finnow-api/source/transaction/service.d +++ b/finnow-api/source/transaction/service.d @@ -286,6 +286,11 @@ TransactionCategoryResponse createCategory(ProfileDataSource ds, in CategoryPayl if (!payload.parentId.isNull && !repo.existsById(payload.parentId.get)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid parent id."); } + import std.regex; + const colorHexRegex = ctRegex!`^(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$`; + if (payload.color is null || matchFirst(payload.color, colorHexRegex).empty) { + throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid color hex string."); + } auto category = repo.insert( toOptional(payload.parentId), payload.name, diff --git a/finnow-api/sql/schema.sql b/finnow-api/sql/schema.sql index 2c8dbe6..3846c8a 100644 --- a/finnow-api/sql/schema.sql +++ b/finnow-api/sql/schema.sql @@ -110,7 +110,7 @@ CREATE TABLE transaction_line_item ( ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_transaction_line_item_category FOREIGN KEY (category_id) REFERENCES transaction_category(id) - ON UPDATE CASCADE ON DELETE CASCADE + ON UPDATE CASCADE ON DELETE SET NULL ); CREATE TABLE account_journal_entry ( diff --git a/web-app/src/api/transaction.ts b/web-app/src/api/transaction.ts index 9234bda..07a8ad2 100644 --- a/web-app/src/api/transaction.ts +++ b/web-app/src/api/transaction.ts @@ -1,7 +1,7 @@ +import { useProfileStore } from '@/stores/profile-store' import { ApiClient } from './base' import type { Currency } from './data' import { type Page, type PageRequest } from './pagination' -import type { Profile } from './profile' export interface TransactionVendor { id: number @@ -134,9 +134,11 @@ export interface CreateCategoryPayload { export class TransactionApiClient extends ApiClient { readonly path: string - constructor(profile: Profile) { + constructor() { super() - this.path = `/profiles/${profile.name}` + const profileStore = useProfileStore() + if (!profileStore.state) throw new Error('No profile state!') + this.path = `/profiles/${profileStore.state.name}` } getVendors(): Promise { @@ -172,7 +174,7 @@ export class TransactionApiClient extends ApiClient { } updateCategory(id: number, data: CreateCategoryPayload): Promise { - return super.postJson(this.path + '/categories/' + id, data) + return super.putJson(this.path + '/categories/' + id, data) } deleteCategory(id: number): Promise { diff --git a/web-app/src/components/CategoryDisplayItem.vue b/web-app/src/components/CategoryDisplayItem.vue new file mode 100644 index 0000000..92238cd --- /dev/null +++ b/web-app/src/components/CategoryDisplayItem.vue @@ -0,0 +1,78 @@ + + + diff --git a/web-app/src/components/EditCategoryModal.vue b/web-app/src/components/EditCategoryModal.vue new file mode 100644 index 0000000..fc75d3d --- /dev/null +++ b/web-app/src/components/EditCategoryModal.vue @@ -0,0 +1,96 @@ + + diff --git a/web-app/src/components/EditVendorModal.vue b/web-app/src/components/EditVendorModal.vue index 8efa78b..22a669d 100644 --- a/web-app/src/components/EditVendorModal.vue +++ b/web-app/src/components/EditVendorModal.vue @@ -5,14 +5,12 @@ import FormControl from './form/FormControl.vue'; import FormGroup from './form/FormGroup.vue'; import ModalWrapper from './ModalWrapper.vue'; import { TransactionApiClient, type TransactionVendor } from '@/api/transaction'; -import { useProfileStore } from '@/stores/profile-store'; import AppButton from './AppButton.vue'; const props = defineProps<{ vendor?: TransactionVendor }>() const emit = defineEmits<{ saved: [TransactionVendor] }>() -const profileStore = useProfileStore() const modal = useTemplateRef('modal') // Form data: @@ -23,7 +21,7 @@ function show(): Promise { if (!modal.value) return Promise.resolve(undefined) name.value = props.vendor?.name ?? '' description.value = props.vendor?.description ?? '' - return modal.value?.show() + return modal.value.show() } function canSave() { @@ -37,8 +35,7 @@ function canSave() { } async function doSave() { - if (!profileStore.state) return - const api = new TransactionApiClient(profileStore.state) + const api = new TransactionApiClient() const payload = { name: name.value.trim(), description: description.value.trim() @@ -63,7 +60,7 @@ defineExpose({ show })