'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 (
)
}