diff --git a/app/(dashboard)/settings/company/page.tsx b/app/(dashboard)/settings/company/page.tsx index acda550d..ff656980 100644 --- a/app/(dashboard)/settings/company/page.tsx +++ b/app/(dashboard)/settings/company/page.tsx @@ -1,5 +1,6 @@ 'use client' +import { useRouter } from 'next/navigation' import { CompanyDangerZone } from '@/components/settings/CompanyDangerZone' import { CompanyInfoForm } from '@/components/settings/CompanyInfoForm' import { CompanyMembersSection } from '@/components/settings/CompanyMembersSection' @@ -11,6 +12,7 @@ import { useSettings } from '@/components/settings/useSettings' import type { CompanySettings } from '@/types' export default function CompanySettingsPage() { + const router = useRouter() const { settings, isLoading, updateSettings } = useSettings() if (isLoading || !settings) return @@ -30,6 +32,11 @@ export default function CompanySettingsPage() { updates, onSuccess: (data: Record) => { updateSettings(data as Partial) + // Refresh server components so the company switcher and DashboardNav + // pick up the new company_name (rendered from server in the dashboard layout). + if ('company_name' in updates) { + router.refresh() + } }, } } diff --git a/components/settings/PdfPrintSettings.tsx b/components/settings/PdfPrintSettings.tsx index e48f40d7..1af960b3 100644 --- a/components/settings/PdfPrintSettings.tsx +++ b/components/settings/PdfPrintSettings.tsx @@ -31,6 +31,20 @@ export function PdfPrintSettings({ settings, onUpdate }: PdfPrintSettingsProps) } }, [onUpdate, toast]) + const savePosition = useCallback(async (value: 'header' | 'footer') => { + try { + const response = await fetch('/api/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ invoice_company_name_position: value }), + }) + if (!response.ok) throw new Error() + onUpdate({ invoice_company_name_position: value }) + } catch { + toast({ title: 'Kunde inte spara', variant: 'destructive' }) + } + }, [onUpdate, toast]) + const saveText = useCallback(async (field: string, value: string) => { try { const response = await fetch('/api/settings', { @@ -107,15 +121,47 @@ export function PdfPrintSettings({ settings, onUpdate }: PdfPrintSettingsProps) /> -
-
- -

Visa företagsnamn under loggan i fakturahuvudet

+
+
+
+ +

Visa företagsnamn i fakturan

+
+ saveToggle('invoice_show_company_name', v)} + />
- saveToggle('invoice_show_company_name', v)} - /> + {(settings.invoice_show_company_name ?? true) && ( +
+

Placering

+
+ {(['header', 'footer'] as const).map((pos) => { + const active = (settings.invoice_company_name_position ?? 'header') === pos + return ( + + ) + })} +
+
+ )}
diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts index bb879f55..f8bcf836 100644 --- a/lib/api/schemas.ts +++ b/lib/api/schemas.ts @@ -400,6 +400,7 @@ export const UpdateSettingsSchema = z.object({ invoice_show_plusgiro: z.boolean().optional(), invoice_show_logo: z.boolean().optional(), invoice_show_company_name: z.boolean().optional(), + invoice_company_name_position: z.enum(['header', 'footer']).optional(), invoice_late_fee_text: z.string().nullable().optional(), invoice_credit_terms_text: z.string().nullable().optional(), // AI agent flow diff --git a/lib/invoices/pdf-template.tsx b/lib/invoices/pdf-template.tsx index 7a966b92..ce7412da 100644 --- a/lib/invoices/pdf-template.tsx +++ b/lib/invoices/pdf-template.tsx @@ -377,9 +377,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN {company.logo_url && (company.invoice_show_logo ?? true) && ( )} - {(company.invoice_show_company_name ?? true) && ( - {company.company_name} - )} + {(company.invoice_show_company_name ?? true) && + (company.invoice_company_name_position ?? 'header') === 'header' && ( + {company.company_name} + )} @@ -667,6 +668,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN {[ + (company.invoice_show_company_name ?? true) && + (company.invoice_company_name_position ?? 'header') === 'footer' + ? company.company_name + : null, company.address_line1, (company.postal_code || company.city) ? `${company.postal_code ?? ''} ${company.city ?? ''}`.trim() : null, company.org_number ? `Org.nr: ${formatOrgNumber(company.org_number)}` : null, diff --git a/supabase/migrations/20260513130000_invoice_company_name_position.sql b/supabase/migrations/20260513130000_invoice_company_name_position.sql new file mode 100644 index 00000000..b628a713 --- /dev/null +++ b/supabase/migrations/20260513130000_invoice_company_name_position.sql @@ -0,0 +1,10 @@ +-- Add invoice_company_name_position to control whether the company name +-- appears in the invoice PDF header (under the logo) or in the footer. +-- Default 'header' preserves existing layout for all current users. + +ALTER TABLE public.company_settings + ADD COLUMN IF NOT EXISTS invoice_company_name_position text + NOT NULL DEFAULT 'header' + CHECK (invoice_company_name_position IN ('header', 'footer')); + +NOTIFY pgrst, 'reload schema'; diff --git a/supabase/migrations/20260513130001_backfill_vat_account_labels.sql b/supabase/migrations/20260513130001_backfill_vat_account_labels.sql new file mode 100644 index 00000000..524d2b52 --- /dev/null +++ b/supabase/migrations/20260513130001_backfill_vat_account_labels.sql @@ -0,0 +1,90 @@ +-- Backfill: fix VAT account labels on companies that were seeded by the +-- regressed seed_chart_of_accounts() between 2026-03-30 and 2026-05-13. +-- +-- Companion to 20260513120000_fix_vat_seed_chart_of_accounts.sql. The earlier +-- migration patched the seed function so new companies get correct labels. +-- This one cleans up companies that were already created with the bad seed. +-- +-- Safety notes: +-- * Every WHERE clause matches the EXACT bad-seed name, so customers who +-- have already manually renamed an account are left alone. +-- * Orphan 2610 / 2612 rows are only deleted if no journal line references +-- them. The engine never routes to 2610/2612, so this should be true for +-- all bad-seed companies; the guard is defense in depth. +-- * 2621 / 2631 are inserted only for companies that show the bad-seed +-- fingerprint (a mislabelled 2611 carrying one of the two bad-seed names). +-- plan_type is derived from that sibling 2611 row rather than hardcoded +-- to 'k1', so any company that manually adjusted plan_type keeps it. + +BEGIN; + +-- 1. Rename mislabelled 2611 -> 25% +UPDATE public.chart_of_accounts + SET account_name = 'Utgaende moms forsaljning inom Sverige, 25%', + updated_at = now() + WHERE account_number = '2611' + AND account_name IN ('Utgaende moms 12%', 'Utgående moms 12%'); + +-- 2. Insert missing 2621 (12%) for bad-seed companies that lack it. +-- Scoped to companies that had the bad-seed fingerprint on 2611 +-- (rename in step 1 above, or the orphan 2610/2612 pattern in step 4). +INSERT INTO public.chart_of_accounts + (user_id, company_id, account_number, account_name, account_class, + account_group, account_type, normal_balance, plan_type, is_system_account) +SELECT c.created_by, + c.id, + '2621', + 'Utgaende moms forsaljning inom Sverige, 12%', + 2, '26', 'liability', 'credit', sibling.plan_type, true + FROM public.companies c + JOIN public.chart_of_accounts sibling + ON sibling.company_id = c.id + AND sibling.account_number = '2611' + WHERE sibling.account_name = 'Utgaende moms forsaljning inom Sverige, 25%' + AND NOT EXISTS ( + SELECT 1 FROM public.chart_of_accounts coa + WHERE coa.company_id = c.id + AND coa.account_number = '2621' + ); + +-- 3. Insert missing 2631 (6%) for bad-seed companies that lack it. +INSERT INTO public.chart_of_accounts + (user_id, company_id, account_number, account_name, account_class, + account_group, account_type, normal_balance, plan_type, is_system_account) +SELECT c.created_by, + c.id, + '2631', + 'Utgaende moms forsaljning inom Sverige, 6%', + 2, '26', 'liability', 'credit', sibling.plan_type, true + FROM public.companies c + JOIN public.chart_of_accounts sibling + ON sibling.company_id = c.id + AND sibling.account_number = '2611' + WHERE sibling.account_name = 'Utgaende moms forsaljning inom Sverige, 25%' + AND NOT EXISTS ( + SELECT 1 FROM public.chart_of_accounts coa + WHERE coa.company_id = c.id + AND coa.account_number = '2631' + ); + +-- 4. Remove orphan 2610 / 2612 rows created by the bad seed. +-- Only rows that (a) still carry the bad-seed name verbatim, and +-- (b) have zero postings on journal_entry_lines are removed. +DELETE FROM public.chart_of_accounts coa + WHERE coa.account_number IN ('2610', '2612') + AND coa.account_name IN ( + 'Utgaende moms 25%', 'Utgående moms 25%', + 'Utgaende moms 6%', 'Utgående moms 6%' + ) + AND coa.is_system_account = true + AND NOT EXISTS ( + SELECT 1 + FROM public.journal_entry_lines jel + JOIN public.journal_entries je ON je.id = jel.journal_entry_id + WHERE jel.account_number = coa.account_number + AND je.company_id = coa.company_id + ); + +COMMIT; + +NOTIFY pgrst, 'reload schema'; diff --git a/tests/helpers.ts b/tests/helpers.ts index d0df1376..8b00caac 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -532,6 +532,7 @@ export function makeCompanySettings( invoice_show_plusgiro: true, invoice_show_logo: true, invoice_show_company_name: true, + invoice_company_name_position: 'header', invoice_late_fee_text: null, invoice_credit_terms_text: null, logo_url: null, diff --git a/types/index.ts b/types/index.ts index 511bfc4d..4d6facce 100644 --- a/types/index.ts +++ b/types/index.ts @@ -241,6 +241,7 @@ export interface CompanySettings { invoice_show_plusgiro: boolean invoice_show_logo: boolean invoice_show_company_name: boolean + invoice_company_name_position: 'header' | 'footer' invoice_late_fee_text: string | null invoice_credit_terms_text: string | null