From a49ee1c347bb2a4f764957944a4ada8460546e54 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 May 2026 11:54:13 +0200 Subject: [PATCH] fix(audit-wave-9): adopt StatusPill for berth + user status badges - Extend StatusPill with berth (available/under_offer/sold) and user (enabled/disabled) variants so every "this thing is in state X" pill shares one primitive and palette. - Swap berth-card, berth-detail-header, berth-columns from ad-hoc bg-green-100 / bg-yellow-100 / bg-red-100 Tailwind tuples to . - Swap UserList Active/Disabled and user-card Inactive pill to StatusPill; Super-Admin chip kept as a domain-specific accent (violet). Closes ui/ux M1+M2. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/admin/users/user-card.tsx | 9 ++--- src/components/admin/users/user-list.tsx | 13 ++++---- src/components/berths/berth-card.tsx | 26 +++++---------- src/components/berths/berth-columns.tsx | 33 +++++++++---------- src/components/berths/berth-detail-header.tsx | 20 ++++++----- .../clients/client-interests-tab.tsx | 29 ++++++++-------- src/components/ui/status-pill.tsx | 7 ++++ 7 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/components/admin/users/user-card.tsx b/src/components/admin/users/user-card.tsx index 74f82bc4..4f78b875 100644 --- a/src/components/admin/users/user-card.tsx +++ b/src/components/admin/users/user-card.tsx @@ -12,6 +12,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ConfirmationDialog } from '@/components/shared/confirmation-dialog'; +import { StatusPill } from '@/components/ui/status-pill'; import { ListCard, ListCardAvatar, @@ -169,13 +170,9 @@ export function UserCard({ {/* Status + super-admin pills */}
- {!user.isActive ? ( - - Inactive - - ) : null} + {!user.isActive ? Disabled : null} {user.isSuperAdmin ? ( - + Super Admin ) : null} diff --git a/src/components/admin/users/user-list.tsx b/src/components/admin/users/user-list.tsx index f1332f79..19bf19c3 100644 --- a/src/components/admin/users/user-list.tsx +++ b/src/components/admin/users/user-list.tsx @@ -11,6 +11,7 @@ import { ConfirmationDialog } from '@/components/shared/confirmation-dialog'; import { PermissionGate } from '@/components/shared/permission-gate'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { StatusPill } from '@/components/ui/status-pill'; import { apiFetch } from '@/lib/api/client'; import { formatRole } from '@/lib/constants'; import { UserCard } from './user-card'; @@ -98,15 +99,15 @@ export function UserList() { header: 'Status', cell: ({ row }) => row.original.isActive ? ( - - + + Active - + ) : ( - - + + Disabled - + ), }, { diff --git a/src/components/berths/berth-card.tsx b/src/components/berths/berth-card.tsx index 61d5b935..e8fba621 100644 --- a/src/components/berths/berth-card.tsx +++ b/src/components/berths/berth-card.tsx @@ -12,23 +12,23 @@ import { } from '@/components/ui/dropdown-menu'; import { TagBadge } from '@/components/shared/tag-badge'; import { ListCard, ListCardAvatar, ListCardMeta } from '@/components/shared/list-card'; -import { cn } from '@/lib/utils'; +import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { formatCurrency } from '@/lib/utils/currency'; import type { BerthRow } from './berth-columns'; import { mooringLetterDot } from './mooring-letter-tone'; -const STATUS_VARIANTS: Record = { - available: 'bg-green-100 text-green-800 border-green-200', - under_offer: 'bg-yellow-100 text-yellow-800 border-yellow-200', - sold: 'bg-red-100 text-red-800 border-red-200', -}; - const STATUS_LABELS: Record = { available: 'Available', under_offer: 'Under Offer', sold: 'Sold', }; +const BERTH_STATUS_PILL: Record = { + available: 'available', + under_offer: 'under_offer', + sold: 'sold', +}; + interface BerthCardProps { berth: BerthRow; } @@ -39,8 +39,7 @@ export function BerthCard({ berth }: BerthCardProps) { const portSlug = params?.portSlug ?? ''; const statusLabel = STATUS_LABELS[berth.status] ?? berth.status; - const statusColor = - STATUS_VARIANTS[berth.status] ?? 'bg-muted text-muted-foreground border-muted'; + const statusPill = BERTH_STATUS_PILL[berth.status] ?? 'pending'; // Accent stripe groups visually by dock (A-row, B-row, ...). Status is // already conveyed by the pill below, so the stripe is dock-keyed. const accentClass = mooringLetterDot(berth.mooringNumber) ?? 'bg-slate-300'; @@ -168,14 +167,7 @@ export function BerthCard({ berth }: BerthCardProps) { {/* Status pill + tags */}
- - {statusLabel} - + {statusLabel} {tags.slice(0, 2).map((tag) => ( ))} diff --git a/src/components/berths/berth-columns.tsx b/src/components/berths/berth-columns.tsx index 9df3bcda..55d4ce35 100644 --- a/src/components/berths/berth-columns.tsx +++ b/src/components/berths/berth-columns.tsx @@ -12,6 +12,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { TagBadge } from '@/components/shared/tag-badge'; +import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { formatCurrency } from '@/lib/utils/currency'; import { mooringLetterDot } from './mooring-letter-tone'; import { stageBadgeClass, stageLabel } from '@/lib/constants'; @@ -112,25 +113,23 @@ export const BERTH_DEFAULT_HIDDEN: string[] = [ 'pricingValidUntil', ]; +const BERTH_STATUS_PILL: Record = { + available: 'available', + under_offer: 'under_offer', + sold: 'sold', +}; + +const BERTH_STATUS_LABELS: Record = { + available: 'Available', + under_offer: 'Under Offer', + sold: 'Sold', +}; + function StatusBadge({ status }: { status: string }) { - const variants: Record = { - available: 'bg-green-100 text-green-800 border-green-200', - under_offer: 'bg-yellow-100 text-yellow-800 border-yellow-200', - sold: 'bg-red-100 text-red-800 border-red-200', - }; - - const labels: Record = { - available: 'Available', - under_offer: 'Under Offer', - sold: 'Sold', - }; - return ( - - {labels[status] ?? status} - + + {BERTH_STATUS_LABELS[status] ?? status} + ); } diff --git a/src/components/berths/berth-detail-header.tsx b/src/components/berths/berth-detail-header.tsx index 743407d3..7b5368a0 100644 --- a/src/components/berths/berth-detail-header.tsx +++ b/src/components/berths/berth-detail-header.tsx @@ -26,6 +26,7 @@ import { import { Textarea } from '@/components/ui/textarea'; import { DetailHeaderStrip } from '@/components/shared/detail-header-strip'; import { PermissionGate } from '@/components/shared/permission-gate'; +import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { BerthForm } from './berth-form'; import { mooringLetterDot } from './mooring-letter-tone'; import { cn } from '@/lib/utils'; @@ -86,18 +87,18 @@ interface BerthDetailHeaderProps { berth: BerthDetailData; } -const STATUS_COLORS: Record = { - available: 'bg-green-100 text-green-800 border-green-300', - under_offer: 'bg-yellow-100 text-yellow-800 border-yellow-300', - sold: 'bg-red-100 text-red-800 border-red-300', -}; - const STATUS_LABELS: Record = { available: 'Available', under_offer: 'Under Offer', sold: 'Sold', }; +const BERTH_STATUS_PILL: Record = { + available: 'available', + under_offer: 'under_offer', + sold: 'sold', +}; + interface InterestOption { id: string; clientName: string; @@ -276,11 +277,12 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) { > {berth.mooringNumber}
- {STATUS_LABELS[berth.status] ?? berth.status} - +
diff --git a/src/components/clients/client-interests-tab.tsx b/src/components/clients/client-interests-tab.tsx index 2a681833..f376f25e 100644 --- a/src/components/clients/client-interests-tab.tsx +++ b/src/components/clients/client-interests-tab.tsx @@ -97,7 +97,7 @@ function lastActivityFor(interest: ClientInterestRow): string | null { } /** Full interest record returned by `/api/v1/interests/[id]`. Only the fields - * the drawer actually reads are typed here; the API returns more. */ + * the preview sheet actually reads are typed here; the API returns more. */ interface InterestDetail { id: string; pipelineStage: string; @@ -114,7 +114,7 @@ interface InterestDetail { function useInterestDetail(id: string | null) { return useQuery<{ data: InterestDetail }>({ - queryKey: ['interest-detail-drawer', id], + queryKey: ['interest-detail-preview', id], queryFn: () => apiFetch<{ data: InterestDetail }>(`/api/v1/interests/${id}`), enabled: id !== null, // Detail rarely changes during a single drawer-open session; stale-time @@ -132,7 +132,7 @@ function formatDate(value: string | null | undefined): string | null { return format(d, 'MMM d, yyyy'); } -/** A single milestone row inside the drawer's milestone summary. Filled +/** A single milestone row inside the preview sheet's milestone summary. Filled * circle when the step is done, hollow when pending. Trailing meta line * shows the date stamp or a "pending" hint. */ function MilestoneRow({ @@ -162,14 +162,13 @@ function MilestoneRow({ } /** - * Bottom-sheet preview of a single interest. Designed for the mobile - * "tap an interest → see what's happening without leaving the client - * page" flow. Shows the pipeline progress, a compact milestone summary - * (EOI / Deposit / Contract), lead context, last contact, and a notes - * teaser. Tap-out / drag-down dismisses; the full edit page is one tap - * away via "Open full page →". + * Right-side sheet preview of a single interest. "Tap an interest → see + * what's happening without leaving the client page". Shows the pipeline + * progress, a compact milestone summary (EOI / Deposit / Contract), + * lead context, last contact, and a notes teaser. Tap-out / Esc + * dismisses; the full edit page is one tap away via "Open full page →". */ -function InterestPreviewDrawer({ +function InterestPreviewSheet({ interest, portSlug, onClose, @@ -178,10 +177,10 @@ function InterestPreviewDrawer({ portSlug: string; onClose: () => void; }) { - // Pin the most recently selected interest so the drawer stays populated - // during the close-animation tail (Vaul keeps the content mounted ~250ms - // after `open=false`). Conditional setState is safe here - the guard - // ensures it only fires when the prop actually changes to a new row. + // Pin the most recently selected interest so the sheet stays populated + // during the close-animation tail (Radix keeps the content mounted + // through the slide-out). Conditional setState is safe here - the + // guard ensures it only fires when the prop actually changes. const [pinned, setPinned] = useState(interest); if (interest && interest !== pinned) setPinned(interest); const showing = pinned; @@ -448,7 +447,7 @@ export function ClientInterestsTab({ clientId }: ClientInterestsTabProps) {
) : null} - setPreviewInterest(null)} diff --git a/src/components/ui/status-pill.tsx b/src/components/ui/status-pill.tsx index 5b1d065b..73fd5741 100644 --- a/src/components/ui/status-pill.tsx +++ b/src/components/ui/status-pill.tsx @@ -29,6 +29,13 @@ const statusPillVariants = cva( // Delivered (non-signature docs in hub) delivered: 'border-purple-light bg-purple-light/40 text-purple-dark', draft: 'border-slate-200 bg-white text-slate-600', + // Berth lifecycle + available: 'border-success-border bg-success-bg text-success', + under_offer: 'border-warning-border bg-warning-bg text-warning', + sold: 'border-error-border bg-error-bg text-error', + // User/account lifecycle + enabled: 'border-success-border bg-success-bg text-success', + disabled: 'border-slate-200 bg-slate-100 text-slate-500', }, }, defaultVariants: {