Add invoice linking on expenses: optional FK, dropdown on add form, clickable chip
Deploy to LXC / deploy (push) Successful in 1m56s
Validate / validate (push) Successful in 32s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 15:46:20 +07:00
parent 0710d63cc1
commit 283f0d4dd1
3 changed files with 70 additions and 3 deletions
+3
View File
@@ -130,6 +130,9 @@ export const expenses = pgTable(
accountId: uuid('account_id').references((): any => companyAccounts.id, {
onDelete: 'set null'
}),
invoiceId: uuid('invoice_id').references((): any => invoices.id, {
onDelete: 'set null'
}),
submittedBy: text('submitted_by')
.notNull()
.references(() => users.id),
@@ -6,9 +6,11 @@ import {
projects,
users,
categories,
companyAccounts
companyAccounts,
invoices,
parties
} from '$lib/server/db/schema.js';
import { asc, eq, and, sql, isNull } from 'drizzle-orm';
import { asc, eq, and, ne, 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';
@@ -40,6 +42,7 @@ export const load: PageServerLoad = async ({ parent, params, url }) => {
categoryName: categories.name,
accountId: expenses.accountId,
accountName: companyAccounts.name,
invoiceId: expenses.invoiceId,
createdAt: expenses.createdAt
})
.from(expenses)
@@ -87,12 +90,33 @@ export const load: PageServerLoad = async ({ parent, params, url }) => {
.where(eq(categories.companyId, params.companyId))
.orderBy(asc(categories.name));
const invoiceList = await db
.select({
id: invoices.id,
invoiceNumber: invoices.invoiceNumber,
direction: invoices.direction,
partyName: parties.name,
total: invoices.total,
currency: invoices.currency
})
.from(invoices)
.innerJoin(parties, eq(invoices.partyId, parties.id))
.where(
and(
eq(invoices.companyId, params.companyId),
ne(invoices.status, 'voided'),
ne(invoices.status, 'cancelled')
)
)
.orderBy(asc(invoices.invoiceNumber));
return {
expenses: expenseList,
statusFilter: status,
accounts: accountsList,
projects: projectList,
categories: categoryList
categories: categoryList,
invoices: invoiceList
};
};
@@ -122,6 +146,7 @@ export const actions: Actions = {
const projectId = fd.get('projectId')?.toString().trim() || null;
const categoryId = fd.get('categoryId')?.toString().trim() || null;
const accountId = fd.get('accountId')?.toString().trim() || null;
const invoiceId = fd.get('invoiceId')?.toString().trim() || null;
const expenseDate = fd.get('expenseDate')?.toString().trim();
const description = fd.get('description')?.toString().trim() || null;
@@ -144,6 +169,7 @@ export const actions: Actions = {
projectId: resolvedProjectId,
categoryId: categoryId || null,
accountId: accountId || null,
invoiceId: invoiceId || null,
submittedBy: user.id,
title,
description,
@@ -322,6 +348,22 @@ export const actions: Actions = {
{ expenseId, accountId, previousAccountId: expense.accountId }
);
return { success: true };
},
linkInvoice: async ({ request, locals, params }) => {
await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager', 'accountant']);
const fd = await request.formData();
const expenseId = fd.get('expenseId')?.toString();
const invoiceId = fd.get('invoiceId')?.toString().trim() || null;
if (!expenseId) return fail(400, { error: 'Expense ID required' });
await db
.update(expenses)
.set({ invoiceId, updatedAt: new Date() })
.where(eq(expenses.id, expenseId));
return { success: true };
}
};
@@ -88,6 +88,15 @@
{/each}
</select>
</div>
<div>
<label for="exp-invoice" class={labelCls}>Invoice</label>
<select id="exp-invoice" name="invoiceId" class={inputCls}>
<option value=""></option>
{#each data.invoices as inv (inv.id)}
<option value={inv.id}>{inv.invoiceNumber} {inv.partyName} ({inv.direction})</option>
{/each}
</select>
</div>
<div class="md:col-span-2">
<label for="exp-desc" class={labelCls}>Description</label>
<textarea id="exp-desc" name="description" rows="2" class={inputCls}></textarea>
@@ -150,6 +159,19 @@
</span>
</p>
{/if}
{#if expense.invoiceId}
{@const inv = data.invoices.find((i) => i.id === expense.invoiceId)}
{#if inv}
<p class="mt-1 text-xs">
<a
href={`/companies/${data.company.id}/invoices/${inv.id}`}
class="rounded-full bg-indigo-100 px-2 py-0.5 font-medium text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/40 dark:text-indigo-300"
>
Invoice: {inv.invoiceNumber}
</a>
</p>
{/if}
{/if}
</div>
<div class="text-right">
<p class="text-lg font-semibold dark:text-white">{formatCurrency(expense.amount, expense.currency)}</p>