Add opening balance, manual transactions, and cross-currency transfers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,45 @@ import {
|
||||
} from '$lib/server/db/schema.js';
|
||||
import { requireCompanyRoleAny } from '$lib/server/authorization.js';
|
||||
import { logCompanyEvent } from '$lib/server/audit.js';
|
||||
import {
|
||||
postTransaction,
|
||||
postTransfer,
|
||||
type CompanyAccountTxnType
|
||||
} from '$lib/server/accounts/ledger.js';
|
||||
import { and, asc, eq, isNull, sql } from 'drizzle-orm';
|
||||
|
||||
const MANUAL_TXN_TYPES = ['deposit', 'adjustment'] as const;
|
||||
type ManualTxnType = (typeof MANUAL_TXN_TYPES)[number];
|
||||
|
||||
function parseManualTxnType(v: FormDataEntryValue | null): ManualTxnType | null {
|
||||
const s = v?.toString();
|
||||
if (!s) return null;
|
||||
return (MANUAL_TXN_TYPES as readonly string[]).includes(s) ? (s as ManualTxnType) : null;
|
||||
}
|
||||
|
||||
function parseDate(v: FormDataEntryValue | null): Date | null {
|
||||
const s = v?.toString();
|
||||
if (!s) return null;
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
function parsePositiveAmount(v: FormDataEntryValue | null): string | null {
|
||||
const s = v?.toString();
|
||||
if (!s) return null;
|
||||
const n = Number(s);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
return n.toFixed(2);
|
||||
}
|
||||
|
||||
function parseSignedAmount(v: FormDataEntryValue | null): string | null {
|
||||
const s = v?.toString();
|
||||
if (!s) return null;
|
||||
const n = Number(s);
|
||||
if (!Number.isFinite(n) || n === 0) return null;
|
||||
return n.toFixed(2);
|
||||
}
|
||||
|
||||
const ACCOUNT_TYPES = [
|
||||
'bank',
|
||||
'credit_card',
|
||||
@@ -253,41 +290,61 @@ export const actions: Actions = {
|
||||
const f = parsed.fields;
|
||||
|
||||
const sortOrder = await nextAccountSortOrder(params.companyId);
|
||||
const openingBalance = parseSignedAmount(fd.get('openingBalance'));
|
||||
const openingBalanceDate =
|
||||
parseDate(fd.get('openingBalanceDate')) ?? new Date();
|
||||
|
||||
const [inserted] = await db
|
||||
.insert(companyAccounts)
|
||||
.values({
|
||||
companyId: params.companyId,
|
||||
sortOrder,
|
||||
createdBy: user.id,
|
||||
accountType: f.accountType,
|
||||
name: f.name,
|
||||
currency: f.currency,
|
||||
notes: f.notes,
|
||||
bankName: f.bankName,
|
||||
accountNumber: f.accountNumber,
|
||||
branch: f.branch,
|
||||
swiftBic: f.swiftBic,
|
||||
iban: f.iban,
|
||||
accountHolderName: f.accountHolderName,
|
||||
cardBrand: f.cardBrand,
|
||||
last4: f.last4,
|
||||
cardholderName: f.cardholderName,
|
||||
expiryMonth: f.expiryMonth,
|
||||
expiryYear: f.expiryYear,
|
||||
creditLimit: f.creditLimit,
|
||||
statementCloseDay: f.statementCloseDay,
|
||||
paymentDueDay: f.paymentDueDay,
|
||||
externalAccountId: f.externalAccountId
|
||||
})
|
||||
.returning({ id: companyAccounts.id });
|
||||
const inserted = await db.transaction(async (tx) => {
|
||||
const [row] = await tx
|
||||
.insert(companyAccounts)
|
||||
.values({
|
||||
companyId: params.companyId,
|
||||
sortOrder,
|
||||
createdBy: user.id,
|
||||
accountType: f.accountType,
|
||||
name: f.name,
|
||||
currency: f.currency,
|
||||
notes: f.notes,
|
||||
bankName: f.bankName,
|
||||
accountNumber: f.accountNumber,
|
||||
branch: f.branch,
|
||||
swiftBic: f.swiftBic,
|
||||
iban: f.iban,
|
||||
accountHolderName: f.accountHolderName,
|
||||
cardBrand: f.cardBrand,
|
||||
last4: f.last4,
|
||||
cardholderName: f.cardholderName,
|
||||
expiryMonth: f.expiryMonth,
|
||||
expiryYear: f.expiryYear,
|
||||
creditLimit: f.creditLimit,
|
||||
statementCloseDay: f.statementCloseDay,
|
||||
paymentDueDay: f.paymentDueDay,
|
||||
externalAccountId: f.externalAccountId
|
||||
})
|
||||
.returning({ id: companyAccounts.id });
|
||||
|
||||
if (openingBalance !== null) {
|
||||
await postTransaction(tx, {
|
||||
accountId: row.id,
|
||||
companyId: params.companyId,
|
||||
type: 'opening_balance',
|
||||
amount: openingBalance,
|
||||
currency: f.currency,
|
||||
occurredAt: openingBalanceDate,
|
||||
description: 'Opening balance',
|
||||
createdBy: user.id
|
||||
});
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
await logCompanyEvent(
|
||||
params.companyId,
|
||||
user.id,
|
||||
'account_created',
|
||||
`Account "${parsed.fields.name}" created`,
|
||||
{ accountId: inserted.id, accountType: parsed.fields.accountType }
|
||||
`Account "${parsed.fields.name}" created${openingBalance !== null ? ` with opening balance ${openingBalance} ${f.currency}` : ''}`,
|
||||
{ accountId: inserted.id, accountType: parsed.fields.accountType, openingBalance }
|
||||
);
|
||||
|
||||
return { success: true, action: 'addAccount' };
|
||||
@@ -502,5 +559,137 @@ export const actions: Actions = {
|
||||
});
|
||||
|
||||
return { success: true, action: 'reorderAccounts' };
|
||||
},
|
||||
|
||||
postTransfer: async ({ request, locals, params }) => {
|
||||
const { user } = await requireCompanyRoleAny(locals, params.companyId, [
|
||||
'admin',
|
||||
'manager',
|
||||
'accountant'
|
||||
]);
|
||||
const fd = await request.formData();
|
||||
const fromAccountId = trimOrNull(fd.get('fromAccountId'));
|
||||
const toAccountId = trimOrNull(fd.get('toAccountId'));
|
||||
const amount = parsePositiveAmount(fd.get('amount'));
|
||||
const occurredAt = parseDate(fd.get('occurredAt'));
|
||||
const description = trimOrNull(fd.get('description'));
|
||||
const reference = trimOrNull(fd.get('reference'));
|
||||
const fxRate = trimOrNull(fd.get('fxRate'));
|
||||
const destinationAmount = trimOrNull(fd.get('destinationAmount'));
|
||||
|
||||
if (!fromAccountId || !toAccountId) {
|
||||
return fail(400, { action: 'postTransfer', error: 'Both from and to accounts are required' });
|
||||
}
|
||||
if (fromAccountId === toAccountId) {
|
||||
return fail(400, { action: 'postTransfer', error: 'From and to accounts must differ' });
|
||||
}
|
||||
if (!amount) {
|
||||
return fail(400, { action: 'postTransfer', error: 'Amount must be a positive number' });
|
||||
}
|
||||
if (!occurredAt) {
|
||||
return fail(400, { action: 'postTransfer', error: 'Valid date is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await postTransfer({
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
companyId: params.companyId,
|
||||
amount,
|
||||
occurredAt,
|
||||
description,
|
||||
reference,
|
||||
fxRate,
|
||||
destinationAmount,
|
||||
createdBy: user.id
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Transfer failed';
|
||||
return fail(400, { action: 'postTransfer', error: msg });
|
||||
}
|
||||
|
||||
await logCompanyEvent(
|
||||
params.companyId,
|
||||
user.id,
|
||||
'account_transfer_posted',
|
||||
`Transferred ${amount} from account ${fromAccountId} to ${toAccountId}`,
|
||||
{ fromAccountId, toAccountId, amount, fxRate, destinationAmount }
|
||||
);
|
||||
|
||||
return { success: true, action: 'postTransfer' };
|
||||
},
|
||||
|
||||
addManualTransaction: async ({ request, locals, params }) => {
|
||||
const { user } = await requireCompanyRoleAny(locals, params.companyId, [
|
||||
'admin',
|
||||
'manager',
|
||||
'accountant'
|
||||
]);
|
||||
const fd = await request.formData();
|
||||
const accountId = trimOrNull(fd.get('accountId'));
|
||||
const type = parseManualTxnType(fd.get('type'));
|
||||
const amount = parseSignedAmount(fd.get('amount'));
|
||||
const occurredAt = parseDate(fd.get('occurredAt'));
|
||||
const description = trimOrNull(fd.get('description'));
|
||||
const reference = trimOrNull(fd.get('reference'));
|
||||
|
||||
if (!accountId) {
|
||||
return fail(400, { action: 'addManualTransaction', error: 'Account is required' });
|
||||
}
|
||||
if (!type) {
|
||||
return fail(400, {
|
||||
action: 'addManualTransaction',
|
||||
error: 'Type must be deposit or adjustment'
|
||||
});
|
||||
}
|
||||
if (!amount) {
|
||||
return fail(400, {
|
||||
action: 'addManualTransaction',
|
||||
error: 'Amount must be a non-zero number'
|
||||
});
|
||||
}
|
||||
if (!occurredAt) {
|
||||
return fail(400, { action: 'addManualTransaction', error: 'Valid date is required' });
|
||||
}
|
||||
|
||||
const [acct] = await db
|
||||
.select({
|
||||
id: companyAccounts.id,
|
||||
currency: companyAccounts.currency,
|
||||
name: companyAccounts.name
|
||||
})
|
||||
.from(companyAccounts)
|
||||
.where(
|
||||
and(
|
||||
eq(companyAccounts.id, accountId),
|
||||
eq(companyAccounts.companyId, params.companyId),
|
||||
isNull(companyAccounts.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (!acct) error(404, 'Account not found');
|
||||
|
||||
const txnType: CompanyAccountTxnType = type;
|
||||
await postTransaction(db, {
|
||||
accountId,
|
||||
companyId: params.companyId,
|
||||
type: txnType,
|
||||
amount,
|
||||
currency: acct.currency,
|
||||
occurredAt,
|
||||
description,
|
||||
reference,
|
||||
createdBy: user.id
|
||||
});
|
||||
|
||||
await logCompanyEvent(
|
||||
params.companyId,
|
||||
user.id,
|
||||
'account_transaction_added',
|
||||
`${type} of ${amount} ${acct.currency} recorded on "${acct.name}"`,
|
||||
{ accountId, type, amount }
|
||||
);
|
||||
|
||||
return { success: true, action: 'addManualTransaction' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,16 +37,57 @@
|
||||
const ACCOUNT_TYPES = Object.keys(ACCOUNT_TYPE_LABELS) as AccountType[];
|
||||
|
||||
let showAddForm = $state(false);
|
||||
let showTransferModal = $state(false);
|
||||
let showManualTxnModal = $state(false);
|
||||
let editingId = $state<string | null>(null);
|
||||
let confirmDeleteId = $state<string | null>(null);
|
||||
let addType = $state<AccountType>('bank');
|
||||
let transferFrom = $state<string>('');
|
||||
let transferTo = $state<string>('');
|
||||
|
||||
const activeAccounts = $derived(data.accounts.filter((a) => !a.isArchived));
|
||||
const fromAccount = $derived(activeAccounts.find((a) => a.id === transferFrom));
|
||||
const toAccount = $derived(activeAccounts.find((a) => a.id === transferTo));
|
||||
const isCrossCurrency = $derived(
|
||||
fromAccount && toAccount && fromAccount.currency !== toAccount.currency
|
||||
);
|
||||
|
||||
function openAdd() {
|
||||
showAddForm = !showAddForm;
|
||||
showTransferModal = false;
|
||||
showManualTxnModal = false;
|
||||
editingId = null;
|
||||
confirmDeleteId = null;
|
||||
}
|
||||
|
||||
function openTransfer() {
|
||||
showTransferModal = true;
|
||||
showAddForm = false;
|
||||
showManualTxnModal = false;
|
||||
editingId = null;
|
||||
confirmDeleteId = null;
|
||||
if (activeAccounts.length >= 2) {
|
||||
transferFrom = activeAccounts[0].id;
|
||||
transferTo = activeAccounts[1].id;
|
||||
}
|
||||
}
|
||||
|
||||
function openManualTxn() {
|
||||
showManualTxnModal = true;
|
||||
showAddForm = false;
|
||||
showTransferModal = false;
|
||||
editingId = null;
|
||||
confirmDeleteId = null;
|
||||
}
|
||||
|
||||
function todayIso(): string {
|
||||
const d = new Date();
|
||||
const yyyy = d.getFullYear();
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
function formatAmount(amount: string, currency: string): string {
|
||||
const n = Number(amount);
|
||||
const fmt = new Intl.NumberFormat(undefined, {
|
||||
@@ -326,13 +367,33 @@
|
||||
expenses, invoice payments, transfers, and manual entries.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={openAdd}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
{showAddForm ? 'Cancel' : '+ New Account'}
|
||||
</button>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#if activeAccounts.length >= 1}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openManualTxn}
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
Record Transaction
|
||||
</button>
|
||||
{/if}
|
||||
{#if activeAccounts.length >= 2}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openTransfer}
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
Transfer
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openAdd}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
{showAddForm ? 'Cancel' : '+ New Account'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if form?.error}
|
||||
@@ -377,6 +438,38 @@
|
||||
|
||||
{@render accountFields(addType, 'add')}
|
||||
|
||||
<fieldset class="mt-4 rounded-md border border-gray-200 p-3 dark:border-gray-600">
|
||||
<legend class="px-2 text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
Opening balance (optional)
|
||||
</legend>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="add-openingBalance" class={labelCls}>
|
||||
Opening Balance
|
||||
<span class="ml-1 text-xs text-gray-400">(negative for credit-card debt)</span>
|
||||
</label>
|
||||
<input
|
||||
id="add-openingBalance"
|
||||
name="openingBalance"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="add-openingBalanceDate" class={labelCls}>As of Date</label>
|
||||
<input
|
||||
id="add-openingBalanceDate"
|
||||
name="openingBalanceDate"
|
||||
type="date"
|
||||
value={todayIso()}
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -395,6 +488,221 @@
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if showTransferModal}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/postTransfer"
|
||||
use:enhance={() => async ({ result, update, formElement }) => {
|
||||
await update({ reset: false });
|
||||
if (result.type === 'success') {
|
||||
showTransferModal = false;
|
||||
formElement.reset();
|
||||
}
|
||||
}}
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="mb-4 font-semibold text-gray-900 dark:text-white">Transfer Between Accounts</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="xfer-from" class={labelCls}>From <span class="text-red-500">*</span></label>
|
||||
<select id="xfer-from" name="fromAccountId" required bind:value={transferFrom} class={inputCls}>
|
||||
{#each activeAccounts as a (a.id)}
|
||||
<option value={a.id}>{a.name} ({a.currency})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="xfer-to" class={labelCls}>To <span class="text-red-500">*</span></label>
|
||||
<select id="xfer-to" name="toAccountId" required bind:value={transferTo} class={inputCls}>
|
||||
{#each activeAccounts as a (a.id)}
|
||||
<option value={a.id}>{a.name} ({a.currency})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="xfer-amount" class={labelCls}>
|
||||
Amount <span class="text-red-500">*</span>
|
||||
{#if fromAccount}<span class="ml-1 text-xs text-gray-400">({fromAccount.currency})</span>{/if}
|
||||
</label>
|
||||
<input
|
||||
id="xfer-amount"
|
||||
name="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
required
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="xfer-date" class={labelCls}>Date <span class="text-red-500">*</span></label>
|
||||
<input
|
||||
id="xfer-date"
|
||||
name="occurredAt"
|
||||
type="date"
|
||||
value={todayIso()}
|
||||
required
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if isCrossCurrency}
|
||||
<div class="md:col-span-2">
|
||||
<p class="mb-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
Cross-currency transfer: enter either an FX rate OR a destination amount.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="xfer-rate" class={labelCls}>
|
||||
FX Rate
|
||||
{#if fromAccount && toAccount}
|
||||
<span class="ml-1 text-xs text-gray-400"
|
||||
>(1 {fromAccount.currency} = ? {toAccount.currency})</span
|
||||
>
|
||||
{/if}
|
||||
</label>
|
||||
<input
|
||||
id="xfer-rate"
|
||||
name="fxRate"
|
||||
type="number"
|
||||
step="0.00000001"
|
||||
min="0"
|
||||
placeholder="e.g. 36.5"
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="xfer-destAmt" class={labelCls}>
|
||||
— or — Destination Amount
|
||||
{#if toAccount}<span class="ml-1 text-xs text-gray-400">({toAccount.currency})</span>{/if}
|
||||
</label>
|
||||
<input
|
||||
id="xfer-destAmt"
|
||||
name="destinationAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="e.g. 36500.00"
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label for="xfer-description" class={labelCls}>Description</label>
|
||||
<input
|
||||
id="xfer-description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Optional note"
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="xfer-reference" class={labelCls}>Reference</label>
|
||||
<input id="xfer-reference" name="reference" type="text" class={inputCls} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showTransferModal = false)}
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Post Transfer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if showManualTxnModal}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/addManualTransaction"
|
||||
use:enhance={() => async ({ result, update, formElement }) => {
|
||||
await update({ reset: false });
|
||||
if (result.type === 'success') {
|
||||
showManualTxnModal = false;
|
||||
formElement.reset();
|
||||
}
|
||||
}}
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="mb-4 font-semibold text-gray-900 dark:text-white">Record Transaction</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="mtxn-account" class={labelCls}>Account <span class="text-red-500">*</span></label>
|
||||
<select id="mtxn-account" name="accountId" required class={inputCls}>
|
||||
{#each activeAccounts as a (a.id)}
|
||||
<option value={a.id}>{a.name} ({a.currency})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mtxn-type" class={labelCls}>Type <span class="text-red-500">*</span></label>
|
||||
<select id="mtxn-type" name="type" required class={inputCls}>
|
||||
<option value="deposit">Deposit (credit)</option>
|
||||
<option value="adjustment">Adjustment (debit or credit)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mtxn-amount" class={labelCls}>
|
||||
Amount <span class="text-red-500">*</span>
|
||||
<span class="ml-1 text-xs text-gray-400">(positive = credit, negative = debit)</span>
|
||||
</label>
|
||||
<input
|
||||
id="mtxn-amount"
|
||||
name="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
required
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mtxn-date" class={labelCls}>Date <span class="text-red-500">*</span></label>
|
||||
<input
|
||||
id="mtxn-date"
|
||||
name="occurredAt"
|
||||
type="date"
|
||||
value={todayIso()}
|
||||
required
|
||||
class={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="mtxn-description" class={labelCls}>Description</label>
|
||||
<input id="mtxn-description" name="description" type="text" class={inputCls} />
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="mtxn-reference" class={labelCls}>Reference</label>
|
||||
<input id="mtxn-reference" name="reference" type="text" class={inputCls} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showManualTxnModal = false)}
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if data.accounts.length === 0}
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-gray-300 bg-white p-10 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
|
||||
Reference in New Issue
Block a user