chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { z } from "zod";
|
||||
import { AllocationStatus } from "../types/enums.js";
|
||||
|
||||
export const CreateAllocationBaseSchema = z.object({
|
||||
resourceId: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
hoursPerDay: z.number().min(0).max(24),
|
||||
percentage: z.number().min(0).max(100),
|
||||
role: z.string().max(200).optional(),
|
||||
roleId: z.string().optional(),
|
||||
headcount: z.number().int().min(1).default(1),
|
||||
status: z.nativeEnum(AllocationStatus).default(AllocationStatus.PROPOSED),
|
||||
metadata: z.record(z.string(), z.unknown()).default({}),
|
||||
});
|
||||
|
||||
export const CreateDemandRequirementBaseSchema = z.object({
|
||||
projectId: z.string(),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
hoursPerDay: z.number().min(0).max(24),
|
||||
percentage: z.number().min(0).max(100),
|
||||
role: z.string().max(200).optional(),
|
||||
roleId: z.string().optional(),
|
||||
headcount: z.number().int().min(1).default(1),
|
||||
status: z.nativeEnum(AllocationStatus).default(AllocationStatus.PROPOSED),
|
||||
metadata: z.record(z.string(), z.unknown()).default({}),
|
||||
});
|
||||
|
||||
export const CreateAssignmentBaseSchema = z.object({
|
||||
demandRequirementId: z.string().optional(),
|
||||
resourceId: z.string(),
|
||||
projectId: z.string(),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
hoursPerDay: z.number().min(0).max(24),
|
||||
percentage: z.number().min(0).max(100),
|
||||
role: z.string().max(200).optional(),
|
||||
roleId: z.string().optional(),
|
||||
dailyCostCents: z.number().int().min(0).optional(),
|
||||
status: z.nativeEnum(AllocationStatus).default(AllocationStatus.PROPOSED),
|
||||
metadata: z.record(z.string(), z.unknown()).default({}),
|
||||
});
|
||||
|
||||
export const CreateAllocationSchema = CreateAllocationBaseSchema.refine(
|
||||
(data) => data.endDate >= data.startDate,
|
||||
{ message: "End date must be after start date", path: ["endDate"] },
|
||||
);
|
||||
|
||||
export const UpdateAllocationSchema = CreateAllocationBaseSchema.partial();
|
||||
|
||||
export const CreateDemandRequirementSchema = CreateDemandRequirementBaseSchema.refine(
|
||||
(data) => data.endDate >= data.startDate,
|
||||
{ message: "End date must be after start date", path: ["endDate"] },
|
||||
);
|
||||
|
||||
export const UpdateDemandRequirementSchema = CreateDemandRequirementBaseSchema.partial();
|
||||
|
||||
export const CreateAssignmentSchema = CreateAssignmentBaseSchema.refine(
|
||||
(data) => data.endDate >= data.startDate,
|
||||
{ message: "End date must be after start date", path: ["endDate"] },
|
||||
);
|
||||
|
||||
export const UpdateAssignmentSchema = CreateAssignmentBaseSchema.partial();
|
||||
|
||||
export const FillDemandRequirementSchema = z.object({
|
||||
demandRequirementId: z.string(),
|
||||
resourceId: z.string(),
|
||||
hoursPerDay: z.number().min(0.5).max(24).optional(),
|
||||
status: z.nativeEnum(AllocationStatus).optional(),
|
||||
});
|
||||
|
||||
export const FillOpenDemandByAllocationSchema = z.object({
|
||||
allocationId: z.string(),
|
||||
resourceId: z.string(),
|
||||
hoursPerDay: z.number().min(0.5).max(24).optional(),
|
||||
status: z.nativeEnum(AllocationStatus).optional(),
|
||||
});
|
||||
|
||||
export const ShiftProjectSchema = z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
newStartDate: z.coerce.date(),
|
||||
newEndDate: z.coerce.date(),
|
||||
})
|
||||
.refine((data) => data.newEndDate >= data.newStartDate, {
|
||||
message: "New end date must be after new start date",
|
||||
path: ["newEndDate"],
|
||||
});
|
||||
|
||||
export const UpdateAllocationHoursSchema = z.object({
|
||||
allocationId: z.string(),
|
||||
hoursPerDay: z.number().min(0.5).max(24).optional(),
|
||||
startDate: z.coerce.date().optional(),
|
||||
endDate: z.coerce.date().optional(),
|
||||
includeSaturday: z.boolean().optional(),
|
||||
role: z.string().min(1).max(200).optional(),
|
||||
});
|
||||
|
||||
export type CreateAllocationInput = z.infer<typeof CreateAllocationSchema>;
|
||||
export type UpdateAllocationInput = z.infer<typeof UpdateAllocationSchema>;
|
||||
export type CreateDemandRequirementInput = z.infer<typeof CreateDemandRequirementSchema>;
|
||||
export type UpdateDemandRequirementInput = z.infer<typeof UpdateDemandRequirementSchema>;
|
||||
export type CreateAssignmentInput = z.infer<typeof CreateAssignmentSchema>;
|
||||
export type UpdateAssignmentInput = z.infer<typeof UpdateAssignmentSchema>;
|
||||
export type FillDemandRequirementInput = z.infer<typeof FillDemandRequirementSchema>;
|
||||
export type FillOpenDemandByAllocationInput = z.infer<typeof FillOpenDemandByAllocationSchema>;
|
||||
export type ShiftProjectInput = z.infer<typeof ShiftProjectSchema>;
|
||||
export type UpdateAllocationHoursInput = z.infer<typeof UpdateAllocationHoursSchema>;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { z } from "zod";
|
||||
import { BlueprintTarget, FieldType } from "../types/enums.js";
|
||||
|
||||
export const FieldOptionSchema = z.object({
|
||||
value: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
color: z.string().optional(),
|
||||
});
|
||||
|
||||
export const FieldValidationSchema = z.object({
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
minLength: z.number().int().optional(),
|
||||
maxLength: z.number().int().optional(),
|
||||
pattern: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
|
||||
export const BlueprintFieldDefinitionSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().min(1).max(200),
|
||||
key: z.string().min(1).max(100).regex(/^[a-z_][a-z0-9_]*$/, "Must be snake_case"),
|
||||
type: z.nativeEnum(FieldType),
|
||||
required: z.boolean().default(false),
|
||||
description: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
defaultValue: z.unknown().optional(),
|
||||
options: z.array(FieldOptionSchema).optional(),
|
||||
validation: FieldValidationSchema.optional(),
|
||||
order: z.number().int().min(0),
|
||||
group: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CreateBlueprintSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
target: z.nativeEnum(BlueprintTarget),
|
||||
description: z.string().optional(),
|
||||
fieldDefs: z.array(BlueprintFieldDefinitionSchema).default([]),
|
||||
defaults: z.record(z.string(), z.unknown()).default({}),
|
||||
validationRules: z.array(z.object({
|
||||
field: z.string(),
|
||||
rule: z.enum(["required_if", "unique", "min", "max"]),
|
||||
params: z.unknown().optional(),
|
||||
message: z.string().optional(),
|
||||
})).default([]),
|
||||
});
|
||||
|
||||
export const UpdateBlueprintSchema = CreateBlueprintSchema.partial();
|
||||
|
||||
export type CreateBlueprintInput = z.infer<typeof CreateBlueprintSchema>;
|
||||
export type UpdateBlueprintInput = z.infer<typeof UpdateBlueprintSchema>;
|
||||
|
||||
/** Generate a Zod schema from blueprint field definitions at runtime */
|
||||
export function generateDynamicZodSchema(
|
||||
fieldDefs: z.infer<typeof BlueprintFieldDefinitionSchema>[],
|
||||
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
|
||||
for (const field of fieldDefs) {
|
||||
let fieldSchema: z.ZodTypeAny;
|
||||
|
||||
switch (field.type) {
|
||||
case FieldType.TEXT:
|
||||
case FieldType.TEXTAREA:
|
||||
case FieldType.URL:
|
||||
case FieldType.EMAIL:
|
||||
fieldSchema = z.string();
|
||||
if (field.validation?.minLength !== undefined) {
|
||||
fieldSchema = (fieldSchema as z.ZodString).min(field.validation.minLength);
|
||||
}
|
||||
if (field.validation?.maxLength !== undefined) {
|
||||
fieldSchema = (fieldSchema as z.ZodString).max(field.validation.maxLength);
|
||||
}
|
||||
if (field.type === FieldType.EMAIL) {
|
||||
fieldSchema = (fieldSchema as z.ZodString).email();
|
||||
}
|
||||
if (field.type === FieldType.URL) {
|
||||
fieldSchema = (fieldSchema as z.ZodString).url();
|
||||
}
|
||||
break;
|
||||
case FieldType.NUMBER:
|
||||
fieldSchema = z.number();
|
||||
if (field.validation?.min !== undefined) {
|
||||
fieldSchema = (fieldSchema as z.ZodNumber).min(field.validation.min);
|
||||
}
|
||||
if (field.validation?.max !== undefined) {
|
||||
fieldSchema = (fieldSchema as z.ZodNumber).max(field.validation.max);
|
||||
}
|
||||
break;
|
||||
case FieldType.BOOLEAN:
|
||||
fieldSchema = z.boolean();
|
||||
break;
|
||||
case FieldType.DATE:
|
||||
fieldSchema = z.coerce.date();
|
||||
break;
|
||||
case FieldType.SELECT:
|
||||
if (field.options && field.options.length > 0) {
|
||||
const values = field.options.map((o) => o.value) as [string, ...string[]];
|
||||
fieldSchema = z.enum(values);
|
||||
} else {
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
break;
|
||||
case FieldType.MULTI_SELECT:
|
||||
if (field.options && field.options.length > 0) {
|
||||
const values = field.options.map((o) => o.value) as [string, ...string[]];
|
||||
fieldSchema = z.array(z.enum(values));
|
||||
} else {
|
||||
fieldSchema = z.array(z.string());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
fieldSchema = z.unknown();
|
||||
}
|
||||
|
||||
if (!field.required) {
|
||||
fieldSchema = fieldSchema.optional();
|
||||
}
|
||||
|
||||
shape[field.key] = fieldSchema;
|
||||
}
|
||||
|
||||
return z.object(shape);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateClientSchema = z.object({
|
||||
name: z.string().min(1).max(300),
|
||||
code: z.string().max(50).optional(),
|
||||
parentId: z.string().optional(),
|
||||
sortOrder: z.number().int().default(0),
|
||||
});
|
||||
|
||||
export const UpdateClientSchema = z.object({
|
||||
name: z.string().min(1).max(300).optional(),
|
||||
code: z.string().max(50).nullable().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
parentId: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type CreateClientInput = z.infer<typeof CreateClientSchema>;
|
||||
export type UpdateClientInput = z.infer<typeof UpdateClientSchema>;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const SpainScheduleRuleSchema = z.object({
|
||||
type: z.literal("spain"),
|
||||
fridayHours: z.number().positive(),
|
||||
summerPeriod: z.object({
|
||||
from: z.string().regex(/^\d{2}-\d{2}$/),
|
||||
to: z.string().regex(/^\d{2}-\d{2}$/),
|
||||
}),
|
||||
summerHours: z.number().positive(),
|
||||
regularHours: z.number().positive(),
|
||||
});
|
||||
|
||||
export const CreateCountrySchema = z.object({
|
||||
code: z.string().min(2).max(3).toUpperCase(),
|
||||
name: z.string().min(1).max(100),
|
||||
dailyWorkingHours: z.number().positive().max(24).default(8),
|
||||
scheduleRules: SpainScheduleRuleSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export const UpdateCountrySchema = CreateCountrySchema.partial().extend({
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const CreateMetroCitySchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
countryId: z.string(),
|
||||
});
|
||||
|
||||
export const UpdateMetroCitySchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
});
|
||||
|
||||
export type CreateCountryInput = z.infer<typeof CreateCountrySchema>;
|
||||
export type UpdateCountryInput = z.infer<typeof UpdateCountrySchema>;
|
||||
export type CreateMetroCityInput = z.infer<typeof CreateMetroCitySchema>;
|
||||
export type UpdateMetroCityInput = z.infer<typeof UpdateMetroCitySchema>;
|
||||
@@ -0,0 +1,228 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
DASHBOARD_GRID_COLUMNS,
|
||||
DASHBOARD_LAYOUT_VERSION,
|
||||
DASHBOARD_WIDGET_CATALOG,
|
||||
DASHBOARD_WIDGET_TYPES,
|
||||
type DashboardLayoutConfig,
|
||||
type DashboardWidgetCatalogEntry,
|
||||
type DashboardWidgetConfigMap,
|
||||
type DashboardWidgetInstance,
|
||||
type DashboardWidgetType,
|
||||
} from "../types/dashboard.js";
|
||||
import { ProjectStatus } from "../types/enums.js";
|
||||
|
||||
const DASHBOARD_WIDGET_BY_TYPE = Object.fromEntries(
|
||||
DASHBOARD_WIDGET_CATALOG.map((widget) => [widget.type, widget]),
|
||||
) as Record<DashboardWidgetType, DashboardWidgetCatalogEntry>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function toInt(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : undefined;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
const dashboardWidgetTypeSchema = z.enum(DASHBOARD_WIDGET_TYPES);
|
||||
|
||||
const resourceTableWidgetConfigSchema = z.object({
|
||||
chapter: z.preprocess(toNonEmptyString, z.string().optional()),
|
||||
});
|
||||
|
||||
const projectTableWidgetConfigSchema = z.object({
|
||||
search: z.preprocess(toNonEmptyString, z.string().optional()),
|
||||
status: z.nativeEnum(ProjectStatus).optional(),
|
||||
});
|
||||
|
||||
const peakTimesWidgetConfigSchema = z.object({
|
||||
granularity: z.enum(["week", "month"]).optional(),
|
||||
groupBy: z.enum(["project", "chapter", "resource"]).optional(),
|
||||
});
|
||||
|
||||
const demandWidgetConfigSchema = z.object({
|
||||
groupBy: z.enum(["project", "person", "chapter"]).optional(),
|
||||
});
|
||||
|
||||
const topValueWidgetConfigSchema = z.object({
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
});
|
||||
|
||||
const chargeabilityWidgetConfigSchema = z.object({
|
||||
topN: z.number().int().min(1).max(100).optional(),
|
||||
watchlistThreshold: z.number().int().min(0).max(100).optional(),
|
||||
});
|
||||
|
||||
export const dashboardWidgetConfigSchemas = {
|
||||
"stat-cards": z.object({}),
|
||||
"resource-table": resourceTableWidgetConfigSchema,
|
||||
"project-table": projectTableWidgetConfigSchema,
|
||||
"peak-times-chart": peakTimesWidgetConfigSchema,
|
||||
"demand-view": demandWidgetConfigSchema,
|
||||
"top-value-resources": topValueWidgetConfigSchema,
|
||||
"chargeability-overview": chargeabilityWidgetConfigSchema,
|
||||
} as const;
|
||||
|
||||
type DashboardWidgetConfigSchemaMap = typeof dashboardWidgetConfigSchemas;
|
||||
|
||||
export function getDashboardWidgetCatalogEntry(type: DashboardWidgetType): DashboardWidgetCatalogEntry {
|
||||
return DASHBOARD_WIDGET_BY_TYPE[type];
|
||||
}
|
||||
|
||||
export function getNextDashboardWidgetY(widgets: DashboardWidgetInstance[]): number {
|
||||
return widgets.reduce((max, widget) => Math.max(max, widget.y + widget.h), 0);
|
||||
}
|
||||
|
||||
export function normalizeDashboardWidgetConfig<T extends DashboardWidgetType>(
|
||||
type: T,
|
||||
config: unknown,
|
||||
): DashboardWidgetConfigMap[T] {
|
||||
const schema = dashboardWidgetConfigSchemas[type];
|
||||
const parsed = schema.safeParse(isRecord(config) ? config : {});
|
||||
return {
|
||||
...DASHBOARD_WIDGET_BY_TYPE[type].defaultConfig,
|
||||
...(parsed.success ? parsed.data : {}),
|
||||
} as DashboardWidgetConfigMap[T];
|
||||
}
|
||||
|
||||
export function createDashboardWidget<T extends DashboardWidgetType>(
|
||||
type: T,
|
||||
options: {
|
||||
id: string;
|
||||
title?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
w?: number;
|
||||
h?: number;
|
||||
minW?: number;
|
||||
minH?: number;
|
||||
config?: unknown;
|
||||
},
|
||||
): DashboardWidgetInstance<T> {
|
||||
const widgetDef = DASHBOARD_WIDGET_BY_TYPE[type];
|
||||
const minW = Math.max(1, toInt(options.minW) ?? widgetDef.minSize.w);
|
||||
const minH = Math.max(1, toInt(options.minH) ?? widgetDef.minSize.h);
|
||||
const w = clamp(Math.max(minW, toInt(options.w) ?? widgetDef.defaultSize.w), minW, DASHBOARD_GRID_COLUMNS);
|
||||
const h = Math.max(minH, toInt(options.h) ?? widgetDef.defaultSize.h);
|
||||
const title = toNonEmptyString(options.title);
|
||||
|
||||
return {
|
||||
id: options.id,
|
||||
type,
|
||||
x: clamp(Math.max(0, toInt(options.x) ?? 0), 0, Math.max(0, DASHBOARD_GRID_COLUMNS - w)),
|
||||
y: Math.max(0, toInt(options.y) ?? 0),
|
||||
w,
|
||||
h,
|
||||
minW,
|
||||
minH,
|
||||
config: normalizeDashboardWidgetConfig(type, options.config),
|
||||
...(title ? { title } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultDashboardLayout(): DashboardLayoutConfig {
|
||||
return {
|
||||
version: DASHBOARD_LAYOUT_VERSION,
|
||||
gridCols: DASHBOARD_GRID_COLUMNS,
|
||||
widgets: [
|
||||
createDashboardWidget("stat-cards", {
|
||||
id: "default-stat-cards",
|
||||
x: 0,
|
||||
y: 0,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDashboardLayout(input: unknown): DashboardLayoutConfig {
|
||||
if (!isRecord(input)) {
|
||||
return createDefaultDashboardLayout();
|
||||
}
|
||||
|
||||
const gridCols = clamp(Math.max(1, toInt(input.gridCols) ?? DASHBOARD_GRID_COLUMNS), 1, 24);
|
||||
const rawWidgets = Array.isArray(input.widgets) ? input.widgets : [];
|
||||
const widgets: DashboardWidgetInstance[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
let nextY = 0;
|
||||
|
||||
rawWidgets.forEach((rawWidget, index) => {
|
||||
if (!isRecord(rawWidget)) return;
|
||||
|
||||
const typeResult = dashboardWidgetTypeSchema.safeParse(rawWidget.type);
|
||||
if (!typeResult.success) return;
|
||||
|
||||
const type = typeResult.data;
|
||||
const baseId = toNonEmptyString(rawWidget.id) ?? `${type}-${index + 1}`;
|
||||
let id = baseId;
|
||||
let suffix = 1;
|
||||
while (seenIds.has(id)) {
|
||||
suffix += 1;
|
||||
id = `${baseId}-${suffix}`;
|
||||
}
|
||||
seenIds.add(id);
|
||||
|
||||
const widgetOptions: Parameters<typeof createDashboardWidget<typeof type>>[1] = {
|
||||
id,
|
||||
...(typeof rawWidget.title === "string" ? { title: rawWidget.title } : {}),
|
||||
...(typeof rawWidget.x === "number" ? { x: rawWidget.x } : {}),
|
||||
...(typeof rawWidget.y === "number" ? { y: rawWidget.y } : {}),
|
||||
...(typeof rawWidget.w === "number" ? { w: rawWidget.w } : {}),
|
||||
...(typeof rawWidget.h === "number" ? { h: rawWidget.h } : {}),
|
||||
...(typeof rawWidget.minW === "number" ? { minW: rawWidget.minW } : {}),
|
||||
...(typeof rawWidget.minH === "number" ? { minH: rawWidget.minH } : {}),
|
||||
...(rawWidget.config !== undefined ? { config: rawWidget.config } : {}),
|
||||
};
|
||||
|
||||
const widget = createDashboardWidget(type, widgetOptions);
|
||||
|
||||
const placedWidget = {
|
||||
...widget,
|
||||
x: clamp(widget.x, 0, Math.max(0, gridCols - widget.w)),
|
||||
y: toInt(rawWidget.y) !== undefined && (toInt(rawWidget.y) ?? 0) >= 0 ? widget.y : nextY,
|
||||
w: clamp(widget.w, widget.minW, gridCols),
|
||||
};
|
||||
|
||||
widgets.push(placedWidget);
|
||||
nextY = Math.max(nextY, placedWidget.y + placedWidget.h);
|
||||
});
|
||||
|
||||
return {
|
||||
version: DASHBOARD_LAYOUT_VERSION,
|
||||
gridCols,
|
||||
widgets,
|
||||
};
|
||||
}
|
||||
|
||||
export const dashboardWidgetInstanceSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
type: dashboardWidgetTypeSchema,
|
||||
title: z.string().min(1).optional(),
|
||||
x: z.number().int().min(0),
|
||||
y: z.number().int().min(0),
|
||||
w: z.number().int().min(1),
|
||||
h: z.number().int().min(1),
|
||||
minW: z.number().int().min(1),
|
||||
minH: z.number().int().min(1),
|
||||
config: z.record(z.unknown()),
|
||||
});
|
||||
|
||||
export const dashboardLayoutSchema = z
|
||||
.unknown()
|
||||
.transform((value) => normalizeDashboardLayout(value))
|
||||
.pipe(
|
||||
z.object({
|
||||
version: z.literal(DASHBOARD_LAYOUT_VERSION),
|
||||
widgets: z.array(dashboardWidgetInstanceSchema),
|
||||
gridCols: z.number().int().min(1).max(24),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,169 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
AllocationType,
|
||||
DispoImportSourceKind,
|
||||
DispoStagedRecordType,
|
||||
ImportBatchStatus,
|
||||
OrderType,
|
||||
ResourceType,
|
||||
StagedRecordStatus,
|
||||
VacationType,
|
||||
} from "../types/enums.js";
|
||||
|
||||
const JsonRecordSchema = z.record(z.string(), z.unknown());
|
||||
const StagedTraceSchema = z.object({
|
||||
importBatchId: z.string(),
|
||||
status: z.nativeEnum(StagedRecordStatus),
|
||||
sourceKind: z.nativeEnum(DispoImportSourceKind),
|
||||
sourceWorkbook: z.string().min(1),
|
||||
sourceSheet: z.string().min(1),
|
||||
sourceRow: z.number().int().nonnegative(),
|
||||
sourceColumn: z.string().min(1).optional().nullable(),
|
||||
warnings: z.array(z.string()).default([]),
|
||||
errorMessage: z.string().optional().nullable(),
|
||||
rawPayload: z.unknown(),
|
||||
normalizedData: JsonRecordSchema.default({}),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const ImportBatchSchema = z.object({
|
||||
id: z.string(),
|
||||
sourceSystem: z.string().min(1),
|
||||
status: z.nativeEnum(ImportBatchStatus),
|
||||
referenceSourceFile: z.string().optional().nullable(),
|
||||
planningSourceFile: z.string().optional().nullable(),
|
||||
chargeabilitySourceFile: z.string().optional().nullable(),
|
||||
notes: z.string().optional().nullable(),
|
||||
summary: JsonRecordSchema.default({}),
|
||||
startedAt: z.coerce.date().optional().nullable(),
|
||||
stagedAt: z.coerce.date().optional().nullable(),
|
||||
approvedAt: z.coerce.date().optional().nullable(),
|
||||
committedAt: z.coerce.date().optional().nullable(),
|
||||
failedAt: z.coerce.date().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const StagedResourceSchema = StagedTraceSchema.extend({
|
||||
id: z.string(),
|
||||
canonicalExternalId: z.string().min(1),
|
||||
enterpriseId: z.string().optional().nullable(),
|
||||
eid: z.string().optional().nullable(),
|
||||
displayName: z.string().optional().nullable(),
|
||||
email: z.string().email().optional().nullable(),
|
||||
chapter: z.string().optional().nullable(),
|
||||
chapterCode: z.string().optional().nullable(),
|
||||
managementLevelGroupName: z.string().optional().nullable(),
|
||||
managementLevelName: z.string().optional().nullable(),
|
||||
countryCode: z.string().optional().nullable(),
|
||||
metroCityName: z.string().optional().nullable(),
|
||||
clientUnitName: z.string().optional().nullable(),
|
||||
resourceType: z.nativeEnum(ResourceType).optional().nullable(),
|
||||
chargeabilityTarget: z.number().min(0).max(100).optional().nullable(),
|
||||
fte: z.number().min(0).max(1).optional().nullable(),
|
||||
lcrCents: z.number().int().min(0).optional().nullable(),
|
||||
ucrCents: z.number().int().min(0).optional().nullable(),
|
||||
availability: JsonRecordSchema.optional().nullable(),
|
||||
roleTokens: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export const StagedClientSchema = StagedTraceSchema.extend({
|
||||
id: z.string(),
|
||||
clientCode: z.string().optional().nullable(),
|
||||
parentClientCode: z.string().optional().nullable(),
|
||||
name: z.string().min(1),
|
||||
sortOrder: z.number().int().optional().nullable(),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const StagedProjectSchema = StagedTraceSchema.extend({
|
||||
id: z.string(),
|
||||
projectKey: z.string().min(1),
|
||||
shortCode: z.string().optional().nullable(),
|
||||
name: z.string().optional().nullable(),
|
||||
clientCode: z.string().optional().nullable(),
|
||||
utilizationCategoryCode: z.string().optional().nullable(),
|
||||
orderType: z.nativeEnum(OrderType).optional().nullable(),
|
||||
allocationType: z.nativeEnum(AllocationType).optional().nullable(),
|
||||
winProbability: z.number().int().min(0).max(100).optional().nullable(),
|
||||
isInternal: z.boolean().default(false),
|
||||
isTbd: z.boolean().default(false),
|
||||
startDate: z.coerce.date().optional().nullable(),
|
||||
endDate: z.coerce.date().optional().nullable(),
|
||||
});
|
||||
|
||||
export const StagedAssignmentSchema = StagedTraceSchema.extend({
|
||||
id: z.string(),
|
||||
resourceExternalId: z.string().min(1),
|
||||
projectKey: z.string().optional().nullable(),
|
||||
assignmentDate: z.coerce.date().optional().nullable(),
|
||||
startDate: z.coerce.date().optional().nullable(),
|
||||
endDate: z.coerce.date().optional().nullable(),
|
||||
hoursPerDay: z.number().min(0).max(24).optional().nullable(),
|
||||
percentage: z.number().min(0).max(100).optional().nullable(),
|
||||
slotFraction: z.number().min(0).max(1).optional().nullable(),
|
||||
roleToken: z.string().optional().nullable(),
|
||||
roleName: z.string().optional().nullable(),
|
||||
chapterToken: z.string().optional().nullable(),
|
||||
utilizationCategoryCode: z.string().optional().nullable(),
|
||||
winProbability: z.number().int().min(0).max(100).optional().nullable(),
|
||||
isInternal: z.boolean().default(false),
|
||||
isUnassigned: z.boolean().default(false),
|
||||
isTbd: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const StagedVacationSchema = StagedTraceSchema.extend({
|
||||
id: z.string(),
|
||||
resourceExternalId: z.string().min(1),
|
||||
vacationType: z.nativeEnum(VacationType),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
note: z.string().optional().nullable(),
|
||||
holidayName: z.string().optional().nullable(),
|
||||
isHalfDay: z.boolean().default(false),
|
||||
halfDayPart: z.string().optional().nullable(),
|
||||
isPublicHoliday: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const StagedAvailabilityRuleSchema = StagedTraceSchema.extend({
|
||||
id: z.string(),
|
||||
resourceExternalId: z.string().min(1),
|
||||
ruleType: z.string().min(1),
|
||||
weekday: z.number().int().min(0).max(6).optional().nullable(),
|
||||
effectiveStartDate: z.coerce.date().optional().nullable(),
|
||||
effectiveEndDate: z.coerce.date().optional().nullable(),
|
||||
availableHours: z.number().min(0).max(24).optional().nullable(),
|
||||
percentage: z.number().min(0).max(100).optional().nullable(),
|
||||
isResolved: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const StagedUnresolvedRecordSchema = z.object({
|
||||
id: z.string(),
|
||||
importBatchId: z.string(),
|
||||
status: z.nativeEnum(StagedRecordStatus),
|
||||
sourceKind: z.nativeEnum(DispoImportSourceKind),
|
||||
sourceWorkbook: z.string().min(1),
|
||||
sourceSheet: z.string().min(1),
|
||||
sourceRow: z.number().int().nonnegative(),
|
||||
sourceColumn: z.string().min(1).optional().nullable(),
|
||||
recordType: z.nativeEnum(DispoStagedRecordType),
|
||||
resourceExternalId: z.string().optional().nullable(),
|
||||
projectKey: z.string().optional().nullable(),
|
||||
message: z.string().min(1),
|
||||
resolutionHint: z.string().optional().nullable(),
|
||||
warnings: z.array(z.string()).default([]),
|
||||
rawPayload: z.unknown(),
|
||||
normalizedData: JsonRecordSchema.default({}),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export type ImportBatchInput = z.infer<typeof ImportBatchSchema>;
|
||||
export type StagedResourceInput = z.infer<typeof StagedResourceSchema>;
|
||||
export type StagedClientInput = z.infer<typeof StagedClientSchema>;
|
||||
export type StagedProjectInput = z.infer<typeof StagedProjectSchema>;
|
||||
export type StagedAssignmentInput = z.infer<typeof StagedAssignmentSchema>;
|
||||
export type StagedVacationInput = z.infer<typeof StagedVacationSchema>;
|
||||
export type StagedAvailabilityRuleInput = z.infer<typeof StagedAvailabilityRuleSchema>;
|
||||
export type StagedUnresolvedRecordInput = z.infer<typeof StagedUnresolvedRecordSchema>;
|
||||
@@ -0,0 +1,356 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EstimateExportFormat,
|
||||
EstimateStatus,
|
||||
EstimateVersionStatus,
|
||||
} from "../types/enums.js";
|
||||
|
||||
const jsonRecordSchema = z.record(z.string(), z.unknown());
|
||||
const numericRecordSchema = z.record(z.string(), z.number());
|
||||
const demandLineRateModeSchema = z.enum(["resource", "manual"]);
|
||||
|
||||
export const EstimateDemandLineCalculationMetadataSchema = z.object({
|
||||
costRateMode: demandLineRateModeSchema.default("manual"),
|
||||
billRateMode: demandLineRateModeSchema.default("manual"),
|
||||
totalMode: z.literal("computed").default("computed"),
|
||||
liveCostRateCents: z.number().int().min(0).nullable().optional(),
|
||||
liveBillRateCents: z.number().int().min(0).nullable().optional(),
|
||||
liveCurrency: z.string().length(3).nullable().optional(),
|
||||
});
|
||||
|
||||
export const EstimateDemandLineMetadataSchema = z
|
||||
.object({
|
||||
calculation: EstimateDemandLineCalculationMetadataSchema.optional(),
|
||||
})
|
||||
.catchall(z.unknown());
|
||||
|
||||
export const EstimateAssumptionSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
category: z.string().min(1).max(100),
|
||||
key: z.string().min(1).max(100),
|
||||
label: z.string().min(1).max(200),
|
||||
valueType: z.string().min(1).max(50).default("json"),
|
||||
value: z.unknown(),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
notes: z.string().max(2_000).optional(),
|
||||
});
|
||||
|
||||
export const ScopeItemSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
sequenceNo: z.number().int().min(0),
|
||||
scopeType: z.string().min(1).max(100),
|
||||
packageCode: z.string().max(100).optional(),
|
||||
name: z.string().min(1).max(500),
|
||||
description: z.string().max(5_000).optional(),
|
||||
scene: z.string().max(200).optional(),
|
||||
page: z.string().max(100).optional(),
|
||||
location: z.string().max(200).optional(),
|
||||
assumptionCategory: z.string().max(100).optional(),
|
||||
technicalSpec: jsonRecordSchema.default({}),
|
||||
frameCount: z.number().int().min(0).optional(),
|
||||
itemCount: z.number().min(0).optional(),
|
||||
unitMode: z.string().max(100).optional(),
|
||||
internalComments: z.string().max(5_000).optional(),
|
||||
externalComments: z.string().max(5_000).optional(),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
metadata: jsonRecordSchema.default({}),
|
||||
});
|
||||
|
||||
export const EstimateDemandLineSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
scopeItemId: z.string().optional(),
|
||||
roleId: z.string().optional(),
|
||||
resourceId: z.string().optional(),
|
||||
lineType: z.string().min(1).max(100).default("LABOR"),
|
||||
name: z.string().min(1).max(500),
|
||||
chapter: z.string().max(200).optional(),
|
||||
hours: z.number().min(0),
|
||||
days: z.number().min(0).optional(),
|
||||
fte: z.number().min(0).optional(),
|
||||
rateSource: z.string().max(200).optional(),
|
||||
costRateCents: z.number().int().min(0).default(0),
|
||||
billRateCents: z.number().int().min(0).default(0),
|
||||
currency: z.string().length(3).default("EUR"),
|
||||
costTotalCents: z.number().int().min(0).default(0),
|
||||
priceTotalCents: z.number().int().min(0).default(0),
|
||||
monthlySpread: numericRecordSchema.default({}),
|
||||
staffingAttributes: jsonRecordSchema.default({}),
|
||||
metadata: EstimateDemandLineMetadataSchema.default({}),
|
||||
});
|
||||
|
||||
export const ResourceCostSnapshotSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
resourceId: z.string().optional(),
|
||||
sourceEid: z.string().max(100).optional(),
|
||||
displayName: z.string().min(1).max(500),
|
||||
chapter: z.string().max(200).optional(),
|
||||
roleId: z.string().optional(),
|
||||
currency: z.string().length(3).default("EUR"),
|
||||
lcrCents: z.number().int().min(0),
|
||||
ucrCents: z.number().int().min(0),
|
||||
fte: z.number().min(0).optional(),
|
||||
location: z.string().max(200).optional(),
|
||||
country: z.string().max(200).optional(),
|
||||
level: z.string().max(100).optional(),
|
||||
workType: z.string().max(100).optional(),
|
||||
attributes: jsonRecordSchema.default({}),
|
||||
});
|
||||
|
||||
export const EstimateMetricSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
key: z.string().min(1).max(100),
|
||||
label: z.string().min(1).max(200),
|
||||
metricGroup: z.string().max(100).optional(),
|
||||
valueDecimal: z.number(),
|
||||
valueCents: z.number().int().optional(),
|
||||
currency: z.string().length(3).optional(),
|
||||
metadata: jsonRecordSchema.default({}),
|
||||
});
|
||||
|
||||
export const EstimateExportSummarySchema = z.object({
|
||||
estimateId: z.string(),
|
||||
estimateName: z.string().min(1).max(500),
|
||||
versionId: z.string(),
|
||||
versionNumber: z.number().int().min(1),
|
||||
versionStatus: z.nativeEnum(EstimateVersionStatus),
|
||||
projectId: z.string().nullable().optional(),
|
||||
projectName: z.string().nullable().optional(),
|
||||
baseCurrency: z.string().length(3),
|
||||
assumptionCount: z.number().int().min(0),
|
||||
scopeItemCount: z.number().int().min(0),
|
||||
demandLineCount: z.number().int().min(0),
|
||||
resourceSnapshotCount: z.number().int().min(0),
|
||||
totalHours: z.number().min(0),
|
||||
totalCostCents: z.number().int(),
|
||||
totalPriceCents: z.number().int(),
|
||||
marginCents: z.number().int(),
|
||||
marginPercent: z.number(),
|
||||
});
|
||||
|
||||
export const EstimateExportArtifactPayloadSchema = z.object({
|
||||
schemaVersion: z.number().int().min(1).default(1),
|
||||
format: z.nativeEnum(EstimateExportFormat),
|
||||
mimeType: z.string().min(1).max(200),
|
||||
encoding: z.enum(["utf8", "base64"]),
|
||||
fileExtension: z.string().min(1).max(20),
|
||||
generatedAt: z.string().datetime(),
|
||||
byteLength: z.number().int().min(0),
|
||||
rowCount: z.number().int().min(0).nullable().optional(),
|
||||
lineCount: z.number().int().min(0).nullable().optional(),
|
||||
sheetNames: z.array(z.string().min(1).max(200)).optional(),
|
||||
previewText: z.string().nullable().optional(),
|
||||
content: z.string(),
|
||||
summary: EstimateExportSummarySchema,
|
||||
});
|
||||
|
||||
export const EstimateExportSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
format: z.nativeEnum(EstimateExportFormat),
|
||||
fileName: z.string().min(1).max(500),
|
||||
storageKey: z.string().max(500).optional(),
|
||||
payload: z.union([EstimateExportArtifactPayloadSchema, jsonRecordSchema]).optional(),
|
||||
});
|
||||
|
||||
export const EstimateVersionSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
versionNumber: z.number().int().min(1).default(1),
|
||||
label: z.string().max(200).optional(),
|
||||
status: z.nativeEnum(EstimateVersionStatus).default(
|
||||
EstimateVersionStatus.WORKING,
|
||||
),
|
||||
notes: z.string().max(5_000).optional(),
|
||||
lockedAt: z.coerce.date().optional(),
|
||||
projectSnapshot: jsonRecordSchema.default({}),
|
||||
assumptions: z.array(EstimateAssumptionSchema).default([]),
|
||||
scopeItems: z.array(ScopeItemSchema).default([]),
|
||||
demandLines: z.array(EstimateDemandLineSchema).default([]),
|
||||
resourceSnapshots: z.array(ResourceCostSnapshotSchema).default([]),
|
||||
metrics: z.array(EstimateMetricSchema).default([]),
|
||||
exports: z.array(EstimateExportSchema).default([]),
|
||||
});
|
||||
|
||||
export const CreateEstimateSchema = z.object({
|
||||
projectId: z.string().optional(),
|
||||
name: z.string().min(1).max(500),
|
||||
opportunityId: z.string().max(200).optional(),
|
||||
baseCurrency: z.string().length(3).default("EUR"),
|
||||
status: z.nativeEnum(EstimateStatus).default(EstimateStatus.DRAFT),
|
||||
versionLabel: z.string().max(200).optional(),
|
||||
versionNotes: z.string().max(5_000).optional(),
|
||||
assumptions: z.array(EstimateAssumptionSchema).default([]),
|
||||
scopeItems: z.array(ScopeItemSchema).default([]),
|
||||
demandLines: z.array(EstimateDemandLineSchema).default([]),
|
||||
resourceSnapshots: z.array(ResourceCostSnapshotSchema).default([]),
|
||||
metrics: z.array(EstimateMetricSchema).default([]),
|
||||
});
|
||||
|
||||
export const UpdateEstimateSchema = CreateEstimateSchema.partial();
|
||||
export const UpdateEstimateDraftSchema = CreateEstimateSchema.partial().extend({
|
||||
id: z.string(),
|
||||
assumptions: z.array(EstimateAssumptionSchema).default([]),
|
||||
scopeItems: z.array(ScopeItemSchema).default([]),
|
||||
demandLines: z.array(EstimateDemandLineSchema).default([]),
|
||||
resourceSnapshots: z.array(ResourceCostSnapshotSchema).default([]),
|
||||
metrics: z.array(EstimateMetricSchema).default([]),
|
||||
});
|
||||
|
||||
export const SubmitEstimateVersionSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
versionId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ApproveEstimateVersionSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
versionId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CreateEstimateRevisionSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
sourceVersionId: z.string().optional(),
|
||||
label: z.string().max(200).optional(),
|
||||
notes: z.string().max(5_000).optional(),
|
||||
});
|
||||
|
||||
export const CreateEstimateExportSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
versionId: z.string().optional(),
|
||||
format: z.nativeEnum(EstimateExportFormat),
|
||||
});
|
||||
|
||||
export const CreateEstimatePlanningHandoffSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
versionId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CloneEstimateSchema = z.object({
|
||||
sourceEstimateId: z.string(),
|
||||
name: z.string().min(1).max(500).optional(),
|
||||
projectId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const EffortUnitModeSchema = z.enum(["per_frame", "per_item", "flat"]);
|
||||
|
||||
export const EffortRuleSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
scopeType: z.string().min(1).max(100),
|
||||
discipline: z.string().min(1).max(200),
|
||||
chapter: z.string().max(200).optional(),
|
||||
unitMode: EffortUnitModeSchema,
|
||||
hoursPerUnit: z.number().min(0),
|
||||
description: z.string().max(1000).optional(),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
export const CreateEffortRuleSetSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().max(2000).optional(),
|
||||
isDefault: z.boolean().default(false),
|
||||
rules: z.array(EffortRuleSchema).default([]),
|
||||
});
|
||||
|
||||
export const UpdateEffortRuleSetSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
description: z.string().max(2000).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
rules: z.array(EffortRuleSchema).optional(),
|
||||
});
|
||||
|
||||
export const ApplyEffortRulesSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
ruleSetId: z.string(),
|
||||
mode: z.enum(["replace", "append"]).default("replace"),
|
||||
});
|
||||
|
||||
export type CreateEffortRuleSetInput = z.infer<typeof CreateEffortRuleSetSchema>;
|
||||
export type UpdateEffortRuleSetInput = z.infer<typeof UpdateEffortRuleSetSchema>;
|
||||
export type ApplyEffortRulesInput = z.infer<typeof ApplyEffortRulesSchema>;
|
||||
|
||||
// ─── Experience Multipliers ──────────────────────────────────────────────────
|
||||
|
||||
export const ExperienceMultiplierRuleSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
chapter: z.string().max(200).optional(),
|
||||
location: z.string().max(200).optional(),
|
||||
level: z.string().max(100).optional(),
|
||||
costMultiplier: z.number().min(0).default(1.0),
|
||||
billMultiplier: z.number().min(0).default(1.0),
|
||||
shoringRatio: z.number().min(0).max(1).optional(),
|
||||
additionalEffortRatio: z.number().min(0).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
export const CreateExperienceMultiplierSetSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().max(2000).optional(),
|
||||
isDefault: z.boolean().default(false),
|
||||
rules: z.array(ExperienceMultiplierRuleSchema).default([]),
|
||||
});
|
||||
|
||||
export const UpdateExperienceMultiplierSetSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
description: z.string().max(2000).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
rules: z.array(ExperienceMultiplierRuleSchema).optional(),
|
||||
});
|
||||
|
||||
export const ApplyExperienceMultipliersSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
multiplierSetId: z.string(),
|
||||
});
|
||||
|
||||
export type CreateExperienceMultiplierSetInput = z.infer<typeof CreateExperienceMultiplierSetSchema>;
|
||||
export type UpdateExperienceMultiplierSetInput = z.infer<typeof UpdateExperienceMultiplierSetSchema>;
|
||||
export type ApplyExperienceMultipliersInput = z.infer<typeof ApplyExperienceMultipliersSchema>;
|
||||
|
||||
export const EstimateListFiltersSchema = z.object({
|
||||
projectId: z.string().optional(),
|
||||
status: z.nativeEnum(EstimateStatus).optional(),
|
||||
query: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
export const PhasingPatternSchema = z.enum([
|
||||
"even",
|
||||
"front_loaded",
|
||||
"back_loaded",
|
||||
"custom",
|
||||
]);
|
||||
|
||||
export const GenerateWeeklyPhasingSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
pattern: PhasingPatternSchema.default("even"),
|
||||
});
|
||||
|
||||
export const UpdateWeeklyPhasingSchema = z.object({
|
||||
estimateId: z.string(),
|
||||
demandLineId: z.string(),
|
||||
weeklyHours: z.record(z.string(), z.number().min(0)),
|
||||
});
|
||||
|
||||
export type GenerateWeeklyPhasingInput = z.infer<typeof GenerateWeeklyPhasingSchema>;
|
||||
export type UpdateWeeklyPhasingInput = z.infer<typeof UpdateWeeklyPhasingSchema>;
|
||||
|
||||
export type CreateEstimateInput = z.infer<typeof CreateEstimateSchema>;
|
||||
export type UpdateEstimateInput = z.infer<typeof UpdateEstimateSchema>;
|
||||
export type UpdateEstimateDraftInput = z.infer<typeof UpdateEstimateDraftSchema>;
|
||||
export type EstimateListFilters = z.infer<typeof EstimateListFiltersSchema>;
|
||||
export type SubmitEstimateVersionInput = z.infer<
|
||||
typeof SubmitEstimateVersionSchema
|
||||
>;
|
||||
export type ApproveEstimateVersionInput = z.infer<
|
||||
typeof ApproveEstimateVersionSchema
|
||||
>;
|
||||
export type CreateEstimateRevisionInput = z.infer<
|
||||
typeof CreateEstimateRevisionSchema
|
||||
>;
|
||||
export type CreateEstimateExportInput = z.infer<
|
||||
typeof CreateEstimateExportSchema
|
||||
>;
|
||||
export type CreateEstimatePlanningHandoffInput = z.infer<
|
||||
typeof CreateEstimatePlanningHandoffSchema
|
||||
>;
|
||||
export type CloneEstimateInput = z.infer<typeof CloneEstimateSchema>;
|
||||
@@ -0,0 +1,15 @@
|
||||
export * from "./resource.schema.js";
|
||||
export * from "./project.schema.js";
|
||||
export * from "./allocation.schema.js";
|
||||
export * from "./blueprint.schema.js";
|
||||
export * from "./vacation.schema.js";
|
||||
export * from "./role.schema.js";
|
||||
export * from "./dashboard.schema.js";
|
||||
export * from "./estimate.schema.js";
|
||||
export * from "./country.schema.js";
|
||||
export * from "./org-unit.schema.js";
|
||||
export * from "./utilization-category.schema.js";
|
||||
export * from "./client.schema.js";
|
||||
export * from "./management-level.schema.js";
|
||||
export * from "./rate-card.schema.js";
|
||||
export * from "./dispo-import.schema.js";
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateManagementLevelGroupSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
targetPercentage: z.number().min(0).max(1),
|
||||
sortOrder: z.number().int().default(0),
|
||||
});
|
||||
|
||||
export const UpdateManagementLevelGroupSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
targetPercentage: z.number().min(0).max(1).optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export const CreateManagementLevelSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
groupId: z.string(),
|
||||
});
|
||||
|
||||
export const UpdateManagementLevelSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
groupId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateManagementLevelGroupInput = z.infer<typeof CreateManagementLevelGroupSchema>;
|
||||
export type UpdateManagementLevelGroupInput = z.infer<typeof UpdateManagementLevelGroupSchema>;
|
||||
export type CreateManagementLevelInput = z.infer<typeof CreateManagementLevelSchema>;
|
||||
export type UpdateManagementLevelInput = z.infer<typeof UpdateManagementLevelSchema>;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateOrgUnitSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
shortName: z.string().max(50).optional(),
|
||||
level: z.number().int().min(5).max(7),
|
||||
parentId: z.string().optional(),
|
||||
sortOrder: z.number().int().default(0),
|
||||
});
|
||||
|
||||
export const UpdateOrgUnitSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
shortName: z.string().max(50).nullable().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
parentId: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type CreateOrgUnitInput = z.infer<typeof CreateOrgUnitSchema>;
|
||||
export type UpdateOrgUnitInput = z.infer<typeof UpdateOrgUnitSchema>;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod";
|
||||
import { AllocationType, OrderType, ProjectStatus } from "../types/enums.js";
|
||||
|
||||
export const StaffingRequirementSchema = z.object({
|
||||
id: z.string().uuid().default(() => crypto.randomUUID()),
|
||||
role: z.string().min(1).max(200),
|
||||
requiredSkills: z.array(z.string()),
|
||||
preferredSkills: z.array(z.string()).optional(),
|
||||
hoursPerDay: z.number().min(0).max(24),
|
||||
headcount: z.number().int().min(1),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
chapter: z.string().optional(),
|
||||
});
|
||||
|
||||
// Base object schema — used for .partial() in UpdateProjectSchema
|
||||
export const CreateProjectBaseSchema = z.object({
|
||||
shortCode: z.string().min(1).max(20).regex(/^[A-Z0-9_-]+$/, "Must be uppercase alphanumeric"),
|
||||
name: z.string().min(1).max(500),
|
||||
orderType: z.nativeEnum(OrderType),
|
||||
allocationType: z.nativeEnum(AllocationType),
|
||||
winProbability: z.number().int().min(0).max(100).default(100),
|
||||
budgetCents: z.number().int().min(0),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
staffingReqs: z.array(StaffingRequirementSchema).default([]),
|
||||
dynamicFields: z.record(z.string(), z.unknown()).default({}),
|
||||
blueprintId: z.string().optional(),
|
||||
status: z.nativeEnum(ProjectStatus).default(ProjectStatus.DRAFT),
|
||||
responsiblePerson: z.string().max(200).optional(),
|
||||
utilizationCategoryId: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
});
|
||||
|
||||
// Full schema with date-range validation
|
||||
export const CreateProjectSchema = CreateProjectBaseSchema.refine(
|
||||
(data) => data.endDate >= data.startDate,
|
||||
{ message: "End date must be after start date", path: ["endDate"] },
|
||||
);
|
||||
|
||||
export const UpdateProjectSchema = CreateProjectBaseSchema.partial();
|
||||
|
||||
export type CreateProjectInput = z.infer<typeof CreateProjectSchema>;
|
||||
export type UpdateProjectInput = z.infer<typeof UpdateProjectSchema>;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// ─── Rate Card Line ──────────────────────────────────────────────────────────
|
||||
|
||||
export const CreateRateCardLineSchema = z.object({
|
||||
roleId: z.string().optional(),
|
||||
chapter: z.string().max(200).optional(),
|
||||
location: z.string().max(200).optional(),
|
||||
seniority: z.string().max(100).optional(),
|
||||
workType: z.string().max(100).optional(),
|
||||
serviceGroup: z.string().max(100).optional(),
|
||||
costRateCents: z.number().int().min(0),
|
||||
billRateCents: z.number().int().min(0).optional(),
|
||||
machineRateCents: z.number().int().min(0).optional(),
|
||||
attributes: z.record(z.unknown()).default({}),
|
||||
});
|
||||
|
||||
export const UpdateRateCardLineSchema = CreateRateCardLineSchema.partial();
|
||||
|
||||
export type CreateRateCardLineInput = z.infer<typeof CreateRateCardLineSchema>;
|
||||
export type UpdateRateCardLineInput = z.infer<typeof UpdateRateCardLineSchema>;
|
||||
|
||||
// ─── Rate Card ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const CreateRateCardSchema = z.object({
|
||||
name: z.string().min(1).max(300),
|
||||
currency: z.string().length(3).default("EUR"),
|
||||
effectiveFrom: z.coerce.date().optional(),
|
||||
effectiveTo: z.coerce.date().optional(),
|
||||
source: z.string().max(200).optional(),
|
||||
clientId: z.string().optional(),
|
||||
lines: z.array(CreateRateCardLineSchema).default([]),
|
||||
});
|
||||
|
||||
export const UpdateRateCardSchema = z.object({
|
||||
name: z.string().min(1).max(300).optional(),
|
||||
currency: z.string().length(3).optional(),
|
||||
effectiveFrom: z.coerce.date().nullable().optional(),
|
||||
effectiveTo: z.coerce.date().nullable().optional(),
|
||||
source: z.string().max(200).nullable().optional(),
|
||||
clientId: z.string().nullable().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateRateCardInput = z.infer<typeof CreateRateCardSchema>;
|
||||
export type UpdateRateCardInput = z.infer<typeof UpdateRateCardSchema>;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { z } from "zod";
|
||||
import { ResourceType } from "../types/enums.js";
|
||||
|
||||
export const WeekdayAvailabilitySchema = z.object({
|
||||
monday: z.number().min(0).max(24),
|
||||
tuesday: z.number().min(0).max(24),
|
||||
wednesday: z.number().min(0).max(24),
|
||||
thursday: z.number().min(0).max(24),
|
||||
friday: z.number().min(0).max(24),
|
||||
saturday: z.number().min(0).max(24).optional(),
|
||||
sunday: z.number().min(0).max(24).optional(),
|
||||
});
|
||||
|
||||
export const SkillEntrySchema = z.object({
|
||||
skill: z.string().min(1).max(100),
|
||||
category: z.string().max(100).optional(),
|
||||
proficiency: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]),
|
||||
yearsExperience: z.number().min(0).max(50).optional(),
|
||||
certified: z.boolean().optional(),
|
||||
isMainSkill: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const CreateResourceSchema = z.object({
|
||||
eid: z.string().min(1).max(50),
|
||||
displayName: z.string().min(1).max(200),
|
||||
email: z.string().email(),
|
||||
chapter: z.string().max(100).optional(),
|
||||
lcrCents: z.number().int().min(0),
|
||||
ucrCents: z.number().int().min(0),
|
||||
currency: z.string().length(3).default("EUR"),
|
||||
chargeabilityTarget: z.number().min(0).max(100).default(80),
|
||||
availability: WeekdayAvailabilitySchema.default({
|
||||
monday: 8,
|
||||
tuesday: 8,
|
||||
wednesday: 8,
|
||||
thursday: 8,
|
||||
friday: 8,
|
||||
}),
|
||||
skills: z.array(SkillEntrySchema).default([]),
|
||||
dynamicFields: z.record(z.string(), z.unknown()).default({}),
|
||||
blueprintId: z.string().optional(),
|
||||
portfolioUrl: z.string().url().optional().or(z.literal("")),
|
||||
roleId: z.string().optional(),
|
||||
postalCode: z.string().max(10).optional(),
|
||||
federalState: z.string().max(5).optional(),
|
||||
countryId: z.string().optional(),
|
||||
metroCityId: z.string().optional(),
|
||||
orgUnitId: z.string().optional(),
|
||||
managementLevelGroupId: z.string().optional(),
|
||||
managementLevelId: z.string().optional(),
|
||||
resourceType: z.nativeEnum(ResourceType).optional(),
|
||||
chgResponsibility: z.boolean().optional(),
|
||||
rolledOff: z.boolean().optional(),
|
||||
departed: z.boolean().optional(),
|
||||
enterpriseId: z.string().max(100).optional(),
|
||||
clientUnitId: z.string().optional(),
|
||||
fte: z.number().min(0.01).max(1).optional(),
|
||||
});
|
||||
|
||||
export const UpdateResourceSchema = CreateResourceSchema.partial().extend({
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateResourceInput = z.infer<typeof CreateResourceSchema>;
|
||||
export type UpdateResourceInput = z.infer<typeof UpdateResourceSchema>;
|
||||
export type WeekdayAvailabilityInput = z.infer<typeof WeekdayAvailabilitySchema>;
|
||||
export type SkillEntryInput = z.infer<typeof SkillEntrySchema>;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateRoleSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
||||
});
|
||||
|
||||
export const UpdateRoleSchema = CreateRoleSchema.partial().extend({
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const ResourceRoleSchema = z.object({
|
||||
roleId: z.string(),
|
||||
isPrimary: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type CreateRoleInput = z.infer<typeof CreateRoleSchema>;
|
||||
export type UpdateRoleInput = z.infer<typeof UpdateRoleSchema>;
|
||||
export type ResourceRoleInput = z.infer<typeof ResourceRoleSchema>;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateUtilizationCategorySchema = z.object({
|
||||
code: z.string().min(1).max(20),
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().max(500).optional(),
|
||||
sortOrder: z.number().int().default(0),
|
||||
isDefault: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const UpdateUtilizationCategorySchema = z.object({
|
||||
code: z.string().min(1).max(20).optional(),
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
description: z.string().max(500).nullable().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateUtilizationCategoryInput = z.infer<typeof CreateUtilizationCategorySchema>;
|
||||
export type UpdateUtilizationCategoryInput = z.infer<typeof UpdateUtilizationCategorySchema>;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
import { VacationType } from "../types/enums.js";
|
||||
|
||||
export const CreateVacationSchema = z
|
||||
.object({
|
||||
resourceId: z.string(),
|
||||
type: z.nativeEnum(VacationType),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
note: z.string().max(500).optional(),
|
||||
})
|
||||
.refine((d) => d.endDate >= d.startDate, {
|
||||
message: "End date must be after start date",
|
||||
path: ["endDate"],
|
||||
});
|
||||
|
||||
export type CreateVacationInput = z.infer<typeof CreateVacationSchema>;
|
||||
|
||||
export const UpdateVacationStatusSchema = z.object({
|
||||
id: z.string(),
|
||||
status: z.enum(["APPROVED", "REJECTED", "CANCELLED"]),
|
||||
note: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
export type UpdateVacationStatusInput = z.infer<typeof UpdateVacationStatusSchema>;
|
||||
Reference in New Issue
Block a user