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
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -33,23 +33,35 @@ import {
import { Switch } from '@/components/ui/switch';
import { cn } from '@/lib/utils';
import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets';
import type { DashboardWidget } from './widget-registry';
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'];
/**
* Combined visibility + reorder picker for the dashboard header. Two
* sections in one modal:
* Combined visibility + reorder picker for the dashboard header.
*
* 1. "On dashboard" — visible widgets, each row with a drag handle
* (reorder via dnd-kit single SortableContext, no buckets); flipping
* a switch off moves the row to section 2.
* 2. "Hidden" — widgets currently off; flipping a switch on appends to
* the bottom of section 1.
* 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.
*
* Both visibility toggles and order changes commit optimistically via
* `useDashboardWidgets` so the dashboard reflows in the background and
* the rep can keep editing. The "Rearrange" button on the header is
* gone — order lives here too now, keeping all dashboard layout
* controls in one place.
* the rep can keep editing.
*/
export function CustomizeWidgetsMenu() {
const [open, setOpen] = useState(false);
@@ -57,6 +69,7 @@ export function CustomizeWidgetsMenu() {
allWidgets,
visibleWidgets,
visibility,
isXlLayout,
setVisible,
setAll,
setOrder,
@@ -79,7 +92,53 @@ export function CustomizeWidgetsMenu() {
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
function onDragEnd(event: DragEndEvent) {
// 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) {
const { active, over } = event;
if (!over || active.id === over.id) return;
const ids = visibleWidgets.map((w) => w.id);
@@ -97,24 +156,64 @@ export function CustomizeWidgetsMenu() {
Customize
</Button>
</DialogTrigger>
<DialogContent className="max-w-xl">
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Customize dashboard</DialogTitle>
<DialogDescription>
Drag a visible widget to change its position. Toggle the switch to show or hide. Hidden
widgets leave no empty space - the layout reflows to fill the available width.
{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.
</DialogDescription>
</DialogHeader>
{/* Toggle + reorder list. Capped at ~60vh with internal scroll so
the modal doesn't push the action footer off-screen. */}
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. */}
<div className="max-h-[60vh] -mx-2 overflow-y-auto px-2">
{visibleWidgets.length > 0 ? (
{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 ? (
<Section title={`On dashboard (${visibleWidgets.length})`}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
onDragEnd={onFlatDragEnd}
>
<SortableContext
items={visibleWidgets.map((w) => w.id)}