@fabriktor/fx 0.0.138 → 0.0.140

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.
@@ -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
+ }
@@ -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,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
@@ -117,6 +117,7 @@ export class FX {
117
117
  timesheets: opts.timesheets,
118
118
  contacts_fx: this.contacts,
119
119
  billable: opts.billable,
120
+ now: opts.timesheets_now,
120
121
  });
121
122
  this.users = newUsersFX({
122
123
  users: opts.users,
@@ -135,10 +136,11 @@ export function newFX(opts) {
135
136
  return new FX(opts);
136
137
  }
137
138
  export function newDefaultFX(opts) {
138
- const http = newHTTPClient();
139
+ const http = opts.http ?? newHTTPClient();
139
140
  const get_token = () => opts.auth.getToken();
140
141
  return newFX({
141
142
  auth: opts.auth,
143
+ timesheets_now: opts.timesheets_now,
142
144
  billable: newBillableClient({
143
145
  http,
144
146
  base_url: opts.base_url,
@@ -2,6 +2,7 @@ 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";
@@ -19,6 +20,7 @@ export * from "./core/queue.js";
19
20
  export * from "./core/scope.js";
20
21
  export * from "./core/search-controller.js";
21
22
  export * from "./core/search.js";
23
+ export * from "./core/server-clock.js";
22
24
  export * from "./feed/feed.js";
23
25
  export * from "./fulfillments/fulfillments.js";
24
26
  export * from "./fx.js";
@@ -32,7 +34,14 @@ export * from "./payrolls/workspace.js";
32
34
  export * from "./preferences/preferences.js";
33
35
  export * from "./privacy/privacy.js";
34
36
  export * from "./processes/processes.js";
37
+ export * from "./routes/arrival.js";
38
+ export * from "./routes/availability.js";
39
+ export * from "./routes/planning.js";
40
+ export * from "./routes/punch.js";
35
41
  export * from "./routes/routes.js";
42
+ export * from "./routes/tracking.js";
43
+ export * from "./routes/validation.js";
44
+ export * from "./routes/workspace.js";
36
45
  export * from "./session/session.js";
37
46
  export * from "./storage/storage.js";
38
47
  export * from "./timesheets/access.js";
package/dist/src/index.js CHANGED
@@ -2,6 +2,7 @@ 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";
@@ -19,6 +20,7 @@ export * from "./core/queue.js";
19
20
  export * from "./core/scope.js";
20
21
  export * from "./core/search-controller.js";
21
22
  export * from "./core/search.js";
23
+ export * from "./core/server-clock.js";
22
24
  export * from "./feed/feed.js";
23
25
  export * from "./fulfillments/fulfillments.js";
24
26
  export * from "./fx.js";
@@ -32,7 +34,14 @@ export * from "./payrolls/workspace.js";
32
34
  export * from "./preferences/preferences.js";
33
35
  export * from "./privacy/privacy.js";
34
36
  export * from "./processes/processes.js";
37
+ export * from "./routes/arrival.js";
38
+ export * from "./routes/availability.js";
39
+ export * from "./routes/planning.js";
40
+ export * from "./routes/punch.js";
35
41
  export * from "./routes/routes.js";
42
+ export * from "./routes/tracking.js";
43
+ export * from "./routes/validation.js";
44
+ export * from "./routes/workspace.js";
36
45
  export * from "./session/session.js";
37
46
  export * from "./storage/storage.js";
38
47
  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,131 @@
1
+ import { UserTypeClient, UserTypeEmployer, } from "@fabriktor/schema";
2
+ import { dateTimezone, hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
3
+ import { errValidation } from "../core/err.js";
4
+ import { normalizeIDs, resolvedID } from "../core/id.js";
5
+ import { newChatData } from "../chat/data.js";
6
+ import { routeCanPlan } from "./workspace.js";
7
+ import { validateClientAvailabilityInput } from "./validation.js";
8
+ export const clientAvailabilityMinimumWindows = 3;
9
+ export const RoutesChatDataType = {
10
+ AvailabilityRequest: "routes.availability.request",
11
+ AvailabilitySubmitted: "routes.availability.submitted",
12
+ };
13
+ const msgAvailabilityAccess = "This availability does not match the client and employer of the location.";
14
+ const msgAvailabilityTimezone = "The employer needs a valid saved address timezone.";
15
+ const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
16
+ const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows within the employer's working hours.";
17
+ const msgAvailabilityData = "The availability request is invalid.";
18
+ export function availabilityReference(data) {
19
+ if (data.type !== RoutesChatDataType.AvailabilityRequest &&
20
+ data.type !== RoutesChatDataType.AvailabilitySubmitted)
21
+ return;
22
+ const { employer_id, client_id, location_id } = data.payload;
23
+ if (typeof employer_id !== "string" ||
24
+ !resolvedID(employer_id) ||
25
+ typeof client_id !== "string" ||
26
+ !resolvedID(client_id) ||
27
+ typeof location_id !== "string" ||
28
+ !resolvedID(location_id))
29
+ return;
30
+ if (data.type === RoutesChatDataType.AvailabilitySubmitted) {
31
+ const { request_id, availability_ids } = data.payload;
32
+ if (typeof request_id !== "string" ||
33
+ !resolvedID(request_id) ||
34
+ !Array.isArray(availability_ids) ||
35
+ !availability_ids.every((id) => typeof id === "string") ||
36
+ availability_ids.length < clientAvailabilityMinimumWindows ||
37
+ normalizeIDs(availability_ids).length !== availability_ids.length)
38
+ return;
39
+ }
40
+ return { employer_id, client_id, location_id };
41
+ }
42
+ export function availabilityRequest(location, actor) {
43
+ if (!routeCanPlan(actor) ||
44
+ location.owner_id !== actor.owner_id ||
45
+ location.is_archived ||
46
+ !resolvedID(location.client_id) ||
47
+ !resolvedID(location.id))
48
+ throw errValidation({ msg: msgAvailabilityAccess });
49
+ return newChatData(RoutesChatDataType.AvailabilityRequest, {
50
+ employer_id: actor.owner_id,
51
+ client_id: location.client_id,
52
+ location_id: location.id,
53
+ });
54
+ }
55
+ export function availabilitySubmission(reference, request_id, ids) {
56
+ const data = newChatData(RoutesChatDataType.AvailabilitySubmitted, {
57
+ ...reference,
58
+ request_id,
59
+ availability_ids: [...ids],
60
+ });
61
+ if (!availabilityReference(data))
62
+ throw errValidation({ msg: msgAvailabilityData });
63
+ return data;
64
+ }
65
+ export async function loadAvailabilityContext(readers, actor, reference, signal) {
66
+ const location = (await readers.jobs.findOneLocation({ id: reference.location_id, signal })).value;
67
+ if (!location ||
68
+ location.id !== reference.location_id ||
69
+ location.is_archived ||
70
+ location.owner_id !== reference.employer_id ||
71
+ location.client_id !== reference.client_id ||
72
+ (actor.active_mode === UserTypeClient
73
+ ? actor.owner_id !== reference.client_id
74
+ : !routeCanPlan(actor) || actor.owner_id !== reference.employer_id))
75
+ throw errValidation({ msg: msgAvailabilityAccess });
76
+ const [result, company] = await Promise.all([
77
+ readers.users.findOneUser({ id: reference.employer_id, signal }),
78
+ readers.preferences.getPrefCompany({ owner_id: reference.employer_id }),
79
+ ]);
80
+ const employer = result.value;
81
+ if (!employer ||
82
+ employer.id !== reference.employer_id ||
83
+ employer.account_identity !== UserTypeEmployer)
84
+ throw errValidation({ msg: msgAvailabilityAccess });
85
+ const timezone = employer.address_timezone.trim();
86
+ if (!timezone || dateTimezone(timezone) !== timezone)
87
+ throw errValidation({ msg: msgAvailabilityTimezone });
88
+ const { work_day_start_hour: start, work_day_end_hour: end } = company;
89
+ if (![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
90
+ start >= end)
91
+ throw errValidation({ msg: msgAvailabilityHours });
92
+ return { location, employer, company, timezone };
93
+ }
94
+ export function validateAvailabilityWindows(inputs, context, minimum = clientAvailabilityMinimumWindows) {
95
+ if (inputs.length < minimum)
96
+ throw errValidation({ msg: msgAvailabilityWindows });
97
+ for (const [index, input] of inputs.entries()) {
98
+ validateClientAvailabilityInput(input);
99
+ if (input.employer_id !== context.employer.id ||
100
+ input.location_id !== context.location.id ||
101
+ input.timezone !== context.timezone ||
102
+ minutesOfDay(input.from) <
103
+ context.company.work_day_start_hour * minutesPerHour ||
104
+ minutesOfDay(input.to) >
105
+ context.company.work_day_end_hour * minutesPerHour ||
106
+ inputs
107
+ .slice(0, index)
108
+ .some((other) => sameCalendarDay(input.from, other.from) &&
109
+ minutesOfDay(input.from) < minutesOfDay(other.to) &&
110
+ minutesOfDay(input.to) > minutesOfDay(other.from)))
111
+ throw errValidation({ msg: msgAvailabilityWindows });
112
+ }
113
+ }
114
+ // A chat response contains references, never authority or an appointment confirmation.
115
+ // Reload each chosen record before preparing the response, including after a resumed batch.
116
+ export async function loadAvailabilitySelection(readers, actor, reference, ids, signal) {
117
+ if (ids.length < clientAvailabilityMinimumWindows ||
118
+ normalizeIDs(ids).length !== ids.length)
119
+ throw errValidation({ msg: msgAvailabilityData });
120
+ const context = await loadAvailabilityContext(readers, actor, reference, signal);
121
+ const windows = await Promise.all(ids.map(async (id) => {
122
+ const window = (await readers.routes.findOneClientAvailability({ id, signal })).value;
123
+ if (!window ||
124
+ window.id !== id ||
125
+ window.owner_id !== reference.client_id)
126
+ throw errValidation({ msg: msgAvailabilityAccess });
127
+ return window;
128
+ }));
129
+ validateAvailabilityWindows(windows, context);
130
+ return { context, windows };
131
+ }
@@ -0,0 +1,34 @@
1
+ import type { ClientAvailability, InDate, InRoute, Location, PrefCompany, RPRoutesAvailability, Route } from "@fabriktor/schema";
2
+ import { PreActionBlocked } from "../core/pre-action.js";
3
+ declare const routePreparationBlocked = "route_preparation";
4
+ export declare const RouteMutationBlock: {
5
+ readonly Busy: "route_mutation_busy";
6
+ readonly Changed: "route_mutation_changed";
7
+ readonly Unconfirmed: "route_mutation_unconfirmed";
8
+ };
9
+ export declare function routePlanChanged(current: Route, input: InRoute): boolean;
10
+ export declare class RoutePreparationBlocked extends PreActionBlocked<typeof routePreparationBlocked> {
11
+ readonly cause: unknown;
12
+ constructor(cause: unknown);
13
+ }
14
+ export declare const routePlanningDefaults: {
15
+ readonly days_ago: 0;
16
+ readonly duration_in_minutes: 0;
17
+ readonly consider_traffic: false;
18
+ readonly start_early: false;
19
+ readonly override_itinerary: false;
20
+ };
21
+ export declare function newRoutePlan(opts: {
22
+ day: InDate;
23
+ company: Pick<PrefCompany, "work_day_start_hour" | "work_day_end_hour">;
24
+ start_place_id: string;
25
+ }): InRoute;
26
+ export declare function routeDestination(location: Location, owner_id: string): InRoute["destinations"][number];
27
+ export declare function validateRouteEmployees(ids: readonly string[], allowed: readonly string[], occupied?: readonly string[]): void;
28
+ export type RouteAvailabilityWindow = {
29
+ availability: ClientAvailability;
30
+ start: number;
31
+ end: number;
32
+ };
33
+ export declare function routeAvailabilityWindows(input: Pick<InRoute, "from" | "to" | "timezone">, availability: RPRoutesAvailability, location_id: string, client_id: string): RouteAvailabilityWindow[];
34
+ export {};