'use client' import { useEffect, useRef, useState } from 'react' import { Eye } from 'lucide-react' import { useLocale, useTranslations } from 'next-intl' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' 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 } export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) { const t = useTranslations('settings_invoicing_preview') const locale = useLocale() as ErrorLocale const { company } = useCompany() const [open, setOpen] = useState(false) const [blobUrl, setBlobUrl] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const currentUrlRef = useRef(null) const sampleItemDescription = t('sample_item_description') useEffect(() => { if (!open || !company?.id) return const companyId = company.id let cancelled = false const controller = new AbortController() // Debounce so rapid settings toggles (PDF print options on the invoicing // page) don't burst-fire requests at /api/invoices/preview-pdf while the // dialog is open. AbortController still cancels any in-flight fetch. const timer = setTimeout(() => { run() }, 500) async function run() { setIsLoading(true) setError(null) try { const supabase = createClient() const { data: customer, error: customerError } = await supabase .from('customers') .select('id') .eq('company_id', companyId) .is('archived_at', null) .limit(1) .maybeSingle() if (customerError) throw customerError if (cancelled) return // Non-momsregistrerade säljare ska inte få en exempel-rad med 25 % // VAT: förhandsvisningen är hårdkodad sample-data, inte ett val // användaren gjort, så vi följer settings.vat_registered direkt här // (till skillnad från /invoices/new som låter användaren välja och // bara varnar vid submit). const previewVatRate = settings.vat_registered === false ? 0 : 25 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: previewVatRate, }, ], }), }) 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) 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) } } return () => { cancelled = true clearTimeout(timer) controller.abort() } }, [open, settings, company?.id, sampleItemDescription, locale]) useEffect(() => { return () => { if (currentUrlRef.current) { URL.revokeObjectURL(currentUrlRef.current) currentUrlRef.current = null } } }, []) return ( {/* Quiet header-action trigger (Fönster): the preview lives in the section header's action slot, not as a card in the page flow. */} {t('title')} {isLoading && (

{t('loading')}

)} {!isLoading && error && (

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

)} {!isLoading && !error && blobUrl && ( // + type="application/pdf" invokes Chrome's PDF plugin // directly.