* feat: implement viewer role permissions for bank transaction imports and connections * feat: add booking_template_library table with 30 system templates Three-level scoping (system/team/company), RLS policies for read/write/delete, and pre-seeded templates for EU reverse charge, tax account, private transfers, salary, representation, year-end, VAT netting, and bank/finance scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add template library types, helpers, and tests - BookingTemplateLibrary/Line/Category types in types/index.ts - applyTemplate() converts template lines + amount into form lines - Category labels, scope helpers for UI display - 8 unit tests for amount calculation, VAT, rounding, and scope detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add booking template CRUD, export, and import API routes - GET/POST/DELETE /api/settings/booking-templates (list, create, soft-delete) - PUT /api/settings/booking-templates/[id] (update non-system templates) - GET /api/settings/booking-templates/export (JSON download) - POST /api/settings/booking-templates/import (bulk import from JSON) All routes enforce auth, write permissions, and Zod validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add template picker UI and settings management panel - BookingTemplatePicker: dialog with search, category/entity-type filter, line preview, and amount input — integrated into JournalEntryForm - BookingTemplatesPanel: settings page with grouped templates (system/team/company), create dialog, export/import, soft-delete - Settings templates page now shows both booking and counterparty templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update supabase/migrations/20260413160000_booking_template_library.sql Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
import type { BookingTemplateCategory, BookingTemplateLibraryLine } from '@/types'
|
||
import type { FormLine } from '@/components/bookkeeping/JournalEntryForm'
|
||
|
||
/**
|
||
* Category labels in Swedish for UI display.
|
||
*/
|
||
export const TEMPLATE_CATEGORY_LABELS: Record<BookingTemplateCategory, string> = {
|
||
eu_trade: 'EU-handel',
|
||
tax_account: 'Skattekonto',
|
||
private_transfer: 'Egna transaktioner',
|
||
salary: 'Lön',
|
||
representation: 'Representation',
|
||
year_end: 'Bokslut',
|
||
vat: 'Moms',
|
||
financial: 'Bank & finans',
|
||
other: 'Övrigt',
|
||
}
|
||
|
||
/**
|
||
* Convert a template's line pattern + total amount into form lines
|
||
* ready for the JournalEntryForm.
|
||
*
|
||
* The algorithm:
|
||
* 1. VAT lines: amount = totalAmount × vat_rate / (1 + vat_rate)
|
||
* 2. Settlement lines: amount = totalAmount (the full payment)
|
||
* 3. Business lines: amount = totalAmount × ratio (cost/revenue net of VAT handled separately)
|
||
*
|
||
* For simple two-line templates (no VAT), the ratio is typically 1.0
|
||
* on both sides and totalAmount is used directly.
|
||
*/
|
||
export function applyTemplate(
|
||
lines: BookingTemplateLibraryLine[],
|
||
totalAmount: number,
|
||
): FormLine[] {
|
||
const result: FormLine[] = []
|
||
|
||
for (const line of lines) {
|
||
let amount = 0
|
||
|
||
if (line.type === 'vat' && line.vat_rate) {
|
||
// VAT calculated on the total inclusive amount
|
||
amount = Math.round(totalAmount * line.vat_rate / (1 + line.vat_rate) * 100) / 100
|
||
} else if (line.type === 'settlement') {
|
||
amount = Math.round(totalAmount * (line.ratio ?? 1) * 100) / 100
|
||
} else {
|
||
// Business lines — use ratio (default 1.0)
|
||
amount = Math.round(totalAmount * (line.ratio ?? 1) * 100) / 100
|
||
}
|
||
|
||
result.push({
|
||
account_number: line.account,
|
||
debit_amount: line.side === 'debit' ? amount.toFixed(2) : '',
|
||
credit_amount: line.side === 'credit' ? amount.toFixed(2) : '',
|
||
line_description: line.label,
|
||
})
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* Scope label for displaying where a template comes from.
|
||
*/
|
||
export function getTemplateScope(template: {
|
||
is_system: boolean
|
||
team_id: string | null
|
||
company_id: string | null
|
||
}): 'system' | 'team' | 'company' {
|
||
if (template.is_system) return 'system'
|
||
if (template.team_id) return 'team'
|
||
return 'company'
|
||
}
|
||
|
||
export const SCOPE_LABELS: Record<ReturnType<typeof getTemplateScope>, string> = {
|
||
system: 'Standard',
|
||
team: 'Team',
|
||
company: 'Företag',
|
||
}
|