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:
@@ -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()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user