Fix/minor UI fixes (#459)

* feat(settings): add option for company name position in invoice PDF

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* fix(ui): improve accessibility for company name position toggle in PDF settings
This commit is contained in:
Mattsson
2026-05-13 10:45:08 +02:00
committed by GitHub
parent 37ccda5cad
commit 64e6aa67a7
8 changed files with 172 additions and 11 deletions
@@ -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 <SettingsLoadingSkeleton />
@@ -30,6 +32,11 @@ export default function CompanySettingsPage() {
updates,
onSuccess: (data: Record<string, unknown>) => {
updateSettings(data as Partial<CompanySettings>)
// 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()
}
},
}
}
+54 -8
View File
@@ -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)
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Visa företagsnamn i faktura</Label>
<p className="text-xs text-muted-foreground">Visa företagsnamn under loggan i fakturahuvudet</p>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<Label>Visa företagsnamn i faktura</Label>
<p className="text-xs text-muted-foreground">Visa företagsnamn i fakturan</p>
</div>
<Switch
checked={settings.invoice_show_company_name ?? true}
onCheckedChange={(v) => saveToggle('invoice_show_company_name', v)}
/>
</div>
<Switch
checked={settings.invoice_show_company_name ?? true}
onCheckedChange={(v) => saveToggle('invoice_show_company_name', v)}
/>
{(settings.invoice_show_company_name ?? true) && (
<div className="flex items-center justify-between pl-0">
<p className="text-xs text-muted-foreground">Placering</p>
<div
role="group"
aria-label="Placering av företagsnamn"
className="inline-flex rounded-md border border-border/60 p-0.5"
>
{(['header', 'footer'] as const).map((pos) => {
const active = (settings.invoice_company_name_position ?? 'header') === pos
return (
<button
key={pos}
type="button"
aria-pressed={active}
onClick={() => savePosition(pos)}
className={
'h-10 px-4 text-sm rounded-sm transition-colors ' +
(active
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground')
}
>
{pos === 'header' ? 'Huvud' : 'Sidfot'}
</button>
)
})}
</div>
</div>
)}
</div>
</div>
+1
View File
@@ -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
+8 -3
View File
@@ -377,9 +377,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{company.logo_url && (company.invoice_show_logo ?? true) && (
<Image src={company.logo_url} style={{ maxHeight: 40, maxWidth: 150, marginBottom: 6, alignSelf: 'flex-start' }} />
)}
{(company.invoice_show_company_name ?? true) && (
<Text style={styles.companyName}>{company.company_name}</Text>
)}
{(company.invoice_show_company_name ?? true) &&
(company.invoice_company_name_position ?? 'header') === 'header' && (
<Text style={styles.companyName}>{company.company_name}</Text>
)}
</View>
<View style={{ textAlign: 'right' }}>
<Text style={[styles.title, isCreditNote ? styles.creditNoteTitle : {}]}>
@@ -667,6 +668,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
<View style={styles.footer}>
<Text style={styles.footerText}>
{[
(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,
@@ -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';
@@ -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';
+1
View File
@@ -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,
+1
View File
@@ -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