-
-
+
+
Permission overrides save on the button below, separately from the
- Profile & role tab. Switching tabs or closing the drawer without clicking
- Save overrides drops your changes.
-
-
+ Profile & role tab. Switching tabs or closing the drawer without clicking{' '}
+
Save overrides drops your changes.
+
+
Each toggle defaults to Inherit (role + port override decide). Switch to
Grant or Deny to force the value for this user only.
diff --git a/src/components/admin/vocabularies/vocabularies-manager.tsx b/src/components/admin/vocabularies/vocabularies-manager.tsx
index 08435d71..7d1b4a78 100644
--- a/src/components/admin/vocabularies/vocabularies-manager.tsx
+++ b/src/components/admin/vocabularies/vocabularies-manager.tsx
@@ -84,6 +84,8 @@ export function VocabulariesManager() {
}, []);
useEffect(() => {
+ // Initial vocabularies load on mount.
+ // eslint-disable-next-line react-hooks/set-state-in-effect
void fetchAll();
}, [fetchAll]);
diff --git a/src/components/berths/berth-detail.tsx b/src/components/berths/berth-detail.tsx
index 6f691332..8dc5e089 100644
--- a/src/components/berths/berth-detail.tsx
+++ b/src/components/berths/berth-detail.tsx
@@ -44,16 +44,17 @@ export function BerthDetail({ berthId }: BerthDetailProps) {
useEffect(() => {
if (searchParams.get('edit') === 'true') {
+ // setState in effect is the right shape here — the URL is an
+ // external store and the trigger is a query-param change, not a
+ // prop in the React tree.
+ // eslint-disable-next-line react-hooks/set-state-in-effect
setEditOpen(true);
// Strip the param without adding a history entry
const params = new URLSearchParams(searchParams.toString());
params.delete('edit');
const newUrl = params.toString() ? `?${params.toString()}` : window.location.pathname;
- // typedRoutes can't statically validate this dynamic path; cast is safe
- // because we're always replacing within the same route segment.
router.replace(newUrl as never);
}
- // Only run once on mount / when searchParams changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
diff --git a/src/components/berths/berth-tabs.tsx b/src/components/berths/berth-tabs.tsx
index e7d1f04a..51dceedd 100644
--- a/src/components/berths/berth-tabs.tsx
+++ b/src/components/berths/berth-tabs.tsx
@@ -222,11 +222,13 @@ function OverviewTab({ berth }: { berth: BerthData }) {
const patch = useBerthPatch(berth.id);
// User-selected display unit for dimensions. Persisted in localStorage
// so reps' preferred unit sticks across navigations + sessions.
- const [units, setUnits] = useState<'ft' | 'm'>('ft');
- useEffect(() => {
- const stored = localStorage.getItem('berth-overview-units');
- if (stored === 'ft' || stored === 'm') setUnits(stored);
- }, []);
+ // Lazy initializer reads localStorage on first render — avoids the
+ // mount-effect-setState shape the compiler flags.
+ const [units, setUnits] = useState<'ft' | 'm'>(() => {
+ if (typeof window === 'undefined') return 'ft';
+ const stored = window.localStorage.getItem('berth-overview-units');
+ return stored === 'ft' || stored === 'm' ? stored : 'ft';
+ });
useEffect(() => {
localStorage.setItem('berth-overview-units', units);
}, [units]);
diff --git a/src/components/clients/bulk-archive-wizard.tsx b/src/components/clients/bulk-archive-wizard.tsx
index 4384ac9d..37241b37 100644
--- a/src/components/clients/bulk-archive-wizard.tsx
+++ b/src/components/clients/bulk-archive-wizard.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AlertTriangle, ArrowLeft, ArrowRight, CheckCircle2, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
@@ -16,6 +16,7 @@ import {
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
+import { WarningCallout } from '@/components/ui/warning-callout';
import { apiFetch } from '@/lib/api/client';
interface PreflightItem {
@@ -36,7 +37,19 @@ interface Props {
type Stage = 'preflight' | 'reasons' | 'confirm';
-export function BulkArchiveWizard({ open, onOpenChange, clientIds, onSuccess }: Props) {
+export function BulkArchiveWizard(props: Props) {
+ // Key-based remount: body keyed on open + clientIds so its useState
+ // initializers re-run each time the wizard opens fresh. Replaces the
+ // useEffect(setState, [open]) reset the Compiler flagged.
+ return (
+
+ );
+}
+
+function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Props) {
const qc = useQueryClient();
const [stage, setStage] = useState('preflight');
const [reasons, setReasons] = useState>({});
@@ -52,14 +65,6 @@ export function BulkArchiveWizard({ open, onOpenChange, clientIds, onSuccess }:
enabled: open && clientIds.length > 0,
});
- useEffect(() => {
- if (open) {
- setStage('preflight');
- setReasons({});
- setCarouselIndex(0);
- }
- }, [open]);
-
const items = preflight.data ?? [];
const blocked = useMemo(() => items.filter((i) => i.blockers.length > 0), [items]);
const highStakes = useMemo(
@@ -192,15 +197,17 @@ export function BulkArchiveWizard({ open, onOpenChange, clientIds, onSuccess }:
))}
-
-
-
-
{currentHighStakes.fullName}
-
- {currentHighStakes.highStakesStage}
-
-
-
+
+ {currentHighStakes.fullName}
+
+ {currentHighStakes.highStakesStage}
+
+
+ }
+ >
+
{currentHighStakes.summary.berths > 0
? `${currentHighStakes.summary.berths} berth(s), `
: ''}
@@ -210,8 +217,8 @@ export function BulkArchiveWizard({ open, onOpenChange, clientIds, onSuccess }:
{currentHighStakes.summary.reservations > 0
? `${currentHighStakes.summary.reservations} reservation(s)`
: ''}
-
-
+
+