From b6f07fe4dfb9edc7abbea2a6302a2a850f0c5ee2 Mon Sep 17 00:00:00 2001 From: grabowski Date: Tue, 14 Apr 2026 16:35:13 +0700 Subject: [PATCH] Major expansion: HR module, CRM, integrations, packages, validation pipeline HR module: - Multi-role per company (admin/manager/user/viewer/hr orthogonal) - Employees with salary history, terminate/reactivate - Per-company public holidays (seeded from ppraserts/thailand-open-data with manual fallback for unsupported years) - Leave types (editable defaults), leave requests with approve/reject - Per-employee leave balances (auto-seeded), remaining-days hint on request form, HR balance summary on requests page - Thai-compliant payroll: SSO 5% capped, PND1 brackets, monthly WHT - Payslip generation with editable line items, finalize/mark-paid, pdf-lib PDF download - CSV export of leave per employee or company-wide CRM & invoicing: - Customer/supplier party database with archive - Invoice line items, VAT 7%, status transitions, PDF generation - Outgoing/incoming direction; incoming auto-creates linked expense Package tracking: - packages + package_events + shipping_accounts tables - 8 carrier stubs (UPS/FedEx/DHL/USPS/Flash Express/Kerry/J&T/TH Post) with API doc references for future implementation - Manual status updates with timeline - Customs duty invoice flow on delivery - Per-company carrier credentials (admin only) Integrations scaffold: - external_accounts + external_transactions (Kasikorn K-Biz, Ether.fi) - Manual transaction matching to expenses Infrastructure: - APP_NAME env var for branding - Soft-delete for companies and parties - Light/dark mode toggle, dark-mode classes throughout - pre-push hook (husky) + Gitea/GitHub Actions running svelte-check with --threshold warning + vite build - npm run validate combines both checks Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 3 + .gitea/workflows/validate.yml | 26 + .github/workflows/validate.yml | 26 + .husky/pre-push | 2 + package-lock.json | 70 ++ package.json | 11 +- src/lib/components/layout/Sidebar.svelte | 9 +- src/lib/server/authorization.ts | 72 +- src/lib/server/db/schema.ts | 448 +++++++++++- src/lib/server/integrations/etherfi.ts | 25 + src/lib/server/integrations/kasikorn.ts | 26 + src/lib/server/integrations/types.ts | 10 + src/lib/server/invoices/pdf.ts | 297 ++++++++ src/lib/server/payroll/pdf.ts | 215 ++++++ src/lib/server/payroll/thailand.ts | 93 +++ src/lib/server/seeds/thai-holidays.ts | 99 +++ src/lib/server/shipping/carriers/dhl.ts | 21 + src/lib/server/shipping/carriers/fedex.ts | 23 + .../server/shipping/carriers/flash_express.ts | 22 + .../server/shipping/carriers/jnt_express.ts | 22 + src/lib/server/shipping/carriers/kerry_th.ts | 21 + .../server/shipping/carriers/thailand_post.ts | 22 + src/lib/server/shipping/carriers/ups.ts | 23 + src/lib/server/shipping/carriers/usps.ts | 21 + src/lib/server/shipping/index.ts | 64 ++ src/lib/server/shipping/types.ts | 34 + src/lib/types/index.ts | 16 +- src/lib/utils/csv.ts | 31 + src/lib/utils/leave.ts | 29 + src/routes/(app)/+layout.server.ts | 2 +- src/routes/(app)/+layout.svelte | 2 + src/routes/(app)/companies/+page.server.ts | 25 +- src/routes/(app)/companies/+page.svelte | 4 +- .../companies/[companyId]/+layout.server.ts | 11 +- .../companies/[companyId]/+layout.svelte | 24 +- .../(app)/companies/[companyId]/+page.svelte | 4 +- .../companies/[companyId]/budget/+page.svelte | 10 +- .../[companyId]/categories/+page.svelte | 4 +- .../[companyId]/employees/+page.server.ts | 65 ++ .../[companyId]/employees/+page.svelte | 81 ++ .../employees/[id]/+page.server.ts | 380 ++++++++++ .../[companyId]/employees/[id]/+page.svelte | 561 ++++++++++++++ .../employees/[id]/leave/export/+server.ts | 163 +++++ .../[companyId]/employees/new/+page.server.ts | 107 +++ .../[companyId]/employees/new/+page.svelte | 233 ++++++ .../[companyId]/expenses/+page.svelte | 6 +- .../[companyId]/hr/holidays/+page.server.ts | 138 ++++ .../[companyId]/hr/holidays/+page.svelte | 189 +++++ .../hr/leave-requests/+page.server.ts | 321 ++++++++ .../hr/leave-requests/+page.svelte | 206 ++++++ .../hr/leave-requests/export/+server.ts | 198 +++++ .../hr/leave-requests/new/+page.server.ts | 261 +++++++ .../hr/leave-requests/new/+page.svelte | 219 ++++++ .../hr/leave-types/+page.server.ts | 106 +++ .../[companyId]/hr/leave-types/+page.svelte | 205 ++++++ .../[companyId]/hr/payroll/+page.server.ts | 270 +++++++ .../[companyId]/hr/payroll/+page.svelte | 212 ++++++ .../hr/payroll/[payslipId]/+page.server.ts | 280 +++++++ .../hr/payroll/[payslipId]/+page.svelte | 458 ++++++++++++ .../hr/payroll/[payslipId]/pdf/+server.ts | 128 ++++ .../[companyId]/integrations/+page.server.ts | 233 ++++++ .../[companyId]/integrations/+page.svelte | 359 +++++++++ .../integrations/transactions/+page.server.ts | 149 ++++ .../integrations/transactions/+page.svelte | 160 ++++ .../[companyId]/invoices/+page.server.ts | 118 +++ .../[companyId]/invoices/+page.svelte | 161 ++++ .../invoices/[invoiceId]/+page.server.ts | 175 +++++ .../invoices/[invoiceId]/+page.svelte | 278 +++++++ .../invoices/[invoiceId]/pdf/+server.ts | 93 +++ .../[companyId]/invoices/new/+page.server.ts | 122 ++++ .../[companyId]/invoices/new/+page.svelte | 260 +++++++ .../[companyId]/packages/+page.server.ts | 90 +++ .../[companyId]/packages/+page.svelte | 223 ++++++ .../packages/[packageId]/+page.server.ts | 505 +++++++++++++ .../packages/[packageId]/+page.svelte | 690 ++++++++++++++++++ .../[companyId]/packages/new/+page.server.ts | 148 ++++ .../[companyId]/packages/new/+page.svelte | 281 +++++++ .../[companyId]/parties/+page.server.ts | 38 + .../[companyId]/parties/+page.svelte | 106 +++ .../parties/[partyId]/+page.server.ts | 115 +++ .../parties/[partyId]/+page.svelte | 321 ++++++++ .../[companyId]/parties/new/+page.server.ts | 67 ++ .../[companyId]/parties/new/+page.svelte | 210 ++++++ .../[companyId]/projects/+page.svelte | 4 +- .../projects/[projectId]/+page.svelte | 4 +- .../[projectId]/expenses/new/+page.svelte | 9 +- .../[companyId]/projects/new/+page.svelte | 7 +- .../[companyId]/reports/+page.svelte | 28 +- .../[companyId]/settings/+page.server.ts | 71 +- .../[companyId]/settings/+page.svelte | 91 ++- .../settings/shipping/+page.server.ts | 123 ++++ .../settings/shipping/+page.svelte | 166 +++++ src/routes/(app)/dashboard/+page.server.ts | 47 +- src/routes/(app)/dashboard/+page.svelte | 24 +- src/routes/(auth)/login/+page.svelte | 2 +- src/routes/(auth)/signup/+page.svelte | 6 +- src/routes/+layout.server.ts | 4 +- src/routes/+page.svelte | 15 - 98 files changed, 12012 insertions(+), 145 deletions(-) create mode 100644 .gitea/workflows/validate.yml create mode 100644 .github/workflows/validate.yml create mode 100644 .husky/pre-push create mode 100644 src/lib/server/integrations/etherfi.ts create mode 100644 src/lib/server/integrations/kasikorn.ts create mode 100644 src/lib/server/integrations/types.ts create mode 100644 src/lib/server/invoices/pdf.ts create mode 100644 src/lib/server/payroll/pdf.ts create mode 100644 src/lib/server/payroll/thailand.ts create mode 100644 src/lib/server/seeds/thai-holidays.ts create mode 100644 src/lib/server/shipping/carriers/dhl.ts create mode 100644 src/lib/server/shipping/carriers/fedex.ts create mode 100644 src/lib/server/shipping/carriers/flash_express.ts create mode 100644 src/lib/server/shipping/carriers/jnt_express.ts create mode 100644 src/lib/server/shipping/carriers/kerry_th.ts create mode 100644 src/lib/server/shipping/carriers/thailand_post.ts create mode 100644 src/lib/server/shipping/carriers/ups.ts create mode 100644 src/lib/server/shipping/carriers/usps.ts create mode 100644 src/lib/server/shipping/index.ts create mode 100644 src/lib/server/shipping/types.ts create mode 100644 src/lib/utils/csv.ts create mode 100644 src/lib/utils/leave.ts create mode 100644 src/routes/(app)/companies/[companyId]/employees/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/employees/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/employees/[id]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/employees/[id]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/employees/[id]/leave/export/+server.ts create mode 100644 src/routes/(app)/companies/[companyId]/employees/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/employees/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/holidays/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/holidays/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-requests/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-requests/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-requests/export/+server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-requests/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-requests/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-types/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/leave-types/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/payroll/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/payroll/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/payroll/[payslipId]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/hr/payroll/[payslipId]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/hr/payroll/[payslipId]/pdf/+server.ts create mode 100644 src/routes/(app)/companies/[companyId]/integrations/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/integrations/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/integrations/transactions/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/integrations/transactions/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/invoices/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/invoices/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/invoices/[invoiceId]/pdf/+server.ts create mode 100644 src/routes/(app)/companies/[companyId]/invoices/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/invoices/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/packages/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/packages/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/packages/[packageId]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/packages/[packageId]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/packages/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/packages/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/parties/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/parties/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/parties/[partyId]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/parties/[partyId]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/parties/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/parties/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/settings/shipping/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/settings/shipping/+page.svelte delete mode 100644 src/routes/+page.svelte diff --git a/.env.example b/.env.example index 09d7e54..01cc93f 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ PORT=3000 HOST=127.0.0.1 ORIGIN=http://localhost:3000 +# Branding +APP_NAME=B4L Budget + # Database DATABASE_URL=postgresql://budget_app:password@localhost:5432/buildfor_life_budget diff --git a/.gitea/workflows/validate.yml b/.gitea/workflows/validate.yml new file mode 100644 index 0000000..18a6754 --- /dev/null +++ b/.gitea/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run validation (svelte-check + build) + run: npm run validate diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..18a6754 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run validation (svelte-check + build) + run: npm run validate diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..d1dde59 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,2 @@ +echo "Running validation before push (svelte-check + build)..." +npm run validate diff --git a/package-lock.json b/package-lock.json index a53c647..ce23a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", + "@pdf-lib/fontkit": "^1.1.1", "chart.js": "^4.4.7", "date-fns": "^4.1.0", "drizzle-orm": "^0.38.4", "papaparse": "^5.5.2", + "pdf-lib": "^1.17.1", "pg": "^8.13.1", "zod": "^3.24.2" }, @@ -26,6 +28,7 @@ "@types/papaparse": "^5.3.15", "@types/pg": "^8.11.11", "drizzle-kit": "^0.30.5", + "husky": "^9.1.7", "svelte": "^5.19.0", "svelte-check": "^4.1.4", "tailwindcss": "^4.1.3", @@ -1297,6 +1300,33 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@pdf-lib/fontkit": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz", + "integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@petamoriken/float16": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", @@ -2721,6 +2751,22 @@ "node": ">= 0.4" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3108,6 +3154,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", @@ -3121,6 +3173,24 @@ "dev": true, "license": "MIT" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", diff --git a/package.json b/package.json index 95c4b54..0de8f4e 100644 --- a/package.json +++ b/package.json @@ -7,20 +7,24 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold warning", + "validate": "npm run check && npm run build", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "prepare": "husky" }, "dependencies": { "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", + "@pdf-lib/fontkit": "^1.1.1", "chart.js": "^4.4.7", "date-fns": "^4.1.0", "drizzle-orm": "^0.38.4", "papaparse": "^5.5.2", + "pdf-lib": "^1.17.1", "pg": "^8.13.1", "zod": "^3.24.2" }, @@ -29,9 +33,10 @@ "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/vite": "^4.1.3", - "@types/pg": "^8.11.11", "@types/papaparse": "^5.3.15", + "@types/pg": "^8.11.11", "drizzle-kit": "^0.30.5", + "husky": "^9.1.7", "svelte": "^5.19.0", "svelte-check": "^4.1.4", "tailwindcss": "^4.1.3", diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index c977fbc..d9db9bf 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -3,12 +3,13 @@ interface Props { user: { id: string; email: string; displayName: string | null; isSystemAdmin: boolean }; - companies: Array<{ companyId: string; companyName: string; role: CompanyRole }>; + companies: Array<{ companyId: string; companyName: string; roles: CompanyRole[] }>; + appName: string; open: boolean; onToggle: () => void; } - let { user, companies, open, onToggle }: Props = $props(); + let { user, companies, appName, open, onToggle }: Props = $props();