2026-05-12 14:50:58 +02:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
2026-05-22 15:54:41 +02:00
|
|
|
import {
|
|
|
|
|
DndContext,
|
|
|
|
|
KeyboardSensor,
|
|
|
|
|
PointerSensor,
|
|
|
|
|
closestCenter,
|
|
|
|
|
useSensor,
|
|
|
|
|
useSensors,
|
|
|
|
|
type DragEndEvent,
|
|
|
|
|
} from '@dnd-kit/core';
|
|
|
|
|
import {
|
|
|
|
|
SortableContext,
|
|
|
|
|
arrayMove,
|
|
|
|
|
sortableKeyboardCoordinates,
|
|
|
|
|
useSortable,
|
|
|
|
|
verticalListSortingStrategy,
|
|
|
|
|
} from '@dnd-kit/sortable';
|
|
|
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
|
|
|
import { GripVertical, LayoutGrid } from 'lucide-react';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogTrigger,
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import { Switch } from '@/components/ui/switch';
|
2026-05-22 15:54:41 +02:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-05-12 14:50:58 +02:00
|
|
|
import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets';
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
import type { DashboardWidget, WidgetGroup } from './widget-registry';
|
|
|
|
|
|
|
|
|
|
// The dashboard renders widgets in three independent visual regions at
|
|
|
|
|
// xl (1280+): charts (main column), rails (right aside), feed (full-
|
|
|
|
|
// width). Below xl, all three regions stack into one visual column -
|
|
|
|
|
// from the rep's eye it reads as a single ordered list, so the modal
|
|
|
|
|
// flattens its sortable in that tier. At xl it splits into three
|
|
|
|
|
// region-scoped sortables to match the actual side-by-side layout.
|
|
|
|
|
const GROUP_LABELS: Record<WidgetGroup, string> = {
|
|
|
|
|
chart: 'Charts',
|
|
|
|
|
rail: 'Side rail',
|
|
|
|
|
feed: 'Activity',
|
|
|
|
|
};
|
|
|
|
|
const GROUP_ORDER: readonly WidgetGroup[] = ['chart', 'rail', 'feed'];
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
/**
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* Combined visibility + reorder picker for the dashboard header.
|
2026-05-22 15:54:41 +02:00
|
|
|
*
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* The dashboard renders widgets in three independent visual regions -
|
|
|
|
|
* Charts (main column), Side rail (right aside), Activity (full-width
|
|
|
|
|
* feed). A drag across regions can't change the visual outcome, so the
|
|
|
|
|
* modal exposes one sortable list per region instead of a single flat
|
|
|
|
|
* list that silently fails on cross-region moves. Toggling a widget off
|
|
|
|
|
* moves it to the "Hidden" section; toggling on appends it to the
|
|
|
|
|
* bottom of its native region.
|
2026-05-12 14:50:58 +02:00
|
|
|
*
|
2026-05-22 15:54:41 +02:00
|
|
|
* Both visibility toggles and order changes commit optimistically via
|
|
|
|
|
* `useDashboardWidgets` so the dashboard reflows in the background and
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* the rep can keep editing.
|
2026-05-12 14:50:58 +02:00
|
|
|
*/
|
|
|
|
|
export function CustomizeWidgetsMenu() {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
2026-05-22 15:54:41 +02:00
|
|
|
const {
|
|
|
|
|
allWidgets,
|
|
|
|
|
visibleWidgets,
|
|
|
|
|
visibility,
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
isXlLayout,
|
2026-05-22 15:54:41 +02:00
|
|
|
setVisible,
|
|
|
|
|
setAll,
|
|
|
|
|
setOrder,
|
|
|
|
|
resetToDefaults,
|
|
|
|
|
isSaving,
|
|
|
|
|
} = useDashboardWidgets();
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
const visibleCount = Object.values(visibility).filter(Boolean).length;
|
|
|
|
|
const allVisible = visibleCount === allWidgets.length;
|
|
|
|
|
const allHidden = visibleCount === 0;
|
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units
Berth surfaces
- New compact mooring-chip header (colored plate + status pill, dock-label
in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack
- Berth list gains a "Latest deal stage" column showing the most-advanced
pipeline stage of any active linked interest (server-aggregated, ranks by
PIPELINE_STAGES index)
- "Linked prospect" Select on the status-change dialog rebuilt as a Command
combobox: search, recent-first sort, stage-coloured pills
Pipeline UX
- Reverting an interest to Open with linked berths now prompts: keep the
links, unlink and reset, or cancel. Silent when no berths are linked
- Activity feed + entity-activity feed normalise enum field values via
STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as
"10% Deposit → Contract Sent"
EOI generate dialog
- Inline-editable rows for client name, nationality (country combobox), and
yacht name — pencil affordance saves directly via clients/yachts PATCH
- Replaces the single "Edit on client's page" link with two contextual links
framed by short copy explaining what's inline vs what needs the canonical
page
- Backend EoiContext now includes client.id + yacht.id so the dialog can
PATCH without an extra round-trip
Company form
- New "Connections" section lets the rep attach members (clients) and yachts
during create. Yacht attach uses the existing transfer endpoint so audit
log + ownership history capture the change
- Inline "+ New client" / "+ New yacht" buttons open the canonical forms
stacked over the company sheet
- After save, the form chains to a yacht pull-in prompt (if any attached
client owns yachts not yet linked) and an optional "Create interest" step
pre-filled with the first attached client
Admin
- /admin landing gains a searchable index — typed query flattens groups into
a result list matching label + description + group title
- "Documenso & EOI" card relabelled to "EOI signing service" (consistent
with the user-facing language rename from round 1)
Measurement units (migration 0053)
- interests gains desired_*_m columns + desired_*_unit discriminators so
the rep's literal entry (ft OR m) is preserved verbatim instead of being
reconstructed from a single canonical column on every render
- yachts + berths gain matching *_unit columns alongside their existing
ft + m pairs; defaults to 'ft' so legacy rows still render normally
- Interest form POST/PATCH now sends both ft + m + unit; computed m is
derived from the ft canonical to keep the recommender SQL unchanged
Misc
- Active-deals tile + topbar type their Link href as `Route` instead of `any`
- Unused REPORT_TYPE_LABELS const dropped from generate-report-form
- Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated
to include the new id + unit fields on the EoiContext / Berth shapes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
|
|
|
const matchesDefaults = allWidgets.every((w) => (visibility[w.id] ?? false) === w.defaultVisible);
|
2026-05-12 14:50:58 +02:00
|
|
|
|
2026-05-22 15:54:41 +02:00
|
|
|
// Hidden = everything not currently rendered. Sorted by registry order
|
|
|
|
|
// so it reads predictably (newly-added widgets appear at the bottom
|
|
|
|
|
// until the rep explicitly enables them).
|
|
|
|
|
const hidden = allWidgets.filter((w) => !visibility[w.id]);
|
|
|
|
|
|
|
|
|
|
const sensors = useSensors(
|
|
|
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
|
|
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
|
|
|
);
|
|
|
|
|
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
// Visible widgets split per region. Empty regions render nothing so
|
|
|
|
|
// we don't show an "On dashboard / Side rail (0)" tease.
|
|
|
|
|
const visibleByGroup: Record<WidgetGroup, DashboardWidget[]> = {
|
|
|
|
|
chart: visibleWidgets.filter((w) => w.group === 'chart'),
|
|
|
|
|
rail: visibleWidgets.filter((w) => w.group === 'rail'),
|
|
|
|
|
feed: visibleWidgets.filter((w) => w.group === 'feed'),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A drag inside group X only moves widgets within that group. Rebuild
|
|
|
|
|
// the flat order by walking `visibleWidgets` in its current sequence
|
|
|
|
|
// and replacing each group-X slot with the next id from the reordered
|
|
|
|
|
// group list. This preserves the relative position of every other
|
|
|
|
|
// widget - only the dragged group's internal order changes.
|
|
|
|
|
function reorderGroup(group: WidgetGroup, oldIndex: number, newIndex: number) {
|
|
|
|
|
const groupIds = visibleByGroup[group].map((w) => w.id);
|
|
|
|
|
if (
|
|
|
|
|
oldIndex < 0 ||
|
|
|
|
|
newIndex < 0 ||
|
|
|
|
|
oldIndex >= groupIds.length ||
|
|
|
|
|
newIndex >= groupIds.length
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const reordered = arrayMove(groupIds, oldIndex, newIndex);
|
|
|
|
|
let cursor = 0;
|
|
|
|
|
const nextOrder = visibleWidgets.map((w) =>
|
|
|
|
|
w.group === group ? (reordered[cursor++] ?? w.id) : w.id,
|
|
|
|
|
);
|
|
|
|
|
setOrder(nextOrder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeDragEndHandler(group: WidgetGroup) {
|
|
|
|
|
return (event: DragEndEvent) => {
|
|
|
|
|
const { active, over } = event;
|
|
|
|
|
if (!over || active.id === over.id) return;
|
|
|
|
|
const ids = visibleByGroup[group].map((w) => w.id);
|
|
|
|
|
const oldIndex = ids.indexOf(String(active.id));
|
|
|
|
|
const newIndex = ids.indexOf(String(over.id));
|
|
|
|
|
if (oldIndex === -1 || newIndex === -1) return;
|
|
|
|
|
reorderGroup(group, oldIndex, newIndex);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flat reorder used by the stacked layout (< xl). One SortableContext
|
|
|
|
|
// over every visible widget; drops persist via setOrder, which the
|
|
|
|
|
// hook routes to the mobile order field.
|
|
|
|
|
function onFlatDragEnd(event: DragEndEvent) {
|
2026-05-22 15:54:41 +02:00
|
|
|
const { active, over } = event;
|
|
|
|
|
if (!over || active.id === over.id) return;
|
|
|
|
|
const ids = visibleWidgets.map((w) => w.id);
|
|
|
|
|
const oldIndex = ids.indexOf(String(active.id));
|
|
|
|
|
const newIndex = ids.indexOf(String(over.id));
|
|
|
|
|
if (oldIndex === -1 || newIndex === -1) return;
|
|
|
|
|
setOrder(arrayMove(ids, oldIndex, newIndex));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
|
|
|
<DialogTrigger asChild>
|
|
|
|
|
<Button variant="outline" size="sm" className="gap-1.5">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<LayoutGrid className="h-4 w-4" aria-hidden />
|
2026-05-12 14:50:58 +02:00
|
|
|
Customize
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
<DialogContent className="sm:max-w-2xl">
|
2026-05-12 14:50:58 +02:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Customize dashboard</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
{isXlLayout
|
|
|
|
|
? 'Editing the desktop layout - drag a widget to reorder it within its region.'
|
|
|
|
|
: 'Editing the stacked layout for this device - drag a widget to reorder. Your desktop arrangement is saved separately.'}{' '}
|
|
|
|
|
Toggle the switch to show or hide. Hidden widgets leave no empty space - the layout
|
|
|
|
|
reflows to fill the available width.
|
2026-05-12 14:50:58 +02:00
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
2026-05-22 15:54:41 +02:00
|
|
|
{/* Toggle + reorder list. Capped at ~60vh with internal scroll so
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
the modal doesn't push the action footer off-screen. The
|
|
|
|
|
layout matches what the rep is actually seeing: at xl the
|
|
|
|
|
dashboard renders charts | rails | feed as three independent
|
|
|
|
|
slots, so the picker exposes three region-scoped sortables.
|
|
|
|
|
Below xl everything stacks into one column visually, so the
|
|
|
|
|
picker collapses to a single flat sortable that reorders
|
|
|
|
|
across the whole list. */}
|
2026-05-12 14:50:58 +02:00
|
|
|
<div className="max-h-[60vh] -mx-2 overflow-y-auto px-2">
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
{isXlLayout ? (
|
|
|
|
|
GROUP_ORDER.map((group) => {
|
|
|
|
|
const widgets = visibleByGroup[group];
|
|
|
|
|
if (widgets.length === 0) return null;
|
|
|
|
|
return (
|
|
|
|
|
<Section key={group} title={`${GROUP_LABELS[group]} (${widgets.length})`}>
|
|
|
|
|
<DndContext
|
|
|
|
|
sensors={sensors}
|
|
|
|
|
collisionDetection={closestCenter}
|
|
|
|
|
onDragEnd={makeDragEndHandler(group)}
|
|
|
|
|
>
|
|
|
|
|
<SortableContext
|
|
|
|
|
items={widgets.map((w) => w.id)}
|
|
|
|
|
strategy={verticalListSortingStrategy}
|
|
|
|
|
>
|
|
|
|
|
<ul className="space-y-1">
|
|
|
|
|
{widgets.map((w, idx) => (
|
|
|
|
|
<SortableVisibleRow
|
|
|
|
|
key={w.id}
|
|
|
|
|
widget={w}
|
|
|
|
|
position={idx + 1}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
onToggle={(checked) => setVisible(w.id, checked)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</SortableContext>
|
|
|
|
|
</DndContext>
|
|
|
|
|
</Section>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
) : visibleWidgets.length > 0 ? (
|
2026-05-22 15:54:41 +02:00
|
|
|
<Section title={`On dashboard (${visibleWidgets.length})`}>
|
|
|
|
|
<DndContext
|
|
|
|
|
sensors={sensors}
|
|
|
|
|
collisionDetection={closestCenter}
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
onDragEnd={onFlatDragEnd}
|
2026-05-12 14:50:58 +02:00
|
|
|
>
|
2026-05-22 15:54:41 +02:00
|
|
|
<SortableContext
|
|
|
|
|
items={visibleWidgets.map((w) => w.id)}
|
|
|
|
|
strategy={verticalListSortingStrategy}
|
|
|
|
|
>
|
|
|
|
|
<ul className="space-y-1">
|
|
|
|
|
{visibleWidgets.map((w, idx) => (
|
|
|
|
|
<SortableVisibleRow
|
|
|
|
|
key={w.id}
|
|
|
|
|
widget={w}
|
|
|
|
|
position={idx + 1}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
onToggle={(checked) => setVisible(w.id, checked)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</SortableContext>
|
|
|
|
|
</DndContext>
|
|
|
|
|
</Section>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{hidden.length > 0 ? (
|
|
|
|
|
<Section title={`Hidden (${hidden.length})`}>
|
|
|
|
|
<ul className="space-y-1">
|
|
|
|
|
{hidden.map((w) => (
|
|
|
|
|
<HiddenRow
|
|
|
|
|
key={w.id}
|
|
|
|
|
widget={w}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
onToggle={(checked) => setVisible(w.id, checked)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</Section>
|
|
|
|
|
) : null}
|
2026-05-12 14:50:58 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-2">
|
|
|
|
|
<span className="text-xs text-muted-foreground sm:order-first">
|
|
|
|
|
{visibleCount} of {allWidgets.length} visible
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2 sm:flex-nowrap">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={matchesDefaults || isSaving}
|
|
|
|
|
onClick={resetToDefaults}
|
|
|
|
|
>
|
|
|
|
|
Reset to defaults
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={allHidden || isSaving}
|
|
|
|
|
onClick={() => setAll(false)}
|
|
|
|
|
>
|
|
|
|
|
Hide all
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={allVisible || isSaving}
|
|
|
|
|
onClick={() => setAll(true)}
|
|
|
|
|
>
|
|
|
|
|
Show all
|
|
|
|
|
</Button>
|
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units
Berth surfaces
- New compact mooring-chip header (colored plate + status pill, dock-label
in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack
- Berth list gains a "Latest deal stage" column showing the most-advanced
pipeline stage of any active linked interest (server-aggregated, ranks by
PIPELINE_STAGES index)
- "Linked prospect" Select on the status-change dialog rebuilt as a Command
combobox: search, recent-first sort, stage-coloured pills
Pipeline UX
- Reverting an interest to Open with linked berths now prompts: keep the
links, unlink and reset, or cancel. Silent when no berths are linked
- Activity feed + entity-activity feed normalise enum field values via
STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as
"10% Deposit → Contract Sent"
EOI generate dialog
- Inline-editable rows for client name, nationality (country combobox), and
yacht name — pencil affordance saves directly via clients/yachts PATCH
- Replaces the single "Edit on client's page" link with two contextual links
framed by short copy explaining what's inline vs what needs the canonical
page
- Backend EoiContext now includes client.id + yacht.id so the dialog can
PATCH without an extra round-trip
Company form
- New "Connections" section lets the rep attach members (clients) and yachts
during create. Yacht attach uses the existing transfer endpoint so audit
log + ownership history capture the change
- Inline "+ New client" / "+ New yacht" buttons open the canonical forms
stacked over the company sheet
- After save, the form chains to a yacht pull-in prompt (if any attached
client owns yachts not yet linked) and an optional "Create interest" step
pre-filled with the first attached client
Admin
- /admin landing gains a searchable index — typed query flattens groups into
a result list matching label + description + group title
- "Documenso & EOI" card relabelled to "EOI signing service" (consistent
with the user-facing language rename from round 1)
Measurement units (migration 0053)
- interests gains desired_*_m columns + desired_*_unit discriminators so
the rep's literal entry (ft OR m) is preserved verbatim instead of being
reconstructed from a single canonical column on every render
- yachts + berths gain matching *_unit columns alongside their existing
ft + m pairs; defaults to 'ft' so legacy rows still render normally
- Interest form POST/PATCH now sends both ft + m + unit; computed m is
derived from the ft canonical to keep the recommender SQL unchanged
Misc
- Active-deals tile + topbar type their Link href as `Route` instead of `any`
- Unused REPORT_TYPE_LABELS const dropped from generate-report-form
- Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated
to include the new id + unit fields on the EoiContext / Berth shapes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
|
|
|
<Button size="sm" onClick={() => setOpen(false)} className="w-full sm:w-auto">
|
2026-05-12 14:50:58 +02:00
|
|
|
Done
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-22 15:54:41 +02:00
|
|
|
|
|
|
|
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="py-2 first:pt-1">
|
|
|
|
|
<div className="px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
{title}
|
|
|
|
|
</div>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SortableVisibleRow({
|
|
|
|
|
widget,
|
|
|
|
|
position,
|
|
|
|
|
disabled,
|
|
|
|
|
onToggle,
|
|
|
|
|
}: {
|
|
|
|
|
widget: DashboardWidget;
|
|
|
|
|
position: number;
|
|
|
|
|
disabled: boolean;
|
|
|
|
|
onToggle: (checked: boolean) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
|
|
|
id: widget.id,
|
|
|
|
|
});
|
|
|
|
|
return (
|
|
|
|
|
<li
|
|
|
|
|
ref={setNodeRef}
|
|
|
|
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-start gap-2 rounded-md border bg-background px-2 py-2.5',
|
|
|
|
|
isDragging && 'opacity-60 shadow-md',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="mt-0.5 inline-flex h-7 w-7 shrink-0 cursor-grab items-center justify-center rounded text-muted-foreground hover:bg-accent active:cursor-grabbing"
|
|
|
|
|
aria-label={`Drag handle for ${widget.label}`}
|
|
|
|
|
{...attributes}
|
|
|
|
|
{...listeners}
|
|
|
|
|
>
|
|
|
|
|
<GripVertical className="h-4 w-4" aria-hidden />
|
|
|
|
|
</button>
|
|
|
|
|
<span className="mt-1 w-5 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
|
|
|
|
|
{position}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="text-sm font-medium text-foreground">{widget.label}</div>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{widget.description}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
aria-label={`Show ${widget.label}`}
|
|
|
|
|
checked
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onCheckedChange={onToggle}
|
|
|
|
|
className="mt-0.5 shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function HiddenRow({
|
|
|
|
|
widget,
|
|
|
|
|
disabled,
|
|
|
|
|
onToggle,
|
|
|
|
|
}: {
|
|
|
|
|
widget: DashboardWidget;
|
|
|
|
|
disabled: boolean;
|
|
|
|
|
onToggle: (checked: boolean) => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<li className="flex items-start gap-3 rounded-md px-3 py-2.5 hover:bg-accent/40">
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="text-sm font-medium text-foreground">{widget.label}</div>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{widget.description}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
aria-label={`Show ${widget.label}`}
|
|
|
|
|
checked={false}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onCheckedChange={onToggle}
|
|
|
|
|
className="mt-0.5 shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
}
|