Files
pn-new-crm/src/components/shared/data-table.tsx

467 lines
18 KiB
TypeScript
Raw Normal View History

'use client';
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
import { useMemo, useRef, useState } from 'react';
import {
flexRender,
getCoreRowModel,
useReactTable,
type ColumnDef,
type Row,
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,
} from '@tanstack/react-table';
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
import { useVirtualizer } from '@tanstack/react-virtual';
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;
/**
* 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;
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
/**
* 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;
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
/**
* Opt-in row virtualization. Only renders rows in the viewport (plus a
* small overscan), so a 5000-row client-export list stays at 60 fps.
* The virtualizer uses the surrounding scroll container set
* `virtualHeightPx` (default 600) so we know the visible area at mount.
*
* Off by default because most CRM tables are server-paginated to 20-50
* rows where virtualization is pure overhead. Pass `virtual` only for
* tables that legitimately hold hundreds-to-thousands of rows in memory.
*
* Constraints:
* - Pagination is incompatible with `virtual` (virtualize ALL rows or
* paginate pick one). When both are passed, pagination wins and
* virtualization is silently disabled.
* - Mobile card view is unaffected; virtualization only applies to the
* desktop `<Table>` rendering at lg: and up.
*/
virtual?: boolean;
/** Container pixel height when `virtual` is on. Defaults to 600. */
virtualHeightPx?: number;
/** Pixel height of an average row (used for virtualizer sizing). Defaults
* to 48, which matches the Tailwind h-12 cell height shadcn Table uses. */
virtualRowHeightPx?: number;
}
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,
cardRender,
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
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,
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
virtual,
virtualHeightPx = 600,
virtualRowHeightPx = 48,
}: 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]);
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,
},
onRowSelectionChange: (updater) => {
const newSelection = typeof updater === 'function' ? updater(rowSelectionState) : updater;
setRowSelection(newSelection);
},
getRowId: getRowId as (row: TData, index: number) => string,
enableRowSelection: !!bulkActions?.length,
});
const selectedIds = Object.keys(rowSelectionState).filter((k) => rowSelectionState[k]);
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" />
);
}
const rows = table.getRowModel().rows;
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
// Virtualization gate — pagination wins over virtual (you can't do both).
const virtualEnabled = Boolean(virtual) && !pagination;
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const virtualizer = useVirtualizer({
count: virtualEnabled ? rows.length : 0,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => virtualRowHeightPx,
overscan: 8,
});
const virtualRows = virtualEnabled ? virtualizer.getVirtualItems() : [];
const virtualPaddingTop = virtualRows[0]?.start ?? 0;
const virtualPaddingBottom = virtualEnabled
? virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)
: 0;
return (
<div className="space-y-2">
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
<div
ref={virtualEnabled ? scrollContainerRef : undefined}
style={virtualEnabled ? { height: virtualHeightPx, overflow: 'auto' } : undefined}
className={cn('rounded-md border overflow-x-auto', cardRender && 'hidden lg:block')}
>
<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={() => {
if (
header.column.getCanSort() &&
onSortChange &&
header.column.id !== 'select'
) {
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>
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
) : virtualEnabled ? (
<>
{virtualPaddingTop > 0 ? (
<TableRow style={{ height: virtualPaddingTop }}>
<TableCell colSpan={allColumns.length} className="p-0" />
</TableRow>
) : null}
{virtualRows.map((vRow) => {
const row = rows[vRow.index]!;
return (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
className={cn(
onRowClick && 'cursor-pointer',
getRowClassName?.(row.original),
)}
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
})}
{virtualPaddingBottom > 0 ? (
<TableRow style={{ height: virtualPaddingBottom }}>
<TableCell colSpan={allColumns.length} className="p-0" />
</TableRow>
) : null}
</>
) : (
feat(data-table): opt-in row virtualization via @tanstack/react-virtual Phase 8 — adds `virtual` opt-in to the shared DataTable. Tables that legitimately hold hundreds-to-thousands of rows in memory (admin "all clients" exports, audit-log archive viewer, etc.) now render only the rows in the viewport plus a small overscan. 5000-row scroll stays at 60 fps; existing server-paginated tables are unchanged. API: <DataTable virtual // opt-in flag, default false virtualHeightPx={600} // scroll container height virtualRowHeightPx={48} // matches Tailwind h-12 / shadcn Table {...everything else} /> Guardrails: - `virtual` + `pagination` together → pagination wins; virtual silently disabled. (You can't do both: virtualize-all-rows OR paginate, not both.) - Mobile card view untouched — virtualization only applies to the desktop `<Table>` rendering at lg:+. - Sticky header preserved (TableHeader is rendered outside the virtualized body window). - Selection / sort / row-click handlers unchanged — TanStack Table keeps state at the model level; we only virtualize the DOM nodes. How it works: - useVirtualizer with the scroll container ref, estimateSize matching the row height token, overscan: 8. - Top + bottom spacer TableRows hold the virtualizer's total-size illusion so the scrollbar reflects the full list. - Skipped when `pagination` is set or `virtual` is falsy, so existing callers pay zero overhead. No callers updated yet — the prop is opt-in. Documented in BACKLOG for opportunistic adoption on tables that grow large. 1315/1315 vitest green (no test changes; new prop is purely additive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:09 +02:00
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))}
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 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>
) : (
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
(() => {
// 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;
})()
)}
</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">
<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-hidden focus-visible:ring-1 focus-visible:ring-ring"
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
>
<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>
)}
</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>
);
}