feat(platform): residential module + admin UI + reliability fixes
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:
@@ -30,6 +30,7 @@ interface UserFormProps {
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
role: { id: string; name: string };
|
||||
residentialAccess?: boolean;
|
||||
} | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
@@ -43,6 +44,7 @@ export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps)
|
||||
const [phone, setPhone] = useState('');
|
||||
const [roleId, setRoleId] = useState('');
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [residentialAccess, setResidentialAccess] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -63,6 +65,7 @@ export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps)
|
||||
setPhone(user.phone ?? '');
|
||||
setRoleId(user.role.id);
|
||||
setIsActive(user.isActive);
|
||||
setResidentialAccess(user.residentialAccess ?? false);
|
||||
setPassword('');
|
||||
} else {
|
||||
setName('');
|
||||
@@ -71,6 +74,7 @@ export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps)
|
||||
setPhone('');
|
||||
setRoleId('');
|
||||
setIsActive(true);
|
||||
setResidentialAccess(false);
|
||||
setPassword('');
|
||||
}
|
||||
setError(null);
|
||||
@@ -91,6 +95,7 @@ export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps)
|
||||
phone: phone || null,
|
||||
roleId,
|
||||
isActive,
|
||||
residentialAccess,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -103,6 +108,7 @@ export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps)
|
||||
displayName,
|
||||
phone: phone || undefined,
|
||||
roleId,
|
||||
residentialAccess,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -190,6 +196,21 @@ export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps)
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div>
|
||||
<Label htmlFor="user-residential">Residential access</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Grant this user access to residential clients and interests in addition to their
|
||||
primary role.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="user-residential"
|
||||
checked={residentialAccess}
|
||||
onCheckedChange={setResidentialAccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isEdit && (
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user