feat(api): complete unified v2 mobile surface
This commit is contained in:
301
backend/command-api/src/contracts/commands/mobile.js
Normal file
301
backend/command-api/src/contracts/commands/mobile.js
Normal file
@@ -0,0 +1,301 @@
|
||||
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(),
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const roleSchema = z.object({
|
||||
roleId: z.string().uuid().optional(),
|
||||
roleCode: z.string().min(1).max(100),
|
||||
roleName: z.string().min(1).max(120),
|
||||
workersNeeded: z.number().int().positive(),
|
||||
|
||||
Reference in New Issue
Block a user