@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,23 @@
|
|
|
1
|
+
export declare const chatDataProtocol = "fabriktor.chat.data";
|
|
2
|
+
export declare const chatDataVersion = 1;
|
|
3
|
+
export declare const chatDataMarker = "\n\n[Fabriktor data]\n";
|
|
4
|
+
export declare const chatDataMaximumBytes = 16384;
|
|
5
|
+
export type ChatJSON = null | boolean | number | string | ChatJSON[] | {
|
|
6
|
+
[key: string]: ChatJSON;
|
|
7
|
+
};
|
|
8
|
+
export type ChatData = {
|
|
9
|
+
protocol: typeof chatDataProtocol;
|
|
10
|
+
version: typeof chatDataVersion;
|
|
11
|
+
type: string;
|
|
12
|
+
payload: {
|
|
13
|
+
[key: string]: ChatJSON;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type ChatContent = {
|
|
17
|
+
text: string;
|
|
18
|
+
data?: ChatData;
|
|
19
|
+
unsupported: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function newChatData(type: string, payload: ChatData["payload"]): ChatData;
|
|
22
|
+
export declare function encodeChatContent(text: string, data?: ChatData): string;
|
|
23
|
+
export declare function decodeChatContent(value: string): ChatContent;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { errValidation } from "../core/err.js";
|
|
2
|
+
// Frontend message framing over the existing Chat.text contract. No executable
|
|
3
|
+
// templates, URLs, component names, or permissions are accepted from messages.
|
|
4
|
+
export const chatDataProtocol = "fabriktor.chat.data";
|
|
5
|
+
export const chatDataVersion = 1;
|
|
6
|
+
export const chatDataMarker = "\n\n[Fabriktor data]\n";
|
|
7
|
+
export const chatDataMaximumBytes = 16_384;
|
|
8
|
+
const chatDataMaximumDepth = 8;
|
|
9
|
+
const msgChatData = "The structured chat message is invalid or too large.";
|
|
10
|
+
export function newChatData(type, payload) {
|
|
11
|
+
const data = {
|
|
12
|
+
protocol: chatDataProtocol,
|
|
13
|
+
version: chatDataVersion,
|
|
14
|
+
type,
|
|
15
|
+
payload,
|
|
16
|
+
};
|
|
17
|
+
if (!validChatData(data))
|
|
18
|
+
throw errValidation({ msg: msgChatData });
|
|
19
|
+
return structuredClone(data);
|
|
20
|
+
}
|
|
21
|
+
export function encodeChatContent(text, data) {
|
|
22
|
+
if (!data)
|
|
23
|
+
return text;
|
|
24
|
+
if (!text.trim() || text.includes(chatDataMarker) || !validChatData(data))
|
|
25
|
+
throw errValidation({ msg: msgChatData });
|
|
26
|
+
const value = JSON.stringify(data);
|
|
27
|
+
if (new TextEncoder().encode(value).length > chatDataMaximumBytes)
|
|
28
|
+
throw errValidation({ msg: msgChatData });
|
|
29
|
+
return text.trim() + chatDataMarker + value;
|
|
30
|
+
}
|
|
31
|
+
export function decodeChatContent(value) {
|
|
32
|
+
const boundary = value.indexOf(chatDataMarker);
|
|
33
|
+
if (boundary < 0)
|
|
34
|
+
return { text: value, unsupported: false };
|
|
35
|
+
const text = value.slice(0, boundary).trimEnd();
|
|
36
|
+
const raw = value.slice(boundary + chatDataMarker.length);
|
|
37
|
+
if (raw.length > chatDataMaximumBytes ||
|
|
38
|
+
new TextEncoder().encode(raw).length > chatDataMaximumBytes)
|
|
39
|
+
return { text, unsupported: true };
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(raw);
|
|
42
|
+
return validChatData(data)
|
|
43
|
+
? { text, data, unsupported: false }
|
|
44
|
+
: { text, unsupported: true };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { text, unsupported: true };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validChatData(value) {
|
|
51
|
+
return (jsonObject(value) &&
|
|
52
|
+
value.protocol === chatDataProtocol &&
|
|
53
|
+
value.version === chatDataVersion &&
|
|
54
|
+
typeof value.type === "string" &&
|
|
55
|
+
/^[a-z][a-z0-9_.-]{0,95}$/.test(value.type) &&
|
|
56
|
+
jsonObject(value.payload) &&
|
|
57
|
+
validJSON(value.payload, chatDataMaximumDepth));
|
|
58
|
+
}
|
|
59
|
+
function jsonObject(value) {
|
|
60
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
function validJSON(value, depth) {
|
|
63
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
64
|
+
return true;
|
|
65
|
+
if (typeof value === "number")
|
|
66
|
+
return Number.isFinite(value);
|
|
67
|
+
if (depth <= 0)
|
|
68
|
+
return false;
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return value.every((child) => validJSON(child, depth - 1));
|
|
71
|
+
return (jsonObject(value) &&
|
|
72
|
+
Object.entries(value).every(([key, child]) => key !== "__proto__" &&
|
|
73
|
+
key !== "constructor" &&
|
|
74
|
+
key !== "prototype" &&
|
|
75
|
+
validJSON(child, depth - 1)));
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { dateTimezone } from "./date.js";
|
|
2
|
+
// Always use the saved backend computation, never the device's timezone.
|
|
3
|
+
export function addressTimezone(address) {
|
|
4
|
+
const timezone = address.address_timezone.trim();
|
|
5
|
+
return timezone && dateTimezone(timezone) === timezone ? timezone : null;
|
|
6
|
+
}
|
package/dist/src/core/date.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Date as SchemaDate, InDate } from "@fabriktor/schema";
|
|
2
2
|
export declare const utcTimezone = "UTC";
|
|
3
|
+
export declare const minutesPerHour = 60;
|
|
4
|
+
export declare const hoursPerDay = 24;
|
|
5
|
+
export declare const minutesPerDay: number;
|
|
3
6
|
export type LocalDateParts = {
|
|
4
7
|
y: number;
|
|
5
8
|
m: number;
|
|
@@ -7,6 +10,9 @@ export type LocalDateParts = {
|
|
|
7
10
|
h: number;
|
|
8
11
|
min: number;
|
|
9
12
|
};
|
|
13
|
+
export declare function sameCalendarDay(a: LocalDateParts, b: LocalDateParts): boolean;
|
|
14
|
+
export declare function minutesOfDay(v: LocalDateParts): number;
|
|
15
|
+
export declare function isValidInputDate(v: InDate): boolean;
|
|
10
16
|
export declare function normalizeInputDate(v: InDate): InDate;
|
|
11
17
|
export declare function zeroInputDate(): InDate;
|
|
12
18
|
export declare function zeroDate(): SchemaDate;
|
package/dist/src/core/date.js
CHANGED
|
@@ -17,6 +17,33 @@ const zeroDatePrefix = [
|
|
|
17
17
|
].join("-");
|
|
18
18
|
const zeroUTC = `${zeroDatePrefix}T00:00:00Z`;
|
|
19
19
|
export const utcTimezone = "UTC";
|
|
20
|
+
export const minutesPerHour = 60;
|
|
21
|
+
export const hoursPerDay = 24;
|
|
22
|
+
export const minutesPerDay = hoursPerDay * minutesPerHour;
|
|
23
|
+
export function sameCalendarDay(a, b) {
|
|
24
|
+
return a.y === b.y && a.m === b.m && a.d === b.d;
|
|
25
|
+
}
|
|
26
|
+
export function minutesOfDay(v) {
|
|
27
|
+
return v.h * minutesPerHour + v.min;
|
|
28
|
+
}
|
|
29
|
+
// Civil input checks only. The backend remains responsible for resolving the
|
|
30
|
+
// supplied operational timezone to an instant, including daylight-saving rules.
|
|
31
|
+
export function isValidInputDate(v) {
|
|
32
|
+
if (![v.y, v.m, v.d, v.h, v.min].every(Number.isInteger) ||
|
|
33
|
+
v.y <= 0 ||
|
|
34
|
+
v.h < 0 ||
|
|
35
|
+
v.h >= hoursPerDay ||
|
|
36
|
+
v.min < 0 ||
|
|
37
|
+
v.min >= minutesPerHour ||
|
|
38
|
+
!v.tz ||
|
|
39
|
+
dateTimezone(v.tz) !== v.tz)
|
|
40
|
+
return false;
|
|
41
|
+
const day = new globalThis.Date(0);
|
|
42
|
+
day.setUTCFullYear(v.y, v.m - 1, v.d);
|
|
43
|
+
return (day.getUTCFullYear() === v.y &&
|
|
44
|
+
day.getUTCMonth() + 1 === v.m &&
|
|
45
|
+
day.getUTCDate() === v.d);
|
|
46
|
+
}
|
|
20
47
|
export function normalizeInputDate(v) {
|
|
21
48
|
if (isZeroDate(v)) {
|
|
22
49
|
return zeroInputDate();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DistanceUnit = "meter" | "kilometer" | "foot" | "yard" | "mile";
|
|
2
|
+
export type DurationUnit = "day" | "hour" | "minute" | "second";
|
|
3
|
+
export type Measurement<U extends string> = {
|
|
4
|
+
value: number;
|
|
5
|
+
unit: U;
|
|
6
|
+
};
|
|
7
|
+
type RoadUnits = {
|
|
8
|
+
large: DistanceUnit;
|
|
9
|
+
small: DistanceUnit;
|
|
10
|
+
threshold: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const roadDistanceUnits: Readonly<Record<string, RoadUnits>>;
|
|
13
|
+
export declare function roadDistance(meters: number, country: string): Measurement<DistanceUnit> | undefined;
|
|
14
|
+
export declare function durationParts(value: number, unit: DurationUnit): Measurement<DurationUnit>[];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import convert from "convert";
|
|
2
|
+
// Road-distance preferences, not a country's system for every measurement.
|
|
3
|
+
// Thresholds/units follow Unicode CLDR's road usage; metric is the fallback.
|
|
4
|
+
// https://github.com/unicode-org/cldr/blob/main/common/supplemental/units.xml
|
|
5
|
+
const metricRoadUnits = {
|
|
6
|
+
large: "kilometer",
|
|
7
|
+
small: "meter",
|
|
8
|
+
threshold: 0.9,
|
|
9
|
+
};
|
|
10
|
+
export const roadDistanceUnits = {
|
|
11
|
+
US: { large: "mile", small: "foot", threshold: 0.5 },
|
|
12
|
+
GB: { large: "mile", small: "yard", threshold: 0.5 },
|
|
13
|
+
};
|
|
14
|
+
export function roadDistance(meters, country) {
|
|
15
|
+
if (!Number.isFinite(meters) || meters < 0)
|
|
16
|
+
return;
|
|
17
|
+
const units = roadDistanceUnits[country.trim().toUpperCase()] ?? metricRoadUnits;
|
|
18
|
+
const large = convert(meters, "meter").to(units.large);
|
|
19
|
+
return large >= units.threshold
|
|
20
|
+
? { value: large, unit: units.large }
|
|
21
|
+
: {
|
|
22
|
+
value: convert(meters, "meter").to(units.small),
|
|
23
|
+
unit: units.small,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const durationUnits = [
|
|
27
|
+
"day",
|
|
28
|
+
"hour",
|
|
29
|
+
"minute",
|
|
30
|
+
"second",
|
|
31
|
+
];
|
|
32
|
+
export function durationParts(value, unit) {
|
|
33
|
+
if (!Number.isFinite(value) || value < 0)
|
|
34
|
+
return [];
|
|
35
|
+
let seconds = Math.round(convert(value, unit).to("second"));
|
|
36
|
+
const parts = [];
|
|
37
|
+
for (const target of durationUnits) {
|
|
38
|
+
const amount = Math.floor(convert(seconds, "second").to(target));
|
|
39
|
+
if (!amount)
|
|
40
|
+
continue;
|
|
41
|
+
parts.push({ value: amount, unit: target });
|
|
42
|
+
seconds -= convert(amount, target).to("second");
|
|
43
|
+
}
|
|
44
|
+
return parts.length ? parts : [{ value: 0, unit: "second" }];
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type NowProvider = () => globalThis.Date | Promise<globalThis.Date>;
|
|
2
|
+
export declare const serverClockMaximumAgeMS = 60000;
|
|
3
|
+
export declare class ServerClock {
|
|
4
|
+
private readonly elapsedNow;
|
|
5
|
+
private sample?;
|
|
6
|
+
constructor(elapsedNow: () => number);
|
|
7
|
+
observeHTTPDate(value: string | null): void;
|
|
8
|
+
now(): number | null;
|
|
9
|
+
requireNow(): number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { errValidation } from "./err.js";
|
|
2
|
+
export const serverClockMaximumAgeMS = 60_000;
|
|
3
|
+
const msgServerTime = "Server time is unavailable. Refresh and try again.";
|
|
4
|
+
// Wall time comes only from the API. The injected monotonic timer measures
|
|
5
|
+
// elapsed duration; browser timezone and device wall-clock changes are irrelevant.
|
|
6
|
+
export class ServerClock {
|
|
7
|
+
elapsedNow;
|
|
8
|
+
sample;
|
|
9
|
+
constructor(elapsedNow) {
|
|
10
|
+
this.elapsedNow = elapsedNow;
|
|
11
|
+
}
|
|
12
|
+
observeHTTPDate(value) {
|
|
13
|
+
if (!value || !value.endsWith(" GMT"))
|
|
14
|
+
return;
|
|
15
|
+
const epoch = globalThis.Date.parse(value);
|
|
16
|
+
if (!Number.isFinite(epoch))
|
|
17
|
+
return;
|
|
18
|
+
this.sample = { epoch, elapsed: this.elapsedNow() };
|
|
19
|
+
}
|
|
20
|
+
now() {
|
|
21
|
+
if (!this.sample)
|
|
22
|
+
return null;
|
|
23
|
+
const elapsed = this.elapsedNow() - this.sample.elapsed;
|
|
24
|
+
if (!Number.isFinite(elapsed) ||
|
|
25
|
+
elapsed < 0 ||
|
|
26
|
+
elapsed > serverClockMaximumAgeMS)
|
|
27
|
+
return null;
|
|
28
|
+
return this.sample.epoch + elapsed;
|
|
29
|
+
}
|
|
30
|
+
requireNow() {
|
|
31
|
+
const value = this.now();
|
|
32
|
+
if (value === null)
|
|
33
|
+
throw errValidation({ msg: msgServerTime });
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/src/fx.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BillableOperator, CallsOperator, ChatOperator, ConnectionsOperator, ContactsOperator, FeedOperator, FulfillmentsOperator, JobsOperator, MarketplaceOperator, MemosOperator, NotificationsOperator, PaymentsOperator, PayrollsOperator, PreferencesOperator, PrivacyOperator, ProcessesOperator, RoutesOperator, StorageOperator, TimesheetsOperator, TmpPhonesOperator, TmpUsernamesOperator, UsersOperator } from "@fabriktor/client";
|
|
1
|
+
import type { HTTPDoer, BillableOperator, CallsOperator, ChatOperator, ConnectionsOperator, ContactsOperator, FeedOperator, FulfillmentsOperator, JobsOperator, MarketplaceOperator, MemosOperator, NotificationsOperator, PaymentsOperator, PayrollsOperator, PreferencesOperator, PrivacyOperator, ProcessesOperator, RoutesOperator, StorageOperator, TimesheetsOperator, TmpPhonesOperator, TmpUsernamesOperator, UsersOperator } from "@fabriktor/client";
|
|
2
|
+
import type { NowProvider } from "./core/server-clock.js";
|
|
2
3
|
import type { AuthOperator } from "./auth/auth.js";
|
|
3
4
|
import type { BillableFX, BillableFXOperator } from "./billable/billable.js";
|
|
4
5
|
import type { CallsFX, CallsFXOperator } from "./calls/calls.js";
|
|
@@ -22,6 +23,7 @@ import type { StorageFX, StorageFXOperator } from "./storage/storage.js";
|
|
|
22
23
|
import type { TimesheetsFX, TimesheetsFXOperator } from "./timesheets/timesheets.js";
|
|
23
24
|
import type { UsersFX, UsersFXOperator } from "./users/users.js";
|
|
24
25
|
export type FXOptions = {
|
|
26
|
+
timesheets_now?: NowProvider;
|
|
25
27
|
auth: AuthOperator;
|
|
26
28
|
billable: BillableOperator;
|
|
27
29
|
calls: CallsOperator;
|
|
@@ -50,6 +52,8 @@ export type FXOptions = {
|
|
|
50
52
|
marketplace_keep_alive_interval_ms: number;
|
|
51
53
|
};
|
|
52
54
|
export type DefaultFXOptions = {
|
|
55
|
+
http?: HTTPDoer;
|
|
56
|
+
timesheets_now?: NowProvider;
|
|
53
57
|
base_url: string;
|
|
54
58
|
auth: AuthOperator;
|
|
55
59
|
get_device_token: DeviceTokenProvider;
|
package/dist/src/fx.js
CHANGED
|
@@ -100,6 +100,9 @@ export class FX {
|
|
|
100
100
|
});
|
|
101
101
|
this.routes = newRoutesFX({
|
|
102
102
|
routes: opts.routes,
|
|
103
|
+
jobs: opts.jobs,
|
|
104
|
+
users: opts.users,
|
|
105
|
+
billable: opts.billable,
|
|
103
106
|
});
|
|
104
107
|
this.storage = newStorageFX({
|
|
105
108
|
storage: opts.storage,
|
|
@@ -117,6 +120,7 @@ export class FX {
|
|
|
117
120
|
timesheets: opts.timesheets,
|
|
118
121
|
contacts_fx: this.contacts,
|
|
119
122
|
billable: opts.billable,
|
|
123
|
+
now: opts.timesheets_now,
|
|
120
124
|
});
|
|
121
125
|
this.users = newUsersFX({
|
|
122
126
|
users: opts.users,
|
|
@@ -135,10 +139,11 @@ export function newFX(opts) {
|
|
|
135
139
|
return new FX(opts);
|
|
136
140
|
}
|
|
137
141
|
export function newDefaultFX(opts) {
|
|
138
|
-
const http = newHTTPClient();
|
|
142
|
+
const http = opts.http ?? newHTTPClient();
|
|
139
143
|
const get_token = () => opts.auth.getToken();
|
|
140
144
|
return newFX({
|
|
141
145
|
auth: opts.auth,
|
|
146
|
+
timesheets_now: opts.timesheets_now,
|
|
142
147
|
billable: newBillableClient({
|
|
143
148
|
http,
|
|
144
149
|
base_url: opts.base_url,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,14 +2,17 @@ export * from "./auth/auth.js";
|
|
|
2
2
|
export * from "./billable/billable.js";
|
|
3
3
|
export * from "./calls/calls.js";
|
|
4
4
|
export * from "./chat/chat.js";
|
|
5
|
+
export * from "./chat/data.js";
|
|
5
6
|
export * from "./chat/optimistic.js";
|
|
6
7
|
export * from "./chat/send-queue.js";
|
|
7
8
|
export * from "./contacts/contacts.js";
|
|
8
9
|
export * from "./contacts/scope.js";
|
|
10
|
+
export * from "./core/address.js";
|
|
9
11
|
export * from "./core/date.js";
|
|
10
12
|
export * from "./core/err.js";
|
|
11
13
|
export * from "./core/events.js";
|
|
12
14
|
export * from "./core/id.js";
|
|
15
|
+
export * from "./core/measurement.js";
|
|
13
16
|
export * from "./core/multi-create.js";
|
|
14
17
|
export * from "./core/pre-action.js";
|
|
15
18
|
export * from "./core/pull.js";
|
|
@@ -19,6 +22,7 @@ export * from "./core/queue.js";
|
|
|
19
22
|
export * from "./core/scope.js";
|
|
20
23
|
export * from "./core/search-controller.js";
|
|
21
24
|
export * from "./core/search.js";
|
|
25
|
+
export * from "./core/server-clock.js";
|
|
22
26
|
export * from "./feed/feed.js";
|
|
23
27
|
export * from "./fulfillments/fulfillments.js";
|
|
24
28
|
export * from "./fx.js";
|
|
@@ -32,7 +36,15 @@ export * from "./payrolls/workspace.js";
|
|
|
32
36
|
export * from "./preferences/preferences.js";
|
|
33
37
|
export * from "./privacy/privacy.js";
|
|
34
38
|
export * from "./processes/processes.js";
|
|
39
|
+
export * from "./routes/arrival.js";
|
|
40
|
+
export * from "./routes/availability.js";
|
|
41
|
+
export * from "./routes/departure.js";
|
|
42
|
+
export * from "./routes/planning.js";
|
|
43
|
+
export * from "./routes/punch.js";
|
|
35
44
|
export * from "./routes/routes.js";
|
|
45
|
+
export * from "./routes/tracking.js";
|
|
46
|
+
export * from "./routes/validation.js";
|
|
47
|
+
export * from "./routes/workspace.js";
|
|
36
48
|
export * from "./session/session.js";
|
|
37
49
|
export * from "./storage/storage.js";
|
|
38
50
|
export * from "./timesheets/access.js";
|
package/dist/src/index.js
CHANGED
|
@@ -2,14 +2,17 @@ export * from "./auth/auth.js";
|
|
|
2
2
|
export * from "./billable/billable.js";
|
|
3
3
|
export * from "./calls/calls.js";
|
|
4
4
|
export * from "./chat/chat.js";
|
|
5
|
+
export * from "./chat/data.js";
|
|
5
6
|
export * from "./chat/optimistic.js";
|
|
6
7
|
export * from "./chat/send-queue.js";
|
|
7
8
|
export * from "./contacts/contacts.js";
|
|
8
9
|
export * from "./contacts/scope.js";
|
|
10
|
+
export * from "./core/address.js";
|
|
9
11
|
export * from "./core/date.js";
|
|
10
12
|
export * from "./core/err.js";
|
|
11
13
|
export * from "./core/events.js";
|
|
12
14
|
export * from "./core/id.js";
|
|
15
|
+
export * from "./core/measurement.js";
|
|
13
16
|
export * from "./core/multi-create.js";
|
|
14
17
|
export * from "./core/pre-action.js";
|
|
15
18
|
export * from "./core/pull.js";
|
|
@@ -19,6 +22,7 @@ export * from "./core/queue.js";
|
|
|
19
22
|
export * from "./core/scope.js";
|
|
20
23
|
export * from "./core/search-controller.js";
|
|
21
24
|
export * from "./core/search.js";
|
|
25
|
+
export * from "./core/server-clock.js";
|
|
22
26
|
export * from "./feed/feed.js";
|
|
23
27
|
export * from "./fulfillments/fulfillments.js";
|
|
24
28
|
export * from "./fx.js";
|
|
@@ -32,7 +36,15 @@ export * from "./payrolls/workspace.js";
|
|
|
32
36
|
export * from "./preferences/preferences.js";
|
|
33
37
|
export * from "./privacy/privacy.js";
|
|
34
38
|
export * from "./processes/processes.js";
|
|
39
|
+
export * from "./routes/arrival.js";
|
|
40
|
+
export * from "./routes/availability.js";
|
|
41
|
+
export * from "./routes/departure.js";
|
|
42
|
+
export * from "./routes/planning.js";
|
|
43
|
+
export * from "./routes/punch.js";
|
|
35
44
|
export * from "./routes/routes.js";
|
|
45
|
+
export * from "./routes/tracking.js";
|
|
46
|
+
export * from "./routes/validation.js";
|
|
47
|
+
export * from "./routes/workspace.js";
|
|
36
48
|
export * from "./session/session.js";
|
|
37
49
|
export * from "./storage/storage.js";
|
|
38
50
|
export * from "./timesheets/access.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Route } from "@fabriktor/schema";
|
|
2
|
+
import type { TimesheetPunchState } from "../timesheets/workspace.js";
|
|
3
|
+
import type { RoutesActor } from "./workspace.js";
|
|
4
|
+
export declare const routeArrivalRadiusMeters = 150;
|
|
5
|
+
export declare const routeArrivalMaximumAccuracyMeters = 75;
|
|
6
|
+
export type RouteStopDistance = {
|
|
7
|
+
location_id: string;
|
|
8
|
+
distance_meters: number;
|
|
9
|
+
};
|
|
10
|
+
export declare class RouteArrivalCue {
|
|
11
|
+
private candidate;
|
|
12
|
+
private samples;
|
|
13
|
+
reset(): void;
|
|
14
|
+
observe(distances: readonly RouteStopDistance[], accuracy: number): string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function routeHasActiveTransport(actor: RoutesActor, route: Route, vehicle_id: string, state: TimesheetPunchState, location_id?: string): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PunchStatusWorking, UserTypeEmployee, } from "@fabriktor/schema";
|
|
2
|
+
import { routeCanTrack } from "./workspace.js";
|
|
3
|
+
export const routeArrivalRadiusMeters = 150;
|
|
4
|
+
export const routeArrivalMaximumAccuracyMeters = 75;
|
|
5
|
+
const requiredSamples = 2;
|
|
6
|
+
// Only a visual cue. Two accurate, consecutive foreground samples prevent a
|
|
7
|
+
// passing or imprecise GPS fix from immediately highlighting a destination.
|
|
8
|
+
export class RouteArrivalCue {
|
|
9
|
+
candidate = "";
|
|
10
|
+
samples = 0;
|
|
11
|
+
reset() {
|
|
12
|
+
this.candidate = "";
|
|
13
|
+
this.samples = 0;
|
|
14
|
+
}
|
|
15
|
+
observe(distances, accuracy) {
|
|
16
|
+
if (!Number.isFinite(accuracy) ||
|
|
17
|
+
accuracy < 0 ||
|
|
18
|
+
accuracy > routeArrivalMaximumAccuracyMeters) {
|
|
19
|
+
this.reset();
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const nearest = distances.reduce((best, value) => {
|
|
23
|
+
if (!Number.isFinite(value.distance_meters) ||
|
|
24
|
+
value.distance_meters < 0 ||
|
|
25
|
+
value.distance_meters + accuracy > routeArrivalRadiusMeters)
|
|
26
|
+
return best;
|
|
27
|
+
return !best || value.distance_meters < best.distance_meters
|
|
28
|
+
? value
|
|
29
|
+
: best;
|
|
30
|
+
}, undefined);
|
|
31
|
+
if (!nearest) {
|
|
32
|
+
this.reset();
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
this.samples =
|
|
36
|
+
this.candidate === nearest.location_id ? this.samples + 1 : 1;
|
|
37
|
+
this.candidate = nearest.location_id;
|
|
38
|
+
return this.samples >= requiredSamples ? this.candidate : null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function routeHasActiveTransport(actor, route, vehicle_id, state, location_id) {
|
|
42
|
+
return (actor.active_mode === UserTypeEmployee &&
|
|
43
|
+
routeCanTrack(actor, route, vehicle_id, actor.owner_id) &&
|
|
44
|
+
!state.stale_day &&
|
|
45
|
+
!state.week_sheet?.is_archived &&
|
|
46
|
+
state.punch?.employer_id === route.owner_id &&
|
|
47
|
+
state.punch.punch_status === PunchStatusWorking &&
|
|
48
|
+
(location_id === undefined || state.punch.location_id === location_id) &&
|
|
49
|
+
state.punch.rides.at(-1)?.is_active === true);
|
|
50
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { JobsOperator, RoutesOperator, UsersOperator } from "@fabriktor/client";
|
|
2
|
+
import { type ClientAvailability, type InClientAvailability, type Location, type PrefCompany, type User } from "@fabriktor/schema";
|
|
3
|
+
import type { PreferencesFXOperator } from "../preferences/preferences.js";
|
|
4
|
+
import { type ChatData } from "../chat/data.js";
|
|
5
|
+
import { type RoutesActor } from "./workspace.js";
|
|
6
|
+
export declare const clientAvailabilityMinimumWindows = 3;
|
|
7
|
+
export declare const RoutesChatDataType: {
|
|
8
|
+
readonly AvailabilityRequest: "routes.availability.request";
|
|
9
|
+
readonly AvailabilitySubmitted: "routes.availability.submitted";
|
|
10
|
+
};
|
|
11
|
+
export type AvailabilityReference = Pick<InClientAvailability, "employer_id" | "location_id"> & {
|
|
12
|
+
client_id: string;
|
|
13
|
+
};
|
|
14
|
+
export type AvailabilityContext = {
|
|
15
|
+
location: Location;
|
|
16
|
+
employer: User;
|
|
17
|
+
company: PrefCompany;
|
|
18
|
+
timezone: string;
|
|
19
|
+
};
|
|
20
|
+
export type AvailabilityReaders = {
|
|
21
|
+
jobs: Pick<JobsOperator, "findOneLocation">;
|
|
22
|
+
users: Pick<UsersOperator, "findOneUser">;
|
|
23
|
+
preferences: Pick<PreferencesFXOperator, "getPrefCompany">;
|
|
24
|
+
};
|
|
25
|
+
export declare function availabilityReference(data: ChatData): AvailabilityReference | undefined;
|
|
26
|
+
export declare function availabilityRequest(location: Location, actor: RoutesActor): ChatData;
|
|
27
|
+
export declare function availabilitySubmission(reference: AvailabilityReference, request_id: string, ids: readonly string[]): ChatData;
|
|
28
|
+
export declare function loadAvailabilityContext(readers: AvailabilityReaders, actor: RoutesActor, reference: AvailabilityReference, signal?: AbortSignal): Promise<AvailabilityContext>;
|
|
29
|
+
export declare function validateAvailabilityWindows(inputs: readonly InClientAvailability[], context: AvailabilityContext, minimum?: number): void;
|
|
30
|
+
export declare function loadAvailabilitySelection(readers: AvailabilityReaders & {
|
|
31
|
+
routes: Pick<RoutesOperator, "findOneClientAvailability">;
|
|
32
|
+
}, actor: RoutesActor, reference: AvailabilityReference, ids: readonly string[], signal?: AbortSignal): Promise<{
|
|
33
|
+
context: AvailabilityContext;
|
|
34
|
+
windows: ClientAvailability[];
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { UserTypeClient, UserTypeEmployer, } from "@fabriktor/schema";
|
|
2
|
+
import { addressTimezone } from "../core/address.js";
|
|
3
|
+
import { hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
|
|
4
|
+
import { errValidation } from "../core/err.js";
|
|
5
|
+
import { normalizeIDs, resolvedID } from "../core/id.js";
|
|
6
|
+
import { newChatData } from "../chat/data.js";
|
|
7
|
+
import { routeCanPlan } from "./workspace.js";
|
|
8
|
+
import { validateClientAvailabilityInput } from "./validation.js";
|
|
9
|
+
export const clientAvailabilityMinimumWindows = 3;
|
|
10
|
+
export const RoutesChatDataType = {
|
|
11
|
+
AvailabilityRequest: "routes.availability.request",
|
|
12
|
+
AvailabilitySubmitted: "routes.availability.submitted",
|
|
13
|
+
};
|
|
14
|
+
const msgAvailabilityAccess = "This availability does not match the client and employer of the location.";
|
|
15
|
+
const msgAvailabilityTimezone = "The location needs a valid saved address timezone.";
|
|
16
|
+
const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
|
|
17
|
+
const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows within the employer's working hours.";
|
|
18
|
+
const msgAvailabilityData = "The availability request is invalid.";
|
|
19
|
+
export function availabilityReference(data) {
|
|
20
|
+
if (data.type !== RoutesChatDataType.AvailabilityRequest &&
|
|
21
|
+
data.type !== RoutesChatDataType.AvailabilitySubmitted)
|
|
22
|
+
return;
|
|
23
|
+
const { employer_id, client_id, location_id } = data.payload;
|
|
24
|
+
if (typeof employer_id !== "string" ||
|
|
25
|
+
!resolvedID(employer_id) ||
|
|
26
|
+
typeof client_id !== "string" ||
|
|
27
|
+
!resolvedID(client_id) ||
|
|
28
|
+
typeof location_id !== "string" ||
|
|
29
|
+
!resolvedID(location_id))
|
|
30
|
+
return;
|
|
31
|
+
if (data.type === RoutesChatDataType.AvailabilitySubmitted) {
|
|
32
|
+
const { request_id, availability_ids } = data.payload;
|
|
33
|
+
if (typeof request_id !== "string" ||
|
|
34
|
+
!resolvedID(request_id) ||
|
|
35
|
+
!Array.isArray(availability_ids) ||
|
|
36
|
+
!availability_ids.every((id) => typeof id === "string") ||
|
|
37
|
+
availability_ids.length < clientAvailabilityMinimumWindows ||
|
|
38
|
+
normalizeIDs(availability_ids).length !== availability_ids.length)
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
return { employer_id, client_id, location_id };
|
|
42
|
+
}
|
|
43
|
+
export function availabilityRequest(location, actor) {
|
|
44
|
+
if (!routeCanPlan(actor) ||
|
|
45
|
+
location.owner_id !== actor.owner_id ||
|
|
46
|
+
location.is_archived ||
|
|
47
|
+
!resolvedID(location.client_id) ||
|
|
48
|
+
!resolvedID(location.id))
|
|
49
|
+
throw errValidation({ msg: msgAvailabilityAccess });
|
|
50
|
+
return newChatData(RoutesChatDataType.AvailabilityRequest, {
|
|
51
|
+
employer_id: actor.owner_id,
|
|
52
|
+
client_id: location.client_id,
|
|
53
|
+
location_id: location.id,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function availabilitySubmission(reference, request_id, ids) {
|
|
57
|
+
const data = newChatData(RoutesChatDataType.AvailabilitySubmitted, {
|
|
58
|
+
...reference,
|
|
59
|
+
request_id,
|
|
60
|
+
availability_ids: [...ids],
|
|
61
|
+
});
|
|
62
|
+
if (!availabilityReference(data))
|
|
63
|
+
throw errValidation({ msg: msgAvailabilityData });
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
export async function loadAvailabilityContext(readers, actor, reference, signal) {
|
|
67
|
+
const location = (await readers.jobs.findOneLocation({ id: reference.location_id, signal })).value;
|
|
68
|
+
if (!location ||
|
|
69
|
+
location.id !== reference.location_id ||
|
|
70
|
+
location.is_archived ||
|
|
71
|
+
location.owner_id !== reference.employer_id ||
|
|
72
|
+
location.client_id !== reference.client_id ||
|
|
73
|
+
(actor.active_mode === UserTypeClient
|
|
74
|
+
? actor.owner_id !== reference.client_id
|
|
75
|
+
: !routeCanPlan(actor) || actor.owner_id !== reference.employer_id))
|
|
76
|
+
throw errValidation({ msg: msgAvailabilityAccess });
|
|
77
|
+
const [result, company] = await Promise.all([
|
|
78
|
+
readers.users.findOneUser({ id: reference.employer_id, signal }),
|
|
79
|
+
readers.preferences.getPrefCompany({ owner_id: reference.employer_id }),
|
|
80
|
+
]);
|
|
81
|
+
const employer = result.value;
|
|
82
|
+
if (!employer ||
|
|
83
|
+
employer.id !== reference.employer_id ||
|
|
84
|
+
employer.account_identity !== UserTypeEmployer)
|
|
85
|
+
throw errValidation({ msg: msgAvailabilityAccess });
|
|
86
|
+
const timezone = addressTimezone(location);
|
|
87
|
+
if (!timezone)
|
|
88
|
+
throw errValidation({ msg: msgAvailabilityTimezone });
|
|
89
|
+
const { work_day_start_hour: start, work_day_end_hour: end } = company;
|
|
90
|
+
if (![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
|
|
91
|
+
start >= end)
|
|
92
|
+
throw errValidation({ msg: msgAvailabilityHours });
|
|
93
|
+
return { location, employer, company, timezone };
|
|
94
|
+
}
|
|
95
|
+
export function validateAvailabilityWindows(inputs, context, minimum = clientAvailabilityMinimumWindows) {
|
|
96
|
+
if (inputs.length < minimum)
|
|
97
|
+
throw errValidation({ msg: msgAvailabilityWindows });
|
|
98
|
+
for (const [index, input] of inputs.entries()) {
|
|
99
|
+
validateClientAvailabilityInput(input);
|
|
100
|
+
if (input.employer_id !== context.employer.id ||
|
|
101
|
+
input.location_id !== context.location.id ||
|
|
102
|
+
input.timezone !== context.timezone ||
|
|
103
|
+
minutesOfDay(input.from) <
|
|
104
|
+
context.company.work_day_start_hour * minutesPerHour ||
|
|
105
|
+
minutesOfDay(input.to) >
|
|
106
|
+
context.company.work_day_end_hour * minutesPerHour ||
|
|
107
|
+
inputs
|
|
108
|
+
.slice(0, index)
|
|
109
|
+
.some((other) => sameCalendarDay(input.from, other.from) &&
|
|
110
|
+
minutesOfDay(input.from) < minutesOfDay(other.to) &&
|
|
111
|
+
minutesOfDay(input.to) > minutesOfDay(other.from)))
|
|
112
|
+
throw errValidation({ msg: msgAvailabilityWindows });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// A chat response contains references, never authority or an appointment confirmation.
|
|
116
|
+
// Reload each chosen record before preparing the response, including after a resumed batch.
|
|
117
|
+
export async function loadAvailabilitySelection(readers, actor, reference, ids, signal) {
|
|
118
|
+
if (ids.length < clientAvailabilityMinimumWindows ||
|
|
119
|
+
normalizeIDs(ids).length !== ids.length)
|
|
120
|
+
throw errValidation({ msg: msgAvailabilityData });
|
|
121
|
+
const context = await loadAvailabilityContext(readers, actor, reference, signal);
|
|
122
|
+
const windows = await Promise.all(ids.map(async (id) => {
|
|
123
|
+
const window = (await readers.routes.findOneClientAvailability({ id, signal })).value;
|
|
124
|
+
if (!window ||
|
|
125
|
+
window.id !== id ||
|
|
126
|
+
window.owner_id !== reference.client_id)
|
|
127
|
+
throw errValidation({ msg: msgAvailabilityAccess });
|
|
128
|
+
return window;
|
|
129
|
+
}));
|
|
130
|
+
validateAvailabilityWindows(windows, context);
|
|
131
|
+
return { context, windows };
|
|
132
|
+
}
|