feat(platform): residential module + admin UI + reliability fixes
All checks were successful
Build & Push Docker Images / lint (pull_request) Successful in 1m2s
Build & Push Docker Images / build-and-push (pull_request) Has been skipped

Residential platform
- New schema: residentialClients, residentialInterests (separate from
  marina/yacht clients) with migration 0010
- Service layer with CRUD + audit + sockets + per-port portal toggle
- v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries)
- List + detail pages with inline editing for clients and interests
- Per-user residentialAccess toggle on userPortRoles (migration 0011)
- Permission keys: residential_clients, residential_interests
- Sidebar nav + role form integration
- Smoke spec covering page loads, UI create flow, public endpoint

Admin & shared UI
- Admin → Forms (form templates CRUD) with validators + service
- Notification preferences page (in-app + email per type)
- Email composition + accounts list + threads view
- Branded auth shell shared across CRM + portal auth surfaces
- Inline editing extended to yacht/company/interest detail pages
- InlineTagEditor + per-entity tags endpoints (yachts, companies)
- Notes service polymorphic across clients/interests/yachts/companies
- Client list columns: yachtCount + companyCount badges
- Reservation file-download via presigned URL (replaces stale <a href>)

Route handler refactor
- Extracted yachts/companies/berths reservation handlers to sibling
  handlers.ts files (Next.js 15 route.ts only allows specific exports)

Reliability fixes
- apiFetch double-stringify bug fixed across 13 components
  (apiFetch already JSON.stringifies its body; passing a stringified
  body produced double-encoded JSON which failed zod validation)
- SocketProvider gated behind useSyncExternalStore-based mount check
  to avoid useSession() SSR crashes under React 19 + Next 15
- apiFetch falls back to URL-pathname → port-id resolution when the
  Zustand store hasn't hydrated yet (fresh contexts, e2e tests)
- CRM invite flow (schema, service, route, email, dev script)
- Dashboard route → [portSlug]/dashboard/page.tsx + redirect
- Document the dev-server restart-after-migration gotcha in CLAUDE.md

Tests
- 5-case residential smoke spec
- Integration test updates for new service signatures

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-27 21:54:32 +02:00
parent fac8021156
commit e8d61c91c4
121 changed files with 34105 additions and 1016 deletions

View File

@@ -41,8 +41,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
});
const sendMutation = useMutation({
mutationFn: () =>
apiFetch(`/api/v1/invoices/${invoiceId}/send`, { method: 'POST' }),
mutationFn: () => apiFetch(`/api/v1/invoices/${invoiceId}/send`, { method: 'POST' }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invoices', invoiceId] });
queryClient.invalidateQueries({ queryKey: ['invoices'] });
@@ -58,7 +57,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
mutationFn: (values: RecordPaymentInput) =>
apiFetch(`/api/v1/invoices/${invoiceId}/payment`, {
method: 'PATCH',
body: JSON.stringify(values),
body: values,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invoices', invoiceId] });
@@ -76,9 +75,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
if (error || !data?.data) {
return (
<div className="p-6 text-center text-muted-foreground">
Failed to load invoice details.
</div>
<div className="p-6 text-center text-muted-foreground">Failed to load invoice details.</div>
);
}
@@ -230,9 +227,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
<CardTitle className="text-sm font-medium">Notes</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
{invoice.notes}
</p>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{invoice.notes}</p>
</CardContent>
</Card>
)}
@@ -249,9 +244,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
className="flex items-center justify-between p-3 border rounded-md text-sm"
>
<div>
<p className="font-medium">
{exp.establishmentName ?? 'Unnamed Expense'}
</p>
<p className="font-medium">{exp.establishmentName ?? 'Unnamed Expense'}</p>
<p className="text-muted-foreground text-xs">
{exp.category ?? '—'} &middot; {exp.expenseDate}
</p>
@@ -271,10 +264,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
{/* PDF Preview */}
<TabsContent value="pdf" className="pt-4">
<InvoicePdfPreview
invoiceId={invoiceId}
pdfFileId={invoice.pdfFileId}
/>
<InvoicePdfPreview invoiceId={invoiceId} pdfFileId={invoice.pdfFileId} />
</TabsContent>
{/* Payment */}
@@ -283,10 +273,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
<Card>
<CardContent className="pt-6 space-y-3 text-sm">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className="bg-green-100 text-green-700 border-green-200"
>
<Badge variant="outline" className="bg-green-100 text-green-700 border-green-200">
Paid
</Badge>
</div>
@@ -297,9 +284,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
</div>
<div>
<span className="text-muted-foreground">Method</span>
<p className="mt-0.5 capitalize">
{invoice.paymentMethod ?? '—'}
</p>
<p className="mt-0.5 capitalize">{invoice.paymentMethod ?? '—'}</p>
</div>
<div>
<span className="text-muted-foreground">Reference</span>
@@ -315,18 +300,12 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
</CardHeader>
<CardContent>
<form
onSubmit={paymentForm.handleSubmit((values) =>
paymentMutation.mutate(values),
)}
onSubmit={paymentForm.handleSubmit((values) => paymentMutation.mutate(values))}
className="space-y-4"
>
<div className="space-y-1">
<Label htmlFor="paymentDate">Payment Date</Label>
<Input
id="paymentDate"
type="date"
{...paymentForm.register('paymentDate')}
/>
<Input id="paymentDate" type="date" {...paymentForm.register('paymentDate')} />
{paymentForm.formState.errors.paymentDate && (
<p className="text-xs text-destructive">
{paymentForm.formState.errors.paymentDate.message}
@@ -349,10 +328,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
{...paymentForm.register('paymentReference')}
/>
</div>
<Button
type="submit"
disabled={paymentMutation.isPending}
>
<Button type="submit" disabled={paymentMutation.isPending}>
{paymentMutation.isPending ? (
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
) : (