625a842d89
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>
387 lines
14 KiB
TypeScript
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>;
|