@fabriktor/fx 0.0.139 → 0.0.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/chat/data.d.ts +23 -0
- package/dist/src/chat/data.js +76 -0
- package/dist/src/core/address.d.ts +2 -0
- package/dist/src/core/address.js +6 -0
- package/dist/src/core/date.d.ts +6 -0
- package/dist/src/core/date.js +27 -0
- package/dist/src/core/measurement.d.ts +15 -0
- package/dist/src/core/measurement.js +45 -0
- package/dist/src/core/server-clock.d.ts +10 -0
- package/dist/src/core/server-clock.js +36 -0
- package/dist/src/fx.d.ts +5 -1
- package/dist/src/fx.js +6 -1
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +12 -0
- package/dist/src/routes/arrival.d.ts +16 -0
- package/dist/src/routes/arrival.js +50 -0
- package/dist/src/routes/availability.d.ts +35 -0
- package/dist/src/routes/availability.js +132 -0
- package/dist/src/routes/departure.d.ts +45 -0
- package/dist/src/routes/departure.js +98 -0
- package/dist/src/routes/planning.d.ts +34 -0
- package/dist/src/routes/planning.js +94 -0
- package/dist/src/routes/punch.d.ts +9 -0
- package/dist/src/routes/punch.js +23 -0
- package/dist/src/routes/routes.d.ts +45 -12
- package/dist/src/routes/routes.js +223 -139
- package/dist/src/routes/tracking.d.ts +59 -0
- package/dist/src/routes/tracking.js +178 -0
- package/dist/src/routes/validation.d.ts +13 -0
- package/dist/src/routes/validation.js +227 -0
- package/dist/src/routes/workspace.d.ts +74 -0
- package/dist/src/routes/workspace.js +212 -0
- package/dist/src/timesheets/timesheets.d.ts +3 -1
- package/dist/src/timesheets/timesheets.js +3 -0
- package/dist/src/timesheets/workspace.d.ts +9 -1
- package/dist/src/timesheets/workspace.js +50 -8
- package/package.json +4 -3
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Copyright (C) Fabriktor, Inc. 2025-present.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
// not use this file except in compliance with the License. You may obtain
|
|
5
|
+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
import { queryNestedParam, queryParams } from "@fabriktor/client";
|
|
7
|
+
import { ColRoutesClientAvailabilities, ColRoutesRouteTeams, ColRoutesRoutes, ColRoutesTags, ColRoutesTracking, UserTypeClient, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
|
|
8
|
+
import { isValidInputDate } from "../core/date.js";
|
|
9
|
+
import { errResolveFailed, errValidation } from "../core/err.js";
|
|
10
|
+
import { normalizeIDs, resolvedID } from "../core/id.js";
|
|
11
|
+
import { routeInput, validateRouteInput, validateTrackingInput, } from "./validation.js";
|
|
12
|
+
export const routeRefreshDefaults = {
|
|
13
|
+
delay_ms: 30_000,
|
|
14
|
+
max_attempts: 120,
|
|
15
|
+
timeout_ms: 15_000,
|
|
16
|
+
};
|
|
17
|
+
export const RoutesResource = {
|
|
18
|
+
Routes: ColRoutesRoutes,
|
|
19
|
+
Tags: ColRoutesTags,
|
|
20
|
+
Teams: ColRoutesRouteTeams,
|
|
21
|
+
ClientAvailabilities: ColRoutesClientAvailabilities,
|
|
22
|
+
Tracking: ColRoutesTracking,
|
|
23
|
+
};
|
|
24
|
+
// Only tags and teams expose Atlas search. These are UI capabilities, never
|
|
25
|
+
// a substitute for the service's per-record authorization.
|
|
26
|
+
export const routesSearchableResources = [
|
|
27
|
+
RoutesResource.Tags,
|
|
28
|
+
RoutesResource.Teams,
|
|
29
|
+
];
|
|
30
|
+
const msgAccess = "This Routes operation is not available for the current actor.";
|
|
31
|
+
const msgOwner = "An explicit employer scope is required.";
|
|
32
|
+
const msgDate = "A valid date with an explicit operational timezone is required.";
|
|
33
|
+
const msgDaysAgo = "days_ago must be a nonnegative whole number.";
|
|
34
|
+
const msgEmployees = "At least one employee must be supplied for availability.";
|
|
35
|
+
const msgAvailabilityMissing = "The availability response did not contain a value.";
|
|
36
|
+
const msgDesignationMissing = "The designation response did not contain a value.";
|
|
37
|
+
const msgAssignment = "Tracking requires an assigned employee and a vehicle with scheduled stops.";
|
|
38
|
+
export function routeCanPlan(actor) {
|
|
39
|
+
return !!resolvedID(actor.owner_id) && actor.active_mode === UserTypeEmployer;
|
|
40
|
+
}
|
|
41
|
+
export function routeCanManage(actor, route) {
|
|
42
|
+
return routeCanPlan(actor) && actor.owner_id === route.owner_id;
|
|
43
|
+
}
|
|
44
|
+
export function routeCanRead(actor, route) {
|
|
45
|
+
if (!resolvedID(actor.owner_id))
|
|
46
|
+
return false;
|
|
47
|
+
if (routeCanManage(actor, route))
|
|
48
|
+
return true;
|
|
49
|
+
return ((actor.active_mode === UserTypeEmployee &&
|
|
50
|
+
route.utilized_employee_ids.includes(actor.owner_id)) ||
|
|
51
|
+
(actor.active_mode === UserTypeClient &&
|
|
52
|
+
route.utilized_client_ids.includes(actor.owner_id)));
|
|
53
|
+
}
|
|
54
|
+
export function routeTrackingVehicle(route, vehicle_id, employee_id) {
|
|
55
|
+
if (route.override_itinerary)
|
|
56
|
+
return undefined;
|
|
57
|
+
return route.itinerary.routes.find((vehicle) => vehicle.vehicle_id === vehicle_id &&
|
|
58
|
+
!vehicle.is_omitted &&
|
|
59
|
+
vehicle.stops.length > 0 &&
|
|
60
|
+
vehicle.employee_ids.includes(employee_id));
|
|
61
|
+
}
|
|
62
|
+
export function routeCanTrack(actor, route, vehicle_id, employee_id) {
|
|
63
|
+
return (!!resolvedID(actor.owner_id) &&
|
|
64
|
+
!!routeTrackingVehicle(route, vehicle_id, employee_id) &&
|
|
65
|
+
(routeCanManage(actor, route) ||
|
|
66
|
+
(actor.active_mode === UserTypeEmployee &&
|
|
67
|
+
actor.owner_id === employee_id)));
|
|
68
|
+
}
|
|
69
|
+
export function routeTrackingInput(actor, route, vehicle_id, employee_id, position) {
|
|
70
|
+
requireRoutes(routeCanTrack(actor, route, vehicle_id, employee_id), msgAssignment);
|
|
71
|
+
const input = {
|
|
72
|
+
...position,
|
|
73
|
+
timestamp: { ...position.timestamp },
|
|
74
|
+
route_id: route.id,
|
|
75
|
+
vehicle_id,
|
|
76
|
+
employee_id,
|
|
77
|
+
};
|
|
78
|
+
validateTrackingInput(input);
|
|
79
|
+
return input;
|
|
80
|
+
}
|
|
81
|
+
export class RoutesWorkspace {
|
|
82
|
+
routes;
|
|
83
|
+
constructor(routes) {
|
|
84
|
+
this.routes = routes;
|
|
85
|
+
}
|
|
86
|
+
async readRoutes(opts) {
|
|
87
|
+
const actor = opts.actor;
|
|
88
|
+
const owner_id = routeReadOwner(actor, opts.employer_id);
|
|
89
|
+
if (opts.day)
|
|
90
|
+
requireRoutes(isValidInputDate(opts.day), msgDate);
|
|
91
|
+
const query = {
|
|
92
|
+
...queryParams({
|
|
93
|
+
owner_id,
|
|
94
|
+
// A day filter refers to the calendar day stored on each route.
|
|
95
|
+
// Employers may operate depots in different timezones.
|
|
96
|
+
utilized_employee_ids: actor.active_mode === UserTypeEmployee
|
|
97
|
+
? actor.owner_id
|
|
98
|
+
: optionalIDs(opts.employee_ids),
|
|
99
|
+
utilized_client_ids: actor.active_mode === UserTypeClient
|
|
100
|
+
? actor.owner_id
|
|
101
|
+
: optionalIDs(opts.client_ids),
|
|
102
|
+
}),
|
|
103
|
+
...(opts.day
|
|
104
|
+
? {
|
|
105
|
+
...queryNestedParam(["from", "y"], opts.day.y),
|
|
106
|
+
...queryNestedParam(["from", "m"], opts.day.m),
|
|
107
|
+
...queryNestedParam(["from", "d"], opts.day.d),
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
};
|
|
111
|
+
const out = await this.routes.queryRoutes({ query, signal: opts.signal });
|
|
112
|
+
// Preserve backend membership/order; never emulate a collection search.
|
|
113
|
+
return out.value ?? [];
|
|
114
|
+
}
|
|
115
|
+
async availability(opts) {
|
|
116
|
+
requireRoutes(routeCanPlan(opts.actor), msgAccess);
|
|
117
|
+
requireRoutes(isValidInputDate(opts.date), msgDate);
|
|
118
|
+
requireRoutes(Number.isSafeInteger(opts.days_ago) && opts.days_ago >= 0, msgDaysAgo);
|
|
119
|
+
const employee_ids = normalizeIDs(opts.employee_ids);
|
|
120
|
+
requireRoutes(employee_ids.length > 0, msgEmployees);
|
|
121
|
+
const out = await this.routes.getAvailability({ employee_ids, days_ago: opts.days_ago, date: opts.date }, opts.request);
|
|
122
|
+
if (out.value === undefined)
|
|
123
|
+
throw errResolveFailed({
|
|
124
|
+
resource: RoutesResource.ClientAvailabilities,
|
|
125
|
+
msg: msgAvailabilityMissing,
|
|
126
|
+
});
|
|
127
|
+
return out.value;
|
|
128
|
+
}
|
|
129
|
+
async prepareRoute(opts) {
|
|
130
|
+
requireRoutes(routeCanPlan(opts.actor), msgAccess);
|
|
131
|
+
const input = routeInput(opts.in);
|
|
132
|
+
validateRouteInput(input);
|
|
133
|
+
const availability = await this.availability({
|
|
134
|
+
actor: opts.actor,
|
|
135
|
+
employee_ids: input.vehicles.flatMap((vehicle) => vehicle.employee_ids),
|
|
136
|
+
days_ago: opts.days_ago,
|
|
137
|
+
date: input.from,
|
|
138
|
+
request: opts.request,
|
|
139
|
+
});
|
|
140
|
+
validateRouteInput(input, availability);
|
|
141
|
+
return { in: input, availability };
|
|
142
|
+
}
|
|
143
|
+
async designations(opts) {
|
|
144
|
+
requireRoutes(!!resolvedID(opts.actor.owner_id) &&
|
|
145
|
+
(opts.actor.active_mode === UserTypeEmployee ||
|
|
146
|
+
opts.actor.active_mode === UserTypeClient), msgAccess);
|
|
147
|
+
const employer_id = resolvedID(opts.employer_id);
|
|
148
|
+
requireRoutes(employer_id, msgOwner);
|
|
149
|
+
requireRoutes(isValidInputDate(opts.date), msgDate);
|
|
150
|
+
// The server derives the viewer from authentication. There is no viewer
|
|
151
|
+
// ID override in this payload; date means this day onward, not one day.
|
|
152
|
+
const out = await this.routes.getDesignation({ employer_id, date: opts.date }, opts.request);
|
|
153
|
+
if (out.value === undefined)
|
|
154
|
+
throw errResolveFailed({
|
|
155
|
+
resource: RoutesResource.Routes,
|
|
156
|
+
msg: msgDesignationMissing,
|
|
157
|
+
});
|
|
158
|
+
return out.value;
|
|
159
|
+
}
|
|
160
|
+
async clientAvailabilities(opts) {
|
|
161
|
+
requireRoutes(!!resolvedID(opts.actor.owner_id) &&
|
|
162
|
+
opts.actor.active_mode === UserTypeClient, msgAccess);
|
|
163
|
+
const out = await this.routes.queryClientAvailabilities({
|
|
164
|
+
signal: opts.signal,
|
|
165
|
+
query: queryParams({
|
|
166
|
+
owner_id: opts.actor.owner_id,
|
|
167
|
+
employer_id: optionalIDs(opts.employer_ids),
|
|
168
|
+
location_id: optionalIDs(opts.location_ids),
|
|
169
|
+
timezone: opts.timezone,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
return out.value ?? [];
|
|
173
|
+
}
|
|
174
|
+
async tracking(opts) {
|
|
175
|
+
requireRoutes(routeCanRead(opts.actor, opts.route) &&
|
|
176
|
+
opts.actor.active_mode !== UserTypeClient, msgAccess);
|
|
177
|
+
const out = await this.routes.queryTracking({
|
|
178
|
+
signal: opts.signal,
|
|
179
|
+
query: queryParams({
|
|
180
|
+
owner_id: opts.route.owner_id,
|
|
181
|
+
route_id: opts.route.id,
|
|
182
|
+
employee_id: opts.actor.active_mode === UserTypeEmployee
|
|
183
|
+
? opts.actor.owner_id
|
|
184
|
+
: optionalIDs(opts.employee_ids),
|
|
185
|
+
vehicle_id: optionalIDs(opts.vehicle_ids),
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
return out.value ?? [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function routeReadOwner(actor, employer_id) {
|
|
192
|
+
requireRoutes(resolvedID(actor.owner_id), msgAccess);
|
|
193
|
+
if (routeCanPlan(actor)) {
|
|
194
|
+
requireRoutes(employer_id === undefined || resolvedID(employer_id) === actor.owner_id, msgAccess);
|
|
195
|
+
return actor.owner_id;
|
|
196
|
+
}
|
|
197
|
+
requireRoutes(actor.active_mode === UserTypeEmployee ||
|
|
198
|
+
actor.active_mode === UserTypeClient, msgAccess);
|
|
199
|
+
const owner_id = resolvedID(employer_id);
|
|
200
|
+
requireRoutes(owner_id, msgOwner);
|
|
201
|
+
return owner_id;
|
|
202
|
+
}
|
|
203
|
+
function optionalIDs(values) {
|
|
204
|
+
if (values === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
const ids = normalizeIDs(values);
|
|
207
|
+
return ids.length ? ids : undefined;
|
|
208
|
+
}
|
|
209
|
+
function requireRoutes(value, msg) {
|
|
210
|
+
if (!value)
|
|
211
|
+
throw errValidation({ msg });
|
|
212
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Date as SchemaDate, type OperationResult, type Summary, type WeekSheet } from "@fabriktor/schema";
|
|
2
2
|
import type { BillableOperator, TimesheetsOperator } from "@fabriktor/client";
|
|
3
3
|
import type { ContactsFXOperator } from "../contacts/contacts.js";
|
|
4
|
+
import type { NowProvider } from "../core/server-clock.js";
|
|
4
5
|
import type { MultiCreateAttempt } from "../core/multi-create.js";
|
|
5
6
|
import { type LoadTimesheetsReadContextOptions, type QueryTimesheetSyncDestinationsOptions, type QueryTimesheetSyncSourcesOptions, type TimesheetsReadContext, type TimesheetsReadOptions, type TimesheetSyncTarget } from "./access.js";
|
|
6
7
|
import { TimesheetsWorkspace } from "./workspace.js";
|
|
@@ -13,7 +14,7 @@ export type TimesheetsFXOptions = {
|
|
|
13
14
|
timesheets: TimesheetsOperator;
|
|
14
15
|
contacts_fx: ContactsFXOperator;
|
|
15
16
|
billable?: BillableOperator;
|
|
16
|
-
now?:
|
|
17
|
+
now?: NowProvider;
|
|
17
18
|
};
|
|
18
19
|
export type PunchContext = Pick<PunchPayload, "timezone" | "location_id" | "employer_id">;
|
|
19
20
|
export type PunchInOptions = PunchContext & {
|
|
@@ -55,6 +56,7 @@ export type PunchControls = {
|
|
|
55
56
|
can_punch_out: boolean;
|
|
56
57
|
can_ride_in: boolean;
|
|
57
58
|
can_ride_out: boolean;
|
|
59
|
+
can_start_transport: boolean;
|
|
58
60
|
};
|
|
59
61
|
export type SyncWeekOptions = {
|
|
60
62
|
from: WeekSheet;
|
|
@@ -129,6 +129,7 @@ export class TimesheetsFX {
|
|
|
129
129
|
can_punch_out: true,
|
|
130
130
|
can_ride_in: !ride_active,
|
|
131
131
|
can_ride_out: ride_active,
|
|
132
|
+
can_start_transport: !ride_active,
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
async punchIn(opts) {
|
|
@@ -454,6 +455,7 @@ function disabledPunchControls() {
|
|
|
454
455
|
can_punch_out: false,
|
|
455
456
|
can_ride_in: false,
|
|
456
457
|
can_ride_out: false,
|
|
458
|
+
can_start_transport: false,
|
|
457
459
|
};
|
|
458
460
|
}
|
|
459
461
|
function readyToPunchInControls() {
|
|
@@ -462,6 +464,7 @@ function readyToPunchInControls() {
|
|
|
462
464
|
can_punch_out: false,
|
|
463
465
|
can_ride_in: false,
|
|
464
466
|
can_ride_out: false,
|
|
467
|
+
can_start_transport: true,
|
|
465
468
|
};
|
|
466
469
|
}
|
|
467
470
|
function cloneDate(v) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BillableOperator, type TimesheetsOperator } from "@fabriktor/client";
|
|
2
2
|
import { PunchTypeIn, PunchTypeOut, PunchTypeRideIn, PunchTypeRideOut, type InDate, type Punch, type Summary, type WeekSheet } from "@fabriktor/schema";
|
|
3
|
+
import type { NowProvider } from "../core/server-clock.js";
|
|
3
4
|
import type { TimesheetsFXOperator } from "./timesheets.js";
|
|
4
5
|
export type TimesheetFilters = {
|
|
5
6
|
owner_ids?: WeekSheet["owner_id"][];
|
|
@@ -24,12 +25,14 @@ export type TimesheetPunchState = {
|
|
|
24
25
|
};
|
|
25
26
|
export type TimesheetPunchAction = typeof PunchTypeIn | typeof PunchTypeOut | typeof PunchTypeRideIn | typeof PunchTypeRideOut;
|
|
26
27
|
export type CheckedPunchOptions = PunchStateOptions & {
|
|
28
|
+
before_request?: () => void;
|
|
27
29
|
location_id: string;
|
|
28
30
|
action: TimesheetPunchAction;
|
|
29
31
|
expected: TimesheetPunchState;
|
|
30
32
|
total_pause_minutes?: number;
|
|
31
33
|
total_meal_minutes?: number;
|
|
32
34
|
};
|
|
35
|
+
export type StartTimesheetTransportOptions = Omit<CheckedPunchOptions, "action" | "total_pause_minutes" | "total_meal_minutes">;
|
|
33
36
|
export type SaveTimesheetDayOptions = PunchStateOptions & {
|
|
34
37
|
location_id: string;
|
|
35
38
|
start: InDate;
|
|
@@ -39,6 +42,7 @@ export type SaveTimesheetDayOptions = PunchStateOptions & {
|
|
|
39
42
|
replace?: WeekSheet;
|
|
40
43
|
};
|
|
41
44
|
export declare const TimesheetsBlock: {
|
|
45
|
+
readonly Busy: "timesheets_busy";
|
|
42
46
|
readonly Changed: "timesheets_changed";
|
|
43
47
|
readonly Locked: "timesheets_locked";
|
|
44
48
|
readonly Active: "timesheets_active";
|
|
@@ -59,7 +63,8 @@ export declare class TimesheetsWorkspace {
|
|
|
59
63
|
private readonly actions;
|
|
60
64
|
private readonly billable?;
|
|
61
65
|
private readonly now;
|
|
62
|
-
|
|
66
|
+
private punchPending;
|
|
67
|
+
constructor(client: TimesheetsOperator, actions: TimesheetsFXOperator, billable?: BillableOperator | undefined, now?: NowProvider);
|
|
63
68
|
weeks(opts: TimesheetFilters): Promise<WeekSheet[]>;
|
|
64
69
|
summaries(opts: TimesheetFilters): Promise<Summary[]>;
|
|
65
70
|
week(id: string, signal?: AbortSignal): Promise<WeekSheet>;
|
|
@@ -67,6 +72,9 @@ export declare class TimesheetsWorkspace {
|
|
|
67
72
|
periods(employer_id: string, start: InDate, end?: InDate): Promise<number[]>;
|
|
68
73
|
punchState(opts: PunchStateOptions): Promise<TimesheetPunchState>;
|
|
69
74
|
punch(opts: CheckedPunchOptions): Promise<WeekSheet>;
|
|
75
|
+
startTransport(opts: StartTimesheetTransportOptions): Promise<WeekSheet>;
|
|
76
|
+
private withPunch;
|
|
77
|
+
private performPunch;
|
|
70
78
|
saveDay(opts: SaveTimesheetDayOptions): Promise<WeekSheet>;
|
|
71
79
|
removeDay(owner_id: string, expected: WeekSheet, date: InDate): Promise<void>;
|
|
72
80
|
removeWeek(actor_id: string, expected: WeekSheet): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import { errRequired } from "../core/err.js";
|
|
|
5
5
|
import { PreActionBlocked } from "../core/pre-action.js";
|
|
6
6
|
import { queryRecordParams, runOwnerScopeQuery, } from "../core/query.js";
|
|
7
7
|
export const TimesheetsBlock = {
|
|
8
|
+
Busy: "timesheets_busy",
|
|
8
9
|
Changed: "timesheets_changed",
|
|
9
10
|
Locked: "timesheets_locked",
|
|
10
11
|
Active: "timesheets_active",
|
|
@@ -74,6 +75,7 @@ export class TimesheetsWorkspace {
|
|
|
74
75
|
actions;
|
|
75
76
|
billable;
|
|
76
77
|
now;
|
|
78
|
+
punchPending = false;
|
|
77
79
|
constructor(client, actions, billable, now = () => new Date()) {
|
|
78
80
|
this.client = client;
|
|
79
81
|
this.actions = actions;
|
|
@@ -109,7 +111,7 @@ export class TimesheetsWorkspace {
|
|
|
109
111
|
}
|
|
110
112
|
async punchState(opts) {
|
|
111
113
|
requireState(!!opts.owner_id && !!opts.timezone, TimesheetsBlock.InvalidDay);
|
|
112
|
-
const today = opts.date ?? inDateAtTimestamp(this.now().toISOString(), opts.timezone);
|
|
114
|
+
const today = opts.date ?? inDateAtTimestamp((await this.now()).toISOString(), opts.timezone);
|
|
113
115
|
const week_day = timesheetWeekDay(today);
|
|
114
116
|
// Check every open work punch, including other employers/locations/weeks.
|
|
115
117
|
// A scope selection must never hide an already open shift.
|
|
@@ -124,7 +126,7 @@ export class TimesheetsWorkspace {
|
|
|
124
126
|
requireState(open.length <= 1, TimesheetsBlock.Active);
|
|
125
127
|
if (open[0]) {
|
|
126
128
|
const current = open[0];
|
|
127
|
-
const date = inDateAtTimestamp(this.now().toISOString(), current.punch.timezone);
|
|
129
|
+
const date = inDateAtTimestamp((await this.now()).toISOString(), current.punch.timezone);
|
|
128
130
|
return {
|
|
129
131
|
...current,
|
|
130
132
|
stale_day: !sameDay(current.punch.punch_in_time, opts.date ?? date),
|
|
@@ -146,8 +148,47 @@ export class TimesheetsWorkspace {
|
|
|
146
148
|
return { week_sheet, punch, week_day, stale_day: false };
|
|
147
149
|
}
|
|
148
150
|
async punch(opts) {
|
|
151
|
+
return await this.withPunch(() => this.performPunch(opts));
|
|
152
|
+
}
|
|
153
|
+
// A trip belongs to its destination's work session. Keep both steps ordered
|
|
154
|
+
// and validate the acknowledged work state before starting its transport.
|
|
155
|
+
// Never retry or roll back a punch automatically after an uncertain response.
|
|
156
|
+
async startTransport(opts) {
|
|
157
|
+
return await this.withPunch(async () => {
|
|
158
|
+
let expected = opts.expected;
|
|
159
|
+
if (expected.punch?.punch_status !== PunchStatusWorking) {
|
|
160
|
+
const week_sheet = await this.performPunch({
|
|
161
|
+
...opts,
|
|
162
|
+
action: PunchTypeIn,
|
|
163
|
+
});
|
|
164
|
+
const week_day = expected.week_day;
|
|
165
|
+
const punch = week_sheet.sheets
|
|
166
|
+
.find((day) => day.week_day === week_day)
|
|
167
|
+
?.punches.at(-1);
|
|
168
|
+
expected = { week_sheet, punch, week_day, stale_day: false };
|
|
169
|
+
}
|
|
170
|
+
return await this.performPunch({
|
|
171
|
+
...opts,
|
|
172
|
+
expected,
|
|
173
|
+
action: PunchTypeRideIn,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// The existing FX request guard protects individual writes. This guard also
|
|
178
|
+
// keeps other checked punch actions out of the gap between these two steps.
|
|
179
|
+
async withPunch(fn) {
|
|
180
|
+
requireState(!this.punchPending, TimesheetsBlock.Busy);
|
|
181
|
+
this.punchPending = true;
|
|
182
|
+
try {
|
|
183
|
+
return await fn();
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
this.punchPending = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async performPunch(opts) {
|
|
149
190
|
if (opts.date)
|
|
150
|
-
this.validateDay(opts.date, opts.timezone);
|
|
191
|
+
await this.validateDay(opts.date, opts.timezone);
|
|
151
192
|
const current = await this.punchState(opts);
|
|
152
193
|
requireState(timesheetStateKey(current) === timesheetStateKey(opts.expected), TimesheetsBlock.Changed);
|
|
153
194
|
requireState(!current.stale_day, TimesheetsBlock.InvalidDay);
|
|
@@ -169,6 +210,7 @@ export class TimesheetsWorkspace {
|
|
|
169
210
|
requireState(sameDay(opts.date, current.punch.punch_in_time) &&
|
|
170
211
|
wallTime(opts.date) >= wallTime(current.punch.punch_in_time), TimesheetsBlock.InvalidDay);
|
|
171
212
|
}
|
|
213
|
+
opts.before_request?.();
|
|
172
214
|
const context = {
|
|
173
215
|
employer_id: opts.employer_id,
|
|
174
216
|
location_id: opts.location_id,
|
|
@@ -204,8 +246,8 @@ export class TimesheetsWorkspace {
|
|
|
204
246
|
return sheet;
|
|
205
247
|
}
|
|
206
248
|
async saveDay(opts) {
|
|
207
|
-
this.validateDay(opts.start, opts.timezone);
|
|
208
|
-
this.validateDay(opts.end, opts.timezone);
|
|
249
|
+
await this.validateDay(opts.start, opts.timezone);
|
|
250
|
+
await this.validateDay(opts.end, opts.timezone);
|
|
209
251
|
requireState(sameDay(opts.start, opts.end) &&
|
|
210
252
|
wallTime(opts.end) > wallTime(opts.start), TimesheetsBlock.InvalidDay);
|
|
211
253
|
const indexes = await this.periods(opts.employer_id, opts.start);
|
|
@@ -267,7 +309,7 @@ export class TimesheetsWorkspace {
|
|
|
267
309
|
await this.client.deleteOneWeekSheet({ id: sheet.id });
|
|
268
310
|
}
|
|
269
311
|
async blankWeek(opts) {
|
|
270
|
-
this.validateDay(opts.date, opts.timezone);
|
|
312
|
+
await this.validateDay(opts.date, opts.timezone);
|
|
271
313
|
const indexes = await this.periods(opts.employer_id, opts.date);
|
|
272
314
|
const matches = await this.weeks({
|
|
273
315
|
owner_ids: [opts.owner_id],
|
|
@@ -304,8 +346,8 @@ export class TimesheetsWorkspace {
|
|
|
304
346
|
requireState(timesheetIsMutable(sheet), TimesheetsBlock.Locked);
|
|
305
347
|
return sheet;
|
|
306
348
|
}
|
|
307
|
-
validateDay(date, timezone) {
|
|
308
|
-
const now = inDateAtTimestamp(this.now().toISOString(), timezone);
|
|
349
|
+
async validateDay(date, timezone) {
|
|
350
|
+
const now = inDateAtTimestamp((await this.now()).toISOString(), timezone);
|
|
309
351
|
const calendar = new Date(Date.UTC(date.y, date.m - 1, date.d));
|
|
310
352
|
requireState(!!now &&
|
|
311
353
|
date.tz === timezone &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.141",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"typescript": "^6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fabriktor/client": "0.0.
|
|
20
|
+
"@fabriktor/client": "0.0.98",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
|
-
"@fabriktor/schema": "0.0.68"
|
|
22
|
+
"@fabriktor/schema": "0.0.68",
|
|
23
|
+
"convert": "8.0.2"
|
|
23
24
|
}
|
|
24
25
|
}
|