Files
CapaKraken/packages/shared/src/schemas/estimate.schema.ts
T
Hartmut 625a842d89 feat: dashboard overhaul, chargeability reports, dispo import enhancements, UI polish
Dashboard: expanded chargeability widget, resource/project table widgets
with sorting and filters, stat cards with formatMoney integration.

Chargeability: new report client with filtering, chargeability-bookings
use case, updated dashboard overview logic.

Dispo import: TBD project handling, parse-dispo-matrix improvements,
stage-dispo-projects resource value scores, new tests.

Estimates: CommercialTermsEditor component, commercial-terms engine
module, expanded estimate schemas and types.

UI: AppShell navigation updates, timeline filter/toolbar enhancements,
role management improvements, signin page redesign, Tailwind/globals
polish, SystemSettings SMTP section, anonymization support.

Tests: new router tests (anonymization, chargeability, effort-rule,
entitlement, estimate, experience-multiplier, notification, resource,
staffing, vacation).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 23:29:07 +01:00

387 lines
14 KiB
TypeScript

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>;
// ─── Commercial Terms ───────────────────────────────────────────────────────
export const PricingModelSchema = z.enum(["fixed_price", "time_and_materials", "hybrid"]);
export const PaymentMilestoneSchema = z.object({
label: z.string().min(1).max(200),
percent: z.number().min(0).max(100),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
description: z.string().max(1000).nullable().optional(),
});
export const CommercialTermsSchema = z.object({
pricingModel: PricingModelSchema.default("fixed_price"),
contingencyPercent: z.number().min(0).max(100).default(0),
discountPercent: z.number().min(0).max(100).default(0),
paymentTermDays: z.number().int().min(0).max(365).default(30),
paymentMilestones: z.array(PaymentMilestoneSchema).default([]),
warrantyMonths: z.number().int().min(0).max(60).default(0),
notes: z.string().max(5_000).nullable().optional(),
});
export const UpdateCommercialTermsSchema = z.object({
estimateId: z.string(),
versionId: z.string().optional(),
terms: CommercialTermsSchema,
});
export type CommercialTermsInput = z.infer<typeof CommercialTermsSchema>;
export type UpdateCommercialTermsInput = z.infer<typeof UpdateCommercialTermsSchema>;
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>;