feat(attendance): add geofence monitoring and policy controls

This commit is contained in:
zouantchaw
2026-03-16 15:31:13 +01:00
parent b455455a49
commit 5d8240ed51
22 changed files with 1667 additions and 162 deletions

View File

@@ -16,6 +16,7 @@ const preferredLocationSchema = z.object({
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 clockInModeSchema = z.enum(['NFC_REQUIRED', 'GEO_REQUIRED', 'EITHER']);
const shiftPositionSchema = z.object({
roleId: z.string().uuid().optional(),
@@ -68,6 +69,8 @@ export const hubCreateSchema = z.object({
costCenterId: z.string().uuid().optional(),
geofenceRadiusMeters: z.number().int().positive().optional(),
nfcTagId: z.string().max(255).optional(),
clockInMode: clockInModeSchema.optional(),
allowClockInOverride: z.boolean().optional(),
});
export const hubUpdateSchema = hubCreateSchema.extend({
@@ -203,6 +206,7 @@ export const staffClockInSchema = z.object({
accuracyMeters: z.number().int().nonnegative().optional(),
capturedAt: z.string().datetime().optional(),
notes: z.string().max(2000).optional(),
overrideReason: z.string().max(2000).optional(),
rawPayload: z.record(z.any()).optional(),
}).refine((value) => value.assignmentId || value.shiftId, {
message: 'assignmentId or shiftId is required',
@@ -221,12 +225,34 @@ export const staffClockOutSchema = z.object({
accuracyMeters: z.number().int().nonnegative().optional(),
capturedAt: z.string().datetime().optional(),
notes: z.string().max(2000).optional(),
overrideReason: 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',
});
const locationPointSchema = z.object({
capturedAt: z.string().datetime(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
accuracyMeters: z.number().int().nonnegative().optional(),
speedMps: z.number().nonnegative().optional(),
isMocked: z.boolean().optional(),
metadata: z.record(z.any()).optional(),
});
export const staffLocationBatchSchema = z.object({
assignmentId: z.string().uuid().optional(),
shiftId: z.string().uuid().optional(),
sourceType: z.enum(['NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM']).default('GEO'),
deviceId: z.string().max(255).optional(),
points: z.array(locationPointSchema).min(1).max(96),
metadata: z.record(z.any()).optional(),
}).refine((value) => value.assignmentId || value.shiftId, {
message: 'assignmentId or shiftId is required',
});
export const staffProfileSetupSchema = z.object({
fullName: z.string().min(2).max(160),
bio: z.string().max(5000).optional(),