diff --git a/.gitignore b/.gitignore index 69158e0..49f2dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,8 @@ tsconfig.tsbuildinfo docker-compose.override.yml .remember/ .DS_Store -eoi/ +# Root-only ad-hoc EOI scratch dir; routes under src/app/.../eoi/ must NOT match. +/eoi/ # Brainstorming companion mockup files .superpowers/ diff --git a/src/app/(dashboard)/[portSlug]/documents/eoi/page.tsx b/src/app/(dashboard)/[portSlug]/documents/eoi/page.tsx new file mode 100644 index 0000000..c49e825 --- /dev/null +++ b/src/app/(dashboard)/[portSlug]/documents/eoi/page.tsx @@ -0,0 +1,10 @@ +import { DocumentsHub } from '@/components/documents/documents-hub'; + +interface PageProps { + params: Promise<{ portSlug: string }>; +} + +export default async function EoiQueuePage({ params }: PageProps) { + const { portSlug } = await params; + return ; +} diff --git a/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx b/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx index 62f55c3..01b4b65 100644 --- a/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx +++ b/src/app/(dashboard)/[portSlug]/invoices/new/page.tsx @@ -1,11 +1,11 @@ 'use client'; import { useEffect, useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useForm, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation } from '@tanstack/react-query'; -import { ChevronLeft, ChevronRight, Check, Loader2 } from 'lucide-react'; +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'; @@ -45,6 +45,10 @@ 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); @@ -54,6 +58,22 @@ export default function NewInvoicePage() { 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: { @@ -61,6 +81,8 @@ export default function NewInvoicePage() { currency: 'USD', lineItems: [], expenseIds: [], + interestId: prefilledInterestId, + kind: prefilledKind, }, }); @@ -73,6 +95,21 @@ export default function NewInvoicePage() { } = methods; const watchedValues = watch(); + const isDepositInvoice = watchedValues.kind === 'deposit'; + + // 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), @@ -165,6 +202,23 @@ export default function NewInvoicePage() { 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}