diff --git a/src/components/interests/linked-berths-list.tsx b/src/components/interests/linked-berths-list.tsx
index 538a782a..2d3d2e4d 100644
--- a/src/components/interests/linked-berths-list.tsx
+++ b/src/components/interests/linked-berths-list.tsx
@@ -16,14 +16,22 @@
* - "Remove" — calls `removeInterestBerth`.
*/
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { Anchor, Loader2, Star, Trash2 } from 'lucide-react';
+import { Anchor, Loader2, Plus, Star, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command';
import {
Dialog,
DialogContent,
@@ -39,7 +47,9 @@ import { Textarea } from '@/components/ui/textarea';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { HelpCircle } from 'lucide-react';
import { apiFetch } from '@/lib/api/client';
+import { toastError } from '@/lib/api/toast-error';
import { cn } from '@/lib/utils';
+import { toast } from 'sonner';
// ─── Types (mirror the API GET shape — see interest-berths.service.ts) ─────
@@ -151,6 +161,22 @@ function useUpdateLink(interestId: string) {
});
}
+function useAddLink(interestId: string) {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (args: { berthId: string; isSpecificInterest: boolean }) =>
+ apiFetch(`/api/v1/interests/${interestId}/berths`, {
+ method: 'POST',
+ body: args,
+ }),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['interest-berths', interestId] });
+ qc.invalidateQueries({ queryKey: ['interests', interestId] });
+ qc.invalidateQueries({ queryKey: ['berth-recommendations', interestId] });
+ },
+ });
+}
+
function useRemoveLink(interestId: string) {
const qc = useQueryClient();
return useMutation({
@@ -481,6 +507,134 @@ function LinkedBerthRowItem({
);
}
+// ─── Add berth dialog ───────────────────────────────────────────────────────
+
+interface BerthOption {
+ id: string;
+ mooringNumber: string;
+ area: string | null;
+ status: string;
+}
+
+interface AddBerthDialogProps {
+ open: boolean;
+ onOpenChange: (next: boolean) => void;
+ interestId: string;
+ /** IDs already linked — filtered out of the picker so the rep can't double-add. */
+ excludeIds: Set;
+}
+
+function AddBerthDialog({ open, onOpenChange, interestId, excludeIds }: AddBerthDialogProps) {
+ const [selected, setSelected] = useState(null);
+ const [markSpecific, setMarkSpecific] = useState(false);
+ const mutation = useAddLink(interestId);
+
+ const { data: options, isLoading } = useQuery<{ data: BerthOption[] }>({
+ queryKey: ['berth-options', 'add-to-interest'],
+ queryFn: () => apiFetch<{ data: BerthOption[] }>('/api/v1/berths/options'),
+ enabled: open,
+ staleTime: 60_000,
+ });
+
+ // Filter out berths already linked to the interest so the rep can't
+ // double-add. Pre-natural-sorted by the service.
+ const available = useMemo(() => {
+ const list = options?.data ?? [];
+ return list.filter((b) => !excludeIds.has(b.id));
+ }, [options, excludeIds]);
+
+ async function handleAdd() {
+ if (!selected) return;
+ try {
+ await mutation.mutateAsync({
+ berthId: selected.id,
+ isSpecificInterest: markSpecific,
+ });
+ toast.success(`Linked berth ${selected.mooringNumber}`);
+ onOpenChange(false);
+ } catch (err) {
+ toastError(err);
+ }
+ }
+
+ return (
+
+ );
+}
+
// ─── Component ──────────────────────────────────────────────────────────────
export function LinkedBerthsList({ interestId }: LinkedBerthsListProps) {
@@ -493,6 +647,8 @@ export function LinkedBerthsList({ interestId }: LinkedBerthsListProps) {
const rows = data?.data ?? [];
const eoiStatus = data?.meta.eoiStatus ?? null;
const isPending = updateMutation.isPending || removeMutation.isPending;
+ const [addOpen, setAddOpen] = useState(false);
+ const linkedBerthIds = useMemo(() => new Set(rows.map((r) => r.berthId)), [rows]);
// Three-bucket split per the Deal-berth + Bundle model:
// • dealBerth: the single is_primary row — the one templates/EOI
@@ -520,11 +676,21 @@ export function LinkedBerthsList({ interestId }: LinkedBerthsListProps) {
return (
-
+
Linked berths{rows.length > 0 ? ` (${rows.length})` : ''}
+
{isLoading ? (
@@ -580,6 +746,14 @@ export function LinkedBerthsList({ interestId }: LinkedBerthsListProps) {
) : null}
+ {addOpen ? (
+
+ ) : null}
);
}