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';
|
|
|
|
|
|
audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.
Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.
Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.
Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.
Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.
Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.
Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
|
|
|
import { useMemo, useState } from 'react';
|
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 {
|
|
|
|
|
flexRender,
|
|
|
|
|
getCoreRowModel,
|
|
|
|
|
useReactTable,
|
|
|
|
|
type ColumnDef,
|
2026-05-01 15:27:53 +02:00
|
|
|
type Row,
|
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
|
|
|
type RowSelectionState,
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
type VisibilityState,
|
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
|
|
|
} from '@tanstack/react-table';
|
|
|
|
|
import { ArrowDown, ArrowUp, ArrowUpDown, Loader2 } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from '@/components/ui/table';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
export interface DataTablePagination {
|
|
|
|
|
page: number;
|
|
|
|
|
pageSize: number;
|
|
|
|
|
total: number;
|
|
|
|
|
totalPages: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BulkAction {
|
|
|
|
|
label: string;
|
|
|
|
|
icon?: React.ElementType;
|
|
|
|
|
variant?: 'default' | 'destructive';
|
|
|
|
|
onClick: (selectedIds: string[]) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DataTableProps<TData> {
|
|
|
|
|
columns: ColumnDef<TData, unknown>[];
|
|
|
|
|
data: TData[];
|
|
|
|
|
pagination?: DataTablePagination;
|
|
|
|
|
onPaginationChange?: (page: number, pageSize: number) => void;
|
|
|
|
|
sort?: { field: string; direction: 'asc' | 'desc' };
|
|
|
|
|
onSortChange?: (field: string, direction: 'asc' | 'desc') => void;
|
|
|
|
|
rowSelection?: RowSelectionState;
|
|
|
|
|
onRowSelectionChange?: (selection: RowSelectionState) => void;
|
|
|
|
|
bulkActions?: BulkAction[];
|
|
|
|
|
emptyState?: React.ReactNode;
|
|
|
|
|
isLoading?: boolean;
|
|
|
|
|
getRowId?: (row: TData) => string;
|
|
|
|
|
onRowClick?: (row: TData) => void;
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
/**
|
|
|
|
|
* Optional row class hook — return a string of Tailwind utilities
|
|
|
|
|
* applied to the `<TableRow>`. Use for visual grouping (e.g. tinting
|
|
|
|
|
* berths by mooring letter so the kanban-like grid reads at a glance).
|
|
|
|
|
*/
|
|
|
|
|
getRowClassName?: (row: TData) => string | undefined;
|
2026-05-01 15:27:53 +02:00
|
|
|
/**
|
|
|
|
|
* Mobile card renderer. When provided, the table is hidden below `lg:`
|
|
|
|
|
* and replaced with a vertical list of cards built from this callback.
|
|
|
|
|
* The same TanStack `table` instance powers both views, so pagination,
|
|
|
|
|
* sort, and selection stay in sync across the breakpoint.
|
|
|
|
|
*/
|
|
|
|
|
cardRender?: (row: Row<TData>) => React.ReactNode;
|
2026-05-12 14:50:58 +02:00
|
|
|
/**
|
|
|
|
|
* Optional grouping key for the mobile card list. When set, consecutive
|
|
|
|
|
* rows that share the same returned key are visually grouped under a
|
|
|
|
|
* header showing the key. Rendered only on mobile (next to cardRender);
|
|
|
|
|
* the desktop table is unaffected. Useful for berths-by-area,
|
|
|
|
|
* documents-by-folder, etc. — pre-sort the data on the same key so
|
|
|
|
|
* adjacent rows already share groups.
|
|
|
|
|
*/
|
|
|
|
|
mobileGroupBy?: (row: TData) => string | null | undefined;
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
/**
|
|
|
|
|
* Per-column visibility map. Keys are column IDs, values mean
|
|
|
|
|
* "currently visible". Columns absent from the map are visible by
|
|
|
|
|
* default — newly-added columns surface for existing users without
|
|
|
|
|
* needing a preferences migration.
|
|
|
|
|
*/
|
|
|
|
|
columnVisibility?: VisibilityState;
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function DataTable<TData>({
|
|
|
|
|
columns,
|
|
|
|
|
data,
|
|
|
|
|
pagination,
|
|
|
|
|
onPaginationChange,
|
|
|
|
|
sort,
|
|
|
|
|
onSortChange,
|
|
|
|
|
rowSelection: externalSelection,
|
|
|
|
|
onRowSelectionChange,
|
|
|
|
|
bulkActions,
|
|
|
|
|
emptyState,
|
|
|
|
|
isLoading,
|
|
|
|
|
getRowId,
|
|
|
|
|
onRowClick,
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
getRowClassName,
|
2026-05-01 15:27:53 +02:00
|
|
|
cardRender,
|
2026-05-12 14:50:58 +02:00
|
|
|
mobileGroupBy,
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
columnVisibility,
|
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
|
|
|
}: DataTableProps<TData>) {
|
|
|
|
|
const [internalSelection, setInternalSelection] = useState<RowSelectionState>({});
|
|
|
|
|
const rowSelectionState = externalSelection ?? internalSelection;
|
|
|
|
|
const setRowSelection = onRowSelectionChange ?? setInternalSelection;
|
|
|
|
|
|
audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.
Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.
Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.
Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.
Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.
Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.
Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
|
|
|
// Memoize the assembled columns array. perf-test-auditor HIGH H2:
|
|
|
|
|
// TanStack docs explicitly warn that rebuilding `columns` on every
|
|
|
|
|
// render resets the table's internal state (sort, filter, sizing).
|
|
|
|
|
// Re-derive only when the source columns or bulkActions presence
|
|
|
|
|
// actually change.
|
|
|
|
|
const hasBulkActions = Boolean(bulkActions && bulkActions.length > 0);
|
|
|
|
|
const allColumns = useMemo<ColumnDef<TData, unknown>[]>(() => {
|
|
|
|
|
const cols: ColumnDef<TData, unknown>[] = [];
|
|
|
|
|
if (hasBulkActions) {
|
|
|
|
|
cols.push({
|
|
|
|
|
id: 'select',
|
|
|
|
|
header: ({ table }) => (
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={
|
|
|
|
|
table.getIsAllPageRowsSelected() ||
|
|
|
|
|
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
|
|
|
|
}
|
|
|
|
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
|
|
|
aria-label="Select all"
|
|
|
|
|
className="translate-y-[2px]"
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
cell: ({ row }) => (
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={row.getIsSelected()}
|
|
|
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
|
|
|
aria-label="Select row"
|
|
|
|
|
className="translate-y-[2px]"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
enableSorting: false,
|
|
|
|
|
size: 40,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
cols.push(...columns);
|
|
|
|
|
return cols;
|
|
|
|
|
}, [columns, hasBulkActions]);
|
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 table = useReactTable({
|
|
|
|
|
data,
|
|
|
|
|
columns: allColumns,
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
|
manualPagination: true,
|
|
|
|
|
manualSorting: true,
|
|
|
|
|
rowCount: pagination?.total ?? data.length,
|
|
|
|
|
state: {
|
|
|
|
|
rowSelection: rowSelectionState,
|
|
|
|
|
pagination: pagination
|
|
|
|
|
? { pageIndex: pagination.page - 1, pageSize: pagination.pageSize }
|
|
|
|
|
: undefined,
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
columnVisibility,
|
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
|
|
|
},
|
|
|
|
|
onRowSelectionChange: (updater) => {
|
2026-04-28 12:10:21 +02:00
|
|
|
const newSelection = typeof updater === 'function' ? updater(rowSelectionState) : updater;
|
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
|
|
|
setRowSelection(newSelection);
|
|
|
|
|
},
|
|
|
|
|
getRowId: getRowId as (row: TData, index: number) => string,
|
|
|
|
|
enableRowSelection: !!bulkActions?.length,
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 12:10:21 +02:00
|
|
|
const selectedIds = Object.keys(rowSelectionState).filter((k) => rowSelectionState[k]);
|
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 handleSort(columnId: string) {
|
|
|
|
|
if (!onSortChange) return;
|
|
|
|
|
if (sort?.field === columnId) {
|
|
|
|
|
onSortChange(columnId, sort.direction === 'asc' ? 'desc' : 'asc');
|
|
|
|
|
} else {
|
|
|
|
|
onSortChange(columnId, 'asc');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSortIcon(columnId: string) {
|
|
|
|
|
if (sort?.field !== columnId) return <ArrowUpDown className="ml-1 h-3.5 w-3.5" />;
|
|
|
|
|
return sort.direction === 'asc' ? (
|
|
|
|
|
<ArrowUp className="ml-1 h-3.5 w-3.5" />
|
|
|
|
|
) : (
|
|
|
|
|
<ArrowDown className="ml-1 h-3.5 w-3.5" />
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 15:27:53 +02:00
|
|
|
const rows = table.getRowModel().rows;
|
|
|
|
|
|
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 (
|
|
|
|
|
<div className="space-y-2">
|
2026-05-01 15:27:53 +02:00
|
|
|
<div className={cn('rounded-md border overflow-x-auto', cardRender && 'hidden lg:block')}>
|
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
|
|
|
<Table>
|
|
|
|
|
<TableHeader className="sticky top-0 z-10 bg-muted/50">
|
|
|
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
|
|
|
<TableRow key={headerGroup.id}>
|
|
|
|
|
{headerGroup.headers.map((header) => (
|
|
|
|
|
<TableHead
|
|
|
|
|
key={header.id}
|
|
|
|
|
style={{ width: header.getSize() !== 150 ? header.getSize() : undefined }}
|
|
|
|
|
className={cn(
|
|
|
|
|
header.column.getCanSort() && onSortChange && 'cursor-pointer select-none',
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
2026-04-28 12:10:21 +02:00
|
|
|
if (
|
|
|
|
|
header.column.getCanSort() &&
|
|
|
|
|
onSortChange &&
|
|
|
|
|
header.column.id !== 'select'
|
|
|
|
|
) {
|
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
|
|
|
handleSort(header.column.id);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
{header.isPlaceholder
|
|
|
|
|
? null
|
|
|
|
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
|
|
|
{header.column.getCanSort() &&
|
|
|
|
|
onSortChange &&
|
|
|
|
|
header.column.id !== 'select' &&
|
|
|
|
|
header.column.id !== 'actions' &&
|
|
|
|
|
getSortIcon(header.column.id)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableHead>
|
|
|
|
|
))}
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={allColumns.length} className="h-40 text-center">
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
|
|
|
<span className="text-muted-foreground">Loading...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
) : table.getRowModel().rows.length === 0 ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={allColumns.length} className="h-40">
|
|
|
|
|
{emptyState ?? (
|
|
|
|
|
<div className="text-center text-muted-foreground">No results.</div>
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
) : (
|
|
|
|
|
table.getRowModel().rows.map((row) => (
|
|
|
|
|
<TableRow
|
|
|
|
|
key={row.id}
|
|
|
|
|
data-state={row.getIsSelected() && 'selected'}
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
className={cn(onRowClick && 'cursor-pointer', getRowClassName?.(row.original))}
|
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
|
|
|
onClick={() => onRowClick?.(row.original)}
|
|
|
|
|
>
|
|
|
|
|
{row.getVisibleCells().map((cell) => (
|
|
|
|
|
<TableCell key={cell.id}>
|
|
|
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
|
|
|
</TableCell>
|
|
|
|
|
))}
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-01 15:27:53 +02:00
|
|
|
{/* Mobile card list */}
|
|
|
|
|
{cardRender && (
|
|
|
|
|
<ul className="lg:hidden flex flex-col gap-2">
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<li className="rounded-md border bg-card p-6 text-center">
|
|
|
|
|
<Loader2 className="mx-auto h-5 w-5 animate-spin text-muted-foreground" />
|
|
|
|
|
</li>
|
|
|
|
|
) : rows.length === 0 ? (
|
|
|
|
|
<li className="rounded-md border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
|
|
|
{emptyState ?? 'No results.'}
|
|
|
|
|
</li>
|
|
|
|
|
) : (
|
2026-05-12 14:50:58 +02:00
|
|
|
(() => {
|
|
|
|
|
// Walk rows once, emitting a section header <li> every time
|
|
|
|
|
// the groupBy key changes. Keeps the existing flex-col gap-2
|
|
|
|
|
// rhythm; the header sits above the first card of each group
|
|
|
|
|
// with a faint top divider for visual rest between blocks.
|
|
|
|
|
let lastGroup: string | null | undefined;
|
|
|
|
|
const nodes: React.ReactNode[] = [];
|
|
|
|
|
rows.forEach((row, i) => {
|
|
|
|
|
const group = mobileGroupBy ? mobileGroupBy(row.original) : undefined;
|
|
|
|
|
if (mobileGroupBy && group !== lastGroup) {
|
|
|
|
|
nodes.push(
|
|
|
|
|
<li key={`__group_${group ?? '_none'}_${i}`} className="px-1 pt-3">
|
|
|
|
|
<div className="flex items-center gap-3 text-base font-bold tracking-tight text-foreground">
|
|
|
|
|
<span>{group ?? 'Other'}</span>
|
|
|
|
|
<span aria-hidden className="h-px flex-1 bg-border" />
|
|
|
|
|
</div>
|
|
|
|
|
</li>,
|
|
|
|
|
);
|
|
|
|
|
lastGroup = group;
|
|
|
|
|
}
|
|
|
|
|
nodes.push(<li key={row.id}>{cardRender(row)}</li>);
|
|
|
|
|
});
|
|
|
|
|
return nodes;
|
|
|
|
|
})()
|
2026-05-01 15:27:53 +02:00
|
|
|
)}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
{/* Pagination — render whenever pagination is defined so the
|
|
|
|
|
page-size selector is reachable even on single-page tables.
|
|
|
|
|
Prev/Next group only renders when there's actually more than
|
|
|
|
|
one page. */}
|
|
|
|
|
{pagination && (
|
|
|
|
|
<div className="flex items-center justify-between px-2 gap-3 flex-wrap">
|
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="text-sm text-muted-foreground">
|
|
|
|
|
{selectedIds.length > 0
|
|
|
|
|
? `${selectedIds.length} of ${pagination.total} row(s) selected`
|
|
|
|
|
: `${pagination.total} row(s) total`}
|
|
|
|
|
</div>
|
feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
{/* Page-size selector — "All" maps to the validator's
|
|
|
|
|
max(1000) cap. If a port has more than 1000 active rows
|
|
|
|
|
the user paginates; we don't quietly drop rows. */}
|
|
|
|
|
<label className="text-sm text-muted-foreground inline-flex items-center gap-1.5">
|
|
|
|
|
Show
|
|
|
|
|
<select
|
|
|
|
|
value={String(pagination.pageSize)}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const next = e.target.value === 'all' ? 1000 : Number(e.target.value);
|
|
|
|
|
onPaginationChange?.(1, next);
|
|
|
|
|
}}
|
|
|
|
|
className="h-8 rounded-md border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
|
|
|
>
|
|
|
|
|
<option value="25">25</option>
|
|
|
|
|
<option value="50">50</option>
|
|
|
|
|
<option value="100">100</option>
|
|
|
|
|
<option value="250">250</option>
|
|
|
|
|
<option value="all">All</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
{pagination.totalPages > 1 && (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={pagination.page <= 1}
|
|
|
|
|
onClick={() => onPaginationChange?.(pagination.page - 1, pagination.pageSize)}
|
|
|
|
|
>
|
|
|
|
|
Previous
|
|
|
|
|
</Button>
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
Page {pagination.page} of {pagination.totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={pagination.page >= pagination.totalPages}
|
|
|
|
|
onClick={() => onPaginationChange?.(pagination.page + 1, pagination.pageSize)}
|
|
|
|
|
>
|
|
|
|
|
Next
|
|
|
|
|
</Button>
|
|
|
|
|
</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>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Bulk actions bar */}
|
|
|
|
|
{bulkActions && selectedIds.length > 0 && (
|
|
|
|
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-lg border bg-background px-4 py-3 shadow-lg animate-in slide-in-from-bottom-4">
|
|
|
|
|
<span className="text-sm font-medium">{selectedIds.length} selected</span>
|
|
|
|
|
{bulkActions.map((action) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={action.label}
|
|
|
|
|
variant={action.variant ?? 'default'}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => action.onClick(selectedIds)}
|
|
|
|
|
>
|
|
|
|
|
{action.icon && <action.icon className="mr-1.5 h-4 w-4" />}
|
|
|
|
|
{action.label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|