'use client' import dynamic from 'next/dynamic' import { useEffect, useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' import { Skeleton } from '@/components/ui/skeleton' import { Button } from '@/components/ui/button' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' import { fetchAllRows } from '@/lib/supabase/fetch-all' import { buildInvoiceCopyInitial, canCopyInvoice, type InvoiceCopyInitial, type InvoiceCopySource, } from '@/lib/invoices/copy-invoice' // Deferred: the editor (and its framer-motion dependency) is a large chunk // that would otherwise ship with the invoice LIST bundle: it is only needed // once this dialog actually opens. const InvoiceEditor = dynamic(() => import('@/components/invoices/InvoiceEditor'), { ssr: false, loading: () => (
), }) interface Props { open: boolean onOpenChange: (open: boolean) => void copyFromId?: string | null /** Preselect the självfaktura tab (split-button entry on /invoices). */ selfBilled?: boolean } /** * "Ny faktura" as a modal: mirrors NewJournalEntryDialog. Wraps the bare * InvoiceEditor; the editor's own review/confirm/send dialogs stack on top of * this one, and every successful create navigates to the invoice detail page * (unmounting the host list page and this dialog with it). * * The accessible title is visually hidden: the bare editor renders its own * live heading, which tracks document type (faktura/proforma/följesedel) and * shows the invoice-number preview: a static DialogTitle would duplicate or * contradict it. */ export default function NewInvoiceDialog({ open, onOpenChange, copyFromId = null, selfBilled = false }: Props) { const t = useTranslations('invoice_editor') const { company } = useCompany() const supabase = useMemo(() => createClient(), []) const [copyLoad, setCopyLoad] = useState<{ sourceId: string | null initial: InvoiceCopyInitial | null failed: boolean }>({ sourceId: null, initial: null, failed: false }) useEffect(() => { if (!open || !copyFromId) return if (!company?.id) return let cancelled = false const loadCopySource = async () => { const { data, error } = await supabase .from('invoices') .select('*') .eq('id', copyFromId) .eq('company_id', company.id) .single() let items: Record[] = [] if (!error && data) { try { // invoice_items has NO company_id column: it is scoped through its // parent invoice. Filtering on it made PostgREST answer 42703, which // fetchAllRows rethrows, so the copy flow hit the "kunde inte // laddas" panel for EVERY company. Tenancy is unaffected: the // invoices lookup above already pins copyFromId to this company, and // the invoice_items RLS policy joins back to the parent invoice. items = await fetchAllRows>(({ from, to }) => supabase .from('invoice_items') .select('*') .eq('invoice_id', copyFromId) .order('id', { ascending: true }) .range(from, to), ) } catch { if (!cancelled) setCopyLoad({ sourceId: copyFromId, initial: null, failed: true }) return } } if (cancelled) return const source = data ? { ...data, items } : null if (error || !source || !canCopyInvoice(source)) { setCopyLoad({ sourceId: copyFromId, initial: null, failed: true }) return } setCopyLoad({ sourceId: copyFromId, initial: buildInvoiceCopyInitial(source as InvoiceCopySource), failed: false, }) } void loadCopySource() return () => { cancelled = true } }, [company?.id, copyFromId, open, supabase]) const copyInitial = copyLoad.sourceId === copyFromId ? copyLoad.initial : null const copyLoadFailed = copyLoad.sourceId === copyFromId && copyLoad.failed return ( e.preventDefault()} onPointerDownOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} > {copyFromId ? t('title_copy') : t('title_invoice')} {copyFromId ? ( copyLoadFailed ? (

{t('copy_load_failed_title')}

{t('copy_load_failed_description')}

) : copyInitial ? ( ) : (
) ) : ( )}
) }