@fabriktor/fx 0.0.140 → 0.0.142

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,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
+ }
@@ -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
+ }
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,
@@ -7,10 +7,12 @@ export * from "./chat/optimistic.js";
7
7
  export * from "./chat/send-queue.js";
8
8
  export * from "./contacts/contacts.js";
9
9
  export * from "./contacts/scope.js";
10
+ export * from "./core/address.js";
10
11
  export * from "./core/date.js";
11
12
  export * from "./core/err.js";
12
13
  export * from "./core/events.js";
13
14
  export * from "./core/id.js";
15
+ export * from "./core/measurement.js";
14
16
  export * from "./core/multi-create.js";
15
17
  export * from "./core/pre-action.js";
16
18
  export * from "./core/pull.js";
@@ -36,6 +38,7 @@ export * from "./privacy/privacy.js";
36
38
  export * from "./processes/processes.js";
37
39
  export * from "./routes/arrival.js";
38
40
  export * from "./routes/availability.js";
41
+ export * from "./routes/departure.js";
39
42
  export * from "./routes/planning.js";
40
43
  export * from "./routes/punch.js";
41
44
  export * from "./routes/routes.js";
package/dist/src/index.js CHANGED
@@ -7,10 +7,12 @@ export * from "./chat/optimistic.js";
7
7
  export * from "./chat/send-queue.js";
8
8
  export * from "./contacts/contacts.js";
9
9
  export * from "./contacts/scope.js";
10
+ export * from "./core/address.js";
10
11
  export * from "./core/date.js";
11
12
  export * from "./core/err.js";
12
13
  export * from "./core/events.js";
13
14
  export * from "./core/id.js";
15
+ export * from "./core/measurement.js";
14
16
  export * from "./core/multi-create.js";
15
17
  export * from "./core/pre-action.js";
16
18
  export * from "./core/pull.js";
@@ -36,6 +38,7 @@ export * from "./privacy/privacy.js";
36
38
  export * from "./processes/processes.js";
37
39
  export * from "./routes/arrival.js";
38
40
  export * from "./routes/availability.js";
41
+ export * from "./routes/departure.js";
39
42
  export * from "./routes/planning.js";
40
43
  export * from "./routes/punch.js";
41
44
  export * from "./routes/routes.js";
@@ -1,5 +1,6 @@
1
1
  import { UserTypeClient, UserTypeEmployer, } from "@fabriktor/schema";
2
- import { dateTimezone, hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
2
+ import { addressTimezone } from "../core/address.js";
3
+ import { hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
3
4
  import { errValidation } from "../core/err.js";
4
5
  import { normalizeIDs, resolvedID } from "../core/id.js";
5
6
  import { newChatData } from "../chat/data.js";
@@ -11,7 +12,7 @@ export const RoutesChatDataType = {
11
12
  AvailabilitySubmitted: "routes.availability.submitted",
12
13
  };
13
14
  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 msgAvailabilityTimezone = "The location needs a valid saved address timezone.";
15
16
  const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
16
17
  const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows within the employer's working hours.";
17
18
  const msgAvailabilityData = "The availability request is invalid.";
@@ -82,8 +83,8 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
82
83
  employer.id !== reference.employer_id ||
83
84
  employer.account_identity !== UserTypeEmployer)
84
85
  throw errValidation({ msg: msgAvailabilityAccess });
85
- const timezone = employer.address_timezone.trim();
86
- if (!timezone || dateTimezone(timezone) !== timezone)
86
+ const timezone = addressTimezone(location);
87
+ if (!timezone)
87
88
  throw errValidation({ msg: msgAvailabilityTimezone });
88
89
  const { work_day_start_hour: start, work_day_end_hour: end } = company;
89
90
  if (![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
@@ -0,0 +1,45 @@
1
+ import type { BillableOperator, JobsOperator, UsersOperator } from "@fabriktor/client";
2
+ import type { Address, InRoute, Location, Route, User, Warehouse } from "@fabriktor/schema";
3
+ export declare const RouteDepartureSource: {
4
+ readonly Company: "company";
5
+ readonly Warehouse: "warehouse";
6
+ readonly SavedRoute: "saved_route";
7
+ };
8
+ export type RouteDepartureReference = {
9
+ source: typeof RouteDepartureSource.Company;
10
+ } | {
11
+ source: typeof RouteDepartureSource.Warehouse;
12
+ warehouse_id: string;
13
+ } | {
14
+ source: typeof RouteDepartureSource.SavedRoute;
15
+ route_id: string;
16
+ };
17
+ export type RouteDeparture = {
18
+ reference: RouteDepartureReference;
19
+ address: Address;
20
+ name: string;
21
+ };
22
+ export type RoutePlanningReaders = {
23
+ users: Pick<UsersOperator, "findOneUser">;
24
+ billable: Pick<BillableOperator, "findOneWarehouse">;
25
+ jobs: Pick<JobsOperator, "findOneLocation">;
26
+ };
27
+ export declare const RoutePlanningBlock: {
28
+ readonly Departure: "route_departure_invalid";
29
+ readonly DepartureChanged: "route_departure_changed";
30
+ readonly DestinationTimezone: "route_destination_timezone";
31
+ readonly DestinationChanged: "route_destination_changed";
32
+ };
33
+ export declare function companyRouteDeparture(user: User): RouteDeparture;
34
+ export declare function warehouseRouteDeparture(warehouse: Warehouse, owner_id: string): RouteDeparture;
35
+ export declare function loadRouteDeparture(readers: RoutePlanningReaders, owner_id: string, reference: Exclude<RouteDepartureReference, {
36
+ source: typeof RouteDepartureSource.SavedRoute;
37
+ }>, signal?: AbortSignal): Promise<RouteDeparture>;
38
+ export declare function validateRoutePlanLocation(location: Location, owner_id: string, timezone: string): void;
39
+ export declare function validateRoutePlanAddresses(readers: RoutePlanningReaders, opts: {
40
+ owner_id: string;
41
+ input: InRoute;
42
+ departure: RouteDepartureReference;
43
+ current?: Route;
44
+ signal?: AbortSignal;
45
+ }): Promise<void>;
@@ -0,0 +1,98 @@
1
+ import { UserTypeEmployer } from "@fabriktor/schema";
2
+ import { addressTimezone } from "../core/address.js";
3
+ import { resolvedID } from "../core/id.js";
4
+ import { PreActionBlocked } from "../core/pre-action.js";
5
+ export const RouteDepartureSource = {
6
+ Company: "company",
7
+ Warehouse: "warehouse",
8
+ SavedRoute: "saved_route",
9
+ };
10
+ export const RoutePlanningBlock = {
11
+ Departure: "route_departure_invalid",
12
+ DepartureChanged: "route_departure_changed",
13
+ DestinationTimezone: "route_destination_timezone",
14
+ DestinationChanged: "route_destination_changed",
15
+ };
16
+ export function companyRouteDeparture(user) {
17
+ if (user.account_identity !== UserTypeEmployer ||
18
+ !user.place_id.trim() ||
19
+ !addressTimezone(user))
20
+ throw new PreActionBlocked(RoutePlanningBlock.Departure);
21
+ return {
22
+ reference: { source: RouteDepartureSource.Company },
23
+ address: user,
24
+ name: "",
25
+ };
26
+ }
27
+ export function warehouseRouteDeparture(warehouse, owner_id) {
28
+ if (warehouse.owner_id !== owner_id ||
29
+ warehouse.is_archived ||
30
+ !warehouse.place_id.trim() ||
31
+ !addressTimezone(warehouse))
32
+ throw new PreActionBlocked(RoutePlanningBlock.Departure);
33
+ return {
34
+ reference: {
35
+ source: RouteDepartureSource.Warehouse,
36
+ warehouse_id: warehouse.id,
37
+ },
38
+ address: warehouse,
39
+ name: warehouse.name,
40
+ };
41
+ }
42
+ export async function loadRouteDeparture(readers, owner_id, reference, signal) {
43
+ if (reference.source === RouteDepartureSource.Warehouse) {
44
+ const warehouse = (await readers.billable.findOneWarehouse({
45
+ id: reference.warehouse_id,
46
+ signal,
47
+ })).value;
48
+ if (warehouse?.id === reference.warehouse_id)
49
+ return warehouseRouteDeparture(warehouse, owner_id);
50
+ }
51
+ else {
52
+ const user = (await readers.users.findOneUser({ id: owner_id, signal }))
53
+ .value;
54
+ if (user?.id === owner_id)
55
+ return companyRouteDeparture(user);
56
+ }
57
+ throw new PreActionBlocked(RoutePlanningBlock.Departure);
58
+ }
59
+ export function validateRoutePlanLocation(location, owner_id, timezone) {
60
+ if (location.owner_id !== owner_id ||
61
+ location.is_archived ||
62
+ !location.place_id.trim() ||
63
+ !resolvedID(location.client_id))
64
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationChanged);
65
+ if (addressTimezone(location) !== timezone)
66
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationTimezone);
67
+ }
68
+ // Re-read saved operational addresses before optimization. A queued draft must
69
+ // not submit stale location ownership, Place IDs or address timezones.
70
+ export async function validateRoutePlanAddresses(readers, opts) {
71
+ const { input, departure, current, signal } = opts;
72
+ if (departure.source === RouteDepartureSource.SavedRoute) {
73
+ if (!current ||
74
+ current.id !== departure.route_id ||
75
+ current.owner_id !== opts.owner_id ||
76
+ current.start_place_id !== input.start_place_id ||
77
+ current.timezone !== input.timezone)
78
+ throw new PreActionBlocked(RoutePlanningBlock.DepartureChanged);
79
+ }
80
+ else {
81
+ const saved = await loadRouteDeparture(readers, opts.owner_id, departure, signal);
82
+ if (saved.address.place_id !== input.start_place_id ||
83
+ addressTimezone(saved.address) !== input.timezone)
84
+ throw new PreActionBlocked(RoutePlanningBlock.DepartureChanged);
85
+ }
86
+ for (const destination of input.destinations) {
87
+ const location = (await readers.jobs.findOneLocation({
88
+ id: destination.location_id,
89
+ signal,
90
+ })).value;
91
+ if (!location ||
92
+ location.id !== destination.location_id ||
93
+ location.client_id !== destination.client_id ||
94
+ location.place_id !== destination.place_id)
95
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationChanged);
96
+ validateRoutePlanLocation(location, opts.owner_id, input.timezone);
97
+ }
98
+ }
@@ -23,7 +23,7 @@ export declare function newRoutePlan(opts: {
23
23
  company: Pick<PrefCompany, "work_day_start_hour" | "work_day_end_hour">;
24
24
  start_place_id: string;
25
25
  }): InRoute;
26
- export declare function routeDestination(location: Location, owner_id: string): InRoute["destinations"][number];
26
+ export declare function routeDestination(location: Location, owner_id: string, timezone: string): InRoute["destinations"][number];
27
27
  export declare function validateRouteEmployees(ids: readonly string[], allowed: readonly string[], occupied?: readonly string[]): void;
28
28
  export type RouteAvailabilityWindow = {
29
29
  availability: ClientAvailability;
@@ -1,10 +1,10 @@
1
1
  import { isValidInputDate, minutesOfDay, sameCalendarDay, } from "../core/date.js";
2
2
  import { errValidation } from "../core/err.js";
3
- import { normalizeIDs, resolvedID } from "../core/id.js";
3
+ import { normalizeIDs } from "../core/id.js";
4
4
  import { routeInput, validateRouteTimeWindow } from "./validation.js";
5
+ import { validateRoutePlanLocation } from "./departure.js";
5
6
  import { PreActionBlocked } from "../core/pre-action.js";
6
7
  const routePreparationBlocked = "route_preparation";
7
- const msgPlanningLocation = "Select an active location with a client and a saved address.";
8
8
  const msgPlanningEmployees = "Choose available employees who are not already in another vehicle.";
9
9
  export const RouteMutationBlock = {
10
10
  Busy: "route_mutation_busy",
@@ -46,15 +46,8 @@ export function newRoutePlan(opts) {
46
46
  override_itinerary: routePlanningDefaults.override_itinerary,
47
47
  };
48
48
  }
49
- export function routeDestination(location, owner_id) {
50
- if (location.owner_id !== owner_id ||
51
- location.is_archived ||
52
- !location.place_id.trim() ||
53
- !resolvedID(location.client_id))
54
- throw errValidation({
55
- field: "destinations",
56
- msg: msgPlanningLocation,
57
- });
49
+ export function routeDestination(location, owner_id, timezone) {
50
+ validateRoutePlanLocation(location, owner_id, timezone);
58
51
  return {
59
52
  location_id: location.id,
60
53
  client_id: location.client_id,
@@ -1,3 +1,4 @@
1
+ import { type RouteDepartureReference, type RoutePlanningReaders } from "./departure.js";
1
2
  import type { RoutesOperator } from "@fabriktor/client";
2
3
  import type { ClientAvailability, InRoute, Route, RouteTeam, RPRoutesAvailability, Tag, Tracking } from "@fabriktor/schema";
3
4
  import { RoutesWorkspace, type PrepareRouteOptions, type RoutesActor } from "./workspace.js";
@@ -21,6 +22,7 @@ export type CreateValidatedRouteResult = Awaited<ReturnType<RoutesOperator["inse
21
22
  export type CreateRouteResult = CreateValidatedRouteResult;
22
23
  export type CreatePlannedRouteOptions = Omit<PrepareRouteOptions, "request"> & {
23
24
  attempt: MultiCreateAttempt;
25
+ departure: RouteDepartureReference;
24
26
  };
25
27
  export type ReplaceRouteOptions = RouteReplaceOptions & {
26
28
  availability?: RPRoutesAvailability;
@@ -36,6 +38,7 @@ export type RouteMutationOptions = {
36
38
  export type ReplacePlannedRouteOptions = RouteMutationOptions & {
37
39
  in: InRoute;
38
40
  days_ago: PrepareRouteOptions["days_ago"];
41
+ departure: RouteDepartureReference;
39
42
  };
40
43
  export type CreateTagOptions = TagInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
41
44
  export type ReplaceTagOptions = TagReplaceOptions;
@@ -49,7 +52,7 @@ export type CreateRouteTeamOptions = RouteTeamInsertOptions & Pick<MultiCreateCh
49
52
  export type ReplaceRouteTeamOptions = RouteTeamReplaceOptions;
50
53
  export type SearchRouteTeamsOptions = OwnerSearchOptions;
51
54
  export type SearchRouteTeamsPage = SearchPage<RouteTeam>;
52
- export type RoutesFXOptions = {
55
+ export type RoutesFXOptions = RoutePlanningReaders & {
53
56
  routes: RoutesOperator;
54
57
  };
55
58
  export interface RoutesFXOperator {
@@ -72,6 +75,7 @@ export interface RoutesFXOperator {
72
75
  searchRouteTeams(opts: SearchRouteTeamsOptions): Promise<SearchRouteTeamsPage>;
73
76
  }
74
77
  export declare class RoutesFX implements RoutesFXOperator {
78
+ private readonly opts;
75
79
  private routes;
76
80
  private routeMutationPending;
77
81
  readonly workspace: RoutesWorkspace;
@@ -3,6 +3,7 @@
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ import { validateRoutePlanAddresses, } from "./departure.js";
6
7
  import { errResolveFailed, errValidation } from "../core/err.js";
7
8
  import { RoutesWorkspace, routeCanManage, routeCanPlan, } from "./workspace.js";
8
9
  import { RoutePreparationBlocked, RouteMutationBlock, routePlanChanged, } from "./planning.js";
@@ -27,10 +28,12 @@ const msgTrackingMissing = "Tracking could not be resolved after the mutation.";
27
28
  const msgCreatedRouteTeamIDMissing = "Created route team response did not contain an ID.";
28
29
  const msgRouteTeamMissing = "Route team could not be resolved after the mutation.";
29
30
  export class RoutesFX {
31
+ opts;
30
32
  routes;
31
33
  routeMutationPending = false;
32
34
  workspace;
33
35
  constructor(opts) {
36
+ this.opts = opts;
34
37
  this.routes = opts.routes;
35
38
  this.workspace = new RoutesWorkspace(opts.routes);
36
39
  }
@@ -51,6 +54,18 @@ export class RoutesFX {
51
54
  throw errValidation({ msg: msgRouteOwner });
52
55
  return route;
53
56
  }
57
+ try {
58
+ validateRouteInput(opts.in);
59
+ await validateRoutePlanAddresses(this.opts, {
60
+ owner_id: actor.owner_id,
61
+ input: opts.in,
62
+ departure: opts.departure,
63
+ signal: attempt.signal,
64
+ });
65
+ }
66
+ catch (error) {
67
+ throw new RoutePreparationBlocked(error);
68
+ }
54
69
  const prepared = await this.workspace
55
70
  .prepareRoute({
56
71
  actor,
@@ -121,6 +136,13 @@ export class RoutesFX {
121
136
  validateRouteInput(input);
122
137
  if (!routePlanChanged(current, input))
123
138
  return current;
139
+ await validateRoutePlanAddresses(this.opts, {
140
+ owner_id: opts.actor.owner_id,
141
+ input,
142
+ departure: opts.departure,
143
+ current,
144
+ signal: opts.signal,
145
+ });
124
146
  const availability = await this.workspace.availability({
125
147
  actor: opts.actor,
126
148
  employee_ids: input.vehicles.flatMap((vehicle) => vehicle.employee_ids),
@@ -128,7 +150,7 @@ export class RoutesFX {
128
150
  days_ago: opts.days_ago,
129
151
  request: { signal: opts.signal },
130
152
  });
131
- // Retain this route's own assignments, but never another route's.
153
+ // Repeated locations are allowed; employee conflicts remain enforced.
132
154
  validateRouteInput(input, availability, current.id);
133
155
  const result = await this.replaceRoute({
134
156
  id: current.id,
@@ -6,6 +6,7 @@ export declare function routeInput(input: InRoute): InRoute;
6
6
  export declare function validateRouteInput(input: InRoute, availability?: RPRoutesAvailability, replacing_route_id?: string): void;
7
7
  export declare function validateRouteDestination(destination: InRoute["destinations"][number], window: WindowInput): void;
8
8
  export declare function availableRouteIDs(availability: RPRoutesAvailability, kind: keyof RPRoutesAvailability["availables"], replacing_route_id?: string): Set<string>;
9
+ export declare function routeAssignmentsByEntity(assignments: RPRoutesAvailability["assignations"]["locations"], replacing_route_id?: string): Map<string, string[]>;
9
10
  export declare function validateClientAvailabilityInput(input: InClientAvailability): void;
10
11
  export declare function validateRouteTeamInput(input: InRouteTeam): void;
11
12
  export declare function normalizedTagInput(input: InTag): InTag;
@@ -165,10 +165,17 @@ export function validateRouteDestination(destination, window) {
165
165
  });
166
166
  }
167
167
  }
168
- // Editing may retain this route's assignments, but must not re-enable an ID
169
- // also assigned to another route. Availability is still a backend snapshot.
168
+ // Locations may be visited on several routes. Employees stay exclusive for
169
+ // the checked day; editing may retain only this route's employee assignments.
170
170
  export function availableRouteIDs(availability, kind, replacing_route_id) {
171
171
  const ids = new Set(normalizeIDs(availability.availables[kind]));
172
+ if (kind === "locations") {
173
+ for (const assignment of availability.assignations.locations) {
174
+ for (const id of normalizeIDs(assignment.entity_ids))
175
+ ids.add(id);
176
+ }
177
+ return ids;
178
+ }
172
179
  for (const assignment of availability.assignations[kind]) {
173
180
  if (assignment.route_id === replacing_route_id) {
174
181
  for (const id of normalizeIDs(assignment.entity_ids))
@@ -183,6 +190,23 @@ export function availableRouteIDs(availability, kind, replacing_route_id) {
183
190
  }
184
191
  return ids;
185
192
  }
193
+ // Group the backend's existing route references for informational display.
194
+ // Exclude the route being edited and count each route only once per entity.
195
+ export function routeAssignmentsByEntity(assignments, replacing_route_id) {
196
+ const out = new Map();
197
+ for (const assignment of assignments) {
198
+ const routeID = resolvedID(assignment.route_id);
199
+ if (!routeID || routeID === replacing_route_id)
200
+ continue;
201
+ for (const entityID of normalizeIDs(assignment.entity_ids)) {
202
+ const routes = out.get(entityID) ?? [];
203
+ if (!routes.includes(routeID))
204
+ routes.push(routeID);
205
+ out.set(entityID, routes);
206
+ }
207
+ }
208
+ return out;
209
+ }
186
210
  export function validateClientAvailabilityInput(input) {
187
211
  validateRouteTimeWindow(input);
188
212
  for (const field of ["employer_id", "location_id"]) {
@@ -91,7 +91,8 @@ export class RoutesWorkspace {
91
91
  const query = {
92
92
  ...queryParams({
93
93
  owner_id,
94
- timezone: opts.day?.tz,
94
+ // A day filter refers to the calendar day stored on each route.
95
+ // Employers may operate depots in different timezones.
95
96
  utilized_employee_ids: actor.active_mode === UserTypeEmployee
96
97
  ? actor.owner_id
97
98
  : optionalIDs(opts.employee_ids),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.140",
3
+ "version": "0.0.142",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,6 +19,7 @@
19
19
  "dependencies": {
20
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
  }