'use client'; import { useState } from 'react'; import { Check, ChevronsUpDown, Lock, Pencil, RefreshCw, RotateCcw } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } 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 { DetailHeaderStrip } from '@/components/shared/detail-header-strip'; 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 { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { BerthForm } from './berth-form'; import { mooringLetterDot } from './mooring-letter-tone'; import { cn } from '@/lib/utils'; 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, stageBadgeClass, stageDotClass, stageLabel } from '@/lib/constants'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command'; export type BerthDetailData = { id: string; mooringNumber: string; area: string | null; status: string; portId: string; lengthFt: string | null; lengthM: string | null; widthFt: string | null; widthM: string | null; draftFt: string | null; draftM: string | null; widthIsMinimum: boolean | null; nominalBoatSize: string | null; nominalBoatSizeM: string | null; waterDepth: string | null; waterDepthM: string | null; waterDepthIsMinimum: boolean | null; sidePontoon: string | null; cleatType: string | null; cleatCapacity: string | null; bollardType: string | null; bollardCapacity: string | null; bowFacing: string | null; price: string | null; priceCurrency: string; tenureType: string; tenureYears: number | null; tenureStartDate: string | null; tenureEndDate: string | null; powerCapacity: string | null; voltage: string | null; mooringType: string | null; access: string | null; berthApproved: boolean | null; statusLastChangedReason: string | null; statusLastModified: string | null; /** 'manual' = a human pinned this status (wins over derived signals); * 'automated' = set by the rules engine; null = pure derived. */ statusOverrideMode: string | null; /** Pipeline stage of the most recent active interest on this berth, if any. * Used to flag a manual pin that diverges from an active deal. */ latestInterestStage: string | null; tags: Array<{ id: string; name: string; color: string }>; }; interface BerthDetailHeaderProps { berth: BerthDetailData; } 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; pipelineStage: string; /** Used to sort the picker - most recently interacted with floats to the top. */ updatedAt?: string; } function StatusChangeDialog({ 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 this port - used to populate the prospect // selector when status moves to under_offer / sold. Only fetched when // the picker is actually visible to avoid an unnecessary round-trip // for available-status changes. const interestsQuery = useQuery<{ data: Array<{ id: string; clientName: string; pipelineStage: string; updatedAt?: string; }>; }>({ queryKey: ['interests', 'status-link-picker'], queryFn: () => apiFetch('/api/v1/interests?pageSize=200&sort=updatedAt&order=desc'), enabled: open && showInterestPicker, staleTime: 60_000, }); const interestOptions: InterestOption[] = 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: unknown) { toastError(err); } } return ( Change Status
{reasonChips.length > 0 && (
{reasonChips.map((chip) => ( ))}
)}