feat(attendance): add geofence monitoring and policy controls
This commit is contained in:
@@ -44,8 +44,8 @@ async function main() {
|
||||
const completedEndsAt = hoursFromNow(-20);
|
||||
const checkedInAt = hoursFromNow(-27.5);
|
||||
const checkedOutAt = hoursFromNow(-20.25);
|
||||
const assignedStartsAt = hoursFromNow(2);
|
||||
const assignedEndsAt = hoursFromNow(10);
|
||||
const assignedStartsAt = hoursFromNow(0.1);
|
||||
const assignedEndsAt = hoursFromNow(8.1);
|
||||
const availableStartsAt = hoursFromNow(30);
|
||||
const availableEndsAt = hoursFromNow(38);
|
||||
const cancelledStartsAt = hoursFromNow(20);
|
||||
@@ -270,9 +270,9 @@ async function main() {
|
||||
`
|
||||
INSERT INTO clock_points (
|
||||
id, tenant_id, business_id, cost_center_id, label, address, latitude, longitude,
|
||||
geofence_radius_meters, nfc_tag_uid, status, metadata
|
||||
geofence_radius_meters, nfc_tag_uid, default_clock_in_mode, allow_clock_in_override, status, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'ACTIVE', $11::jsonb)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'ACTIVE', $13::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.clockPoint.id,
|
||||
@@ -285,6 +285,8 @@ async function main() {
|
||||
fixture.clockPoint.longitude,
|
||||
fixture.clockPoint.geofenceRadiusMeters,
|
||||
fixture.clockPoint.nfcTagUid,
|
||||
fixture.clockPoint.defaultClockInMode,
|
||||
fixture.clockPoint.allowClockInOverride,
|
||||
JSON.stringify({ city: 'Mountain View', state: 'CA', zipCode: '94043', seeded: true }),
|
||||
]
|
||||
);
|
||||
@@ -369,11 +371,12 @@ async function main() {
|
||||
`
|
||||
INSERT INTO shifts (
|
||||
id, tenant_id, order_id, business_id, vendor_id, clock_point_id, shift_code, title, status, starts_at, ends_at, timezone,
|
||||
location_name, location_address, latitude, longitude, geofence_radius_meters, required_workers, assigned_workers, notes, metadata
|
||||
location_name, location_address, latitude, longitude, geofence_radius_meters, clock_in_mode, allow_clock_in_override,
|
||||
required_workers, assigned_workers, notes, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $3, $5, $7, $9, $11, $13, $15, 'OPEN', $17, $18, 'America/Los_Angeles', 'Google Cafe', $19, $21, $22, $23, 1, 0, 'Open staffing need', '{"slice":"open"}'::jsonb),
|
||||
($2, $4, $6, $8, $10, $12, $14, $16, 'COMPLETED', $20, $24, 'America/Los_Angeles', 'Google Catering', $19, $21, $22, $23, 1, 1, 'Completed staffed shift', '{"slice":"completed"}'::jsonb)
|
||||
($1, $3, $5, $7, $9, $11, $13, $15, 'OPEN', $17, $18, 'America/Los_Angeles', 'Google Cafe', $19, $21, $22, $23, NULL, NULL, 1, 0, 'Open staffing need', '{"slice":"open"}'::jsonb),
|
||||
($2, $4, $6, $8, $10, $12, $14, $16, 'COMPLETED', $20, $24, 'America/Los_Angeles', 'Google Catering', $19, $21, $22, $23, NULL, NULL, 1, 1, 'Completed staffed shift', '{"slice":"completed"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.shifts.open.id,
|
||||
@@ -407,13 +410,14 @@ async function main() {
|
||||
`
|
||||
INSERT INTO shifts (
|
||||
id, tenant_id, order_id, business_id, vendor_id, clock_point_id, shift_code, title, status, starts_at, ends_at, timezone,
|
||||
location_name, location_address, latitude, longitude, geofence_radius_meters, required_workers, assigned_workers, notes, metadata
|
||||
location_name, location_address, latitude, longitude, geofence_radius_meters, clock_in_mode, allow_clock_in_override,
|
||||
required_workers, assigned_workers, notes, metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, 'OPEN', $9, $10, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, 1, 0, 'Available shift for staff marketplace', '{"slice":"available"}'::jsonb),
|
||||
($15, $2, $3, $4, $5, $6, $16, $17, 'ASSIGNED', $18, $19, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, 1, 1, 'Assigned shift waiting for staff confirmation', '{"slice":"assigned"}'::jsonb),
|
||||
($20, $2, $3, $4, $5, $6, $21, $22, 'CANCELLED', $23, $24, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, 1, 0, 'Cancelled shift history sample', '{"slice":"cancelled"}'::jsonb),
|
||||
($25, $2, $3, $4, $5, $6, $26, $27, 'COMPLETED', $28, $29, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, 1, 0, 'No-show historical sample', '{"slice":"no_show"}'::jsonb)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, 'OPEN', $9, $10, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, NULL, NULL, 1, 0, 'Available shift for staff marketplace', '{"slice":"available"}'::jsonb),
|
||||
($15, $2, $3, $4, $5, $6, $16, $17, 'ASSIGNED', $18, $19, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, $30, $31, 1, 1, 'Assigned shift waiting for staff confirmation', '{"slice":"assigned"}'::jsonb),
|
||||
($20, $2, $3, $4, $5, $6, $21, $22, 'CANCELLED', $23, $24, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, NULL, NULL, 1, 0, 'Cancelled shift history sample', '{"slice":"cancelled"}'::jsonb),
|
||||
($25, $2, $3, $4, $5, $6, $26, $27, 'COMPLETED', $28, $29, 'America/Los_Angeles', 'Google Cafe', $11, $12, $13, $14, 'GEO_REQUIRED', TRUE, 1, 0, 'No-show historical sample', '{"slice":"no_show"}'::jsonb)
|
||||
`,
|
||||
[
|
||||
fixture.shifts.available.id,
|
||||
@@ -445,6 +449,8 @@ async function main() {
|
||||
fixture.shifts.noShow.title,
|
||||
noShowStartsAt,
|
||||
noShowEndsAt,
|
||||
fixture.shifts.assigned.clockInMode,
|
||||
fixture.shifts.assigned.allowClockInOverride,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -833,6 +839,96 @@ async function main() {
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO location_stream_batches (
|
||||
id, tenant_id, business_id, vendor_id, shift_id, assignment_id, staff_id, actor_user_id,
|
||||
source_type, device_id, object_uri, point_count, out_of_geofence_count, missing_coordinate_count,
|
||||
max_distance_to_clock_point_meters, started_at, ended_at, metadata
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, 'GEO', 'seed-device',
|
||||
$9, 4, 2, 0, 910, $10, $11, '{"seeded":true,"source":"seed-v2-demo"}'::jsonb
|
||||
)
|
||||
`,
|
||||
[
|
||||
fixture.locationStreamBatches.noShowSample.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.vendor.id,
|
||||
fixture.shifts.noShow.id,
|
||||
fixture.assignments.noShowAna.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.users.staffAna.id,
|
||||
`gs://krow-workforce-dev-v2-private/location-streams/${fixture.tenant.id}/${fixture.staff.ana.id}/${fixture.assignments.noShowAna.id}/${fixture.locationStreamBatches.noShowSample.id}.json`,
|
||||
hoursFromNow(-18.25),
|
||||
hoursFromNow(-17.75),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO geofence_incidents (
|
||||
id, tenant_id, business_id, vendor_id, shift_id, assignment_id, staff_id, actor_user_id, location_stream_batch_id,
|
||||
incident_type, severity, status, effective_clock_in_mode, source_type, device_id,
|
||||
latitude, longitude, accuracy_meters, distance_to_clock_point_meters, within_geofence,
|
||||
override_reason, message, occurred_at, metadata
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
||||
'OUTSIDE_GEOFENCE', 'CRITICAL', 'OPEN', 'GEO_REQUIRED', 'GEO', 'seed-device',
|
||||
$10, $11, 12, 910, FALSE, NULL, 'Worker drifted outside hub geofence during active monitoring',
|
||||
$12, '{"seeded":true,"outOfGeofenceCount":2}'::jsonb
|
||||
)
|
||||
`,
|
||||
[
|
||||
fixture.geofenceIncidents.noShowOutsideGeofence.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.vendor.id,
|
||||
fixture.shifts.noShow.id,
|
||||
fixture.assignments.noShowAna.id,
|
||||
fixture.staff.ana.id,
|
||||
fixture.users.staffAna.id,
|
||||
fixture.locationStreamBatches.noShowSample.id,
|
||||
fixture.clockPoint.latitude + 0.0065,
|
||||
fixture.clockPoint.longitude + 0.0065,
|
||||
hoursFromNow(-17.9),
|
||||
]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO notification_outbox (
|
||||
id, tenant_id, business_id, shift_id, assignment_id, related_incident_id, audience_type,
|
||||
recipient_user_id, recipient_business_membership_id, channel, notification_type, priority, dedupe_key,
|
||||
subject, body, payload, status, scheduled_at, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
$1, $2, $3, $4, $5, $6, 'USER',
|
||||
bm.user_id, bm.id, 'PUSH', 'GEOFENCE_BREACH_ALERT', 'CRITICAL', $7,
|
||||
'Worker left the workplace geofence',
|
||||
'Seeded alert for coverage incident review',
|
||||
jsonb_build_object('seeded', TRUE, 'batchId', $8::text),
|
||||
'PENDING', NOW(), NOW(), NOW()
|
||||
FROM business_memberships bm
|
||||
WHERE bm.tenant_id = $2
|
||||
AND bm.business_id = $3
|
||||
AND bm.user_id = $9
|
||||
`,
|
||||
[
|
||||
fixture.notificationOutbox.noShowManagerAlert.id,
|
||||
fixture.tenant.id,
|
||||
fixture.business.id,
|
||||
fixture.shifts.noShow.id,
|
||||
fixture.assignments.noShowAna.id,
|
||||
fixture.geofenceIncidents.noShowOutsideGeofence.id,
|
||||
`seed-geofence-breach:${fixture.geofenceIncidents.noShowOutsideGeofence.id}:${fixture.users.operationsManager.id}`,
|
||||
fixture.locationStreamBatches.noShowSample.id,
|
||||
fixture.users.operationsManager.id,
|
||||
]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -6,8 +6,8 @@ export const V2DemoFixture = {
|
||||
},
|
||||
users: {
|
||||
businessOwner: {
|
||||
id: process.env.V2_DEMO_OWNER_UID || 'dvpWnaBjT6UksS5lo04hfMTyq1q1',
|
||||
email: process.env.V2_DEMO_OWNER_EMAIL || 'legendary@krowd.com',
|
||||
id: process.env.V2_DEMO_OWNER_UID || 'alFf9mYw3uYbm7ZjeLo1KoTgFxq2',
|
||||
email: process.env.V2_DEMO_OWNER_EMAIL || 'legendary.owner+v2@krowd.com',
|
||||
displayName: 'Legendary Demo Owner',
|
||||
},
|
||||
operationsManager: {
|
||||
@@ -21,7 +21,7 @@ export const V2DemoFixture = {
|
||||
displayName: 'Vendor Manager',
|
||||
},
|
||||
staffAna: {
|
||||
id: process.env.V2_DEMO_STAFF_UID || 'demo-staff-ana',
|
||||
id: process.env.V2_DEMO_STAFF_UID || 'vwptrLl5S2Z598WP93cgrQEzqBg1',
|
||||
email: process.env.V2_DEMO_STAFF_EMAIL || 'ana.barista+v2@krowd.com',
|
||||
displayName: 'Ana Barista',
|
||||
},
|
||||
@@ -77,6 +77,8 @@ export const V2DemoFixture = {
|
||||
longitude: -122.0841,
|
||||
geofenceRadiusMeters: 120,
|
||||
nfcTagUid: 'NFC-DEMO-ANA-001',
|
||||
defaultClockInMode: 'GEO_REQUIRED',
|
||||
allowClockInOverride: true,
|
||||
},
|
||||
hubManagers: {
|
||||
opsLead: {
|
||||
@@ -134,6 +136,8 @@ export const V2DemoFixture = {
|
||||
id: '6e7dadad-99e4-45bb-b0da-7bb617954004',
|
||||
code: 'SHIFT-V2-ASSIGNED-1',
|
||||
title: 'Assigned espresso shift',
|
||||
clockInMode: 'GEO_REQUIRED',
|
||||
allowClockInOverride: true,
|
||||
},
|
||||
cancelled: {
|
||||
id: '6e7dadad-99e4-45bb-b0da-7bb617954005',
|
||||
@@ -268,4 +272,19 @@ export const V2DemoFixture = {
|
||||
id: '5d98e0ba-8e89-4ffb-aafd-df6bbe2fe002',
|
||||
},
|
||||
},
|
||||
locationStreamBatches: {
|
||||
noShowSample: {
|
||||
id: '7184a512-b5b2-46b7-a8e0-f4a04bb8f001',
|
||||
},
|
||||
},
|
||||
geofenceIncidents: {
|
||||
noShowOutsideGeofence: {
|
||||
id: '8174a512-b5b2-46b7-a8e0-f4a04bb8f001',
|
||||
},
|
||||
},
|
||||
notificationOutbox: {
|
||||
noShowManagerAlert: {
|
||||
id: '9174a512-b5b2-46b7-a8e0-f4a04bb8f001',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user