Files
pn-new-crm/src/components/berths/berth-form.tsx

485 lines
17 KiB
TypeScript
Raw Normal View History

'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { TagPicker } from '@/components/shared/tag-picker';
import { CurrencyInput } from '@/components/shared/currency-input';
import { CurrencySelect } from '@/components/shared/currency-select';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5 Resolved 65 type errors across the codebase via these v4 migration patterns: - `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth routes + central error handler). - `z.record(value)` now requires explicit key type: `z.record(z.string(), value)`. Updated 7 sites across templates / forms / saved-views / website-inquiries. - `.refine(check, msgFn)` second-arg shape changed — now requires an `{ error: (issue) => ... }` object form. Updated `mergeFieldsSchema` in document-templates validator. - `.transform(...).default(...)` chains: v4 enforces default value type matches transform OUTPUT. Reordered to `.default(...).transform(...)` in list-query / company-memberships handlers. - `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures using `z.input<typeof schema>` (kept for caller flexibility around defaults) now re-parse via `schema.parse(data)` to recover the post-coercion shape Drizzle needs. Done in berth-reservations service. Invoice service narrows `lineItems` locally with a typed cast since re-parsing would double-validate. - `.optional().transform(...)` no longer propagates the optional marker through v4's new ZodPipe. Moved `.optional()` to the END of chain in `optionalDesiredDimSchema` (interests) and documents list query (folderId, signatureOnly). - ZodIssue subtype shapes simplified: `received` removed from invalid_type, `type` renamed to `origin` on too_small. Test fixtures updated. - @hookform/resolvers v5 splits Resolver into 3-generic form (Input, Context, Output). useForm calls in 6 forms (client, yacht, berth, interest, expense, invoices-new-page) now pass explicit generics: `useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`. Verified: tsc clean (0 errors), vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:29:03 +02:00
import type { z } from 'zod';
import { updateBerthSchema, type UpdateBerthInput } from '@/lib/validators/berths';
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
import {
BERTH_AREAS,
feat(berths): NocoDB-aligned dropdown enums + dual-unit auto-fill Pull verbatim SingleSelect choices from NocoDB Berths via MCP and lock them into BERTH_*_OPTIONS / _TYPES in lib/constants.ts: Side Pontoon (10 values), Mooring Type (5), Cleat Type (2), Cleat Capacity (2), Bollard Type (2), Bollard Capacity (2), Access (5), Area (A–E), Bow Facing (4-value UX-only constraint over a SingleLineText). Power Capacity / Voltage stay numeric inputs (NocoDB stores Number). Add `toSelectOptions()` mapper for shadcn `<Select>` `{value,label}` pairs. Wire every berth dropdown — both the modal form and the inline-edit detail tabs — to `<Select>`. Inline `EditableSpec` gains `selectOptions` for the variant and `linkedUnit { field, multiplier }` to auto-patch the metric column on save (× 0.3048 for ft→m on length, width, draft, nominal boat size, water depth). Promote nominal boat size + tenure type from read-only `<SpecRow>` to `<EditableSpec>` so reps can edit them. Tenure type currently uses the validator's `'permanent' | 'fixed_term'` set; will swap to per-port configurable list once Vocabularies admin lands (Wave 5). Mobile berth cards: replace status-coloured stripe with `mooringLetterDot()` so it groups by dock letter; status conveyed by the existing pill below. Berth detail header: "{Letter} Dock" chip instead of bare "A" / "B" text. Berth area filter: `<Select>` over A/B/C/D/E (renamed to "Dock"). Documents tab gets a one-paragraph explainer disambiguating the spec PDF from deal documents (Interests tab). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:10:24 +02:00
BERTH_BOW_FACING_OPTIONS,
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
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 {
berth: {
id: string;
mooringNumber: string;
area: string | null;
status: string;
lengthFt: string | null;
lengthM: string | null;
widthFt: string | null;
widthM: string | null;
draftFt: string | null;
draftM: string | null;
widthIsMinimum: boolean | null;
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
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;
priceCurrency: string;
tenureType: string;
tenureYears: number | null;
tenureStartDate: string | null;
tenureEndDate: string | null;
berthApproved: boolean | null;
tags: Array<{ id: string; name: string; color: string }>;
};
open: boolean;
onOpenChange: (open: boolean) => void;
}
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
/** 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>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
{options.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
const queryClient = useQueryClient();
const [tagIds, setTagIds] = useState<string[]>(berth.tags.map((t) => t.id));
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
const numOrUndef = (v: string | null) => (v != null && v !== '' ? Number(v) : undefined);
const {
register,
handleSubmit,
setValue,
watch,
formState: { isSubmitting },
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5 Resolved 65 type errors across the codebase via these v4 migration patterns: - `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth routes + central error handler). - `z.record(value)` now requires explicit key type: `z.record(z.string(), value)`. Updated 7 sites across templates / forms / saved-views / website-inquiries. - `.refine(check, msgFn)` second-arg shape changed — now requires an `{ error: (issue) => ... }` object form. Updated `mergeFieldsSchema` in document-templates validator. - `.transform(...).default(...)` chains: v4 enforces default value type matches transform OUTPUT. Reordered to `.default(...).transform(...)` in list-query / company-memberships handlers. - `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures using `z.input<typeof schema>` (kept for caller flexibility around defaults) now re-parse via `schema.parse(data)` to recover the post-coercion shape Drizzle needs. Done in berth-reservations service. Invoice service narrows `lineItems` locally with a typed cast since re-parsing would double-validate. - `.optional().transform(...)` no longer propagates the optional marker through v4's new ZodPipe. Moved `.optional()` to the END of chain in `optionalDesiredDimSchema` (interests) and documents list query (folderId, signatureOnly). - ZodIssue subtype shapes simplified: `received` removed from invalid_type, `type` renamed to `origin` on too_small. Test fixtures updated. - @hookform/resolvers v5 splits Resolver into 3-generic form (Input, Context, Output). useForm calls in 6 forms (client, yacht, berth, interest, expense, invoices-new-page) now pass explicit generics: `useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`. Verified: tsc clean (0 errors), vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:29:03 +02:00
} = useForm<z.input<typeof updateBerthSchema>, unknown, UpdateBerthInput>({
resolver: zodResolver(updateBerthSchema),
defaultValues: {
area: berth.area ?? undefined,
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
lengthFt: numOrUndef(berth.lengthFt),
lengthM: numOrUndef(berth.lengthM),
widthFt: numOrUndef(berth.widthFt),
widthM: numOrUndef(berth.widthM),
draftFt: numOrUndef(berth.draftFt),
draftM: numOrUndef(berth.draftM),
widthIsMinimum: berth.widthIsMinimum ?? false,
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
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,
tenureType: berth.tenureType as 'permanent' | 'fixed_term',
tenureYears: berth.tenureYears ?? undefined,
tenureStartDate: berth.tenureStartDate ?? undefined,
tenureEndDate: berth.tenureEndDate ?? undefined,
berthApproved: berth.berthApproved ?? false,
},
});
const tagMutation = useMutation({
mutationFn: (ids: string[]) =>
apiFetch(`/api/v1/berths/${berth.id}/tags`, {
method: 'PUT',
body: { tagIds: ids },
}),
});
async function onSubmit(data: UpdateBerthInput) {
try {
await apiFetch(`/api/v1/berths/${berth.id}`, {
method: 'PATCH',
body: data,
});
await tagMutation.mutateAsync(tagIds);
queryClient.invalidateQueries({ queryKey: ['berths'] });
queryClient.invalidateQueries({ queryKey: ['berth', berth.id] });
toast.success('Berth updated');
onOpenChange(false);
} catch (err: unknown) {
toastError(err);
}
}
const tenureType = watch('tenureType');
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
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 (
<Sheet open={open} onOpenChange={onOpenChange}>
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
<SheetContent className="w-full sm:w-[540px] sm:max-w-none overflow-y-auto">
<SheetHeader>
<SheetTitle>Edit Berth {berth.mooringNumber}</SheetTitle>
</SheetHeader>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 py-6">
{/* Basic Info */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Basic Info
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Label>Area</Label>
<SelectOrEmpty
value={area}
onChange={(v) => setValue('area', v)}
options={BERTH_AREAS}
/>
</div>
<div className="space-y-2">
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Label htmlFor="bowFacing">Bow Facing</Label>
feat(berths): NocoDB-aligned dropdown enums + dual-unit auto-fill Pull verbatim SingleSelect choices from NocoDB Berths via MCP and lock them into BERTH_*_OPTIONS / _TYPES in lib/constants.ts: Side Pontoon (10 values), Mooring Type (5), Cleat Type (2), Cleat Capacity (2), Bollard Type (2), Bollard Capacity (2), Access (5), Area (A–E), Bow Facing (4-value UX-only constraint over a SingleLineText). Power Capacity / Voltage stay numeric inputs (NocoDB stores Number). Add `toSelectOptions()` mapper for shadcn `<Select>` `{value,label}` pairs. Wire every berth dropdown — both the modal form and the inline-edit detail tabs — to `<Select>`. Inline `EditableSpec` gains `selectOptions` for the variant and `linkedUnit { field, multiplier }` to auto-patch the metric column on save (× 0.3048 for ft→m on length, width, draft, nominal boat size, water depth). Promote nominal boat size + tenure type from read-only `<SpecRow>` to `<EditableSpec>` so reps can edit them. Tenure type currently uses the validator's `'permanent' | 'fixed_term'` set; will swap to per-port configurable list once Vocabularies admin lands (Wave 5). Mobile berth cards: replace status-coloured stripe with `mooringLetterDot()` so it groups by dock letter; status conveyed by the existing pill below. Berth detail header: "{Letter} Dock" chip instead of bare "A" / "B" text. Berth area filter: `<Select>` over A/B/C/D/E (renamed to "Dock"). Documents tab gets a one-paragraph explainer disambiguating the spec PDF from deal documents (Interests tab). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:10:24 +02:00
<SelectOrEmpty
value={watch('bowFacing')}
onChange={(v) => setValue('bowFacing', v)}
options={BERTH_BOW_FACING_OPTIONS}
/>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
id="berthApproved"
checked={watch('berthApproved') ?? false}
onCheckedChange={(v) => setValue('berthApproved', v)}
/>
<Label htmlFor="berthApproved">Berth Approved</Label>
</div>
</div>
<Separator />
{/* Dimensions */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Dimensions
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Length (ft)</Label>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Input type="number" step="0.01" {...register('lengthFt')} />
</div>
<div className="space-y-2">
<Label>Length (m)</Label>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Input type="number" step="0.01" {...register('lengthM')} />
</div>
<div className="space-y-2">
<Label>Width (ft)</Label>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Input type="number" step="0.01" {...register('widthFt')} />
</div>
<div className="space-y-2">
<Label>Width (m)</Label>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Input type="number" step="0.01" {...register('widthM')} />
</div>
<div className="space-y-2">
<Label>Draft (ft)</Label>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<Input type="number" step="0.01" {...register('draftFt')} />
</div>
<div className="space-y-2">
<Label>Draft (m)</Label>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<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>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
<div className="flex flex-wrap items-center gap-x-6 gap-y-2">
<div className="flex items-center gap-2">
<Switch
id="widthIsMinimum"
checked={watch('widthIsMinimum') ?? false}
onCheckedChange={(v) => setValue('widthIsMinimum', v)}
/>
<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>
<Separator />
{/* Mooring & Hardware */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Mooring &amp; 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}
/>
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
</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 />
feat(berths): full NocoDB field parity, numeric types, sales edit access Aligns the berths schema with the 117 production rows in NocoDB and exposes every field for editing via the BerthForm sheet. Schema (migration 0020): - power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric (NocoDB stores plain numbers; text was wrong shape and broke filter/sort) - ADD status_override_mode text (1/117 legacy rows have a value; carried forward for parity but not yet wired into the UI) - USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty strings convert cleanly Validator + service: - updateBerthSchema / createBerthSchema use z.coerce.number() for the four numeric fields - berths.service stringifies numeric values for Drizzle's numeric type Form (src/components/berths/berth-form.tsx): - adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag, side pontoon, cleat type/capacity, bollard type/capacity, bow facing - converts to typed selects (with NocoDB option lists in src/lib/constants): area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity, access - power capacity / voltage become numeric inputs (with kW / V hints) Permissions (seed.ts + dev DB): - sales_manager and sales_agent: berths.edit false -> true ("sales will sometimes have to update these and I cannot be the only one") - super_admin / director already had it; viewer stays read-only - dev DB updated in-place via UPDATE roles ... jsonb_set Verification: - pnpm exec vitest run: 858/858 passing - pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing on feat/mobile-foundation, none introduced) - lint clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
{/* 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 */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Price
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Price</Label>
<CurrencyInput
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5 Resolved 65 type errors across the codebase via these v4 migration patterns: - `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth routes + central error handler). - `z.record(value)` now requires explicit key type: `z.record(z.string(), value)`. Updated 7 sites across templates / forms / saved-views / website-inquiries. - `.refine(check, msgFn)` second-arg shape changed — now requires an `{ error: (issue) => ... }` object form. Updated `mergeFieldsSchema` in document-templates validator. - `.transform(...).default(...)` chains: v4 enforces default value type matches transform OUTPUT. Reordered to `.default(...).transform(...)` in list-query / company-memberships handlers. - `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures using `z.input<typeof schema>` (kept for caller flexibility around defaults) now re-parse via `schema.parse(data)` to recover the post-coercion shape Drizzle needs. Done in berth-reservations service. Invoice service narrows `lineItems` locally with a typed cast since re-parsing would double-validate. - `.optional().transform(...)` no longer propagates the optional marker through v4's new ZodPipe. Moved `.optional()` to the END of chain in `optionalDesiredDimSchema` (interests) and documents list query (folderId, signatureOnly). - ZodIssue subtype shapes simplified: `received` removed from invalid_type, `type` renamed to `origin` on too_small. Test fixtures updated. - @hookform/resolvers v5 splits Resolver into 3-generic form (Input, Context, Output). useForm calls in 6 forms (client, yacht, berth, interest, expense, invoices-new-page) now pass explicit generics: `useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`. Verified: tsc clean (0 errors), vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:29:03 +02:00
value={(watch('price') as number | null | undefined) ?? ''}
currency={watch('priceCurrency') ?? 'USD'}
onChange={(v) => setValue('price', v ?? undefined, { shouldDirty: true })}
/>
</div>
<div className="space-y-2">
<Label>Currency</Label>
<CurrencySelect
value={watch('priceCurrency') ?? 'USD'}
onValueChange={(v) => setValue('priceCurrency', v, { shouldDirty: true })}
/>
</div>
</div>
</div>
<Separator />
{/* Tenure */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Tenure
</h3>
<div className="space-y-2">
<Label>Tenure Type</Label>
<Select
value={tenureType}
onValueChange={(v) => setValue('tenureType', v as 'permanent' | 'fixed_term')}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="permanent">Permanent</SelectItem>
<SelectItem value="fixed_term">Fixed Term</SelectItem>
</SelectContent>
</Select>
</div>
{tenureType === 'fixed_term' && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Years</Label>
<Input type="number" {...register('tenureYears')} />
</div>
<div className="space-y-2">
<Label>Start Date</Label>
<Input type="date" {...register('tenureStartDate')} />
</div>
<div className="space-y-2">
<Label>End Date</Label>
<Input type="date" {...register('tenureEndDate')} />
</div>
</div>
)}
</div>
<Separator />
{/* Tags */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Tags
</h3>
<TagPicker selectedIds={tagIds} onChange={setTagIds} />
</div>
<SheetFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}