diff --git a/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts b/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts index bd6c84f..5b897d9 100644 --- a/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts @@ -8,7 +8,9 @@ import { categories, companyAccounts, invoices, - parties + parties, + packages, + expensePackages } from '$lib/server/db/schema.js'; import { asc, eq, and, ne, sql, isNull } from 'drizzle-orm'; import { requireCompanyRole, requireCompanyRoleAny } from '$lib/server/authorization.js'; @@ -115,13 +117,37 @@ export const load: PageServerLoad = async ({ parent, params, url }) => { ) .orderBy(asc(invoices.invoiceNumber)); + const packageList = await db + .select({ + id: packages.id, + trackingNumber: packages.trackingNumber, + carrier: packages.carrier, + direction: packages.direction, + status: packages.status + }) + .from(packages) + .where(eq(packages.companyId, params.companyId)) + .orderBy(sql`${packages.createdAt} desc`); + + const expensePackageLinks = await db + .select({ + expenseId: expensePackages.expenseId, + packageId: expensePackages.packageId + }) + .from(expensePackages) + .innerJoin(expenses, eq(expensePackages.expenseId, expenses.id)) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where(eq(projects.companyId, params.companyId)); + return { expenses: expenseList, statusFilter: status, accounts: accountsList, projects: projectList, categories: categoryList, - invoices: invoiceList + invoices: invoiceList, + packages: packageList, + expensePackageLinks }; }; @@ -442,6 +468,56 @@ export const actions: Actions = { return { success: true, action: 'uploadExpenseInvoice' }; }, + linkPackage: async ({ request, locals, params }) => { + await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager', 'accountant']); + const fd = await request.formData(); + const expenseId = fd.get('expenseId')?.toString(); + const packageId = fd.get('packageId')?.toString(); + + if (!expenseId || !packageId) return fail(400, { error: 'Expense and package IDs required' }); + + // Verify expense belongs to this company + const [exp] = await db + .select({ id: expenses.id }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where(and(eq(expenses.id, expenseId), eq(projects.companyId, params.companyId))) + .limit(1); + if (!exp) return fail(404, { error: 'Expense not found' }); + + // Verify package belongs to this company + const [pkg] = await db + .select({ id: packages.id }) + .from(packages) + .where(and(eq(packages.id, packageId), eq(packages.companyId, params.companyId))) + .limit(1); + if (!pkg) return fail(404, { error: 'Package not found' }); + + await db + .insert(expensePackages) + .values({ expenseId, packageId }) + .onConflictDoNothing(); + + return { success: true, action: 'linkPackage' }; + }, + + unlinkPackage: async ({ request, locals, params }) => { + await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager', 'accountant']); + const fd = await request.formData(); + const expenseId = fd.get('expenseId')?.toString(); + const packageId = fd.get('packageId')?.toString(); + + if (!expenseId || !packageId) return fail(400, { error: 'Expense and package IDs required' }); + + await db + .delete(expensePackages) + .where( + and(eq(expensePackages.expenseId, expenseId), eq(expensePackages.packageId, packageId)) + ); + + return { success: true, action: 'unlinkPackage' }; + }, + setExpensePaperlessLink: async ({ request, locals, params }) => { const { user } = await requireCompanyRoleAny(locals, params.companyId, [ 'admin', 'manager', 'accountant' diff --git a/src/routes/(app)/companies/[companyId]/expenses/+page.svelte b/src/routes/(app)/companies/[companyId]/expenses/+page.svelte index 7a117fd..4fc283d 100644 --- a/src/routes/(app)/companies/[companyId]/expenses/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/expenses/+page.svelte @@ -135,7 +135,8 @@ {:else}
- {#each data.expenses as expense} + {#each data.expenses as expense (expense.id)} + {@const linkedPkgIds = data.expensePackageLinks.filter((l) => l.expenseId === expense.id).map((l) => l.packageId)}
@@ -197,6 +198,19 @@ {/if}

+ {#if linkedPkgIds.length > 0} +

+ {#each linkedPkgIds as pkgId (pkgId)} + {@const pkg = data.packages.find((p) => p.id === pkgId)} + {#if pkg} + + 📦 {pkg.trackingNumber} + + {/if} + {/each} +

+ {/if}

{formatCurrency(expense.amount, expense.currency)}

@@ -279,6 +293,38 @@ {/if} + {#if canAssignAccount && data.packages.length > 0} + {@const linkedPkgIds = data.expensePackageLinks.filter((l) => l.expenseId === expense.id).map((l) => l.packageId)} +
+ + Link packages ({linkedPkgIds.length}) + +
+ {#each data.packages as pkg (pkg.id)} + {@const isLinked = linkedPkgIds.includes(pkg.id)} +
+ + + + + {pkg.trackingNumber} — {pkg.carrier} ({pkg.direction}) + +
+ {/each} +
+
+ {/if} + {#if canAssignAccount}