'use client'; import { useEffect, useState } from 'react'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useForm, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery } from '@tanstack/react-query'; import { ChevronLeft, ChevronRight, Check, Loader2, Wallet } from 'lucide-react'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { OwnerPicker } from '@/components/shared/owner-picker'; import { InvoiceLineItems } from '@/components/invoices/invoice-line-items'; import { apiFetch } from '@/lib/api/client'; import { createInvoiceSchema, type CreateInvoiceInput } from '@/lib/validators/invoices'; const PAYMENT_TERMS = [ { label: 'Immediate', value: 'immediate' }, { label: 'Net 10', value: 'net10' }, { label: 'Net 15', value: 'net15' }, { label: 'Net 30', value: 'net30' }, { label: 'Net 45', value: 'net45' }, { label: 'Net 60', value: 'net60' }, ]; const STEPS = [ { id: 1, label: 'Client Info' }, { id: 2, label: 'Line Items' }, { id: 3, label: 'Review' }, ]; export default function NewInvoicePage() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const router = useRouter(); const searchParams = useSearchParams(); const prefilledInterestId = searchParams.get('interestId') ?? undefined; const prefilledKind = searchParams.get('kind') === 'deposit' ? ('deposit' as const) : ('general' as const); const [step, setStep] = useState(1); const { setChrome } = useMobileChrome(); useEffect(() => { setChrome({ title: 'New Invoice', showBackButton: true }); return () => setChrome({ title: null, showBackButton: false }); }, [setChrome]); // When the form is launched from an interest detail with `?interestId=…&kind=deposit`, // fetch enough of the interest to display "Deposit for {client} — Berth {n}" in // the review step. Doubles as the source of truth for the billing entity prefill. const { data: prefilledInterest } = useQuery<{ data: { id: string; clientId: string; clientName: string | null; berthMooringNumber: string | null; }; }>({ queryKey: ['interest-prefill', prefilledInterestId], queryFn: () => apiFetch(`/api/v1/interests/${prefilledInterestId}`), enabled: !!prefilledInterestId, }); const methods = useForm({ resolver: zodResolver(createInvoiceSchema), defaultValues: { paymentTerms: 'net30', currency: 'USD', lineItems: [], expenseIds: [], interestId: prefilledInterestId, kind: prefilledKind, }, }); const { register, handleSubmit, watch, setValue, formState: { errors }, } = methods; const watchedValues = watch(); const isDepositInvoice = watchedValues.kind === 'deposit'; // Resolve the selected billing entity to a human name so the review step // shows "Acme Yacht Charters" instead of "company 4f2a1b…". const billingEntityRef = watchedValues.billingEntity ?? null; const { data: billingEntityName } = useQuery<{ name: string }>({ queryKey: ['billing-entity-name', billingEntityRef?.type, billingEntityRef?.id], queryFn: async () => { if (!billingEntityRef) return { name: '' }; const path = billingEntityRef.type === 'company' ? `/api/v1/companies/${billingEntityRef.id}` : `/api/v1/clients/${billingEntityRef.id}`; const res = await apiFetch<{ data: { fullName?: string; name?: string }; }>(path); return { name: res?.data?.fullName ?? res?.data?.name ?? '', }; }, enabled: !!billingEntityRef?.id, staleTime: 60_000, }); // Pre-fill the billing entity from the linked interest's client on launch. useEffect(() => { if (prefilledInterest?.data && !watchedValues.billingEntity) { setValue( 'billingEntity', { type: 'client', id: prefilledInterest.data.clientId }, { shouldValidate: true }, ); } // We only want this to run when the interest data first arrives. // eslint-disable-next-line react-hooks/exhaustive-deps }, [prefilledInterest?.data?.clientId]); const lineItems = watchedValues.lineItems ?? []; const subtotal = lineItems.reduce( (sum, li) => sum + (Number(li.quantity) || 0) * (Number(li.unitPrice) || 0), 0, ); const isNet10 = watchedValues.paymentTerms === 'net10'; const discountPct = isNet10 ? 2 : 0; const discountAmount = (subtotal * discountPct) / 100; const total = subtotal - discountAmount; const createMutation = useMutation({ mutationFn: (data: CreateInvoiceInput) => apiFetch<{ data?: { id?: string } }>('/api/v1/invoices', { method: 'POST', body: data, }), onSuccess: (res) => { const id = res?.data?.id; if (id) { router.push(`/${portSlug}/invoices/${id}`); } else { router.push(`/${portSlug}/invoices`); } }, }); async function goNext() { if (step === 1) { const valid = await methods.trigger([ 'billingEntity', 'billingEmail', 'billingAddress', 'dueDate', 'paymentTerms', 'currency', ]); if (valid) setStep(2); } else if (step === 2) { setStep(3); } } function goBack() { setStep((s) => Math.max(1, s - 1)); } function onSubmit(data: CreateInvoiceInput) { createMutation.mutate(data); } return (
{/* Header — desktop only; mobile gets the title from the topbar */}

New Invoice

{/* Step indicator */}
{STEPS.map((s, idx) => (
s.id ? 'bg-primary text-primary-foreground' : step === s.id ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground' }`} > {step > s.id ? : s.id}
{s.label} {idx < STEPS.length - 1 &&
}
))}
{/* Step 1: Client Info */} {step === 1 && ( Client Information {isDepositInvoice ? (

Deposit invoice

{prefilledInterest?.data ? `Linked to ${prefilledInterest.data.clientName ?? 'interest'}${ prefilledInterest.data.berthMooringNumber ? ` — Berth ${prefilledInterest.data.berthMooringNumber}` : '' }. Marking this invoice as paid will advance the interest to "Deposit 10%".` : 'Marking this invoice as paid will advance the linked interest to "Deposit 10%".'}

) : null}
{ if (ref) { setValue('billingEntity', ref, { shouldValidate: true }); } }} /> {errors.billingEntity && (

{errors.billingEntity.message ?? errors.billingEntity.id?.message ?? errors.billingEntity.type?.message}

)}

Select the client or company to invoice. Their name will be snapshotted into the invoice.

{errors.billingEmail && (

{errors.billingEmail.message}

)}