'use client' import { useTranslations } from 'next-intl' import { useState, useCallback, useRef, type ChangeEvent } from 'react' import { Loader2, Trash2, Upload } from 'lucide-react' import { Switch } from '@/components/ui/switch' import { useToast } from '@/components/ui/use-toast' import { HelpPopover } from '@/components/ui/help-popover' import { SettingsGroup, SettingsReveal, SettingsRow, SettingsRowEnd, SettingsRowNote, SettingsSeg, SettingsSelect, SettingsTextarea, } from '@/components/settings/SettingsRows' import { INVOICE_FONT_FAMILIES } from '@/lib/invoices/branding-constants' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' import type { CompanySettings, InvoiceFontFamily } from '@/types' interface PdfPrintSettingsProps { settings: CompanySettings onUpdate: (updates: Partial) => void } type PdfToggleField = | 'ore_rounding' | 'invoice_show_ocr' | 'invoice_show_bankgiro' | 'invoice_show_plusgiro' | 'invoice_show_swish' | 'invoice_show_logo' | 'invoice_show_company_name' /** Compact switch row for the show/hide grid: small label, "?", Switch. */ function PdfToggleRow({ id, label, help, checked, onCheckedChange, }: { id: string label: string help?: React.ReactNode checked: boolean onCheckedChange: (value: boolean) => void }) { return (
{help ? {help} : null}
) } export function PdfPrintSettings({ settings, onUpdate }: PdfPrintSettingsProps) { const t = useTranslations('settings_pdf_print') const { toast } = useToast() const [lateFeeText, setLateFeeText] = useState(settings.invoice_late_fee_text || '') const [creditTermsText, setCreditTermsText] = useState(settings.invoice_credit_terms_text || '') const [isSavingFont, setIsSavingFont] = useState(false) const [isUploadingFont, setIsUploadingFont] = useState(false) const [isDeletingFont, setIsDeletingFont] = useState(false) const fontInputRef = useRef(null) const saveToggle = useCallback(async (field: string, value: boolean) => { try { const response = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: value }), }) if (!response.ok) throw new Error() onUpdate({ [field]: value } as Partial) } catch { toast({ title: t('toast_save_failed'), variant: 'destructive' }) } }, [onUpdate, toast, t]) 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: t('toast_save_failed'), variant: 'destructive' }) } }, [onUpdate, toast, t]) const saveText = useCallback(async (field: string, value: string) => { try { const response = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: value || null }), }) if (!response.ok) throw new Error() onUpdate({ [field]: value || null } as Partial) } catch { toast({ title: t('toast_save_failed'), variant: 'destructive' }) } }, [onUpdate, toast, t]) const saveFont = useCallback(async (fontFamily: InvoiceFontFamily) => { setIsSavingFont(true) try { const response = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ invoice_font_family: fontFamily }), }) const result = await response.json() if (!response.ok) throw new Error(result.error || t('toast_save_failed')) onUpdate({ invoice_font_family: fontFamily }) } catch (error) { toast({ title: t('toast_save_failed'), description: error instanceof Error ? getUserErrorMessage(error) : undefined, variant: 'destructive', }) } finally { setIsSavingFont(false) } }, [onUpdate, toast, t]) async function uploadFont(file: File) { setIsUploadingFont(true) try { const formData = new FormData() formData.append('file', file) const response = await fetch('/api/settings/invoice-font', { method: 'POST', body: formData, }) const result = await response.json() if (!response.ok) throw new Error(result.error || t('font_upload_failed')) onUpdate(result.data as Partial) } catch (error) { toast({ title: t('font_upload_failed'), description: error instanceof Error ? getUserErrorMessage(error) : undefined, variant: 'destructive', }) } finally { setIsUploadingFont(false) if (fontInputRef.current) fontInputRef.current.value = '' } } async function deleteFont() { setIsDeletingFont(true) try { const response = await fetch('/api/settings/invoice-font', { method: 'DELETE' }) const result = await response.json() if (!response.ok) throw new Error(result.error || t('font_delete_failed')) onUpdate(result.data as Partial) } catch (error) { toast({ title: t('font_delete_failed'), description: error instanceof Error ? getUserErrorMessage(error) : undefined, variant: 'destructive', }) } finally { setIsDeletingFont(false) } } function handleFontChange(event: ChangeEvent) { const file = event.target.files?.[0] if (file) void uploadFont(file) } const fontLabels: Record = { Helvetica: t('font_helvetica'), 'Times-Roman': t('font_times'), Courier: t('font_courier'), 'Source Sans 3': t('font_source_sans'), 'Source Serif 4': t('font_source_serif'), Custom: t('font_custom'), } const toggleRows: Array<{ field: PdfToggleField; label: string; help: string; defaultOn: boolean }> = [ { field: 'ore_rounding', label: t('ore_rounding_label'), help: t('ore_rounding_help'), defaultOn: true }, { field: 'invoice_show_ocr', label: t('show_ocr_label'), help: t('show_ocr_help'), defaultOn: true }, { field: 'invoice_show_bankgiro', label: t('show_bankgiro_label'), help: t('show_bankgiro_help'), defaultOn: true }, { field: 'invoice_show_plusgiro', label: t('show_plusgiro_label'), help: t('show_plusgiro_help'), defaultOn: true }, { field: 'invoice_show_swish', label: t('show_swish_label'), help: t('show_swish_help'), defaultOn: false }, { field: 'invoice_show_logo', label: t('show_logo_label'), help: t('show_logo_help'), defaultOn: true }, { field: 'invoice_show_company_name', label: t('show_company_name_label'), help: t('show_company_name_help'), defaultOn: true }, ] return (

{t('font_help')}

{t('font_file_help')}

} > { if (event.target.value) void saveFont(event.target.value as InvoiceFontFamily) }} disabled={isSavingFont || isUploadingFont || isDeletingFont} > {INVOICE_FONT_FAMILIES .filter((family) => family !== 'Custom' || settings.invoice_custom_font_path) .map((family) => ( ))} {settings.invoice_custom_font_name && ( {t('font_uploaded_name', { name: settings.invoice_custom_font_name })} )} {settings.invoice_custom_font_path && ( )}
{/* Show/hide switches: compact two-column grid of small switch rows. */}
{toggleRows.map(({ field, label, help, defaultOn }) => ( void saveToggle(field, v)} /> ))}
{/* Placement only applies while the company name is shown. */} void savePosition(pos)} options={[ { value: 'header', label: t('placement_header') }, { value: 'footer', label: t('placement_footer') }, ]} aria-label={t('placement_aria_label')} /> setLateFeeText(e.target.value)} onBlur={() => saveText('invoice_late_fee_text', lateFeeText)} /> setCreditTermsText(e.target.value)} onBlur={() => saveText('invoice_credit_terms_text', creditTermsText)} />
) }