Compare commits
3 Commits
e2398099c4
...
feat/berth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21868ee5fc | ||
|
|
c7ab816c99 | ||
|
|
e40b6c3d99 |
@@ -44,6 +44,17 @@ type BerthDetailData = {
|
|||||||
draftFt: string | null;
|
draftFt: string | null;
|
||||||
draftM: string | null;
|
draftM: string | null;
|
||||||
widthIsMinimum: boolean | 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;
|
price: string | null;
|
||||||
priceCurrency: string;
|
priceCurrency: string;
|
||||||
tenureType: string;
|
tenureType: string;
|
||||||
|
|||||||
@@ -16,18 +16,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import {
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetFooter,
|
|
||||||
} from '@/components/ui/sheet';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { TagPicker } from '@/components/shared/tag-picker';
|
import { TagPicker } from '@/components/shared/tag-picker';
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
import { updateBerthSchema, type UpdateBerthInput } from '@/lib/validators/berths';
|
import { updateBerthSchema, type UpdateBerthInput } from '@/lib/validators/berths';
|
||||||
|
import {
|
||||||
|
BERTH_AREAS,
|
||||||
|
BERTH_SIDE_PONTOON_OPTIONS,
|
||||||
|
BERTH_MOORING_TYPES,
|
||||||
|
BERTH_CLEAT_TYPES,
|
||||||
|
BERTH_CLEAT_CAPACITIES,
|
||||||
|
BERTH_BOLLARD_TYPES,
|
||||||
|
BERTH_BOLLARD_CAPACITIES,
|
||||||
|
BERTH_ACCESS_OPTIONS,
|
||||||
|
} from '@/lib/constants';
|
||||||
|
|
||||||
interface BerthFormProps {
|
interface BerthFormProps {
|
||||||
berth: {
|
berth: {
|
||||||
@@ -42,16 +46,27 @@ interface BerthFormProps {
|
|||||||
draftFt: string | null;
|
draftFt: string | null;
|
||||||
draftM: string | null;
|
draftM: string | null;
|
||||||
widthIsMinimum: boolean | null;
|
widthIsMinimum: boolean | null;
|
||||||
|
nominalBoatSize: string | null;
|
||||||
|
nominalBoatSizeM: string | null;
|
||||||
|
waterDepth: string | null;
|
||||||
|
waterDepthM: string | null;
|
||||||
|
waterDepthIsMinimum: boolean | null;
|
||||||
|
sidePontoon: string | null;
|
||||||
|
powerCapacity: string | null;
|
||||||
|
voltage: string | null;
|
||||||
|
mooringType: string | null;
|
||||||
|
cleatType: string | null;
|
||||||
|
cleatCapacity: string | null;
|
||||||
|
bollardType: string | null;
|
||||||
|
bollardCapacity: string | null;
|
||||||
|
access: string | null;
|
||||||
|
bowFacing: string | null;
|
||||||
price: string | null;
|
price: string | null;
|
||||||
priceCurrency: string;
|
priceCurrency: string;
|
||||||
tenureType: string;
|
tenureType: string;
|
||||||
tenureYears: number | null;
|
tenureYears: number | null;
|
||||||
tenureStartDate: string | null;
|
tenureStartDate: string | null;
|
||||||
tenureEndDate: string | null;
|
tenureEndDate: string | null;
|
||||||
powerCapacity: string | null;
|
|
||||||
voltage: string | null;
|
|
||||||
mooringType: string | null;
|
|
||||||
access: string | null;
|
|
||||||
berthApproved: boolean | null;
|
berthApproved: boolean | null;
|
||||||
tags: Array<{ id: string; name: string; color: string }>;
|
tags: Array<{ id: string; name: string; color: string }>;
|
||||||
};
|
};
|
||||||
@@ -59,10 +74,42 @@ interface BerthFormProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Optional select that allows clearing back to "no value". */
|
||||||
|
function SelectOrEmpty({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
placeholder = 'Select…',
|
||||||
|
}: {
|
||||||
|
value: string | undefined;
|
||||||
|
onChange: (next: string | undefined) => void;
|
||||||
|
options: readonly string[];
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
const NONE = '__none';
|
||||||
|
return (
|
||||||
|
<Select value={value ?? NONE} onValueChange={(v) => onChange(v === NONE ? undefined : v)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NONE}>—</SelectItem>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<SelectItem key={opt} value={opt}>
|
||||||
|
{opt}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [tagIds, setTagIds] = useState<string[]>(berth.tags.map((t) => t.id));
|
const [tagIds, setTagIds] = useState<string[]>(berth.tags.map((t) => t.id));
|
||||||
|
|
||||||
|
const numOrUndef = (v: string | null) => (v != null && v !== '' ? Number(v) : undefined);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -73,23 +120,34 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
resolver: zodResolver(updateBerthSchema),
|
resolver: zodResolver(updateBerthSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
area: berth.area ?? undefined,
|
area: berth.area ?? undefined,
|
||||||
lengthFt: berth.lengthFt ? Number(berth.lengthFt) : undefined,
|
lengthFt: numOrUndef(berth.lengthFt),
|
||||||
lengthM: berth.lengthM ? Number(berth.lengthM) : undefined,
|
lengthM: numOrUndef(berth.lengthM),
|
||||||
widthFt: berth.widthFt ? Number(berth.widthFt) : undefined,
|
widthFt: numOrUndef(berth.widthFt),
|
||||||
widthM: berth.widthM ? Number(berth.widthM) : undefined,
|
widthM: numOrUndef(berth.widthM),
|
||||||
draftFt: berth.draftFt ? Number(berth.draftFt) : undefined,
|
draftFt: numOrUndef(berth.draftFt),
|
||||||
draftM: berth.draftM ? Number(berth.draftM) : undefined,
|
draftM: numOrUndef(berth.draftM),
|
||||||
widthIsMinimum: berth.widthIsMinimum ?? false,
|
widthIsMinimum: berth.widthIsMinimum ?? false,
|
||||||
price: berth.price ? Number(berth.price) : undefined,
|
nominalBoatSize: numOrUndef(berth.nominalBoatSize),
|
||||||
|
nominalBoatSizeM: numOrUndef(berth.nominalBoatSizeM),
|
||||||
|
waterDepth: numOrUndef(berth.waterDepth),
|
||||||
|
waterDepthM: numOrUndef(berth.waterDepthM),
|
||||||
|
waterDepthIsMinimum: berth.waterDepthIsMinimum ?? false,
|
||||||
|
sidePontoon: berth.sidePontoon ?? undefined,
|
||||||
|
powerCapacity: numOrUndef(berth.powerCapacity),
|
||||||
|
voltage: numOrUndef(berth.voltage),
|
||||||
|
mooringType: berth.mooringType ?? undefined,
|
||||||
|
cleatType: berth.cleatType ?? undefined,
|
||||||
|
cleatCapacity: berth.cleatCapacity ?? undefined,
|
||||||
|
bollardType: berth.bollardType ?? undefined,
|
||||||
|
bollardCapacity: berth.bollardCapacity ?? undefined,
|
||||||
|
access: berth.access ?? undefined,
|
||||||
|
bowFacing: berth.bowFacing ?? undefined,
|
||||||
|
price: numOrUndef(berth.price),
|
||||||
priceCurrency: berth.priceCurrency,
|
priceCurrency: berth.priceCurrency,
|
||||||
tenureType: berth.tenureType as 'permanent' | 'fixed_term',
|
tenureType: berth.tenureType as 'permanent' | 'fixed_term',
|
||||||
tenureYears: berth.tenureYears ?? undefined,
|
tenureYears: berth.tenureYears ?? undefined,
|
||||||
tenureStartDate: berth.tenureStartDate ?? undefined,
|
tenureStartDate: berth.tenureStartDate ?? undefined,
|
||||||
tenureEndDate: berth.tenureEndDate ?? undefined,
|
tenureEndDate: berth.tenureEndDate ?? undefined,
|
||||||
powerCapacity: berth.powerCapacity ?? undefined,
|
|
||||||
voltage: berth.voltage ?? undefined,
|
|
||||||
mooringType: berth.mooringType ?? undefined,
|
|
||||||
access: berth.access ?? undefined,
|
|
||||||
berthApproved: berth.berthApproved ?? false,
|
berthApproved: berth.berthApproved ?? false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -120,6 +178,14 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tenureType = watch('tenureType');
|
const tenureType = watch('tenureType');
|
||||||
|
const area = watch('area');
|
||||||
|
const sidePontoon = watch('sidePontoon');
|
||||||
|
const mooringType = watch('mooringType');
|
||||||
|
const cleatType = watch('cleatType');
|
||||||
|
const cleatCapacity = watch('cleatCapacity');
|
||||||
|
const bollardType = watch('bollardType');
|
||||||
|
const bollardCapacity = watch('bollardCapacity');
|
||||||
|
const access = watch('access');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -136,18 +202,18 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="area">Area</Label>
|
<Label>Area</Label>
|
||||||
<Input id="area" {...register('area')} placeholder="e.g. Marina A" />
|
<SelectOrEmpty
|
||||||
|
value={area}
|
||||||
|
onChange={(v) => setValue('area', v)}
|
||||||
|
options={BERTH_AREAS}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="mooringType">Mooring Type</Label>
|
<Label htmlFor="bowFacing">Bow Facing</Label>
|
||||||
<Input id="mooringType" {...register('mooringType')} />
|
<Input id="bowFacing" {...register('bowFacing')} placeholder="e.g. East" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="access">Access</Label>
|
|
||||||
<Input id="access" {...register('access')} />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="berthApproved"
|
id="berthApproved"
|
||||||
@@ -168,41 +234,159 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Length (ft)</Label>
|
<Label>Length (ft)</Label>
|
||||||
<Input type="number" step="0.1" {...register('lengthFt')} />
|
<Input type="number" step="0.01" {...register('lengthFt')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Length (m)</Label>
|
<Label>Length (m)</Label>
|
||||||
<Input type="number" step="0.1" {...register('lengthM')} />
|
<Input type="number" step="0.01" {...register('lengthM')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Width (ft)</Label>
|
<Label>Width (ft)</Label>
|
||||||
<Input type="number" step="0.1" {...register('widthFt')} />
|
<Input type="number" step="0.01" {...register('widthFt')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Width (m)</Label>
|
<Label>Width (m)</Label>
|
||||||
<Input type="number" step="0.1" {...register('widthM')} />
|
<Input type="number" step="0.01" {...register('widthM')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Draft (ft)</Label>
|
<Label>Draft (ft)</Label>
|
||||||
<Input type="number" step="0.1" {...register('draftFt')} />
|
<Input type="number" step="0.01" {...register('draftFt')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Draft (m)</Label>
|
<Label>Draft (m)</Label>
|
||||||
<Input type="number" step="0.1" {...register('draftM')} />
|
<Input type="number" step="0.01" {...register('draftM')} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Nominal Boat Size (ft)</Label>
|
||||||
|
<Input type="number" step="1" {...register('nominalBoatSize')} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Nominal Boat Size (m)</Label>
|
||||||
|
<Input type="number" step="0.01" {...register('nominalBoatSizeM')} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Water Depth (ft)</Label>
|
||||||
|
<Input type="number" step="0.01" {...register('waterDepth')} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Water Depth (m)</Label>
|
||||||
|
<Input type="number" step="0.01" {...register('waterDepthM')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2">
|
||||||
<Switch
|
<div className="flex items-center gap-2">
|
||||||
id="widthIsMinimum"
|
<Switch
|
||||||
checked={watch('widthIsMinimum') ?? false}
|
id="widthIsMinimum"
|
||||||
onCheckedChange={(v) => setValue('widthIsMinimum', v)}
|
checked={watch('widthIsMinimum') ?? false}
|
||||||
/>
|
onCheckedChange={(v) => setValue('widthIsMinimum', v)}
|
||||||
<Label htmlFor="widthIsMinimum">Width is minimum</Label>
|
/>
|
||||||
|
<Label htmlFor="widthIsMinimum">Width is minimum</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="waterDepthIsMinimum"
|
||||||
|
checked={watch('waterDepthIsMinimum') ?? false}
|
||||||
|
onCheckedChange={(v) => setValue('waterDepthIsMinimum', v)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="waterDepthIsMinimum">Water depth is minimum</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
{/* Mooring & Hardware */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Mooring & Hardware
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Side Pontoon</Label>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={sidePontoon}
|
||||||
|
onChange={(v) => setValue('sidePontoon', v)}
|
||||||
|
options={BERTH_SIDE_PONTOON_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Mooring Type</Label>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={mooringType}
|
||||||
|
onChange={(v) => setValue('mooringType', v)}
|
||||||
|
options={BERTH_MOORING_TYPES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cleat Type</Label>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={cleatType}
|
||||||
|
onChange={(v) => setValue('cleatType', v)}
|
||||||
|
options={BERTH_CLEAT_TYPES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cleat Capacity</Label>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={cleatCapacity}
|
||||||
|
onChange={(v) => setValue('cleatCapacity', v)}
|
||||||
|
options={BERTH_CLEAT_CAPACITIES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Bollard Type</Label>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={bollardType}
|
||||||
|
onChange={(v) => setValue('bollardType', v)}
|
||||||
|
options={BERTH_BOLLARD_TYPES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Bollard Capacity</Label>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={bollardCapacity}
|
||||||
|
onChange={(v) => setValue('bollardCapacity', v)}
|
||||||
|
options={BERTH_BOLLARD_CAPACITIES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Power */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Power
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Power Capacity (kW)</Label>
|
||||||
|
<Input type="number" step="1" {...register('powerCapacity')} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Voltage (V at 60Hz)</Label>
|
||||||
|
<Input type="number" step="1" {...register('voltage')} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Access */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Access
|
||||||
|
</h3>
|
||||||
|
<SelectOrEmpty
|
||||||
|
value={access}
|
||||||
|
onChange={(v) => setValue('access', v)}
|
||||||
|
options={BERTH_ACCESS_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
{/* Price */}
|
{/* Price */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
@@ -262,25 +446,6 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Infrastructure */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
|
||||||
Infrastructure
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Power Capacity</Label>
|
|
||||||
<Input {...register('powerCapacity')} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Voltage</Label>
|
|
||||||
<Input {...register('voltage')} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
|||||||
@@ -57,13 +57,45 @@ function SpecRow({ label, value }: { label: string; value: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function OverviewTab({ berth }: { berth: BerthData }) {
|
function OverviewTab({ berth }: { berth: BerthData }) {
|
||||||
|
// Round to at most 2 decimals; trim trailing zeros so "5.00" -> "5".
|
||||||
|
const fmt = (v: string | null, fractionDigits = 2): string | null => {
|
||||||
|
if (v == null || v === '') return null;
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isNaN(n)) return v;
|
||||||
|
return n.toLocaleString('en-US', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: fractionDigits,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formatDim = (ft: string | null, m: string | null) => {
|
const formatDim = (ft: string | null, m: string | null) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (ft) parts.push(`${ft} ft`);
|
const ftFmt = fmt(ft);
|
||||||
if (m) parts.push(`${m} m`);
|
const mFmt = fmt(m);
|
||||||
|
if (ftFmt) parts.push(`${ftFmt} ft`);
|
||||||
|
if (mFmt) parts.push(`${mFmt} m`);
|
||||||
return parts.length > 0 ? parts.join(' / ') : null;
|
return parts.length > 0 ? parts.join(' / ') : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatNominalBoatSize = (ft: string | null, m: string | null): string | null => {
|
||||||
|
const ftFmt = fmt(ft, 0);
|
||||||
|
const mFmt = fmt(m);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (ftFmt) parts.push(`${ftFmt} ft`);
|
||||||
|
if (mFmt) parts.push(`${mFmt} m`);
|
||||||
|
return parts.length > 0 ? parts.join(' / ') : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPower = (kw: string | null) => {
|
||||||
|
const v = fmt(kw, 0);
|
||||||
|
return v ? `${v} kW` : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatVoltage = (v: string | null) => {
|
||||||
|
const fv = fmt(v, 0);
|
||||||
|
return fv ? `${fv} V` : null;
|
||||||
|
};
|
||||||
|
|
||||||
const price = berth.price
|
const price = berth.price
|
||||||
? new Intl.NumberFormat('en-US', {
|
? new Intl.NumberFormat('en-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -97,7 +129,7 @@ function OverviewTab({ berth }: { berth: BerthData }) {
|
|||||||
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
||||||
<SpecRow
|
<SpecRow
|
||||||
label="Nominal Boat Size"
|
label="Nominal Boat Size"
|
||||||
value={berth.nominalBoatSize || berth.nominalBoatSizeM}
|
value={formatNominalBoatSize(berth.nominalBoatSize, berth.nominalBoatSizeM)}
|
||||||
/>
|
/>
|
||||||
<SpecRow
|
<SpecRow
|
||||||
label="Water Depth"
|
label="Water Depth"
|
||||||
@@ -122,8 +154,8 @@ function OverviewTab({ berth }: { berth: BerthData }) {
|
|||||||
<CardTitle className="text-sm font-medium">Infrastructure</CardTitle>
|
<CardTitle className="text-sm font-medium">Infrastructure</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0 divide-y">
|
<CardContent className="pt-0 divide-y">
|
||||||
<SpecRow label="Power Capacity" value={berth.powerCapacity} />
|
<SpecRow label="Power Capacity" value={formatPower(berth.powerCapacity)} />
|
||||||
<SpecRow label="Voltage" value={berth.voltage} />
|
<SpecRow label="Voltage" value={formatVoltage(berth.voltage)} />
|
||||||
<SpecRow label="Cleat Type" value={berth.cleatType} />
|
<SpecRow label="Cleat Type" value={berth.cleatType} />
|
||||||
<SpecRow label="Cleat Capacity" value={berth.cleatCapacity} />
|
<SpecRow label="Cleat Capacity" value={berth.cleatCapacity} />
|
||||||
<SpecRow label="Bollard Type" value={berth.bollardType} />
|
<SpecRow label="Bollard Type" value={berth.bollardType} />
|
||||||
|
|||||||
@@ -123,6 +123,49 @@ export const BERTH_STATUSES = ['available', 'under_offer', 'sold'] as const;
|
|||||||
|
|
||||||
export type BerthStatus = (typeof BERTH_STATUSES)[number];
|
export type BerthStatus = (typeof BERTH_STATUSES)[number];
|
||||||
|
|
||||||
|
// ─── Berth single-select catalogues (mirror NocoDB) ──────────────────────────
|
||||||
|
// Stored as free text in the DB so legacy values still load, but the form
|
||||||
|
// presents only the canonical options below.
|
||||||
|
|
||||||
|
export const BERTH_AREAS = ['A', 'B', 'C', 'D', 'E'] as const;
|
||||||
|
|
||||||
|
export const BERTH_SIDE_PONTOON_OPTIONS = [
|
||||||
|
'No',
|
||||||
|
'Quay SB',
|
||||||
|
'Quay PT',
|
||||||
|
'Quay SB, Yes PT',
|
||||||
|
'Quay PT, Yes SB',
|
||||||
|
'Yes SB',
|
||||||
|
'Yes PT',
|
||||||
|
'Yes SB, PT',
|
||||||
|
'Finger SB',
|
||||||
|
'Finger PT',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const BERTH_MOORING_TYPES = [
|
||||||
|
'Side Pier / Med Mooring',
|
||||||
|
'2x Med Mooring',
|
||||||
|
'Side Pier / Finger',
|
||||||
|
'Finger / Med Mooring',
|
||||||
|
'2x Finger',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const BERTH_CLEAT_TYPES = ['A3', 'A5'] as const;
|
||||||
|
|
||||||
|
export const BERTH_CLEAT_CAPACITIES = ['10-14 ton break load', '20-24 ton break load'] as const;
|
||||||
|
|
||||||
|
export const BERTH_BOLLARD_TYPES = ['Bull bollard type A', 'Bull bollard type B'] as const;
|
||||||
|
|
||||||
|
export const BERTH_BOLLARD_CAPACITIES = ['20 ton break load', '40 ton break load'] as const;
|
||||||
|
|
||||||
|
export const BERTH_ACCESS_OPTIONS = [
|
||||||
|
'Car to Vessel',
|
||||||
|
'Car to Quai, Cart to Vessel',
|
||||||
|
'Cart to Vessel',
|
||||||
|
'Car (3t) to Vessel',
|
||||||
|
'Car (3.5t) to Vessel',
|
||||||
|
] as const;
|
||||||
|
|
||||||
// ─── Lead Categories ─────────────────────────────────────────────────────────
|
// ─── Lead Categories ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const LEAD_CATEGORIES = ['general_interest', 'specific_qualified', 'hot_lead'] as const;
|
export const LEAD_CATEGORIES = ['general_interest', 'specific_qualified', 'hot_lead'] as const;
|
||||||
|
|||||||
15
src/lib/db/migrations/0020_medical_betty_brant.sql
Normal file
15
src/lib/db/migrations/0020_medical_betty_brant.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Convert text columns to numeric. NULLs survive; empty strings become NULL;
|
||||||
|
-- whitespace is trimmed before casting so legacy data with stray spaces converts cleanly.
|
||||||
|
ALTER TABLE "berths"
|
||||||
|
ALTER COLUMN "nominal_boat_size" SET DATA TYPE numeric
|
||||||
|
USING NULLIF(TRIM("nominal_boat_size"), '')::numeric;--> statement-breakpoint
|
||||||
|
ALTER TABLE "berths"
|
||||||
|
ALTER COLUMN "nominal_boat_size_m" SET DATA TYPE numeric
|
||||||
|
USING NULLIF(TRIM("nominal_boat_size_m"), '')::numeric;--> statement-breakpoint
|
||||||
|
ALTER TABLE "berths"
|
||||||
|
ALTER COLUMN "power_capacity" SET DATA TYPE numeric
|
||||||
|
USING NULLIF(TRIM("power_capacity"), '')::numeric;--> statement-breakpoint
|
||||||
|
ALTER TABLE "berths"
|
||||||
|
ALTER COLUMN "voltage" SET DATA TYPE numeric
|
||||||
|
USING NULLIF(TRIM("voltage"), '')::numeric;--> statement-breakpoint
|
||||||
|
ALTER TABLE "berths" ADD COLUMN "status_override_mode" text;
|
||||||
10246
src/lib/db/migrations/meta/0020_snapshot.json
Normal file
10246
src/lib/db/migrations/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -141,6 +141,13 @@
|
|||||||
"when": 1777671562738,
|
"when": 1777671562738,
|
||||||
"tag": "0019_lazy_vampiro",
|
"tag": "0019_lazy_vampiro",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777814682110,
|
||||||
|
"tag": "0020_medical_betty_brant",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,14 +33,15 @@ export const berths = pgTable(
|
|||||||
widthM: numeric('width_m'),
|
widthM: numeric('width_m'),
|
||||||
draftM: numeric('draft_m'),
|
draftM: numeric('draft_m'),
|
||||||
widthIsMinimum: boolean('width_is_minimum').default(false),
|
widthIsMinimum: boolean('width_is_minimum').default(false),
|
||||||
nominalBoatSize: text('nominal_boat_size'),
|
// Numeric: ft (legacy NocoDB stored as plain numbers, no units in value).
|
||||||
nominalBoatSizeM: text('nominal_boat_size_m'),
|
nominalBoatSize: numeric('nominal_boat_size'),
|
||||||
|
nominalBoatSizeM: numeric('nominal_boat_size_m'),
|
||||||
waterDepth: numeric('water_depth'),
|
waterDepth: numeric('water_depth'),
|
||||||
waterDepthM: numeric('water_depth_m'),
|
waterDepthM: numeric('water_depth_m'),
|
||||||
waterDepthIsMinimum: boolean('water_depth_is_minimum').default(false),
|
waterDepthIsMinimum: boolean('water_depth_is_minimum').default(false),
|
||||||
sidePontoon: text('side_pontoon'),
|
sidePontoon: text('side_pontoon'),
|
||||||
powerCapacity: text('power_capacity'),
|
powerCapacity: numeric('power_capacity'), // kW
|
||||||
voltage: text('voltage'),
|
voltage: numeric('voltage'), // V at 60Hz
|
||||||
mooringType: text('mooring_type'),
|
mooringType: text('mooring_type'),
|
||||||
cleatType: text('cleat_type'),
|
cleatType: text('cleat_type'),
|
||||||
cleatCapacity: text('cleat_capacity'),
|
cleatCapacity: text('cleat_capacity'),
|
||||||
@@ -58,6 +59,9 @@ export const berths = pgTable(
|
|||||||
statusLastChangedBy: text('status_last_changed_by'), // user ID
|
statusLastChangedBy: text('status_last_changed_by'), // user ID
|
||||||
statusLastChangedReason: text('status_last_changed_reason'),
|
statusLastChangedReason: text('status_last_changed_reason'),
|
||||||
statusLastModified: timestamp('status_last_modified', { withTimezone: true }),
|
statusLastModified: timestamp('status_last_modified', { withTimezone: true }),
|
||||||
|
// Optional override flag carried over from NocoDB ("auto" or null in legacy data).
|
||||||
|
// Reserved for future "manual override" semantics; not surfaced in the UI today.
|
||||||
|
statusOverrideMode: text('status_override_mode'),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
* Exports `seedPortData(portId, portSlug)` — creates a realistic,
|
* Exports `seedPortData(portId, portSlug)` — creates a realistic,
|
||||||
* multi-cardinality data fixture for one port:
|
* multi-cardinality data fixture for one port:
|
||||||
*
|
*
|
||||||
* - 12 berths (5 available / 5 reserved-active / 2 sold)
|
* - 117 berths imported from a snapshot of the legacy NocoDB Berths
|
||||||
|
* table (`src/lib/db/seed-data/berths.json`). The snapshot is reordered
|
||||||
|
* so the first 12 entries satisfy the index assumptions used further
|
||||||
|
* down for interest/reservation linkage:
|
||||||
|
* idx 0..4 — available (small)
|
||||||
|
* idx 5..9 — under_offer (medium)
|
||||||
|
* idx 10..11 — sold (large)
|
||||||
* - 3 companies (2 active, 1 dissolved) with primary billing addresses
|
* - 3 companies (2 active, 1 dissolved) with primary billing addresses
|
||||||
* - 8 clients + contacts + primary addresses
|
* - 8 clients + contacts + primary addresses
|
||||||
* - Memberships tying clients to companies (incl. multi-company + ended)
|
* - Memberships tying clients to companies (incl. multi-company + ended)
|
||||||
@@ -39,6 +45,44 @@ import {
|
|||||||
getStandardEoiTemplateHtml,
|
getStandardEoiTemplateHtml,
|
||||||
STANDARD_EOI_MERGE_FIELDS,
|
STANDARD_EOI_MERGE_FIELDS,
|
||||||
} from '@/lib/pdf/templates/eoi-standard-inapp';
|
} from '@/lib/pdf/templates/eoi-standard-inapp';
|
||||||
|
import berthSnapshot from './seed-data/berths.json';
|
||||||
|
|
||||||
|
// ─── Berth snapshot ──────────────────────────────────────────────────────────
|
||||||
|
// 117 rows imported from the legacy NocoDB Berths table on 2026-05-03.
|
||||||
|
// Refresh by re-running the snapshot script (see git history of this file).
|
||||||
|
type SeedBerth = {
|
||||||
|
legacyId: number;
|
||||||
|
mooringNumber: string;
|
||||||
|
legacyMooringNumber: string;
|
||||||
|
area: string | null;
|
||||||
|
status: 'available' | 'under_offer' | 'sold';
|
||||||
|
lengthFt: number | null;
|
||||||
|
widthFt: number | null;
|
||||||
|
draftFt: number | null;
|
||||||
|
lengthM: number | null;
|
||||||
|
widthM: number | null;
|
||||||
|
draftM: number | null;
|
||||||
|
widthIsMinimum: boolean;
|
||||||
|
nominalBoatSize: number | null;
|
||||||
|
nominalBoatSizeM: number | null;
|
||||||
|
waterDepth: number | null;
|
||||||
|
waterDepthM: number | null;
|
||||||
|
waterDepthIsMinimum: boolean;
|
||||||
|
sidePontoon: string | null;
|
||||||
|
powerCapacity: number | null;
|
||||||
|
voltage: number | null;
|
||||||
|
mooringType: string | null;
|
||||||
|
cleatType: string | null;
|
||||||
|
cleatCapacity: string | null;
|
||||||
|
bollardType: string | null;
|
||||||
|
bollardCapacity: string | null;
|
||||||
|
access: string | null;
|
||||||
|
price: number | null;
|
||||||
|
bowFacing: string | null;
|
||||||
|
berthApproved: boolean;
|
||||||
|
statusOverrideMode: string | null;
|
||||||
|
};
|
||||||
|
const BERTH_SNAPSHOT = berthSnapshot as SeedBerth[];
|
||||||
|
|
||||||
// ─── Tunables ────────────────────────────────────────────────────────────────
|
// ─── Tunables ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -77,144 +121,44 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
|
|
||||||
return withTransaction(async (tx) => {
|
return withTransaction(async (tx) => {
|
||||||
// ── 1. Berths ──────────────────────────────────────────────────────────
|
// ── 1. Berths ──────────────────────────────────────────────────────────
|
||||||
// 12 berths: [0..4] available, [5..9] will be reserved-active, [10..11] sold.
|
// 117 berths seeded from the legacy NocoDB Berths snapshot.
|
||||||
// We mark 5..9 as 'under_offer' (closest to "reserved via active reservation")
|
// The JSON file is pre-sorted so the first 12 indexes satisfy the
|
||||||
// and 10..11 as 'sold'; 0..4 remain 'available'.
|
// status semantics expected by the interest/reservation seeds:
|
||||||
const BERTH_SPECS: Array<{
|
// idx 0..4 available, idx 5..9 under_offer, idx 10..11 sold.
|
||||||
mooring: string;
|
|
||||||
area: string;
|
|
||||||
lengthM: string;
|
|
||||||
widthM: string;
|
|
||||||
draftM: string;
|
|
||||||
price: string;
|
|
||||||
status: 'available' | 'under_offer' | 'sold';
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
mooring: 'A-01',
|
|
||||||
area: 'North Pier',
|
|
||||||
lengthM: '15',
|
|
||||||
widthM: '5',
|
|
||||||
draftM: '2.5',
|
|
||||||
price: '250000',
|
|
||||||
status: 'available',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'A-02',
|
|
||||||
area: 'North Pier',
|
|
||||||
lengthM: '18',
|
|
||||||
widthM: '5.5',
|
|
||||||
draftM: '2.8',
|
|
||||||
price: '320000',
|
|
||||||
status: 'available',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'A-03',
|
|
||||||
area: 'North Pier',
|
|
||||||
lengthM: '20',
|
|
||||||
widthM: '6',
|
|
||||||
draftM: '3.0',
|
|
||||||
price: '420000',
|
|
||||||
status: 'available',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'B-01',
|
|
||||||
area: 'Central Basin',
|
|
||||||
lengthM: '25',
|
|
||||||
widthM: '7',
|
|
||||||
draftM: '3.5',
|
|
||||||
price: '580000',
|
|
||||||
status: 'available',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'B-02',
|
|
||||||
area: 'Central Basin',
|
|
||||||
lengthM: '30',
|
|
||||||
widthM: '8',
|
|
||||||
draftM: '4.0',
|
|
||||||
price: '780000',
|
|
||||||
status: 'available',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'B-03',
|
|
||||||
area: 'Central Basin',
|
|
||||||
lengthM: '35',
|
|
||||||
widthM: '8.5',
|
|
||||||
draftM: '4.2',
|
|
||||||
price: '950000',
|
|
||||||
status: 'under_offer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'C-01',
|
|
||||||
area: 'South Marina',
|
|
||||||
lengthM: '40',
|
|
||||||
widthM: '9',
|
|
||||||
draftM: '4.5',
|
|
||||||
price: '1250000',
|
|
||||||
status: 'under_offer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'C-02',
|
|
||||||
area: 'South Marina',
|
|
||||||
lengthM: '45',
|
|
||||||
widthM: '10',
|
|
||||||
draftM: '4.8',
|
|
||||||
price: '1600000',
|
|
||||||
status: 'under_offer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'C-03',
|
|
||||||
area: 'South Marina',
|
|
||||||
lengthM: '50',
|
|
||||||
widthM: '11',
|
|
||||||
draftM: '5.0',
|
|
||||||
price: '2100000',
|
|
||||||
status: 'under_offer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'D-01',
|
|
||||||
area: 'Superyacht Dock',
|
|
||||||
lengthM: '60',
|
|
||||||
widthM: '13',
|
|
||||||
draftM: '5.5',
|
|
||||||
price: '3200000',
|
|
||||||
status: 'under_offer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'D-02',
|
|
||||||
area: 'Superyacht Dock',
|
|
||||||
lengthM: '70',
|
|
||||||
widthM: '14',
|
|
||||||
draftM: '6.0',
|
|
||||||
price: '4500000',
|
|
||||||
status: 'sold',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mooring: 'D-03',
|
|
||||||
area: 'Superyacht Dock',
|
|
||||||
lengthM: '80',
|
|
||||||
widthM: '15',
|
|
||||||
draftM: '6.5',
|
|
||||||
price: '6800000',
|
|
||||||
status: 'sold',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const berthRows = await tx
|
const berthRows = await tx
|
||||||
.insert(berths)
|
.insert(berths)
|
||||||
.values(
|
.values(
|
||||||
BERTH_SPECS.map((b) => ({
|
BERTH_SNAPSHOT.map((b) => ({
|
||||||
portId,
|
portId,
|
||||||
mooringNumber: b.mooring,
|
mooringNumber: b.mooringNumber,
|
||||||
area: b.area,
|
area: b.area,
|
||||||
status: b.status,
|
status: b.status,
|
||||||
lengthM: b.lengthM,
|
lengthFt: b.lengthFt != null ? String(b.lengthFt) : null,
|
||||||
widthM: b.widthM,
|
widthFt: b.widthFt != null ? String(b.widthFt) : null,
|
||||||
draftM: b.draftM,
|
draftFt: b.draftFt != null ? String(b.draftFt) : null,
|
||||||
lengthFt: (Number(b.lengthM) * 3.28084).toFixed(2),
|
lengthM: b.lengthM != null ? String(b.lengthM) : null,
|
||||||
widthFt: (Number(b.widthM) * 3.28084).toFixed(2),
|
widthM: b.widthM != null ? String(b.widthM) : null,
|
||||||
draftFt: (Number(b.draftM) * 3.28084).toFixed(2),
|
draftM: b.draftM != null ? String(b.draftM) : null,
|
||||||
price: b.price,
|
widthIsMinimum: b.widthIsMinimum,
|
||||||
|
nominalBoatSize: b.nominalBoatSize != null ? String(b.nominalBoatSize) : null,
|
||||||
|
nominalBoatSizeM: b.nominalBoatSizeM != null ? String(b.nominalBoatSizeM) : null,
|
||||||
|
waterDepth: b.waterDepth != null ? String(b.waterDepth) : null,
|
||||||
|
waterDepthM: b.waterDepthM != null ? String(b.waterDepthM) : null,
|
||||||
|
waterDepthIsMinimum: b.waterDepthIsMinimum,
|
||||||
|
sidePontoon: b.sidePontoon,
|
||||||
|
powerCapacity: b.powerCapacity != null ? String(b.powerCapacity) : null,
|
||||||
|
voltage: b.voltage != null ? String(b.voltage) : null,
|
||||||
|
mooringType: b.mooringType,
|
||||||
|
cleatType: b.cleatType,
|
||||||
|
cleatCapacity: b.cleatCapacity,
|
||||||
|
bollardType: b.bollardType,
|
||||||
|
bollardCapacity: b.bollardCapacity,
|
||||||
|
access: b.access,
|
||||||
|
price: b.price != null ? String(b.price) : null,
|
||||||
priceCurrency: 'USD',
|
priceCurrency: 'USD',
|
||||||
|
bowFacing: b.bowFacing,
|
||||||
|
berthApproved: b.berthApproved,
|
||||||
|
statusOverrideMode: b.statusOverrideMode,
|
||||||
tenureType: 'permanent' as const,
|
tenureType: 'permanent' as const,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
3746
src/lib/db/seed-data/berths.json
Normal file
3746
src/lib/db/seed-data/berths.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,16 +2,16 @@
|
|||||||
* Seed script for Port Nimara CRM.
|
* Seed script for Port Nimara CRM.
|
||||||
*
|
*
|
||||||
* Top-level orchestrator:
|
* Top-level orchestrator:
|
||||||
* 1. Create 3 ports (idempotent):
|
* 1. Create the operational ports (idempotent):
|
||||||
* - Port Nimara
|
* - Port Nimara (primary install — the real marina)
|
||||||
* - Marina Azzurra
|
* - Port Amador (secondary, kept for multi-tenant isolation tests
|
||||||
* - Harbor Royale
|
* and as scaffolding for a future Panama install)
|
||||||
* 2. Create 5 system roles with full permission maps
|
* 2. Create 5 system roles with full permission maps
|
||||||
* 3. Create the super admin user profile placeholder (matt@portnimara.com)
|
* 3. Create the super admin user profile placeholder (matt@portnimara.com)
|
||||||
* 4. For each port, call `seedPortData(portId, portSlug)` from seed-data.ts
|
* 4. For each port, call `seedPortData(portId, portSlug)` from seed-data.ts
|
||||||
* to produce the realistic multi-cardinality fixture
|
* to produce the realistic multi-cardinality fixture
|
||||||
* (berths, clients, companies, yachts, memberships, interests,
|
* (117 berths from the NocoDB snapshot, plus clients, companies, yachts,
|
||||||
* reservations, ownership-transfer history).
|
* memberships, interests, reservations, ownership-transfer history).
|
||||||
* 5. Print a summary.
|
* 5. Print a summary.
|
||||||
*
|
*
|
||||||
* Run with: pnpm db:seed
|
* Run with: pnpm db:seed
|
||||||
@@ -186,7 +186,7 @@ const SALES_MANAGER_PERMISSIONS: RolePermissions = {
|
|||||||
generate_eoi: true,
|
generate_eoi: true,
|
||||||
export: true,
|
export: true,
|
||||||
},
|
},
|
||||||
berths: { view: true, edit: false, import: false, manage_waiting_list: true },
|
berths: { view: true, edit: true, import: false, manage_waiting_list: true },
|
||||||
documents: {
|
documents: {
|
||||||
view: true,
|
view: true,
|
||||||
create: true,
|
create: true,
|
||||||
@@ -260,7 +260,7 @@ const SALES_AGENT_PERMISSIONS: RolePermissions = {
|
|||||||
generate_eoi: true,
|
generate_eoi: true,
|
||||||
export: true,
|
export: true,
|
||||||
},
|
},
|
||||||
berths: { view: true, edit: false, import: false, manage_waiting_list: true },
|
berths: { view: true, edit: true, import: false, manage_waiting_list: true },
|
||||||
documents: {
|
documents: {
|
||||||
view: true,
|
view: true,
|
||||||
create: true,
|
create: true,
|
||||||
@@ -413,19 +413,15 @@ const PORT_DEFINITIONS: Array<{
|
|||||||
defaultCurrency: 'USD',
|
defaultCurrency: 'USD',
|
||||||
timezone: 'America/Anguilla',
|
timezone: 'America/Anguilla',
|
||||||
},
|
},
|
||||||
|
// Second port kept for multi-tenant isolation tests (cross-port scoping,
|
||||||
|
// permission boundaries). Drop or rename if the production install is
|
||||||
|
// single-port.
|
||||||
{
|
{
|
||||||
name: 'Marina Azzurra',
|
name: 'Port Amador',
|
||||||
slug: 'marina-azzurra',
|
slug: 'port-amador',
|
||||||
primaryColor: '#2E86AB',
|
primaryColor: '#D97706',
|
||||||
defaultCurrency: 'EUR',
|
defaultCurrency: 'USD',
|
||||||
timezone: 'Europe/Rome',
|
timezone: 'America/Panama',
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Harbor Royale',
|
|
||||||
slug: 'harbor-royale',
|
|
||||||
primaryColor: '#8B1E3F',
|
|
||||||
defaultCurrency: 'GBP',
|
|
||||||
timezone: 'Europe/London',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -180,14 +180,14 @@ export async function updateBerth(
|
|||||||
draftFt: n(data.draftFt),
|
draftFt: n(data.draftFt),
|
||||||
draftM: n(data.draftM),
|
draftM: n(data.draftM),
|
||||||
widthIsMinimum: data.widthIsMinimum,
|
widthIsMinimum: data.widthIsMinimum,
|
||||||
nominalBoatSize: data.nominalBoatSize,
|
nominalBoatSize: n(data.nominalBoatSize),
|
||||||
nominalBoatSizeM: data.nominalBoatSizeM,
|
nominalBoatSizeM: n(data.nominalBoatSizeM),
|
||||||
waterDepth: n(data.waterDepth),
|
waterDepth: n(data.waterDepth),
|
||||||
waterDepthM: n(data.waterDepthM),
|
waterDepthM: n(data.waterDepthM),
|
||||||
waterDepthIsMinimum: data.waterDepthIsMinimum,
|
waterDepthIsMinimum: data.waterDepthIsMinimum,
|
||||||
sidePontoon: data.sidePontoon,
|
sidePontoon: data.sidePontoon,
|
||||||
powerCapacity: data.powerCapacity,
|
powerCapacity: n(data.powerCapacity),
|
||||||
voltage: data.voltage,
|
voltage: n(data.voltage),
|
||||||
mooringType: data.mooringType,
|
mooringType: data.mooringType,
|
||||||
cleatType: data.cleatType,
|
cleatType: data.cleatType,
|
||||||
cleatCapacity: data.cleatCapacity,
|
cleatCapacity: data.cleatCapacity,
|
||||||
@@ -481,8 +481,8 @@ export async function createBerth(portId: string, data: CreateBerthInput, meta:
|
|||||||
priceCurrency: data.priceCurrency ?? 'USD',
|
priceCurrency: data.priceCurrency ?? 'USD',
|
||||||
tenureType: data.tenureType ?? 'permanent',
|
tenureType: data.tenureType ?? 'permanent',
|
||||||
mooringType: data.mooringType,
|
mooringType: data.mooringType,
|
||||||
powerCapacity: data.powerCapacity,
|
powerCapacity: data.powerCapacity?.toString(),
|
||||||
voltage: data.voltage,
|
voltage: data.voltage?.toString(),
|
||||||
access: data.access,
|
access: data.access,
|
||||||
bowFacing: data.bowFacing,
|
bowFacing: data.bowFacing,
|
||||||
sidePontoon: data.sidePontoon,
|
sidePontoon: data.sidePontoon,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const createBerthSchema = z.object({
|
|||||||
status: z.enum(BERTH_STATUSES).default('available'),
|
status: z.enum(BERTH_STATUSES).default('available'),
|
||||||
tenureType: z.enum(['permanent', 'fixed_term']).optional(),
|
tenureType: z.enum(['permanent', 'fixed_term']).optional(),
|
||||||
mooringType: z.string().optional(),
|
mooringType: z.string().optional(),
|
||||||
powerCapacity: z.string().optional(),
|
powerCapacity: z.coerce.number().optional(), // kW
|
||||||
voltage: z.string().optional(),
|
voltage: z.coerce.number().optional(), // V at 60Hz
|
||||||
access: z.string().optional(),
|
access: z.string().optional(),
|
||||||
bowFacing: z.string().optional(),
|
bowFacing: z.string().optional(),
|
||||||
sidePontoon: z.string().optional(),
|
sidePontoon: z.string().optional(),
|
||||||
@@ -38,14 +38,14 @@ export const updateBerthSchema = z.object({
|
|||||||
draftFt: z.coerce.number().optional(),
|
draftFt: z.coerce.number().optional(),
|
||||||
draftM: z.coerce.number().optional(),
|
draftM: z.coerce.number().optional(),
|
||||||
widthIsMinimum: z.boolean().optional(),
|
widthIsMinimum: z.boolean().optional(),
|
||||||
nominalBoatSize: z.string().optional(),
|
nominalBoatSize: z.coerce.number().optional(), // ft
|
||||||
nominalBoatSizeM: z.string().optional(),
|
nominalBoatSizeM: z.coerce.number().optional(), // m
|
||||||
waterDepth: z.coerce.number().optional(),
|
waterDepth: z.coerce.number().optional(),
|
||||||
waterDepthM: z.coerce.number().optional(),
|
waterDepthM: z.coerce.number().optional(),
|
||||||
waterDepthIsMinimum: z.boolean().optional(),
|
waterDepthIsMinimum: z.boolean().optional(),
|
||||||
sidePontoon: z.string().optional(),
|
sidePontoon: z.string().optional(),
|
||||||
powerCapacity: z.string().optional(),
|
powerCapacity: z.coerce.number().optional(), // kW
|
||||||
voltage: z.string().optional(),
|
voltage: z.coerce.number().optional(), // V at 60Hz
|
||||||
mooringType: z.string().optional(),
|
mooringType: z.string().optional(),
|
||||||
cleatType: z.string().optional(),
|
cleatType: z.string().optional(),
|
||||||
cleatCapacity: z.string().optional(),
|
cleatCapacity: z.string().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user