From 3a095851e9ba1018cc3a88bf354ebda4450ad495 Mon Sep 17 00:00:00 2001 From: grabowski Date: Thu, 16 Apr 2026 12:04:15 +0700 Subject: [PATCH] Auto-post expenses and invoice payments to accounts ledger Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/server/accounts/ledger.ts | 10 +- .../[companyId]/expenses/+page.server.ts | 178 +++++++++++++++--- .../[companyId]/expenses/+page.svelte | 47 ++++- .../[companyId]/invoices/+page.server.ts | 64 ++++++- .../[companyId]/invoices/+page.svelte | 30 ++- .../invoices/[invoiceId]/+page.server.ts | 75 +++++++- .../invoices/[invoiceId]/+page.svelte | 40 +++- .../[projectId]/expenses/new/+page.server.ts | 37 +++- .../[projectId]/expenses/new/+page.svelte | 19 ++ 9 files changed, 430 insertions(+), 70 deletions(-) diff --git a/src/lib/server/accounts/ledger.ts b/src/lib/server/accounts/ledger.ts index 5a790fc..5b2ee89 100644 --- a/src/lib/server/accounts/ledger.ts +++ b/src/lib/server/accounts/ledger.ts @@ -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}`, diff --git a/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts b/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts index f0f9cd0..98bb4fb 100644 --- a/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts @@ -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 }; } }; diff --git a/src/routes/(app)/companies/[companyId]/expenses/+page.svelte b/src/routes/(app)/companies/[companyId]/expenses/+page.svelte index e1dfdb0..97747fc 100644 --- a/src/routes/(app)/companies/[companyId]/expenses/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/expenses/+page.svelte @@ -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') + ); @@ -57,6 +62,15 @@

By {expense.submitterName ?? expense.submitterEmail} · {expense.expenseDate}

+ {#if expense.accountName} +

+ + Account: {expense.accountName} + +

+ {/if}

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

@@ -80,7 +94,7 @@ {/if} {#if canApprove && expense.status === 'pending'} -
+
{/if} + + {#if canAssignAccount && data.accounts.length > 0} + + + + + + + {/if}
{/each}
diff --git a/src/routes/(app)/companies/[companyId]/invoices/+page.server.ts b/src/routes/(app)/companies/[companyId]/invoices/+page.server.ts index cdda411..cff6cb0 100644 --- a/src/routes/(app)/companies/[companyId]/invoices/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/invoices/+page.server.ts @@ -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 }; diff --git a/src/routes/(app)/companies/[companyId]/invoices/+page.svelte b/src/routes/(app)/companies/[companyId]/invoices/+page.svelte index 9c70e66..f7daa2e 100644 --- a/src/routes/(app)/companies/[companyId]/invoices/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/invoices/+page.svelte @@ -141,13 +141,29 @@ {/if} {#if inv.status === 'sent' || inv.status === 'overdue'} -
- - -
+ {#if data.accounts.length === 0} + + No account + + {:else} +
+ + + +
+ {/if} {/if} diff --git a/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.server.ts b/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.server.ts index b44ad5b..1661ea2 100644 --- a/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.server.ts @@ -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 }; diff --git a/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.svelte b/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.svelte index 3c90d0d..f429e5e 100644 --- a/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.svelte @@ -202,17 +202,37 @@
{#each nextStatuses[inv.status] ?? [] as targetStatus} -
+ - + {#if targetStatus === 'paid'} + {#if data.accounts.length === 0} + + No account + + {:else} + + {/if} + {/if} + {#if targetStatus !== 'paid' || data.accounts.length > 0} + + {/if}
{/each} diff --git a/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts index d0f8a5d..51f6098 100644 --- a/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts @@ -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, diff --git a/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte index 3cd5f2d..dcd54cf 100644 --- a/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte @@ -83,6 +83,25 @@
+ {#if data.accounts.length > 0} +
+ + +
+ {/if} + {#if data.tags.length > 0}
Tags