feat(dashboard): expand grid to 16 columns with auto-migration for saved 12-col layouts
This commit is contained in:
@@ -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={(
|
||||||
|
|||||||
+2
-2
@@ -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 } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user