chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -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>;
+15
View File
@@ -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>;