feat(uat-batch): Group N — dashboard upgrades
N44, N45, N46 from the 2026-05-21 plan.
Shipped:
N44 Pipeline Value tile respects dashboard timeframe. Tile accepts
optional `range` prop and threads it through
/api/v1/dashboard/kpis?range=<slug> + /forecast?range=<slug>.
Service functions accept optional {from,to} bounds and scope
the pipeline-value SQL to interests created within the window.
New parseRangeSlug helper inverts rangeToSlug. Widget registry
forwards the active dashboard range to the tile.
N45 Clients by country widget. New GET
/api/v1/dashboard/clients-by-country groups non-archived
clients by nationality_iso. <ClientsByCountryWidget> renders a
compact ranked list with mini-bars; rows link to
/clients?nationality=<ISO>. Registered as default-visible rail.
N46 Drag-and-drop dashboard widgets. New
preferences.dashboardWidgetOrder?: string[] on user_profiles;
useDashboardWidgets sorts visibleWidgets by the order
(unlisted ids fall through to registry order) and exposes
setOrder(nextOrder) that PATCHes optimistically.
DashboardShell wires @dnd-kit/core + sortable: Rearrange toggle
turns on per-widget grip handles + sortable-context wraps each
group (charts / rails / feed) so drops stay in-group.
PointerSensor 8px activation distance, KeyboardSensor for a11y.
New <SortableWidget> wraps the render — zero footprint when
off.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import dynamic from 'next/dynamic';
|
||||
import { ActiveDealsTile } from './active-deals-tile';
|
||||
import { ActivityFeed } from './activity-feed';
|
||||
import { BerthHeatWidget } from './berth-heat-widget';
|
||||
import { ClientsByCountryWidget } from './clients-by-country-widget';
|
||||
import { HotDealsCard } from './hot-deals-card';
|
||||
import { PipelineValueTile } from './pipeline-value-tile';
|
||||
import { WebsiteGlanceTile } from './website-glance-tile';
|
||||
@@ -121,7 +122,7 @@ export const DASHBOARD_WIDGETS: readonly DashboardWidget[] = [
|
||||
label: 'Pipeline Value',
|
||||
description:
|
||||
'Gross + weighted forecast, broken down by pipeline stage so leadership can see what is near-close vs speculative.',
|
||||
render: () => <PipelineValueTile />,
|
||||
render: (range) => <PipelineValueTile range={range} />,
|
||||
// Lives in the chart grid (not the narrow rail) so the per-stage
|
||||
// breakdown rows have room to breathe alongside the headline numbers,
|
||||
// and the rail stays reserved for reminders / alerts / glance tiles.
|
||||
@@ -182,6 +183,18 @@ export const DASHBOARD_WIDGETS: readonly DashboardWidget[] = [
|
||||
group: 'chart',
|
||||
defaultVisible: true,
|
||||
},
|
||||
{
|
||||
id: 'clients_by_country',
|
||||
label: 'Clients by country',
|
||||
description:
|
||||
'Per-country distribution of the active client book. Click a row to filter the clients list by country.',
|
||||
render: () => <ClientsByCountryWidget />,
|
||||
// Same rail-tile idiom as BerthHeatWidget + HotDealsCard — compact
|
||||
// ranked list with mini-bars. Variant (a) per the master-doc design;
|
||||
// the world-map variant lands alongside the recharts→ECharts pass.
|
||||
group: 'rail',
|
||||
defaultVisible: true,
|
||||
},
|
||||
{
|
||||
id: 'website_analytics',
|
||||
label: 'Website Analytics',
|
||||
|
||||
Reference in New Issue
Block a user