From 39ac9d3928d2c26161e5caa38de0725a1bb6351a Mon Sep 17 00:00:00 2001 From: grabowski Date: Wed, 15 Apr 2026 09:43:26 +0700 Subject: [PATCH] Add financial export ZIP download for admin and accountant - New /companies/[id]/export page with year selector and big download button - GET /export/zip endpoint generates the financial-export-{name}-{year}.zip by calling buildFinancialExport, then logs financial_exported in the company audit trail - New "Export" tab in company nav, visible to admin or accountant - Page lists all included files and warns about sensitive PII Co-Authored-By: Claude Opus 4.6 (1M context) --- .../companies/[companyId]/+layout.svelte | 3 + .../[companyId]/export/+page.server.ts | 8 ++ .../companies/[companyId]/export/+page.svelte | 90 +++++++++++++++++++ .../[companyId]/export/zip/+server.ts | 32 +++++++ 4 files changed, 133 insertions(+) create mode 100644 src/routes/(app)/companies/[companyId]/export/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/export/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/export/zip/+server.ts diff --git a/src/routes/(app)/companies/[companyId]/+layout.svelte b/src/routes/(app)/companies/[companyId]/+layout.svelte index 6acd151..144e09f 100644 --- a/src/routes/(app)/companies/[companyId]/+layout.svelte +++ b/src/routes/(app)/companies/[companyId]/+layout.svelte @@ -32,6 +32,9 @@ { href: `/companies/${data.company.id}/integrations`, label: 'Integrations' } ] : []), + ...(data.companyRoles.includes('admin') || data.companyRoles.includes('accountant') + ? [{ href: `/companies/${data.company.id}/export`, label: 'Export' }] + : []), ...(data.companyRoles.includes('admin') || data.companyRoles.includes('manager') ? [ { href: `/companies/${data.company.id}/import`, label: 'Import' }, diff --git a/src/routes/(app)/companies/[companyId]/export/+page.server.ts b/src/routes/(app)/companies/[companyId]/export/+page.server.ts new file mode 100644 index 0000000..808cd1b --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/export/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from './$types'; +import { requireCompanyRoleAny } from '$lib/server/authorization.js'; + +export const load: PageServerLoad = async ({ locals, params, parent }) => { + await requireCompanyRoleAny(locals, params.companyId, ['admin', 'accountant']); + await parent(); + return {}; +}; diff --git a/src/routes/(app)/companies/[companyId]/export/+page.svelte b/src/routes/(app)/companies/[companyId]/export/+page.svelte new file mode 100644 index 0000000..a172b7d --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/export/+page.svelte @@ -0,0 +1,90 @@ + + + + Financial Export - {data.company.name} + + +
+

Financial Export

+

+ Download a complete year-scoped snapshot for internal review or tax filing. +

+ + +
+ + +

+ ZIP filename: financial-export-{data.company.name.replace(/[^a-zA-Z0-9_-]+/g, '_')}-{year}.zip +

+
+ + +
+

Contents

+
    + {#each FILES as f} +
  • + {f.name} + {f.desc} +
  • + {/each} +
+
+ + +
+

Sensitive data — handle with care

+

+ This export contains full salaries, bank account numbers, national IDs, and tax IDs. + Store the ZIP securely and only share with parties (auditors, accountants) who require the data. +

+

+ All generations are logged in the company audit trail. +

+
+
diff --git a/src/routes/(app)/companies/[companyId]/export/zip/+server.ts b/src/routes/(app)/companies/[companyId]/export/zip/+server.ts new file mode 100644 index 0000000..3214613 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/export/zip/+server.ts @@ -0,0 +1,32 @@ +import { error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireCompanyRoleAny } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; +import { buildFinancialExport } from '$lib/server/export/financial.js'; + +export const GET: RequestHandler = async ({ locals, params, url }) => { + const { user } = await requireCompanyRoleAny(locals, params.companyId, ['admin', 'accountant']); + + const yearParam = url.searchParams.get('year'); + const year = yearParam ? parseInt(yearParam, 10) : new Date().getFullYear(); + if (isNaN(year) || year < 2000 || year > 2100) { + error(400, 'Invalid year'); + } + + const { filename, bytes } = await buildFinancialExport(params.companyId, year); + + await logCompanyEvent( + params.companyId, + user.id, + 'financial_exported', + `Financial export generated for ${year}`, + { year, sizeBytes: bytes.length } + ); + + return new Response(new Blob([bytes as BlobPart], { type: 'application/zip' }), { + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename="${filename}"` + } + }); +};