Auto-post expenses and invoice payments to accounts ledger

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 12:04:15 +07:00
parent d75fe6ed95
commit 3a095851e9
9 changed files with 430 additions and 70 deletions
+8 -2
View File
@@ -259,7 +259,8 @@ export async function postInvoicePaymentTransaction(
total: invoices.total,
currency: invoices.currency,
issueDate: invoices.issueDate,
invoiceNumber: invoices.invoiceNumber
invoiceNumber: invoices.invoiceNumber,
direction: invoices.direction
})
.from(invoices)
.where(eq(invoices.id, invoiceId))
@@ -273,6 +274,11 @@ export async function postInvoicePaymentTransaction(
.limit(1);
if (!acct) throw new Error(`postInvoicePaymentTransaction: account ${paymentAccountId} not found`);
// outgoing = we billed a customer → cash in (credit).
// incoming = we owe a supplier → cash out (debit).
const sign = inv.direction === 'outgoing' ? 1 : -1;
const signedAmount = sign * Number(inv.total);
await dbOrTx
.delete(companyAccountTransactions)
.where(eq(companyAccountTransactions.sourceInvoiceId, invoiceId));
@@ -281,7 +287,7 @@ export async function postInvoicePaymentTransaction(
accountId: paymentAccountId,
companyId: acct.companyId,
type: 'invoice_payment',
amount: Number(inv.total).toFixed(2),
amount: signedAmount.toFixed(2),
currency: inv.currency,
occurredAt: new Date(inv.issueDate),
description: `Invoice ${inv.invoiceNumber}`,
@@ -1,18 +1,28 @@
import { fail } from '@sveltejs/kit';
import { error, fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
import { expenses, projects, users, categories } from '$lib/server/db/schema.js';
import { eq, and, sql } from 'drizzle-orm';
import { requireCompanyRole } from '$lib/server/authorization.js';
import {
expenses,
projects,
users,
categories,
companyAccounts
} from '$lib/server/db/schema.js';
import { eq, and, sql, isNull } from 'drizzle-orm';
import { requireCompanyRole, requireCompanyRoleAny } from '$lib/server/authorization.js';
import { logCompanyEvent } from '$lib/server/audit.js';
import { formatCurrency } from '$lib/utils/currency.js';
import {
postExpenseTransaction,
removeExpenseTransaction
} from '$lib/server/accounts/ledger.js';
export const load: PageServerLoad = async ({ parent, params, url }) => {
await parent();
const status = url.searchParams.get('status') || 'all';
let query = db
const expenseList = await db
.select({
id: expenses.id,
title: expenses.title,
@@ -28,12 +38,15 @@ export const load: PageServerLoad = async ({ parent, params, url }) => {
projectId: projects.id,
projectName: projects.name,
categoryName: categories.name,
accountId: expenses.accountId,
accountName: companyAccounts.name,
createdAt: expenses.createdAt
})
.from(expenses)
.innerJoin(projects, eq(expenses.projectId, projects.id))
.innerJoin(users, eq(expenses.submittedBy, users.id))
.leftJoin(categories, eq(expenses.categoryId, categories.id))
.leftJoin(companyAccounts, eq(expenses.accountId, companyAccounts.id))
.where(
status === 'all'
? eq(projects.companyId, params.companyId)
@@ -45,9 +58,24 @@ export const load: PageServerLoad = async ({ parent, params, url }) => {
.orderBy(sql`${expenses.createdAt} desc`)
.limit(100);
const expenseList = await query;
const accountsList = await db
.select({
id: companyAccounts.id,
name: companyAccounts.name,
currency: companyAccounts.currency,
accountType: companyAccounts.accountType
})
.from(companyAccounts)
.where(
and(
eq(companyAccounts.companyId, params.companyId),
eq(companyAccounts.isArchived, false),
isNull(companyAccounts.deletedAt)
)
)
.orderBy(companyAccounts.name);
return { expenses: expenseList, statusFilter: status };
return { expenses: expenseList, statusFilter: status, accounts: accountsList };
};
export const actions: Actions = {
@@ -58,27 +86,40 @@ export const actions: Actions = {
if (!expenseId) return fail(400, { error: 'Missing expense ID' });
// Get expense details for the log
const [expense] = await db
.select({ title: expenses.title, amount: expenses.amount, currency: expenses.currency })
.select({
title: expenses.title,
amount: expenses.amount,
currency: expenses.currency,
accountId: expenses.accountId
})
.from(expenses)
.where(eq(expenses.id, expenseId))
.limit(1);
await db
.update(expenses)
.set({
status: 'approved',
approvedBy: user.id,
reviewedAt: new Date(),
updatedAt: new Date()
})
.where(and(eq(expenses.id, expenseId), eq(expenses.status, 'pending')));
await db.transaction(async (tx) => {
await tx
.update(expenses)
.set({
status: 'approved',
approvedBy: user.id,
reviewedAt: new Date(),
updatedAt: new Date()
})
.where(and(eq(expenses.id, expenseId), eq(expenses.status, 'pending')));
if (expense?.accountId) {
await postExpenseTransaction(expenseId, expense.accountId, user.id, tx);
}
});
if (expense) {
await logCompanyEvent(params.companyId, user.id, 'expense_approved',
await logCompanyEvent(
params.companyId,
user.id,
'expense_approved',
`Approved expense "${expense.title}" for ${formatCurrency(expense.amount, expense.currency)}`,
{ expenseId, amount: expense.amount }
{ expenseId, amount: expense.amount, accountId: expense.accountId }
);
}
@@ -99,24 +140,99 @@ export const actions: Actions = {
.where(eq(expenses.id, expenseId))
.limit(1);
await db
.update(expenses)
.set({
status: 'rejected',
approvedBy: user.id,
reviewedAt: new Date(),
rejectionReason: reason,
updatedAt: new Date()
})
.where(and(eq(expenses.id, expenseId), eq(expenses.status, 'pending')));
await db.transaction(async (tx) => {
await tx
.update(expenses)
.set({
status: 'rejected',
approvedBy: user.id,
reviewedAt: new Date(),
rejectionReason: reason,
updatedAt: new Date()
})
.where(and(eq(expenses.id, expenseId), eq(expenses.status, 'pending')));
// Defensive: remove any prior ledger post (e.g. if this expense was previously approved then reopened)
await removeExpenseTransaction(expenseId, tx);
});
if (expense) {
await logCompanyEvent(params.companyId, user.id, 'expense_rejected',
await logCompanyEvent(
params.companyId,
user.id,
'expense_rejected',
`Rejected expense "${expense.title}" (${formatCurrency(expense.amount, expense.currency)})${reason ? `${reason}` : ''}`,
{ expenseId, amount: expense.amount, reason }
);
}
return { success: true };
},
updateExpenseAccount: async ({ request, locals, params }) => {
const { user } = await requireCompanyRoleAny(locals, params.companyId, [
'admin',
'manager',
'accountant'
]);
const formData = await request.formData();
const expenseId = formData.get('expenseId')?.toString();
const rawAccountId = formData.get('accountId')?.toString().trim() ?? '';
const accountId = rawAccountId === '' ? null : rawAccountId;
if (!expenseId) return fail(400, { error: 'Missing expense ID' });
const [expense] = await db
.select({
id: expenses.id,
status: expenses.status,
title: expenses.title,
accountId: expenses.accountId,
projectCompanyId: projects.companyId
})
.from(expenses)
.innerJoin(projects, eq(expenses.projectId, projects.id))
.where(eq(expenses.id, expenseId))
.limit(1);
if (!expense) error(404, 'Expense not found');
if (expense.projectCompanyId !== params.companyId) error(403, 'Forbidden');
if (accountId) {
const [acct] = await db
.select({ id: companyAccounts.id })
.from(companyAccounts)
.where(
and(
eq(companyAccounts.id, accountId),
eq(companyAccounts.companyId, params.companyId),
isNull(companyAccounts.deletedAt)
)
)
.limit(1);
if (!acct) return fail(400, { error: 'Invalid account' });
}
await db.transaction(async (tx) => {
await tx.update(expenses).set({ accountId, updatedAt: new Date() }).where(eq(expenses.id, expenseId));
// Only post to ledger if the expense is approved. Otherwise leave ledger untouched.
if (expense.status === 'approved') {
if (accountId) {
await postExpenseTransaction(expenseId, accountId, user.id, tx);
} else {
await removeExpenseTransaction(expenseId, tx);
}
}
});
await logCompanyEvent(
params.companyId,
user.id,
'account_transaction_added',
`Expense "${expense.title}" ${accountId ? 'assigned to account' : 'unassigned from account'}`,
{ expenseId, accountId, previousAccountId: expense.accountId }
);
return { success: true };
}
};
@@ -10,6 +10,11 @@
const canApprove = $derived(
data.companyRoles.includes('admin') || data.companyRoles.includes('manager')
);
const canAssignAccount = $derived(
data.companyRoles.includes('admin') ||
data.companyRoles.includes('manager') ||
data.companyRoles.includes('accountant')
);
</script>
<svelte:head>
@@ -57,6 +62,15 @@
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
By {expense.submitterName ?? expense.submitterEmail} · {expense.expenseDate}
</p>
{#if expense.accountName}
<p class="mt-1 text-xs">
<span
class="rounded-full bg-blue-100 px-2 py-0.5 font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
>
Account: {expense.accountName}
</span>
</p>
{/if}
</div>
<div class="text-right">
<p class="text-lg font-semibold dark:text-white">{formatCurrency(expense.amount, expense.currency)}</p>
@@ -80,7 +94,7 @@
{/if}
{#if canApprove && expense.status === 'pending'}
<div class="mt-3 flex gap-2 border-t border-gray-100 dark:border-gray-700 pt-3">
<div class="mt-3 flex flex-wrap gap-2 border-t border-gray-100 dark:border-gray-700 pt-3">
<form method="POST" action="?/approve" use:enhance>
<input type="hidden" name="expenseId" value={expense.id} />
<button
@@ -107,6 +121,37 @@
</form>
</div>
{/if}
{#if canAssignAccount && data.accounts.length > 0}
<form
method="POST"
action="?/updateExpenseAccount"
use:enhance
class="mt-3 flex flex-wrap items-center gap-2 border-t border-gray-100 pt-3 text-sm dark:border-gray-700"
>
<input type="hidden" name="expenseId" value={expense.id} />
<label class="text-xs text-gray-500 dark:text-gray-400" for="acct-{expense.id}">
Assign to account:
</label>
<select
id="acct-{expense.id}"
name="accountId"
value={expense.accountId ?? ''}
class="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option value="">— none —</option>
{#each data.accounts as acct (acct.id)}
<option value={acct.id}>{acct.name} ({acct.currency})</option>
{/each}
</select>
<button
type="submit"
class="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
>
Save
</button>
</form>
{/if}
</div>
{/each}
</div>
@@ -1,10 +1,14 @@
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
import { invoices, parties } from '$lib/server/db/schema.js';
import { eq, and, sql, gte, lte } from 'drizzle-orm';
import { invoices, parties, companyAccounts } from '$lib/server/db/schema.js';
import { eq, and, sql, gte, lte, isNull } from 'drizzle-orm';
import { requireCompanyRoleAny } from '$lib/server/authorization.js';
import { logCompanyEvent } from '$lib/server/audit.js';
import {
postInvoicePaymentTransaction,
removeInvoicePaymentTransaction
} from '$lib/server/accounts/ledger.js';
export const load: PageServerLoad = async ({ locals, params, url }) => {
await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager', 'user']);
@@ -52,7 +56,31 @@ export const load: PageServerLoad = async ({ locals, params, url }) => {
.orderBy(sql`${invoices.issueDate} desc`)
.limit(200);
return { invoices: invoiceList, directionFilter, statusFilter, fromDate, toDate };
const accountsList = await db
.select({
id: companyAccounts.id,
name: companyAccounts.name,
currency: companyAccounts.currency,
accountType: companyAccounts.accountType
})
.from(companyAccounts)
.where(
and(
eq(companyAccounts.companyId, params.companyId),
eq(companyAccounts.isArchived, false),
isNull(companyAccounts.deletedAt)
)
)
.orderBy(companyAccounts.name);
return {
invoices: invoiceList,
directionFilter,
statusFilter,
fromDate,
toDate,
accounts: accountsList
};
};
export const actions: Actions = {
@@ -90,7 +118,10 @@ export const actions: Actions = {
const { user } = await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager']);
const formData = await request.formData();
const invoiceId = formData.get('invoiceId')?.toString();
const paymentAccountId = formData.get('paymentAccountId')?.toString() || null;
if (!invoiceId) return fail(400, { error: 'Missing invoice ID' });
if (!paymentAccountId)
return fail(400, { error: 'Payment account is required to mark an invoice paid' });
const [inv] = await db
.select({ invoiceNumber: invoices.invoiceNumber, total: invoices.total, currency: invoices.currency })
@@ -100,17 +131,34 @@ export const actions: Actions = {
if (!inv) return fail(404, { error: 'Invoice not found' });
await db
.update(invoices)
.set({ status: 'paid', updatedAt: new Date() })
.where(and(eq(invoices.id, invoiceId), eq(invoices.companyId, params.companyId)));
const [acct] = await db
.select({ id: companyAccounts.id })
.from(companyAccounts)
.where(
and(
eq(companyAccounts.id, paymentAccountId),
eq(companyAccounts.companyId, params.companyId),
isNull(companyAccounts.deletedAt)
)
)
.limit(1);
if (!acct) return fail(400, { error: 'Invalid payment account' });
await db.transaction(async (tx) => {
await tx
.update(invoices)
.set({ status: 'paid', paymentAccountId, updatedAt: new Date() })
.where(and(eq(invoices.id, invoiceId), eq(invoices.companyId, params.companyId)));
await postInvoicePaymentTransaction(invoiceId, paymentAccountId, user.id, tx);
});
await logCompanyEvent(
params.companyId,
user.id,
'invoice_paid',
`Marked invoice ${inv.invoiceNumber} as paid`,
{ invoiceId }
{ invoiceId, paymentAccountId }
);
return { success: true };
@@ -141,13 +141,29 @@
</form>
{/if}
{#if inv.status === 'sent' || inv.status === 'overdue'}
<form method="POST" action="?/markPaid" use:enhance>
<input type="hidden" name="invoiceId" value={inv.id} />
<button type="submit"
class="rounded px-2 py-1 text-xs font-medium bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/50">
Mark Paid
</button>
</form>
{#if data.accounts.length === 0}
<span class="text-xs text-gray-400" title="Create an account first to mark invoices paid">
No account
</span>
{:else}
<form method="POST" action="?/markPaid" use:enhance class="flex items-center gap-1">
<input type="hidden" name="invoiceId" value={inv.id} />
<select
name="paymentAccountId"
required
class="rounded border border-gray-300 bg-white px-1 py-0.5 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option value="">— Account —</option>
{#each data.accounts as acct (acct.id)}
<option value={acct.id}>{acct.name}</option>
{/each}
</select>
<button type="submit"
class="rounded px-2 py-1 text-xs font-medium bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/50">
Mark Paid
</button>
</form>
{/if}
{/if}
</div>
</td>
@@ -7,11 +7,16 @@ import {
parties,
expenses,
projects,
packages
packages,
companyAccounts
} from '$lib/server/db/schema.js';
import { eq, and, isNull } from 'drizzle-orm';
import { requireCompanyRoleAny } from '$lib/server/authorization.js';
import { logCompanyEvent } from '$lib/server/audit.js';
import {
postInvoicePaymentTransaction,
removeInvoicePaymentTransaction
} from '$lib/server/accounts/ledger.js';
export const load: PageServerLoad = async ({ locals, params }) => {
await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager', 'user']);
@@ -30,6 +35,7 @@ export const load: PageServerLoad = async ({ locals, params }) => {
currency: invoices.currency,
notes: invoices.notes,
expenseId: invoices.expenseId,
paymentAccountId: invoices.paymentAccountId,
createdAt: invoices.createdAt,
partyId: invoices.partyId,
partyName: parties.name,
@@ -49,6 +55,23 @@ export const load: PageServerLoad = async ({ locals, params }) => {
if (!invoice) error(404, 'Invoice not found');
const accountsList = await db
.select({
id: companyAccounts.id,
name: companyAccounts.name,
currency: companyAccounts.currency,
accountType: companyAccounts.accountType
})
.from(companyAccounts)
.where(
and(
eq(companyAccounts.companyId, params.companyId),
eq(companyAccounts.isArchived, false),
isNull(companyAccounts.deletedAt)
)
)
.orderBy(companyAccounts.name);
const lineItems = await db
.select()
.from(invoiceLineItems)
@@ -72,7 +95,7 @@ export const load: PageServerLoad = async ({ locals, params }) => {
.where(eq(packages.invoiceId, params.invoiceId))
.orderBy(packages.createdAt);
return { invoice, lineItems, projects: projectList, linkedPackages };
return { invoice, lineItems, projects: projectList, linkedPackages, accounts: accountsList };
};
export const actions: Actions = {
@@ -86,6 +109,7 @@ export const actions: Actions = {
| 'overdue'
| 'cancelled'
| undefined;
const paymentAccountId = formData.get('paymentAccountId')?.toString() || null;
const validStatuses = ['draft', 'sent', 'paid', 'overdue', 'cancelled'];
if (!newStatus || !validStatuses.includes(newStatus)) {
@@ -93,22 +117,57 @@ export const actions: Actions = {
}
const [inv] = await db
.select({ invoiceNumber: invoices.invoiceNumber })
.select({
invoiceNumber: invoices.invoiceNumber,
status: invoices.status,
paymentAccountId: invoices.paymentAccountId
})
.from(invoices)
.where(and(eq(invoices.id, params.invoiceId), eq(invoices.companyId, params.companyId)))
.limit(1);
if (!inv) return fail(404, { error: 'Invoice not found' });
await db
.update(invoices)
.set({ status: newStatus, updatedAt: new Date() })
.where(and(eq(invoices.id, params.invoiceId), eq(invoices.companyId, params.companyId)));
if (newStatus === 'paid') {
if (!paymentAccountId) {
return fail(400, { error: 'Payment account is required to mark an invoice paid' });
}
const [acct] = await db
.select({ id: companyAccounts.id })
.from(companyAccounts)
.where(
and(
eq(companyAccounts.id, paymentAccountId),
eq(companyAccounts.companyId, params.companyId),
isNull(companyAccounts.deletedAt)
)
)
.limit(1);
if (!acct) return fail(400, { error: 'Invalid payment account' });
}
await db.transaction(async (tx) => {
await tx
.update(invoices)
.set({
status: newStatus,
paymentAccountId: newStatus === 'paid' ? paymentAccountId : null,
updatedAt: new Date()
})
.where(and(eq(invoices.id, params.invoiceId), eq(invoices.companyId, params.companyId)));
if (newStatus === 'paid' && paymentAccountId) {
await postInvoicePaymentTransaction(params.invoiceId, paymentAccountId, user.id, tx);
} else if (inv.status === 'paid') {
// Status moved away from paid — remove ledger post
await removeInvoicePaymentTransaction(params.invoiceId, tx);
}
});
if (newStatus === 'sent') {
await logCompanyEvent(params.companyId, user.id, 'invoice_sent', `Marked invoice ${inv.invoiceNumber} as sent`, { invoiceId: params.invoiceId });
} else if (newStatus === 'paid') {
await logCompanyEvent(params.companyId, user.id, 'invoice_paid', `Marked invoice ${inv.invoiceNumber} as paid`, { invoiceId: params.invoiceId });
await logCompanyEvent(params.companyId, user.id, 'invoice_paid', `Marked invoice ${inv.invoiceNumber} as paid`, { invoiceId: params.invoiceId, paymentAccountId });
}
return { success: true };
@@ -202,17 +202,37 @@
<div class="flex flex-wrap items-center gap-3">
<!-- Status transitions -->
{#each nextStatuses[inv.status] ?? [] as targetStatus}
<form method="POST" action="?/updateStatus" use:enhance>
<form method="POST" action="?/updateStatus" use:enhance class="flex items-center gap-1">
<input type="hidden" name="status" value={targetStatus} />
<button type="submit"
class="rounded-md px-3 py-1.5 text-sm font-medium transition-colors
{targetStatus === 'paid' ? 'bg-green-600 text-white hover:bg-green-700' :
targetStatus === 'sent' ? 'bg-blue-600 text-white hover:bg-blue-700' :
targetStatus === 'cancelled' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/50' :
targetStatus === 'overdue' ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300 hover:bg-amber-200' :
'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'}">
Mark {targetStatus.charAt(0).toUpperCase() + targetStatus.slice(1)}
</button>
{#if targetStatus === 'paid'}
{#if data.accounts.length === 0}
<span class="text-xs text-gray-400" title="Create an account first to mark as paid">
No account
</span>
{:else}
<select
name="paymentAccountId"
required
class="rounded border border-gray-300 bg-white px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option value="">— Account —</option>
{#each data.accounts as acct (acct.id)}
<option value={acct.id}>{acct.name} ({acct.currency})</option>
{/each}
</select>
{/if}
{/if}
{#if targetStatus !== 'paid' || data.accounts.length > 0}
<button type="submit"
class="rounded-md px-3 py-1.5 text-sm font-medium transition-colors
{targetStatus === 'paid' ? 'bg-green-600 text-white hover:bg-green-700' :
targetStatus === 'sent' ? 'bg-blue-600 text-white hover:bg-blue-700' :
targetStatus === 'cancelled' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/50' :
targetStatus === 'overdue' ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300 hover:bg-amber-200' :
'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'}">
Mark {targetStatus.charAt(0).toUpperCase() + targetStatus.slice(1)}
</button>
{/if}
</form>
{/each}
@@ -1,8 +1,15 @@
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
import { expenses, categories, tags, expenseTags, projects } from '$lib/server/db/schema.js';
import { eq, and } from 'drizzle-orm';
import {
expenses,
categories,
tags,
expenseTags,
projects,
companyAccounts
} from '$lib/server/db/schema.js';
import { eq, and, isNull } from 'drizzle-orm';
import { requireCompanyRole } from '$lib/server/authorization.js';
import { logCompanyEvent } from '$lib/server/audit.js';
import { formatCurrency } from '$lib/utils/currency.js';
@@ -22,6 +29,23 @@ export const load: PageServerLoad = async ({ locals, params }) => {
.where(eq(tags.companyId, params.companyId))
.orderBy(tags.name);
const accountList = await db
.select({
id: companyAccounts.id,
name: companyAccounts.name,
currency: companyAccounts.currency,
accountType: companyAccounts.accountType
})
.from(companyAccounts)
.where(
and(
eq(companyAccounts.companyId, params.companyId),
eq(companyAccounts.isArchived, false),
isNull(companyAccounts.deletedAt)
)
)
.orderBy(companyAccounts.name);
// Get project info for the currency
const [project] = await db
.select({ name: projects.name })
@@ -29,7 +53,12 @@ export const load: PageServerLoad = async ({ locals, params }) => {
.where(eq(projects.id, params.projectId))
.limit(1);
return { categories: categoryList, tags: tagList, projectName: project?.name };
return {
categories: categoryList,
tags: tagList,
accounts: accountList,
projectName: project?.name
};
};
export const actions: Actions = {
@@ -42,6 +71,7 @@ export const actions: Actions = {
const amount = formData.get('amount')?.toString().trim();
const expenseDate = formData.get('expenseDate')?.toString();
const categoryId = formData.get('categoryId')?.toString() || null;
const accountId = formData.get('accountId')?.toString() || null;
const tagIds = formData.getAll('tagIds').map((t) => t.toString());
if (!title || !amount || !expenseDate) {
@@ -69,6 +99,7 @@ export const actions: Actions = {
.values({
projectId: params.projectId,
categoryId: categoryId || null,
accountId: accountId || null,
submittedBy: user.id,
title,
description,
@@ -83,6 +83,25 @@
</select>
</div>
{#if data.accounts.length > 0}
<div class="mb-4">
<label for="accountId" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Funding Account
<span class="ml-1 text-xs text-gray-400">(posts on approval)</span>
</label>
<select
id="accountId"
name="accountId"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
>
<option value="">None — assign later</option>
{#each data.accounts as acct}
<option value={acct.id}>{acct.name} ({acct.currency})</option>
{/each}
</select>
</div>
{/if}
{#if data.tags.length > 0}
<div class="mb-4">
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Tags</span>