feat(dashboard): expand grid to 16 columns with auto-migration for saved 12-col layouts

This commit is contained in:
2026-04-09 20:50:40 +02:00
parent 446aea5319
commit 5ad1048519
5 changed files with 35 additions and 26 deletions
@@ -321,7 +321,7 @@ export function DashboardClient() {
layouts={layouts} layouts={layouts}
width={gridWidth} width={gridWidth}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} 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} rowHeight={80}
compactor={bothCompactor} compactor={bothCompactor}
onLayoutChange={( onLayoutChange={(
@@ -25,7 +25,7 @@ describe("assistant user self-service dashboard layout tools", () => {
findUnique: vi.fn().mockResolvedValue({ findUnique: vi.fn().mockResolvedValue({
dashboardLayout: { dashboardLayout: {
version: 2, version: 2,
gridCols: 12, gridCols: 16,
widgets: [ widgets: [
// Valid widget type so normalization preserves it // Valid widget type so normalization preserves it
{ id: "stat-1", type: "stat-cards", x: 0, y: 0, w: 12, h: 3 }, { 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 ctx = createToolContext(db, SystemRole.ADMIN);
const layout = { const layout = {
version: 2, version: 2,
gridCols: 12, gridCols: 16,
widgets: [], widgets: [],
}; };
@@ -800,7 +800,7 @@ describe("user dashboard and favorites", () => {
const layout = { const layout = {
version: 2, version: 2,
gridCols: 12, gridCols: 16,
widgets: [], widgets: [],
}; };
@@ -841,7 +841,7 @@ describe("user dashboard and favorites", () => {
await caller.saveDashboardLayout({ await caller.saveDashboardLayout({
layout: { layout: {
version: 2, version: 2,
gridCols: 12, gridCols: 16,
widgets: [], widgets: [],
}, },
}); });
@@ -851,7 +851,7 @@ describe("user dashboard and favorites", () => {
data: { data: {
dashboardLayout: { dashboardLayout: {
version: 2, version: 2,
gridCols: 12, gridCols: 16,
widgets: [], widgets: [],
}, },
}, },
@@ -162,7 +162,10 @@ export function normalizeDashboardLayout(input: unknown): DashboardLayoutConfig
return createDefaultDashboardLayout(); 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 rawWidgets = Array.isArray(input.widgets) ? input.widgets : [];
const widgets: DashboardWidgetInstance[] = []; const widgets: DashboardWidgetInstance[] = [];
const seenIds = new Set<string>(); const seenIds = new Set<string>();
@@ -184,14 +187,20 @@ export function normalizeDashboardLayout(input: unknown): DashboardLayoutConfig
} }
seenIds.add(id); 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<typeof createDashboardWidget<typeof type>>[1] = { const widgetOptions: Parameters<typeof createDashboardWidget<typeof type>>[1] = {
id, id,
...(typeof rawWidget.title === "string" ? { title: rawWidget.title } : {}), ...(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.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.h === "number" ? { h: rawWidget.h } : {}),
...(typeof rawWidget.minW === "number" ? { minW: rawWidget.minW } : {}), ...(rawMinW !== undefined ? { minW: rawMinW } : {}),
...(typeof rawWidget.minH === "number" ? { minH: rawWidget.minH } : {}), ...(typeof rawWidget.minH === "number" ? { minH: rawWidget.minH } : {}),
...(rawWidget.config !== undefined ? { config: rawWidget.config } : {}), ...(rawWidget.config !== undefined ? { config: rawWidget.config } : {}),
}; };
+16 -16
View File
@@ -1,7 +1,7 @@
import { ProjectStatus } from "./enums.js"; import { ProjectStatus } from "./enums.js";
export const DASHBOARD_LAYOUT_VERSION = 2; export const DASHBOARD_LAYOUT_VERSION = 2;
export const DASHBOARD_GRID_COLUMNS = 12; export const DASHBOARD_GRID_COLUMNS = 16;
export interface DashboardWidgetSize { export interface DashboardWidgetSize {
w: number; w: number;
@@ -118,8 +118,8 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Overview Stats", label: "Overview Stats",
description: "Key metrics: total resources, active projects, allocations, budget utilization", description: "Key metrics: total resources, active projects, allocations, budget utilization",
icon: "📊", icon: "📊",
defaultSize: { w: 12, h: 3 }, defaultSize: { w: 16, h: 3 },
minSize: { w: 6, h: 2 }, minSize: { w: 8, h: 2 },
defaultConfig: { showDetails: false }, defaultConfig: { showDetails: false },
}, },
{ {
@@ -127,8 +127,8 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Resource Table", label: "Resource Table",
description: "Filterable list of EIDs with utilization and chargeability", description: "Filterable list of EIDs with utilization and chargeability",
icon: "👥", icon: "👥",
defaultSize: { w: 8, h: 6 }, defaultSize: { w: 10, h: 6 },
minSize: { w: 4, h: 4 }, minSize: { w: 5, h: 4 },
defaultConfig: { showDetails: false }, defaultConfig: { showDetails: false },
}, },
{ {
@@ -136,8 +136,8 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Project Overview", label: "Project Overview",
description: "Projects with costs, person days, and timeline", description: "Projects with costs, person days, and timeline",
icon: "📋", icon: "📋",
defaultSize: { w: 8, h: 6 }, defaultSize: { w: 10, h: 6 },
minSize: { w: 4, h: 4 }, minSize: { w: 5, h: 4 },
defaultConfig: { showDetails: false }, defaultConfig: { showDetails: false },
}, },
{ {
@@ -145,8 +145,8 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Peak Times", label: "Peak Times",
description: "Booked hours vs capacity over time", description: "Booked hours vs capacity over time",
icon: "📈", icon: "📈",
defaultSize: { w: 8, h: 5 }, defaultSize: { w: 10, h: 5 },
minSize: { w: 4, h: 4 }, minSize: { w: 5, h: 4 },
defaultConfig: { defaultConfig: {
showDetails: false, showDetails: false,
granularity: "month", granularity: "month",
@@ -158,7 +158,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Demand View", label: "Demand View",
description: "Staffing demand vs supply by project, person, or chapter", description: "Staffing demand vs supply by project, person, or chapter",
icon: "🔍", icon: "🔍",
defaultSize: { w: 6, h: 5 }, defaultSize: { w: 8, h: 5 },
minSize: { w: 4, h: 4 }, minSize: { w: 4, h: 4 },
defaultConfig: { defaultConfig: {
showDetails: false, showDetails: false,
@@ -170,7 +170,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Top Value Resources", label: "Top Value Resources",
description: "Leaderboard of resources ranked by price/quality value score", description: "Leaderboard of resources ranked by price/quality value score",
icon: "★", icon: "★",
defaultSize: { w: 6, h: 5 }, defaultSize: { w: 8, h: 5 },
minSize: { w: 4, h: 4 }, minSize: { w: 4, h: 4 },
defaultConfig: { defaultConfig: {
showDetails: false, showDetails: false,
@@ -182,7 +182,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Chargeability Overview", label: "Chargeability Overview",
description: "Top-list and watchlist by actual chargeability this month", description: "Top-list and watchlist by actual chargeability this month",
icon: "⚡", icon: "⚡",
defaultSize: { w: 6, h: 8 }, defaultSize: { w: 8, h: 8 },
minSize: { w: 4, h: 6 }, minSize: { w: 4, h: 6 },
defaultConfig: { defaultConfig: {
showDetails: false, showDetails: false,
@@ -195,7 +195,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "My Projects", label: "My Projects",
description: "Quick access to your favorite and responsible projects", description: "Quick access to your favorite and responsible projects",
icon: "⭐", icon: "⭐",
defaultSize: { w: 6, h: 6 }, defaultSize: { w: 8, h: 6 },
minSize: { w: 4, h: 3 }, minSize: { w: 4, h: 3 },
defaultConfig: { defaultConfig: {
showDetails: false, showDetails: false,
@@ -208,7 +208,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Budget Forecast", label: "Budget Forecast",
description: "Budget burn rate and projected exhaustion per active project", description: "Budget burn rate and projected exhaustion per active project",
icon: "💰", icon: "💰",
defaultSize: { w: 6, h: 5 }, defaultSize: { w: 8, h: 5 },
minSize: { w: 4, h: 4 }, minSize: { w: 4, h: 4 },
defaultConfig: { showDetails: false }, defaultConfig: { showDetails: false },
}, },
@@ -217,7 +217,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Skill Gap Analysis", label: "Skill Gap Analysis",
description: "Top skill shortages: open demand vs available supply", description: "Top skill shortages: open demand vs available supply",
icon: "🎯", icon: "🎯",
defaultSize: { w: 6, h: 5 }, defaultSize: { w: 8, h: 5 },
minSize: { w: 4, h: 4 }, minSize: { w: 4, h: 4 },
defaultConfig: { showDetails: false }, defaultConfig: { showDetails: false },
}, },
@@ -226,7 +226,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
label: "Project Health", label: "Project Health",
description: "Composite health score per project: budget, staffing, timeline", description: "Composite health score per project: budget, staffing, timeline",
icon: "🏥", icon: "🏥",
defaultSize: { w: 6, h: 5 }, defaultSize: { w: 8, h: 5 },
minSize: { w: 4, h: 4 }, minSize: { w: 4, h: 4 },
defaultConfig: { showDetails: false }, defaultConfig: { showDetails: false },
}, },