From 5ad10485193f076da328732c6c14b044d4f80de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Thu, 9 Apr 2026 20:50:40 +0200 Subject: [PATCH] feat(dashboard): expand grid to 16 columns with auto-migration for saved 12-col layouts --- .../components/dashboard/DashboardClient.tsx | 2 +- ...user-self-service-dashboard-layout.test.ts | 4 +-- .../api/src/__tests__/user-router.test.ts | 6 ++-- .../shared/src/schemas/dashboard.schema.ts | 17 +++++++--- packages/shared/src/types/dashboard.ts | 32 +++++++++---------- 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/apps/web/src/components/dashboard/DashboardClient.tsx b/apps/web/src/components/dashboard/DashboardClient.tsx index af0b07f..7a43285 100644 --- a/apps/web/src/components/dashboard/DashboardClient.tsx +++ b/apps/web/src/components/dashboard/DashboardClient.tsx @@ -321,7 +321,7 @@ export function DashboardClient() { layouts={layouts} width={gridWidth} breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} - cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} + cols={{ lg: 16, md: 12, sm: 8, xs: 4, xxs: 2 }} rowHeight={80} compactor={bothCompactor} onLayoutChange={( diff --git a/packages/api/src/__tests__/assistant-tools-user-self-service-dashboard-layout.test.ts b/packages/api/src/__tests__/assistant-tools-user-self-service-dashboard-layout.test.ts index 348e652..f4377d3 100644 --- a/packages/api/src/__tests__/assistant-tools-user-self-service-dashboard-layout.test.ts +++ b/packages/api/src/__tests__/assistant-tools-user-self-service-dashboard-layout.test.ts @@ -25,7 +25,7 @@ describe("assistant user self-service dashboard layout tools", () => { findUnique: vi.fn().mockResolvedValue({ dashboardLayout: { version: 2, - gridCols: 12, + gridCols: 16, widgets: [ // Valid widget type so normalization preserves it { id: "stat-1", type: "stat-cards", x: 0, y: 0, w: 12, h: 3 }, @@ -76,7 +76,7 @@ describe("assistant user self-service dashboard layout tools", () => { const ctx = createToolContext(db, SystemRole.ADMIN); const layout = { version: 2, - gridCols: 12, + gridCols: 16, widgets: [], }; diff --git a/packages/api/src/__tests__/user-router.test.ts b/packages/api/src/__tests__/user-router.test.ts index 31b27ad..30399b9 100644 --- a/packages/api/src/__tests__/user-router.test.ts +++ b/packages/api/src/__tests__/user-router.test.ts @@ -800,7 +800,7 @@ describe("user dashboard and favorites", () => { const layout = { version: 2, - gridCols: 12, + gridCols: 16, widgets: [], }; @@ -841,7 +841,7 @@ describe("user dashboard and favorites", () => { await caller.saveDashboardLayout({ layout: { version: 2, - gridCols: 12, + gridCols: 16, widgets: [], }, }); @@ -851,7 +851,7 @@ describe("user dashboard and favorites", () => { data: { dashboardLayout: { version: 2, - gridCols: 12, + gridCols: 16, widgets: [], }, }, diff --git a/packages/shared/src/schemas/dashboard.schema.ts b/packages/shared/src/schemas/dashboard.schema.ts index 2caebfe..8d79a25 100644 --- a/packages/shared/src/schemas/dashboard.schema.ts +++ b/packages/shared/src/schemas/dashboard.schema.ts @@ -162,7 +162,10 @@ export function normalizeDashboardLayout(input: unknown): DashboardLayoutConfig return createDefaultDashboardLayout(); } - const gridCols = clamp(Math.max(1, toInt(input.gridCols) ?? DASHBOARD_GRID_COLUMNS), 1, 24); + const savedGridCols = clamp(Math.max(1, toInt(input.gridCols) ?? DASHBOARD_GRID_COLUMNS), 1, 24); + // Migrate layouts saved with 12 columns to the current 16-column grid. + const needsColumnMigration = savedGridCols === 12 && DASHBOARD_GRID_COLUMNS === 16; + const gridCols = needsColumnMigration ? DASHBOARD_GRID_COLUMNS : savedGridCols; const rawWidgets = Array.isArray(input.widgets) ? input.widgets : []; const widgets: DashboardWidgetInstance[] = []; const seenIds = new Set(); @@ -184,14 +187,20 @@ export function normalizeDashboardLayout(input: unknown): DashboardLayoutConfig } seenIds.add(id); + // Scale x and w when migrating from a 12-column to 16-column layout. + const scaleCol = (v: number) => Math.round(v * 16 / 12); + const rawX = typeof rawWidget.x === "number" ? (needsColumnMigration ? scaleCol(rawWidget.x) : rawWidget.x) : undefined; + const rawW = typeof rawWidget.w === "number" ? (needsColumnMigration ? scaleCol(rawWidget.w) : rawWidget.w) : undefined; + const rawMinW = typeof rawWidget.minW === "number" ? (needsColumnMigration ? scaleCol(rawWidget.minW) : rawWidget.minW) : undefined; + const widgetOptions: Parameters>[1] = { id, ...(typeof rawWidget.title === "string" ? { title: rawWidget.title } : {}), - ...(typeof rawWidget.x === "number" ? { x: rawWidget.x } : {}), + ...(rawX !== undefined ? { x: rawX } : {}), ...(typeof rawWidget.y === "number" ? { y: rawWidget.y } : {}), - ...(typeof rawWidget.w === "number" ? { w: rawWidget.w } : {}), + ...(rawW !== undefined ? { w: rawW } : {}), ...(typeof rawWidget.h === "number" ? { h: rawWidget.h } : {}), - ...(typeof rawWidget.minW === "number" ? { minW: rawWidget.minW } : {}), + ...(rawMinW !== undefined ? { minW: rawMinW } : {}), ...(typeof rawWidget.minH === "number" ? { minH: rawWidget.minH } : {}), ...(rawWidget.config !== undefined ? { config: rawWidget.config } : {}), }; diff --git a/packages/shared/src/types/dashboard.ts b/packages/shared/src/types/dashboard.ts index 7a3f2cc..cc60dad 100644 --- a/packages/shared/src/types/dashboard.ts +++ b/packages/shared/src/types/dashboard.ts @@ -1,7 +1,7 @@ import { ProjectStatus } from "./enums.js"; export const DASHBOARD_LAYOUT_VERSION = 2; -export const DASHBOARD_GRID_COLUMNS = 12; +export const DASHBOARD_GRID_COLUMNS = 16; export interface DashboardWidgetSize { w: number; @@ -118,8 +118,8 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Overview Stats", description: "Key metrics: total resources, active projects, allocations, budget utilization", icon: "📊", - defaultSize: { w: 12, h: 3 }, - minSize: { w: 6, h: 2 }, + defaultSize: { w: 16, h: 3 }, + minSize: { w: 8, h: 2 }, defaultConfig: { showDetails: false }, }, { @@ -127,8 +127,8 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Resource Table", description: "Filterable list of EIDs with utilization and chargeability", icon: "👥", - defaultSize: { w: 8, h: 6 }, - minSize: { w: 4, h: 4 }, + defaultSize: { w: 10, h: 6 }, + minSize: { w: 5, h: 4 }, defaultConfig: { showDetails: false }, }, { @@ -136,8 +136,8 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Project Overview", description: "Projects with costs, person days, and timeline", icon: "📋", - defaultSize: { w: 8, h: 6 }, - minSize: { w: 4, h: 4 }, + defaultSize: { w: 10, h: 6 }, + minSize: { w: 5, h: 4 }, defaultConfig: { showDetails: false }, }, { @@ -145,8 +145,8 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Peak Times", description: "Booked hours vs capacity over time", icon: "📈", - defaultSize: { w: 8, h: 5 }, - minSize: { w: 4, h: 4 }, + defaultSize: { w: 10, h: 5 }, + minSize: { w: 5, h: 4 }, defaultConfig: { showDetails: false, granularity: "month", @@ -158,7 +158,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Demand View", description: "Staffing demand vs supply by project, person, or chapter", icon: "🔍", - defaultSize: { w: 6, h: 5 }, + defaultSize: { w: 8, h: 5 }, minSize: { w: 4, h: 4 }, defaultConfig: { showDetails: false, @@ -170,7 +170,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Top Value Resources", description: "Leaderboard of resources ranked by price/quality value score", icon: "★", - defaultSize: { w: 6, h: 5 }, + defaultSize: { w: 8, h: 5 }, minSize: { w: 4, h: 4 }, defaultConfig: { showDetails: false, @@ -182,7 +182,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Chargeability Overview", description: "Top-list and watchlist by actual chargeability this month", icon: "⚡", - defaultSize: { w: 6, h: 8 }, + defaultSize: { w: 8, h: 8 }, minSize: { w: 4, h: 6 }, defaultConfig: { showDetails: false, @@ -195,7 +195,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "My Projects", description: "Quick access to your favorite and responsible projects", icon: "⭐", - defaultSize: { w: 6, h: 6 }, + defaultSize: { w: 8, h: 6 }, minSize: { w: 4, h: 3 }, defaultConfig: { showDetails: false, @@ -208,7 +208,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Budget Forecast", description: "Budget burn rate and projected exhaustion per active project", icon: "💰", - defaultSize: { w: 6, h: 5 }, + defaultSize: { w: 8, h: 5 }, minSize: { w: 4, h: 4 }, defaultConfig: { showDetails: false }, }, @@ -217,7 +217,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Skill Gap Analysis", description: "Top skill shortages: open demand vs available supply", icon: "🎯", - defaultSize: { w: 6, h: 5 }, + defaultSize: { w: 8, h: 5 }, minSize: { w: 4, h: 4 }, defaultConfig: { showDetails: false }, }, @@ -226,7 +226,7 @@ export const DASHBOARD_WIDGET_CATALOG = [ label: "Project Health", description: "Composite health score per project: budget, staffing, timeline", icon: "🏥", - defaultSize: { w: 6, h: 5 }, + defaultSize: { w: 8, h: 5 }, minSize: { w: 4, h: 4 }, defaultConfig: { showDetails: false }, },