Skip to content

Commit

Permalink
feat: basic transaction editing
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelhthomas committed Apr 2, 2024
1 parent 851dd30 commit c1bdef3
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 5 deletions.
49 changes: 49 additions & 0 deletions src/lib/components/table/addEditRow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type {
NewTablePropSet,
TablePlugin
} from 'svelte-headless-table/lib/types/TablePlugin';
import { type Readable, type Writable, derived, writable } from 'svelte/store';

export type EditRowState = {
active: Writable<string | null>;
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-interface, unused-imports/no-unused-vars
export interface EditRowColumnOptions<Item> {}

export type EditRowPropSet = NewTablePropSet<{
'tbody.tr': {
editing: boolean;
edit: () => void;
cancel: () => void;
};
}>;

export function addEditRow<Item>(): TablePlugin<
Item,
EditRowState,
EditRowColumnOptions<Item>,
EditRowPropSet
> {
return () => {
const active = writable<string | null>(null);
const pluginState: EditRowState = { active };

return {
pluginState,
hooks: {
'tbody.tr': ({ id }) => {
const props: Readable<EditRowPropSet['tbody.tr']> = derived(
[active],
([$active]) => ({
editing: $active === id,
edit: () => active.set(id),
cancel: () => active.set(null)
})
);
return { props };
}
}
};
};
}
144 changes: 144 additions & 0 deletions src/lib/components/transactions/TransactionInlineEdit.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import { slide } from 'svelte/transition';
import { superForm, superValidateSync } from 'sveltekit-superforms/client';
import {
type Transaction,
type TransactionSplitUpdate,
TransactionsApi
} from '$lib/api';
import { queryClient } from '$lib/client';
import { transactionSplitSchema } from '$lib/schemas/transaction';
import { useService } from '$lib/services';
import Button from '$lib/components/Button.svelte';
import StatusButton from '$lib/components/StatusButton.svelte';
import SelectField from '$lib/components/form/SelectField.svelte';
import TagsField from '$lib/components/form/TagsField.svelte';
import TextAreaField from '$lib/components/form/TextAreaField.svelte';
import TextField from '$lib/components/form/TextField.svelte';
export let id: string;
export let transaction: Transaction;
export let close: () => void;
const transactionsService = useService(TransactionsApi);
const updateTransactionMutation = createMutation({
mutationFn: (transaction: TransactionSplitUpdate) =>
transactionsService.updateTransaction({
id,
transactionUpdate: {
transactions: [transaction]
}
}),
onSuccess() {
queryClient.invalidateQueries({
queryKey: ['transactions']
});
setTimeout(close, 500);
}
});
const validated = superValidateSync(
transaction.transactions[0],
transactionSplitSchema
);
const form = superForm(validated, {
SPA: true,
dataType: 'json',
validators: transactionSplitSchema,
onUpdate({ form }) {
if (!form.valid) return;
$updateTransactionMutation.mutate(form.data);
}
});
const { enhance, errors } = form;
$: console.error($errors);
</script>

<div class="flex flex-col gap-4" transition:slide|global={{ duration: 300 }}>
<h1 class="text-lg font-bold">Edit transaction</h1>

<form
use:enhance
method="POST"
id="transaction_update_form"
class="grid grid-cols-12 gap-4"
>
<TextField
{form}
field="description"
label="Description"
placeholder="Description"
class="col-span-4"
/>
<TextField
{form}
type="number"
step="0.01"
field="amount"
label="Amount"
placeholder="Amount"
class="col-span-1"
/>
<TextField {form} field="date" label="Date" placeholder="Date" class="col-span-3" />
<TextField
{form}
field="sourceName"
label="Source Account"
placeholder="Source Account"
class="col-span-2"
/>
<TextField
{form}
field="destinationName"
label="Destination Account"
placeholder="Destination Account"
class="col-span-2"
/>

<TextField
{form}
field="categoryName"
label="Category"
placeholder="Category"
class="col-span-2"
/>
<TagsField {form} field="tags" label="Tags" placeholder="Tag" class="col-span-4" />
<TextAreaField
{form}
field="notes"
class="col-span-6 row-span-3 flex flex-col"
unWrappedClass="flex-grow resize-none"
label="Notes"
/>
<SelectField {form} field="budgetId" label="Budget" class="col-span-2" />

<div class="col-span-4 row-span-2">
<p>Attachments</p>
<p>Not yet implemented :(</p>
</div>

<SelectField {form} field="billId" label="Bill" class="col-span-2" />
</form>

<div class="footer flex gap-2">
<Button color="red">Delete</Button>
<div class="mx-auto" />
<Button color="alternative" on:click={close}>Cancel</Button>
<StatusButton
status={$updateTransactionMutation.status}
color="primary"
type="submit"
form="transaction_update_form"
icon="bxs:save"
>
Save
</StatusButton>
</div>
</div>
78 changes: 73 additions & 5 deletions src/lib/components/transactions/TransactionsGrid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,35 @@
import { page } from '$app/stores';
import { createQuery } from '@tanstack/svelte-query';
import dayjs from 'dayjs';
import { type LinkType, Pagination } from 'flowbite-svelte';
import {
type LinkType,
Pagination,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell
} from 'flowbite-svelte';
import { tick } from 'svelte';
import { createRender, createTable } from 'svelte-headless-table';
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table';
import { derived, writable } from 'svelte/store';
import {
type TransactionArray,
TransactionTypeFilter,
TransactionsApi
} from '$lib/api';
import { addEditRow } from '$lib/components/table/addEditRow';
import { DateFormat } from '$lib/models/DateFormat';
import { TransactionCategory } from '$lib/models/TransactionCategory';
import { useService } from '$lib/services';
import { usePreferencesStore } from '$lib/stores/preferences';
import { tryParseInt } from '$lib/utils/number';
import Currency from '$lib/components/format/Currency.svelte';
import DataTable from '$lib/components/table/DataTable.svelte';
import TransactionInlineEdit from './TransactionInlineEdit.svelte';
export let category: TransactionCategory;
export let initialTransactions: TransactionArray;
Expand Down Expand Up @@ -70,7 +81,9 @@
const data = writable($transactionQuery.data.data);
$: data.set($transactionQuery.data.data);
const table = createTable(data);
const table = createTable(data, {
editRow: addEditRow()
});
const columns = table.createColumns([
table.column({
Expand Down Expand Up @@ -110,6 +123,7 @@
return item.id;
}
});
const { headerRows, rows, tableAttrs } = vm;
$: pagination = $transactionQuery.data.meta.pagination;
$: showPagination = pagination && (pagination.totalPages ?? 1) > 1;
Expand Down Expand Up @@ -139,7 +153,61 @@

<div id="transactionsGrid">
{#if $transactionQuery.isSuccess}
<DataTable {vm} />
<Table {...$tableAttrs}>
<TableHead>
{#each $headerRows as headerRow (headerRow.id)}
<Subscribe rowAttrs={headerRow.attrs()}>
{#each headerRow.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs>
<TableHeadCell {...attrs}>
<Render of={cell.render()} />
</TableHeadCell>
</Subscribe>
{/each}
<TableHeadCell />
</Subscribe>
{/each}
</TableHead>
<TableBody>
{#each $rows as row (row.id)}
<Subscribe
rowAttrs={row.attrs()}
rowProps={row.props()}
let:rowAttrs
let:rowProps
>
{#if rowProps.editRow.editing}
<TableBodyRow>
<TableBodyCell colspan="7">
{#if row.isData()}
<TransactionInlineEdit
id={row.original.id}
transaction={row.original.attributes}
close={rowProps.editRow.cancel}
/>
{/if}
</TableBodyCell>
</TableBodyRow>
{:else}
<TableBodyRow {...rowAttrs}>
{#each row.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs>
<TableBodyCell {...attrs}>
<Render of={cell.render()} />
</TableBodyCell>
</Subscribe>
{/each}
<TableBodyCell>
<button class="text-primary-500" on:click={rowProps.editRow.edit}>
Edit
</button>
</TableBodyCell>
</TableBodyRow>
{/if}
</Subscribe>
{/each}
</TableBody>
</Table>

{#if showPagination}
<div class="p-4 flex justify-center">
Expand Down
58 changes: 58 additions & 0 deletions src/lib/schemas/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { z } from 'zod';

export const transactionSplitSchema = z.object({
transactionJournalId: z.string(),
// type: z.enum([
// 'withdrawal',
// 'deposit',
// 'transfer',
// 'reconciliation',
// 'opening balance'
// ]),
date: z.date(),
amount: z.string(),
description: z.string(),
// order: z.number().nullish(),
// currencyId: z.string().nullish(),
// currencyCode: z.string().nullish(),
// foreignAmount: z.string().nullish(),
// foreignCurrencyId: z.string().nullish(),
// foreignCurrencyCode: z.string().nullish(),
budgetId: z.string().nullish(),
// categoryId: z.string().nullish(),
categoryName: z.string().nullish(),
// sourceId: z.string().nullish(),
sourceName: z.string().nullish(),
// sourceIban: z.string().nullish(),
// destinationId: z.string().nullish(),
destinationName: z.string().nullish(),
// destinationIban: z.string().nullish(),
// reconciled: z.boolean(),
billId: z.string().nullish(),
// billName: z.string().nullish(),
tags: z.array(z.string()).nullish(),
notes: z.string().nullish()
// internalReference: z.string().nullish(),
// externalId: z.string().nullish(),
// externalUrl: z.string().nullish(),
// bunqPaymentId: z.string().nullish(),
// sepaCc: z.string().nullish(),
// sepaCtOp: z.string().nullish(),
// sepaCtId: z.string().nullish(),
// sepaDb: z.string().nullish(),
// sepaCountry: z.string().nullish(),
// sepaEp: z.string().nullish(),
// sepaCi: z.string().nullish(),
// sepaBatchId: z.string().nullish(),
// interestDate: z.date().nullish(),
// bookDate: z.date().nullish(),
// processDate: z.date().nullish(),
// dueDate: z.date().nullish(),
// paymentDate: z.date().nullish(),
// invoiceDate: z.date().nullish()
});

export const transactionSchema = z.object({
groupTitle: z.string().optional(),
transactions: z.array(transactionSplitSchema)
});

0 comments on commit c1bdef3

Please sign in to comment.