mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-01 05:53:14 +00:00
295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
// Runtime validation schemas coupled to the wire types in ./index.
|
|
// Separate entrypoint by design: consumers that only need types (mobile)
|
|
// never pull zod. Each schema is compile-time-checked against its type via
|
|
// `satisfies z.ZodType<T>` — if a DTO and its schema drift, tsc fails here.
|
|
import { z } from 'zod';
|
|
import type {
|
|
ProposalStatus,
|
|
ServiceCategory,
|
|
Priority,
|
|
LineItemSource,
|
|
PricingMode,
|
|
UserRole,
|
|
ProposalListItem,
|
|
ProposalDetail,
|
|
CreateProposalRequest,
|
|
UpdateProposalRequest,
|
|
ProposalVersionRequest,
|
|
ConcurrencyConflict,
|
|
LineItem,
|
|
CreateLineItemRequest,
|
|
UpdateLineItemEntry,
|
|
BulkUpdateLineItemsRequest,
|
|
Customer,
|
|
CreateCustomerRequest,
|
|
UpdateCustomerRequest,
|
|
PricingLibraryItem,
|
|
CreatePricingLibraryItemRequest,
|
|
UpdatePricingLibraryItemRequest,
|
|
DashboardStats,
|
|
AuditEntry,
|
|
Site,
|
|
AuthUser,
|
|
UserProfile,
|
|
ProposalStats,
|
|
PdfVersion,
|
|
PresignedUpload,
|
|
ApiProblem,
|
|
} from './index';
|
|
|
|
// ── Enums ─────────────────────────────────────────────────────────────────
|
|
export const proposalStatusSchema = z.enum(['Draft', 'InReview', 'Approved', 'Sent', 'Revised']) satisfies z.ZodType<ProposalStatus>;
|
|
export const serviceCategorySchema = z.enum(['HVAC', 'Plumbing', 'Electrical', 'General', 'Renovation', 'Other']) satisfies z.ZodType<ServiceCategory>;
|
|
export const prioritySchema = z.enum(['Standard', 'Urgent', 'Emergency']) satisfies z.ZodType<Priority>;
|
|
export const lineItemSourceSchema = z.enum(['AI', 'Vendor', 'Manual', 'Historical']) satisfies z.ZodType<LineItemSource>;
|
|
export const pricingModeSchema = z.enum(['UnitPrice', 'TotalPrice', 'Both']) satisfies z.ZodType<PricingMode>;
|
|
export const userRoleSchema = z.enum(['Dispatcher', 'Admin', 'SysAdmin']) satisfies z.ZodType<UserRole>;
|
|
|
|
// ── Proposals ─────────────────────────────────────────────────────────────
|
|
export const proposalListItemSchema = z.object({
|
|
id: z.string(),
|
|
proposalNumber: z.string(),
|
|
customerName: z.string(),
|
|
workOrderNumber: z.string(),
|
|
serviceCategory: serviceCategorySchema,
|
|
priority: prioritySchema,
|
|
status: proposalStatusSchema,
|
|
totalBidAmount: z.number(),
|
|
submittedAt: z.string(),
|
|
submittedByName: z.string().nullable(),
|
|
assignedAdminName: z.string().nullable(),
|
|
rowVersion: z.string(),
|
|
}) satisfies z.ZodType<ProposalListItem>;
|
|
|
|
export const proposalDetailSchema = z.object({
|
|
id: z.string(),
|
|
proposalNumber: z.string(),
|
|
workOrderNumber: z.string(),
|
|
poNumber: z.string().nullable(),
|
|
customerName: z.string(),
|
|
customerAddress: z.string(),
|
|
scopeOfWork: z.string(),
|
|
refinedScope: z.string().nullable(),
|
|
serviceCategory: serviceCategorySchema,
|
|
priority: prioritySchema,
|
|
status: proposalStatusSchema,
|
|
totalBidAmount: z.number(),
|
|
vendorTotalCost: z.number().nullable(),
|
|
notes: z.string(),
|
|
submittedById: z.string(),
|
|
submittedByName: z.string().nullable(),
|
|
submittedAt: z.string(),
|
|
assignedAdminId: z.string().nullable(),
|
|
approvedById: z.string().nullable(),
|
|
approvedAt: z.string().nullable(),
|
|
sentAt: z.string().nullable(),
|
|
currentRevision: z.number(),
|
|
parentProposalId: z.string().nullable(),
|
|
createdAt: z.string(),
|
|
updatedAt: z.string(),
|
|
rowVersion: z.string(),
|
|
}) satisfies z.ZodType<ProposalDetail>;
|
|
|
|
export const createProposalRequestSchema = z.object({
|
|
workOrderNumber: z.string().min(1),
|
|
poNumber: z.string().optional(),
|
|
customerName: z.string().min(1),
|
|
customerAddress: z.string().min(1),
|
|
scopeOfWork: z.string().min(1),
|
|
serviceCategory: serviceCategorySchema,
|
|
priority: prioritySchema,
|
|
notes: z.string().optional(),
|
|
}) satisfies z.ZodType<CreateProposalRequest>;
|
|
|
|
export const updateProposalRequestSchema = z.object({
|
|
refinedScope: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
poNumber: z.string().optional(),
|
|
workOrderNumber: z.string().optional(),
|
|
assignedAdminId: z.string().optional(),
|
|
proposalVersion: z.string().optional(),
|
|
}) satisfies z.ZodType<UpdateProposalRequest>;
|
|
|
|
export const proposalVersionRequestSchema = z.object({
|
|
proposalVersion: z.string().optional(),
|
|
}) satisfies z.ZodType<ProposalVersionRequest>;
|
|
|
|
export const proposalStatsSchema = z.object({
|
|
totalCount: z.number(),
|
|
inReviewCount: z.number(),
|
|
approvedCount: z.number(),
|
|
sentCount: z.number(),
|
|
}) satisfies z.ZodType<ProposalStats>;
|
|
|
|
// ── Line items ────────────────────────────────────────────────────────────
|
|
export const lineItemSchema = z.object({
|
|
id: z.string(),
|
|
proposalId: z.string(),
|
|
description: z.string(),
|
|
quantity: z.number(),
|
|
unit: z.string(),
|
|
unitPrice: z.number().nullable(),
|
|
totalPrice: z.number(),
|
|
pricingMode: pricingModeSchema,
|
|
sortOrder: z.number(),
|
|
source: lineItemSourceSchema,
|
|
createdAt: z.string(),
|
|
updatedAt: z.string(),
|
|
rowVersion: z.string(),
|
|
}) satisfies z.ZodType<LineItem>;
|
|
|
|
export const createLineItemRequestSchema = z.object({
|
|
description: z.string().min(1),
|
|
quantity: z.number(),
|
|
unit: z.string(),
|
|
unitPrice: z.number().nullable(),
|
|
totalPrice: z.number(),
|
|
pricingMode: pricingModeSchema,
|
|
sortOrder: z.number(),
|
|
source: lineItemSourceSchema,
|
|
}) satisfies z.ZodType<CreateLineItemRequest>;
|
|
|
|
export const updateLineItemEntrySchema = z.object({
|
|
id: z.string().nullable(),
|
|
description: z.string().min(1),
|
|
quantity: z.number(),
|
|
unit: z.string(),
|
|
unitPrice: z.number().nullable(),
|
|
totalPrice: z.number(),
|
|
pricingMode: pricingModeSchema,
|
|
sortOrder: z.number(),
|
|
source: lineItemSourceSchema,
|
|
}) satisfies z.ZodType<UpdateLineItemEntry>;
|
|
|
|
export const bulkUpdateLineItemsRequestSchema = z.object({
|
|
lineItems: z.array(updateLineItemEntrySchema),
|
|
proposalVersion: z.string().optional(),
|
|
}) satisfies z.ZodType<BulkUpdateLineItemsRequest>;
|
|
|
|
// ── Customers ─────────────────────────────────────────────────────────────
|
|
export const customerSchema = z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
addresses: z.array(z.string()),
|
|
contactEmail: z.string().nullable(),
|
|
createdAt: z.string(),
|
|
}) satisfies z.ZodType<Customer>;
|
|
|
|
export const createCustomerRequestSchema = z.object({
|
|
name: z.string().min(1),
|
|
addresses: z.array(z.string()).optional(),
|
|
contactEmail: z.string().optional(),
|
|
}) satisfies z.ZodType<CreateCustomerRequest>;
|
|
|
|
export const updateCustomerRequestSchema = z.object({
|
|
name: z.string().optional(),
|
|
addresses: z.array(z.string()).optional(),
|
|
contactEmail: z.string().nullable().optional(),
|
|
}) satisfies z.ZodType<UpdateCustomerRequest>;
|
|
|
|
// ── Pricing library ───────────────────────────────────────────────────────
|
|
export const pricingLibraryItemSchema = z.object({
|
|
id: z.string(),
|
|
title: z.string(),
|
|
description: z.string().nullable(),
|
|
serviceCategory: serviceCategorySchema,
|
|
unit: z.string().nullable(),
|
|
unitPrice: z.number().nullable(),
|
|
keywords: z.string().nullable(),
|
|
source: z.string(),
|
|
createdAt: z.string(),
|
|
updatedAt: z.string(),
|
|
}) satisfies z.ZodType<PricingLibraryItem>;
|
|
|
|
export const createPricingLibraryItemRequestSchema = z.object({
|
|
title: z.string().min(1),
|
|
description: z.string().optional(),
|
|
serviceCategory: serviceCategorySchema,
|
|
unit: z.string().optional(),
|
|
unitPrice: z.number().nullable().optional(),
|
|
keywords: z.string().optional(),
|
|
source: z.string().optional(),
|
|
}) satisfies z.ZodType<CreatePricingLibraryItemRequest>;
|
|
|
|
export const updatePricingLibraryItemRequestSchema = z.object({
|
|
title: z.string().optional(),
|
|
description: z.string().optional(),
|
|
serviceCategory: serviceCategorySchema.optional(),
|
|
unit: z.string().optional(),
|
|
unitPrice: z.number().nullable().optional(),
|
|
keywords: z.string().optional(),
|
|
}) satisfies z.ZodType<UpdatePricingLibraryItemRequest>;
|
|
|
|
// ── Admin ─────────────────────────────────────────────────────────────────
|
|
export const dashboardStatsSchema = z.object({
|
|
pendingCount: z.number(),
|
|
approvedThisWeek: z.number(),
|
|
avgTurnaroundHours: z.number(),
|
|
totalProposals: z.number(),
|
|
}) satisfies z.ZodType<DashboardStats>;
|
|
|
|
export const auditEntrySchema = z.object({
|
|
id: z.string(),
|
|
proposalId: z.string().nullable(),
|
|
userId: z.string(),
|
|
userName: z.string().nullable(),
|
|
action: z.string(),
|
|
details: z.string().nullable(),
|
|
timestamp: z.string(),
|
|
ipAddress: z.string().nullable(),
|
|
}) satisfies z.ZodType<AuditEntry>;
|
|
|
|
// ── Sites ─────────────────────────────────────────────────────────────────
|
|
export const siteSchema = z.object({
|
|
siteCode: z.string(),
|
|
fullAddress: z.string().nullable(),
|
|
address: z.string().nullable(),
|
|
city: z.string().nullable(),
|
|
state: z.string().nullable(),
|
|
zip: z.string().nullable(),
|
|
}) satisfies z.ZodType<Site>;
|
|
|
|
// ── Auth / users ──────────────────────────────────────────────────────────
|
|
export const authUserSchema = z.object({
|
|
id: z.string(),
|
|
email: z.string(),
|
|
displayName: z.string(),
|
|
role: userRoleSchema,
|
|
token: z.string(),
|
|
}) satisfies z.ZodType<AuthUser>;
|
|
|
|
export const userProfileSchema = z.object({
|
|
id: z.string(),
|
|
email: z.string(),
|
|
displayName: z.string(),
|
|
role: userRoleSchema,
|
|
}) satisfies z.ZodType<UserProfile>;
|
|
|
|
// ── Files / PDFs ──────────────────────────────────────────────────────────
|
|
export const pdfVersionSchema = z.object({
|
|
revision: z.number(),
|
|
generatedAt: z.string(),
|
|
}) satisfies z.ZodType<PdfVersion>;
|
|
|
|
export const presignedUploadSchema = z.object({
|
|
uploadUrl: z.string(),
|
|
s3Key: z.string(),
|
|
expiresAt: z.string(),
|
|
vendorProposalId: z.string(),
|
|
}) satisfies z.ZodType<PresignedUpload>;
|
|
|
|
// ── Errors ────────────────────────────────────────────────────────────────
|
|
export const apiProblemSchema = z.object({
|
|
status: z.number(),
|
|
title: z.string(),
|
|
detail: z.string(),
|
|
code: z.string(),
|
|
}) satisfies z.ZodType<ApiProblem>;
|
|
|
|
// 409 concurrency-conflict envelope. The proposal aggregate is the only
|
|
// guarded resource today, so the concrete schema is coupled to ProposalDetail;
|
|
// build others via the same pattern when new aggregates gain guards.
|
|
export const proposalConcurrencyConflictSchema = z.object({
|
|
message: z.string(),
|
|
currentState: proposalDetailSchema.nullable(),
|
|
}) satisfies z.ZodType<ConcurrencyConflict<ProposalDetail>>;
|