Files
Krow-workspace/backend/command-api/src/contracts/commands/mobile.js
2026-03-13 17:02:24 +01:00

302 lines
11 KiB
JavaScript

import { z } from 'zod';
const timeSlotSchema = z.object({
start: z.string().min(1).max(20),
end: z.string().min(1).max(20),
});
const preferredLocationSchema = z.object({
label: z.string().min(1).max(160),
city: z.string().max(120).optional(),
state: z.string().max(80).optional(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
radiusMiles: z.number().nonnegative().optional(),
});
const hhmmSchema = z.string().regex(/^\d{2}:\d{2}$/, 'Time must use HH:MM format');
const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must use YYYY-MM-DD format');
const shiftPositionSchema = z.object({
roleId: z.string().uuid().optional(),
roleCode: z.string().min(1).max(120).optional(),
roleName: z.string().min(1).max(160).optional(),
workerCount: z.number().int().positive().optional(),
workersNeeded: z.number().int().positive().optional(),
startTime: hhmmSchema,
endTime: hhmmSchema,
hourlyRateCents: z.number().int().nonnegative().optional(),
payRateCents: z.number().int().nonnegative().optional(),
billRateCents: z.number().int().nonnegative().optional(),
lunchBreakMinutes: z.number().int().nonnegative().optional(),
paidBreak: z.boolean().optional(),
instantBook: z.boolean().optional(),
metadata: z.record(z.any()).optional(),
}).superRefine((value, ctx) => {
if (!value.roleId && !value.roleCode && !value.roleName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'roleId, roleCode, or roleName is required',
path: ['roleId'],
});
}
});
const baseOrderCreateSchema = z.object({
hubId: z.string().uuid(),
vendorId: z.string().uuid().optional(),
eventName: z.string().min(2).max(160),
timezone: z.string().min(1).max(80).optional(),
description: z.string().max(5000).optional(),
notes: z.string().max(5000).optional(),
serviceType: z.enum(['EVENT', 'CATERING', 'HOTEL', 'RESTAURANT', 'OTHER']).optional(),
positions: z.array(shiftPositionSchema).min(1),
metadata: z.record(z.any()).optional(),
});
export const hubCreateSchema = z.object({
name: z.string().min(1).max(160),
fullAddress: z.string().max(300).optional(),
placeId: z.string().max(255).optional(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
street: z.string().max(160).optional(),
city: z.string().max(120).optional(),
state: z.string().max(80).optional(),
country: z.string().max(80).optional(),
zipCode: z.string().max(40).optional(),
costCenterId: z.string().uuid().optional(),
geofenceRadiusMeters: z.number().int().positive().optional(),
nfcTagId: z.string().max(255).optional(),
});
export const hubUpdateSchema = hubCreateSchema.extend({
hubId: z.string().uuid(),
});
export const hubDeleteSchema = z.object({
hubId: z.string().uuid(),
reason: z.string().max(1000).optional(),
});
export const hubAssignNfcSchema = z.object({
hubId: z.string().uuid(),
nfcTagId: z.string().min(1).max(255),
});
export const hubAssignManagerSchema = z.object({
hubId: z.string().uuid(),
businessMembershipId: z.string().uuid().optional(),
managerUserId: z.string().min(1).optional(),
}).refine((value) => value.businessMembershipId || value.managerUserId, {
message: 'businessMembershipId or managerUserId is required',
});
export const invoiceApproveSchema = z.object({
invoiceId: z.string().uuid(),
});
export const invoiceDisputeSchema = z.object({
invoiceId: z.string().uuid(),
reason: z.string().min(3).max(2000),
});
export const coverageReviewSchema = z.object({
staffId: z.string().uuid(),
assignmentId: z.string().uuid().optional(),
rating: z.number().int().min(1).max(5),
markAsFavorite: z.boolean().optional(),
issueFlags: z.array(z.string().min(1).max(80)).max(20).optional(),
feedback: z.string().max(5000).optional(),
});
export const cancelLateWorkerSchema = z.object({
assignmentId: z.string().uuid(),
reason: z.string().max(1000).optional(),
});
export const clientOneTimeOrderSchema = baseOrderCreateSchema.extend({
orderDate: isoDateSchema,
});
export const clientRecurringOrderSchema = baseOrderCreateSchema.extend({
startDate: isoDateSchema,
endDate: isoDateSchema,
recurrenceDays: z.array(z.number().int().min(0).max(6)).min(1),
});
export const clientPermanentOrderSchema = baseOrderCreateSchema.extend({
startDate: isoDateSchema,
endDate: isoDateSchema.optional(),
daysOfWeek: z.array(z.number().int().min(0).max(6)).min(1).optional(),
horizonDays: z.number().int().min(7).max(180).optional(),
});
export const clientOrderEditSchema = z.object({
orderId: z.string().uuid(),
orderType: z.enum(['ONE_TIME', 'RECURRING', 'PERMANENT']).optional(),
hubId: z.string().uuid().optional(),
vendorId: z.string().uuid().optional(),
eventName: z.string().min(2).max(160).optional(),
orderDate: isoDateSchema.optional(),
startDate: isoDateSchema.optional(),
endDate: isoDateSchema.optional(),
recurrenceDays: z.array(z.number().int().min(0).max(6)).min(1).optional(),
daysOfWeek: z.array(z.number().int().min(0).max(6)).min(1).optional(),
timezone: z.string().min(1).max(80).optional(),
description: z.string().max(5000).optional(),
notes: z.string().max(5000).optional(),
serviceType: z.enum(['EVENT', 'CATERING', 'HOTEL', 'RESTAURANT', 'OTHER']).optional(),
positions: z.array(shiftPositionSchema).min(1).optional(),
metadata: z.record(z.any()).optional(),
}).superRefine((value, ctx) => {
const keys = Object.keys(value).filter((key) => key !== 'orderId');
if (keys.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'At least one field must be provided to create an edited order copy',
path: [],
});
}
});
export const clientOrderCancelSchema = z.object({
orderId: z.string().uuid(),
reason: z.string().max(1000).optional(),
metadata: z.record(z.any()).optional(),
});
export const availabilityDayUpdateSchema = z.object({
dayOfWeek: z.number().int().min(0).max(6),
availabilityStatus: z.enum(['AVAILABLE', 'UNAVAILABLE', 'PARTIAL']),
slots: z.array(timeSlotSchema).max(8).optional(),
metadata: z.record(z.any()).optional(),
});
export const availabilityQuickSetSchema = z.object({
startDate: z.string().datetime().optional(),
endDate: z.string().datetime().optional(),
quickSetType: z.enum(['all', 'weekdays', 'weekends', 'clear']),
slots: z.array(timeSlotSchema).max(8).optional(),
});
export const shiftApplySchema = z.object({
shiftId: z.string().uuid(),
roleId: z.string().uuid().optional(),
instantBook: z.boolean().optional(),
});
export const shiftDecisionSchema = z.object({
shiftId: z.string().uuid(),
reason: z.string().max(1000).optional(),
});
export const staffClockInSchema = z.object({
assignmentId: z.string().uuid().optional(),
shiftId: z.string().uuid().optional(),
sourceType: z.enum(['NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM']).optional(),
sourceReference: z.string().max(255).optional(),
nfcTagId: z.string().max(255).optional(),
deviceId: z.string().max(255).optional(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
accuracyMeters: z.number().int().nonnegative().optional(),
capturedAt: z.string().datetime().optional(),
notes: z.string().max(2000).optional(),
rawPayload: z.record(z.any()).optional(),
}).refine((value) => value.assignmentId || value.shiftId, {
message: 'assignmentId or shiftId is required',
});
export const staffClockOutSchema = z.object({
assignmentId: z.string().uuid().optional(),
shiftId: z.string().uuid().optional(),
applicationId: z.string().uuid().optional(),
sourceType: z.enum(['NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM']).optional(),
sourceReference: z.string().max(255).optional(),
nfcTagId: z.string().max(255).optional(),
deviceId: z.string().max(255).optional(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
accuracyMeters: z.number().int().nonnegative().optional(),
capturedAt: z.string().datetime().optional(),
notes: z.string().max(2000).optional(),
breakMinutes: z.number().int().nonnegative().optional(),
rawPayload: z.record(z.any()).optional(),
}).refine((value) => value.assignmentId || value.shiftId || value.applicationId, {
message: 'assignmentId, shiftId, or applicationId is required',
});
export const staffProfileSetupSchema = z.object({
fullName: z.string().min(2).max(160),
bio: z.string().max(5000).optional(),
email: z.string().email().optional(),
phoneNumber: z.string().min(6).max(40),
preferredLocations: z.array(preferredLocationSchema).max(20).optional(),
maxDistanceMiles: z.number().nonnegative().max(500).optional(),
industries: z.array(z.string().min(1).max(80)).max(30).optional(),
skills: z.array(z.string().min(1).max(80)).max(50).optional(),
primaryRole: z.string().max(120).optional(),
tenantId: z.string().uuid().optional(),
vendorId: z.string().uuid().optional(),
});
export const personalInfoUpdateSchema = z.object({
firstName: z.string().min(1).max(80).optional(),
lastName: z.string().min(1).max(80).optional(),
bio: z.string().max(5000).optional(),
preferredLocations: z.array(preferredLocationSchema).max(20).optional(),
maxDistanceMiles: z.number().nonnegative().max(500).optional(),
email: z.string().email().optional(),
phone: z.string().min(6).max(40).optional(),
displayName: z.string().min(2).max(160).optional(),
});
export const profileExperienceSchema = z.object({
industries: z.array(z.string().min(1).max(80)).max(30).optional(),
skills: z.array(z.string().min(1).max(80)).max(50).optional(),
primaryRole: z.string().max(120).optional(),
});
export const preferredLocationsUpdateSchema = z.object({
preferredLocations: z.array(preferredLocationSchema).max(20),
maxDistanceMiles: z.number().nonnegative().max(500).optional(),
});
export const emergencyContactCreateSchema = z.object({
fullName: z.string().min(2).max(160),
phone: z.string().min(6).max(40),
relationshipType: z.string().min(1).max(120),
isPrimary: z.boolean().optional(),
metadata: z.record(z.any()).optional(),
});
export const emergencyContactUpdateSchema = emergencyContactCreateSchema.partial().extend({
contactId: z.string().uuid(),
});
const taxFormFieldsSchema = z.record(z.any());
export const taxFormDraftSchema = z.object({
formType: z.enum(['I9', 'W4']),
fields: taxFormFieldsSchema,
});
export const taxFormSubmitSchema = z.object({
formType: z.enum(['I9', 'W4']),
fields: taxFormFieldsSchema,
});
export const bankAccountCreateSchema = z.object({
bankName: z.string().min(2).max(160),
accountNumber: z.string().min(4).max(34),
routingNumber: z.string().min(4).max(20),
accountType: z.string()
.transform((value) => value.trim().toUpperCase())
.pipe(z.enum(['CHECKING', 'SAVINGS'])),
});
export const privacyUpdateSchema = z.object({
profileVisible: z.boolean(),
});