'use client' import { useEffect, useRef, useState } from 'react' import { useLocale, useTranslations } from 'next-intl' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' import type { CompanySettings } from '@/types' interface InvoicePreviewCardProps { settings: CompanySettings } /** * Live invoice PDF preview for the invoicing settings page. * * Re-fetches the preview PDF whenever the persisted `settings` change * (debounced 500ms so rapid toggles don't hammer the endpoint). Reads * the first customer in the company as a dummy recipient — the preview * endpoint requires a real `customer_id` and `items` payload. */ export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) { const t = useTranslations('settings_invoicing_preview') const locale = useLocale() as ErrorLocale const { company } = useCompany() const [blobUrl, setBlobUrl] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [noCustomers, setNoCustomers] = useState(false) const currentUrlRef = useRef(null) // Resolve translated sample-line description once per render so the // effect dependency stays referentially stable across renders. const sampleItemDescription = t('sample_item_description') // Debounced refresh whenever `settings` (identity) changes. useEffect(() => { if (!company?.id) return let cancelled = false const controller = new AbortController() const timer = setTimeout(async () => { setIsLoading(true) setError(null) setNoCustomers(false) try { // Pick any customer for the company — preview endpoint requires one. const supabase = createClient() const { data: customer, error: customerError } = await supabase .from('customers') .select('id') .eq('company_id', company.id) .limit(1) .maybeSingle() if (customerError) throw customerError if (cancelled) return if (!customer) { setNoCustomers(true) setIsLoading(false) return } const response = await fetch('/api/invoices/preview-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ customer_id: customer.id, currency: 'SEK', document_type: 'invoice', items: [ { description: sampleItemDescription, quantity: 1, unit: 'st', unit_price: 1000, vat_rate: 25, }, ], }), }) if (!response.ok) { const body = await response.json().catch(() => null) throw new Error(body?.error || `HTTP ${response.status}`) } const blob = await response.blob() if (cancelled) return const url = URL.createObjectURL(blob) // Revoke the previous blob before swapping in the new one so we // never leak object URLs. if (currentUrlRef.current) URL.revokeObjectURL(currentUrlRef.current) currentUrlRef.current = url setBlobUrl(url) setIsLoading(false) } catch (err) { if (cancelled) return if (err instanceof Error && err.name === 'AbortError') return setError(getErrorMessage(err, { locale, context: 'invoice' })) setIsLoading(false) } }, 500) return () => { cancelled = true controller.abort() clearTimeout(timer) } }, [settings, company?.id, sampleItemDescription, locale]) // Final cleanup: revoke the in-flight blob URL when the component unmounts. useEffect(() => { return () => { if (currentUrlRef.current) { URL.revokeObjectURL(currentUrlRef.current) currentUrlRef.current = null } } }, []) return ( {t('title')} {isLoading && (

{t('loading')}

)} {!isLoading && noCustomers && (

{t('no_customers')}

)} {!isLoading && error && (

{t('error')}: {error}

)} {!isLoading && !error && !noCustomers && blobUrl && (