diff --git a/src/components/berths/berth-card.tsx b/src/components/berths/berth-card.tsx
index e1f919df..53e370f9 100644
--- a/src/components/berths/berth-card.tsx
+++ b/src/components/berths/berth-card.tsx
@@ -13,6 +13,7 @@ import {
import { TagBadge } from '@/components/shared/tag-badge';
import { ListCard, ListCardAvatar, ListCardMeta } from '@/components/shared/list-card';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
+import { BerthStatusQuickEdit } from './berth-status-quick-edit';
import { formatCurrency } from '@/lib/utils/currency';
import type { BerthRow } from './berth-columns';
import { mooringLetterDot } from './mooring-letter-tone';
@@ -167,7 +168,9 @@ 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 3826ce82..2d43008e 100644
--- a/src/components/berths/berth-columns.tsx
+++ b/src/components/berths/berth-columns.tsx
@@ -15,6 +15,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { TagBadge } from '@/components/shared/tag-badge';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
+import { BerthStatusQuickEdit } from './berth-status-quick-edit';
import { formatCurrency } from '@/lib/utils/currency';
import { InlineEditableField } from '@/components/shared/inline-editable-field';
import { apiFetch } from '@/lib/api/client';
@@ -342,7 +343,9 @@ export const berthColumns: ColumnDef
[] = [
const isManualUnreconciled = isManual && !r.latestInterestStage;
return (
-
+
+
+
{isManual ? : null}
);
diff --git a/src/components/berths/berth-status-quick-edit.tsx b/src/components/berths/berth-status-quick-edit.tsx
new file mode 100644
index 00000000..f7c96976
--- /dev/null
+++ b/src/components/berths/berth-status-quick-edit.tsx
@@ -0,0 +1,242 @@
+'use client';
+
+import { useState } from 'react';
+import type { ReactNode } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { toast } from 'sonner';
+
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Textarea } from '@/components/ui/textarea';
+import { FormErrorSummary } from '@/components/forms/form-error-summary';
+import { useFormScrollToError } from '@/hooks/use-form-scroll-to-error';
+import { PermissionGate } from '@/components/shared/permission-gate';
+import { apiFetch } from '@/lib/api/client';
+import { toastError } from '@/lib/api/toast-error';
+import { useVocabulary } from '@/hooks/use-vocabulary';
+import { updateBerthStatusSchema, type UpdateBerthStatusInput } from '@/lib/validators/berths';
+import { BERTH_STATUSES, stageLabel } from '@/lib/constants';
+import { cn } from '@/lib/utils';
+
+const STATUS_LABELS: Record = {
+ available: 'Available',
+ under_offer: 'Under Offer',
+ sold: 'Sold',
+};
+
+interface InterestOption {
+ id: string;
+ clientName: string;
+ pipelineStage: string;
+}
+
+/**
+ * Click-to-change berth status from the berths LIST. Wraps the status chip
+ * (passed as children) in a button that opens a compact change-status dialog
+ * — status dropdown + required reason (with quick-pick chips) + an optional
+ * interest link when moving to under_offer/sold. Same PATCH endpoint +
+ * validator + audit as the berth detail page. Reps without `berths.edit` see
+ * a plain, non-interactive chip via the PermissionGate fallback.
+ */
+export function BerthStatusQuickEdit({
+ berthId,
+ currentStatus,
+ children,
+ className,
+}: {
+ berthId: string;
+ currentStatus: string;
+ children: ReactNode;
+ className?: string;
+}) {
+ const [open, setOpen] = useState(false);
+ return (
+ {children}>}>
+
+ {open && (
+
+ )}
+
+ );
+}
+
+function BerthStatusQuickEditDialog({
+ berthId,
+ currentStatus,
+ open,
+ onOpenChange,
+}: {
+ berthId: string;
+ currentStatus: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
+ const queryClient = useQueryClient();
+ const reasonChips = useVocabulary('berth_status_change_reasons');
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ watch,
+ reset,
+ formState: { errors, isSubmitting },
+ } = useForm({
+ resolver: zodResolver(updateBerthStatusSchema),
+ defaultValues: { status: currentStatus as (typeof BERTH_STATUSES)[number], reason: '' },
+ });
+ const submitWithScroll = useFormScrollToError(handleSubmit, errors);
+
+ const status = watch('status');
+ const interestId = watch('interestId');
+ const showInterestPicker = status === 'under_offer' || status === 'sold';
+
+ // Active interests for the picker — only fetched once the picker is shown.
+ const interestsQuery = useQuery<{ data: InterestOption[] }>({
+ queryKey: ['interests', 'status-link-picker'],
+ queryFn: () => apiFetch('/api/v1/interests?pageSize=200&sort=updatedAt&order=desc'),
+ enabled: open && showInterestPicker,
+ staleTime: 60_000,
+ });
+ const interestOptions = interestsQuery.data?.data ?? [];
+
+ async function onSubmit(data: UpdateBerthStatusInput) {
+ try {
+ await apiFetch(`/api/v1/berths/${berthId}/status`, { method: 'PATCH', body: data });
+ queryClient.invalidateQueries({ queryKey: ['berths'] });
+ queryClient.invalidateQueries({ queryKey: ['berth', berthId] });
+ queryClient.invalidateQueries({ queryKey: ['interests'] });
+ toast.success('Status updated');
+ reset();
+ onOpenChange(false);
+ } catch (err) {
+ toastError(err);
+ }
+ }
+
+ return (
+
+ );
+}