Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { Pencil, RefreshCw } from 'lucide-react';
|
2026-05-09 18:37:04 +02:00
|
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
import { useForm } from 'react-hook-form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
2026-04-28 12:09:47 +02:00
|
|
|
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { PermissionGate } from '@/components/shared/permission-gate';
|
|
|
|
|
import { BerthForm } from './berth-form';
|
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
|
|
|
import { mooringLetterDot } from './mooring-letter-tone';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { apiFetch } from '@/lib/api/client';
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
import { toastError } from '@/lib/api/toast-error';
|
2026-05-09 18:37:04 +02:00
|
|
|
import { useVocabulary } from '@/hooks/use-vocabulary';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { updateBerthStatusSchema, type UpdateBerthStatusInput } from '@/lib/validators/berths';
|
|
|
|
|
import { BERTH_STATUSES } from '@/lib/constants';
|
|
|
|
|
|
|
|
|
|
type BerthDetailData = {
|
|
|
|
|
id: string;
|
|
|
|
|
mooringNumber: string;
|
|
|
|
|
area: string | null;
|
|
|
|
|
status: string;
|
|
|
|
|
portId: string;
|
|
|
|
|
lengthFt: string | null;
|
|
|
|
|
lengthM: string | null;
|
|
|
|
|
widthFt: string | null;
|
|
|
|
|
widthM: string | null;
|
|
|
|
|
draftFt: string | null;
|
|
|
|
|
draftM: string | null;
|
|
|
|
|
widthIsMinimum: boolean | null;
|
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;
|
|
|
|
|
cleatType: string | null;
|
|
|
|
|
cleatCapacity: string | null;
|
|
|
|
|
bollardType: string | null;
|
|
|
|
|
bollardCapacity: string | null;
|
|
|
|
|
bowFacing: string | null;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
price: string | null;
|
|
|
|
|
priceCurrency: string;
|
|
|
|
|
tenureType: string;
|
|
|
|
|
tenureYears: number | null;
|
|
|
|
|
tenureStartDate: string | null;
|
|
|
|
|
tenureEndDate: string | null;
|
|
|
|
|
powerCapacity: string | null;
|
|
|
|
|
voltage: string | null;
|
|
|
|
|
mooringType: string | null;
|
|
|
|
|
access: string | null;
|
|
|
|
|
berthApproved: boolean | null;
|
|
|
|
|
tags: Array<{ id: string; name: string; color: string }>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface BerthDetailHeaderProps {
|
|
|
|
|
berth: BerthDetailData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
|
|
|
available: 'bg-green-100 text-green-800 border-green-300',
|
|
|
|
|
under_offer: 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
|
|
|
|
sold: 'bg-red-100 text-red-800 border-red-300',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
|
|
|
available: 'Available',
|
|
|
|
|
under_offer: 'Under Offer',
|
|
|
|
|
sold: 'Sold',
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-09 18:37:04 +02:00
|
|
|
interface InterestOption {
|
|
|
|
|
id: string;
|
|
|
|
|
clientName: string;
|
|
|
|
|
pipelineStage: string;
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
function StatusChangeDialog({
|
|
|
|
|
berthId,
|
|
|
|
|
currentStatus,
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
}: {
|
|
|
|
|
berthId: string;
|
|
|
|
|
currentStatus: string;
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const queryClient = useQueryClient();
|
2026-05-09 18:37:04 +02:00
|
|
|
const reasonChips = useVocabulary('berth_status_change_reasons');
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const {
|
|
|
|
|
register,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
setValue,
|
|
|
|
|
watch,
|
|
|
|
|
reset,
|
|
|
|
|
formState: { isSubmitting },
|
|
|
|
|
} = useForm<UpdateBerthStatusInput>({
|
|
|
|
|
resolver: zodResolver(updateBerthStatusSchema),
|
2026-04-28 12:09:47 +02:00
|
|
|
defaultValues: { status: currentStatus as (typeof BERTH_STATUSES)[number], reason: '' },
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const status = watch('status');
|
2026-05-09 18:37:04 +02:00
|
|
|
const interestId = watch('interestId');
|
|
|
|
|
const showInterestPicker = status === 'under_offer' || status === 'sold';
|
|
|
|
|
|
|
|
|
|
// Active interests for this port — used to populate the prospect
|
|
|
|
|
// selector when status moves to under_offer / sold. Only fetched when
|
|
|
|
|
// the picker is actually visible to avoid an unnecessary round-trip
|
|
|
|
|
// for available-status changes.
|
|
|
|
|
const interestsQuery = useQuery<{
|
|
|
|
|
data: Array<{ id: string; clientName: string; pipelineStage: string }>;
|
|
|
|
|
}>({
|
|
|
|
|
queryKey: ['interests', 'status-link-picker'],
|
|
|
|
|
queryFn: () => apiFetch('/api/v1/interests?pageSize=200'),
|
|
|
|
|
enabled: open && showInterestPicker,
|
|
|
|
|
staleTime: 60_000,
|
|
|
|
|
});
|
|
|
|
|
const interestOptions: InterestOption[] = interestsQuery.data?.data ?? [];
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
async function onSubmit(data: UpdateBerthStatusInput) {
|
|
|
|
|
try {
|
|
|
|
|
await apiFetch(`/api/v1/berths/${berthId}/status`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: data,
|
|
|
|
|
});
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['berths'] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['berth', berthId] });
|
2026-05-09 18:37:04 +02:00
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
toast.success('Status updated');
|
|
|
|
|
reset();
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
} catch (err: unknown) {
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
toastError(err);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Change Status</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>New Status</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={status}
|
2026-05-09 18:37:04 +02:00
|
|
|
onValueChange={(v) => {
|
|
|
|
|
setValue('status', v as (typeof BERTH_STATUSES)[number]);
|
|
|
|
|
// Clear the interest pick when moving back to available so
|
|
|
|
|
// a stale value doesn't sneak through on submit.
|
|
|
|
|
if (v === 'available') setValue('interestId', undefined);
|
|
|
|
|
}}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{BERTH_STATUSES.map((s) => (
|
|
|
|
|
<SelectItem key={s} value={s}>
|
|
|
|
|
{STATUS_LABELS[s]}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Reason *</Label>
|
2026-05-09 18:37:04 +02:00
|
|
|
{reasonChips.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{reasonChips.map((chip) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
key={chip}
|
|
|
|
|
onClick={() => setValue('reason', chip, { shouldDirty: true })}
|
|
|
|
|
className="rounded-full border border-muted-foreground/20 bg-muted px-2.5 py-0.5 text-xs hover:bg-accent"
|
|
|
|
|
>
|
|
|
|
|
{chip}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-28 12:09:47 +02:00
|
|
|
<Textarea {...register('reason')} placeholder="Reason for status change..." rows={3} />
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
2026-05-09 18:37:04 +02:00
|
|
|
{showInterestPicker && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Linked prospect (optional)</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={interestId ?? '__none__'}
|
|
|
|
|
onValueChange={(v) => setValue('interestId', v === '__none__' ? undefined : v)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select an interest…" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="__none__">— No interest —</SelectItem>
|
|
|
|
|
{interestOptions.map((opt) => (
|
|
|
|
|
<SelectItem key={opt.id} value={opt.id}>
|
|
|
|
|
{opt.clientName} · {opt.pipelineStage}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Picking an interest auto-creates a primary berth link if one doesn't already
|
|
|
|
|
exist, so the deal timeline + heat scorer attribute the change correctly.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<DialogFooter>
|
|
|
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
|
|
|
{isSubmitting ? 'Saving...' : 'Update Status'}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
|
|
|
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
|
|
|
const [statusOpen, setStatusOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-04-28 12:09:47 +02:00
|
|
|
<DetailHeaderStrip>
|
2026-05-03 16:03:56 +02:00
|
|
|
{/* Stacks vertically on phone widths so the action buttons don't
|
|
|
|
|
squeeze the area subtitle into a two-line wrap. From sm up the
|
|
|
|
|
title/area block sits side-by-side with the action buttons. */}
|
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-3 flex-wrap">
|
2026-05-11 17:58:42 +02:00
|
|
|
<h1 className="text-xl sm:text-2xl font-bold text-foreground">
|
2026-05-01 16:09:32 +02:00
|
|
|
Berth {berth.mooringNumber}
|
|
|
|
|
</h1>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<span
|
|
|
|
|
className={`inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium ${STATUS_COLORS[berth.status] ?? 'bg-muted text-muted-foreground border-muted'}`}
|
|
|
|
|
>
|
|
|
|
|
{STATUS_LABELS[berth.status] ?? berth.status}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
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.area && (
|
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<span
|
|
|
|
|
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-0.5 text-xs font-semibold uppercase tracking-wide text-white ${mooringLetterDot(berth.mooringNumber) ?? 'bg-slate-400'}`}
|
|
|
|
|
>
|
|
|
|
|
{berth.area} Dock
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-03 16:03:56 +02:00
|
|
|
<div className="flex flex-wrap items-center gap-2 sm:shrink-0">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<PermissionGate resource="berths" action="edit">
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => setStatusOpen(true)}>
|
|
|
|
|
<RefreshCw className="mr-1.5 h-4 w-4" />
|
|
|
|
|
Change Status
|
|
|
|
|
</Button>
|
|
|
|
|
<Button size="sm" onClick={() => setEditOpen(true)}>
|
|
|
|
|
<Pencil className="mr-1.5 h-4 w-4" />
|
|
|
|
|
Edit
|
|
|
|
|
</Button>
|
|
|
|
|
</PermissionGate>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-28 12:09:47 +02:00
|
|
|
</DetailHeaderStrip>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
<BerthForm berth={berth} open={editOpen} onOpenChange={setEditOpen} />
|
|
|
|
|
|
|
|
|
|
<StatusChangeDialog
|
|
|
|
|
berthId={berth.id}
|
|
|
|
|
currentStatus={berth.status}
|
|
|
|
|
open={statusOpen}
|
|
|
|
|
onOpenChange={setStatusOpen}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|