feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -44,9 +44,10 @@ describe("dashboard layout normalization", () => {
|
||||
|
||||
expect(layout.version).toBe(DASHBOARD_LAYOUT_VERSION);
|
||||
expect(layout.widgets).toHaveLength(2);
|
||||
expect(layout.widgets[0]?.config).toEqual({});
|
||||
expect(layout.widgets[0]?.config).toEqual({ showDetails: false });
|
||||
expect(layout.widgets[1]?.y).toBe(3);
|
||||
expect(layout.widgets[1]?.config).toEqual({
|
||||
showDetails: false,
|
||||
granularity: "week",
|
||||
groupBy: "chapter",
|
||||
});
|
||||
|
||||
@@ -36,40 +36,44 @@ function clamp(value: number, min: number, max: number): number {
|
||||
|
||||
const dashboardWidgetTypeSchema = z.enum(DASHBOARD_WIDGET_TYPES);
|
||||
|
||||
const resourceTableWidgetConfigSchema = z.object({
|
||||
const widgetChromeConfigSchema = z.object({
|
||||
showDetails: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const resourceTableWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
chapter: z.preprocess(toNonEmptyString, z.string().optional()),
|
||||
});
|
||||
|
||||
const projectTableWidgetConfigSchema = z.object({
|
||||
const projectTableWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
search: z.preprocess(toNonEmptyString, z.string().optional()),
|
||||
status: z.nativeEnum(ProjectStatus).optional(),
|
||||
});
|
||||
|
||||
const peakTimesWidgetConfigSchema = z.object({
|
||||
const peakTimesWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
granularity: z.enum(["week", "month"]).optional(),
|
||||
groupBy: z.enum(["project", "chapter", "resource"]).optional(),
|
||||
});
|
||||
|
||||
const demandWidgetConfigSchema = z.object({
|
||||
const demandWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
groupBy: z.enum(["project", "person", "chapter"]).optional(),
|
||||
});
|
||||
|
||||
const topValueWidgetConfigSchema = z.object({
|
||||
const topValueWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
});
|
||||
|
||||
const chargeabilityWidgetConfigSchema = z.object({
|
||||
const chargeabilityWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
topN: z.number().int().min(1).max(100).optional(),
|
||||
watchlistThreshold: z.number().int().min(0).max(100).optional(),
|
||||
});
|
||||
|
||||
const myProjectsWidgetConfigSchema = z.object({
|
||||
const myProjectsWidgetConfigSchema = widgetChromeConfigSchema.extend({
|
||||
showFavorites: z.boolean().optional(),
|
||||
showResponsible: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const dashboardWidgetConfigSchemas = {
|
||||
"stat-cards": z.object({}),
|
||||
"stat-cards": widgetChromeConfigSchema,
|
||||
"resource-table": resourceTableWidgetConfigSchema,
|
||||
"project-table": projectTableWidgetConfigSchema,
|
||||
"peak-times-chart": peakTimesWidgetConfigSchema,
|
||||
@@ -77,9 +81,9 @@ export const dashboardWidgetConfigSchemas = {
|
||||
"top-value-resources": topValueWidgetConfigSchema,
|
||||
"chargeability-overview": chargeabilityWidgetConfigSchema,
|
||||
"my-projects": myProjectsWidgetConfigSchema,
|
||||
"budget-forecast": z.object({}),
|
||||
"skill-gap": z.object({}),
|
||||
"project-health": z.object({}),
|
||||
"budget-forecast": widgetChromeConfigSchema,
|
||||
"skill-gap": widgetChromeConfigSchema,
|
||||
"project-health": widgetChromeConfigSchema,
|
||||
} as const;
|
||||
|
||||
type DashboardWidgetConfigSchemaMap = typeof dashboardWidgetConfigSchemas;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const HolidayCalendarScopeSchema = z.enum(["COUNTRY", "STATE", "CITY"]);
|
||||
|
||||
export const CreateHolidayCalendarSchema = z.object({
|
||||
name: z.string().min(1).max(120),
|
||||
scopeType: HolidayCalendarScopeSchema,
|
||||
countryId: z.string(),
|
||||
stateCode: z.string().trim().min(1).max(16).optional(),
|
||||
metroCityId: z.string().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
priority: z.number().int().min(-100).max(100).optional(),
|
||||
});
|
||||
|
||||
export const UpdateHolidayCalendarSchema = z.object({
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
stateCode: z.string().trim().min(1).max(16).nullable().optional(),
|
||||
metroCityId: z.string().nullable().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
priority: z.number().int().min(-100).max(100).optional(),
|
||||
});
|
||||
|
||||
export const CreateHolidayCalendarEntrySchema = z.object({
|
||||
holidayCalendarId: z.string(),
|
||||
date: z.coerce.date(),
|
||||
name: z.string().min(1).max(120),
|
||||
isRecurringAnnual: z.boolean().optional(),
|
||||
source: z.string().max(120).optional(),
|
||||
});
|
||||
|
||||
export const UpdateHolidayCalendarEntrySchema = z.object({
|
||||
date: z.coerce.date().optional(),
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
isRecurringAnnual: z.boolean().optional(),
|
||||
source: z.string().max(120).nullable().optional(),
|
||||
});
|
||||
|
||||
export const PreviewResolvedHolidaysSchema = z.object({
|
||||
countryId: z.string(),
|
||||
stateCode: z.string().trim().min(1).max(16).optional(),
|
||||
metroCityId: z.string().optional(),
|
||||
year: z.number().int().min(2000).max(2100),
|
||||
});
|
||||
|
||||
export type HolidayCalendarScopeInput = z.infer<typeof HolidayCalendarScopeSchema>;
|
||||
export type CreateHolidayCalendarInput = z.infer<typeof CreateHolidayCalendarSchema>;
|
||||
export type UpdateHolidayCalendarInput = z.infer<typeof UpdateHolidayCalendarSchema>;
|
||||
export type CreateHolidayCalendarEntryInput = z.infer<typeof CreateHolidayCalendarEntrySchema>;
|
||||
export type UpdateHolidayCalendarEntryInput = z.infer<typeof UpdateHolidayCalendarEntrySchema>;
|
||||
export type PreviewResolvedHolidaysInput = z.infer<typeof PreviewResolvedHolidaysSchema>;
|
||||
@@ -7,6 +7,7 @@ export * from "./role.schema.js";
|
||||
export * from "./dashboard.schema.js";
|
||||
export * from "./estimate.schema.js";
|
||||
export * from "./country.schema.js";
|
||||
export * from "./holiday-calendar.schema.js";
|
||||
export * from "./org-unit.schema.js";
|
||||
export * from "./utilization-category.schema.js";
|
||||
export * from "./client.schema.js";
|
||||
|
||||
@@ -8,45 +8,49 @@ export interface DashboardWidgetSize {
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface StatCardsWidgetConfig {}
|
||||
export interface DashboardWidgetBaseConfig {
|
||||
showDetails?: boolean;
|
||||
}
|
||||
|
||||
export interface ResourceTableWidgetConfig {
|
||||
export interface StatCardsWidgetConfig extends DashboardWidgetBaseConfig {}
|
||||
|
||||
export interface ResourceTableWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
chapter?: string;
|
||||
}
|
||||
|
||||
export interface ProjectTableWidgetConfig {
|
||||
export interface ProjectTableWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
search?: string;
|
||||
status?: ProjectStatus;
|
||||
}
|
||||
|
||||
export interface PeakTimesWidgetConfig {
|
||||
export interface PeakTimesWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
granularity?: "week" | "month";
|
||||
groupBy?: "project" | "chapter" | "resource";
|
||||
}
|
||||
|
||||
export interface DemandWidgetConfig {
|
||||
export interface DemandWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
groupBy?: "project" | "person" | "chapter";
|
||||
}
|
||||
|
||||
export interface TopValueResourcesWidgetConfig {
|
||||
export interface TopValueResourcesWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface ChargeabilityOverviewWidgetConfig {
|
||||
export interface ChargeabilityOverviewWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
topN?: number;
|
||||
watchlistThreshold?: number;
|
||||
}
|
||||
|
||||
export interface MyProjectsWidgetConfig {
|
||||
export interface MyProjectsWidgetConfig extends DashboardWidgetBaseConfig {
|
||||
showFavorites?: boolean;
|
||||
showResponsible?: boolean;
|
||||
}
|
||||
|
||||
export interface BudgetForecastWidgetConfig {}
|
||||
export interface BudgetForecastWidgetConfig extends DashboardWidgetBaseConfig {}
|
||||
|
||||
export interface SkillGapWidgetConfig {}
|
||||
export interface SkillGapWidgetConfig extends DashboardWidgetBaseConfig {}
|
||||
|
||||
export interface ProjectHealthWidgetConfig {}
|
||||
export interface ProjectHealthWidgetConfig extends DashboardWidgetBaseConfig {}
|
||||
|
||||
export interface DashboardWidgetConfigMap {
|
||||
"stat-cards": StatCardsWidgetConfig;
|
||||
@@ -116,7 +120,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
icon: "📊",
|
||||
defaultSize: { w: 12, h: 3 },
|
||||
minSize: { w: 6, h: 2 },
|
||||
defaultConfig: {},
|
||||
defaultConfig: { showDetails: false },
|
||||
},
|
||||
{
|
||||
type: "resource-table",
|
||||
@@ -125,7 +129,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
icon: "👥",
|
||||
defaultSize: { w: 8, h: 6 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {},
|
||||
defaultConfig: { showDetails: false },
|
||||
},
|
||||
{
|
||||
type: "project-table",
|
||||
@@ -134,7 +138,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
icon: "📋",
|
||||
defaultSize: { w: 8, h: 6 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {},
|
||||
defaultConfig: { showDetails: false },
|
||||
},
|
||||
{
|
||||
type: "peak-times-chart",
|
||||
@@ -144,6 +148,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
defaultSize: { w: 8, h: 5 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {
|
||||
showDetails: false,
|
||||
granularity: "month",
|
||||
groupBy: "project",
|
||||
},
|
||||
@@ -156,6 +161,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
defaultSize: { w: 6, h: 5 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {
|
||||
showDetails: false,
|
||||
groupBy: "project",
|
||||
},
|
||||
},
|
||||
@@ -167,6 +173,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
defaultSize: { w: 6, h: 5 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {
|
||||
showDetails: false,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
@@ -178,6 +185,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
defaultSize: { w: 6, h: 8 },
|
||||
minSize: { w: 4, h: 6 },
|
||||
defaultConfig: {
|
||||
showDetails: false,
|
||||
topN: 10,
|
||||
watchlistThreshold: 15,
|
||||
},
|
||||
@@ -190,6 +198,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
defaultSize: { w: 6, h: 6 },
|
||||
minSize: { w: 4, h: 3 },
|
||||
defaultConfig: {
|
||||
showDetails: false,
|
||||
showFavorites: true,
|
||||
showResponsible: true,
|
||||
},
|
||||
@@ -201,7 +210,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
icon: "💰",
|
||||
defaultSize: { w: 6, h: 5 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {},
|
||||
defaultConfig: { showDetails: false },
|
||||
},
|
||||
{
|
||||
type: "skill-gap",
|
||||
@@ -210,7 +219,7 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
icon: "🎯",
|
||||
defaultSize: { w: 6, h: 5 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {},
|
||||
defaultConfig: { showDetails: false },
|
||||
},
|
||||
{
|
||||
type: "project-health",
|
||||
@@ -219,6 +228,6 @@ export const DASHBOARD_WIDGET_CATALOG = [
|
||||
icon: "🏥",
|
||||
defaultSize: { w: 6, h: 5 },
|
||||
minSize: { w: 4, h: 4 },
|
||||
defaultConfig: {},
|
||||
defaultConfig: { showDetails: false },
|
||||
},
|
||||
] as const satisfies readonly DashboardWidgetCatalogEntry[];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SystemRole } from "./enums.js";
|
||||
|
||||
export const PermissionKey = {
|
||||
VIEW_COSTS: "viewCosts",
|
||||
USE_ASSISTANT_ADVANCED_TOOLS: "useAssistantAdvancedTools",
|
||||
EXPORT_DATA: "exportData",
|
||||
IMPORT_DATA: "importData",
|
||||
APPROVE_VACATIONS: "approveVacations",
|
||||
|
||||
Reference in New Issue
Block a user