@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.
Files changed (37) hide show
  1. package/dist/src/chat/data.d.ts +23 -0
  2. package/dist/src/chat/data.js +76 -0
  3. package/dist/src/core/address.d.ts +2 -0
  4. package/dist/src/core/address.js +6 -0
  5. package/dist/src/core/date.d.ts +6 -0
  6. package/dist/src/core/date.js +27 -0
  7. package/dist/src/core/measurement.d.ts +15 -0
  8. package/dist/src/core/measurement.js +45 -0
  9. package/dist/src/core/server-clock.d.ts +10 -0
  10. package/dist/src/core/server-clock.js +36 -0
  11. package/dist/src/fx.d.ts +5 -1
  12. package/dist/src/fx.js +6 -1
  13. package/dist/src/index.d.ts +12 -0
  14. package/dist/src/index.js +12 -0
  15. package/dist/src/routes/arrival.d.ts +16 -0
  16. package/dist/src/routes/arrival.js +50 -0
  17. package/dist/src/routes/availability.d.ts +35 -0
  18. package/dist/src/routes/availability.js +132 -0
  19. package/dist/src/routes/departure.d.ts +45 -0
  20. package/dist/src/routes/departure.js +98 -0
  21. package/dist/src/routes/planning.d.ts +34 -0
  22. package/dist/src/routes/planning.js +94 -0
  23. package/dist/src/routes/punch.d.ts +9 -0
  24. package/dist/src/routes/punch.js +23 -0
  25. package/dist/src/routes/routes.d.ts +45 -12
  26. package/dist/src/routes/routes.js +223 -139
  27. package/dist/src/routes/tracking.d.ts +59 -0
  28. package/dist/src/routes/tracking.js +178 -0
  29. package/dist/src/routes/validation.d.ts +13 -0
  30. package/dist/src/routes/validation.js +227 -0
  31. package/dist/src/routes/workspace.d.ts +74 -0
  32. package/dist/src/routes/workspace.js +212 -0
  33. package/dist/src/timesheets/timesheets.d.ts +3 -1
  34. package/dist/src/timesheets/timesheets.js +3 -0
  35. package/dist/src/timesheets/workspace.d.ts +9 -1
  36. package/dist/src/timesheets/workspace.js +50 -8
  37. 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,2 @@
1
+ import type { Address } from "@fabriktor/schema";
2
+ export declare function addressTimezone(address: Pick<Address, "address_timezone">): string | null;
@@ -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
+ }
@@ -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;
@@ -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,
@@ -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
+ }