@fabriktor/fx 0.0.141 → 0.0.143

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.
@@ -40,6 +40,7 @@ export declare function validateRoutePlanAddresses(readers: RoutePlanningReaders
40
40
  owner_id: string;
41
41
  input: InRoute;
42
42
  departure: RouteDepartureReference;
43
+ end?: RouteDepartureReference;
43
44
  current?: Route;
44
45
  signal?: AbortSignal;
45
46
  }): Promise<void>;
@@ -68,20 +68,39 @@ export function validateRoutePlanLocation(location, owner_id, timezone) {
68
68
  // Re-read saved operational addresses before optimization. A queued draft must
69
69
  // not submit stale location ownership, Place IDs or address timezones.
70
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);
71
+ const { input, current, signal } = opts;
72
+ const endpoints = [
73
+ {
74
+ place: input.start_place_id,
75
+ reference: opts.departure,
76
+ saved: current?.start_place_id,
77
+ },
78
+ {
79
+ place: input.end_place_id,
80
+ reference: opts.end,
81
+ saved: current?.end_place_id,
82
+ },
83
+ ];
84
+ for (const { place, reference, saved } of endpoints) {
85
+ if (!place)
86
+ continue;
87
+ if (!reference)
88
+ throw new PreActionBlocked(RoutePlanningBlock.Departure);
89
+ if (reference.source === RouteDepartureSource.SavedRoute) {
90
+ // Returning to an unchanged saved departure also works on existing routes.
91
+ if (!current ||
92
+ current.id !== reference.route_id ||
93
+ current.owner_id !== opts.owner_id ||
94
+ current.timezone !== input.timezone ||
95
+ (place !== saved && place !== current.start_place_id))
96
+ throw new PreActionBlocked(RoutePlanningBlock.DepartureChanged);
97
+ }
98
+ else {
99
+ const endpoint = await loadRouteDeparture(readers, opts.owner_id, reference, signal);
100
+ if (endpoint.address.place_id !== place ||
101
+ addressTimezone(endpoint.address) !== input.timezone)
102
+ throw new PreActionBlocked(RoutePlanningBlock.DepartureChanged);
103
+ }
85
104
  }
86
105
  for (const destination of input.destinations) {
87
106
  const location = (await readers.jobs.findOneLocation({
@@ -39,6 +39,7 @@ export function newRoutePlan(opts) {
39
39
  from: { ...opts.day, h: opts.company.work_day_start_hour, min: 0 },
40
40
  to: { ...opts.day, h: opts.company.work_day_end_hour, min: 0 },
41
41
  start_place_id: opts.start_place_id,
42
+ end_place_id: "",
42
43
  vehicles: [],
43
44
  destinations: [],
44
45
  consider_traffic: routePlanningDefaults.consider_traffic,
@@ -23,6 +23,7 @@ export type CreateRouteResult = CreateValidatedRouteResult;
23
23
  export type CreatePlannedRouteOptions = Omit<PrepareRouteOptions, "request"> & {
24
24
  attempt: MultiCreateAttempt;
25
25
  departure: RouteDepartureReference;
26
+ end?: RouteDepartureReference;
26
27
  };
27
28
  export type ReplaceRouteOptions = RouteReplaceOptions & {
28
29
  availability?: RPRoutesAvailability;
@@ -39,6 +40,7 @@ export type ReplacePlannedRouteOptions = RouteMutationOptions & {
39
40
  in: InRoute;
40
41
  days_ago: PrepareRouteOptions["days_ago"];
41
42
  departure: RouteDepartureReference;
43
+ end?: RouteDepartureReference;
42
44
  };
43
45
  export type CreateTagOptions = TagInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
44
46
  export type ReplaceTagOptions = TagReplaceOptions;
@@ -60,6 +60,7 @@ export class RoutesFX {
60
60
  owner_id: actor.owner_id,
61
61
  input: opts.in,
62
62
  departure: opts.departure,
63
+ end: opts.end,
63
64
  signal: attempt.signal,
64
65
  });
65
66
  }
@@ -124,6 +125,9 @@ export class RoutesFX {
124
125
  ...(opts.in.start_place_id !== undefined
125
126
  ? { start_place_id: input.start_place_id }
126
127
  : {}),
128
+ ...(opts.in.end_place_id !== undefined
129
+ ? { end_place_id: input.end_place_id }
130
+ : {}),
127
131
  override_itinerary,
128
132
  },
129
133
  });
@@ -140,6 +144,7 @@ export class RoutesFX {
140
144
  owner_id: opts.actor.owner_id,
141
145
  input,
142
146
  departure: opts.departure,
147
+ end: opts.end,
143
148
  current,
144
149
  signal: opts.signal,
145
150
  });
@@ -150,7 +155,7 @@ export class RoutesFX {
150
155
  days_ago: opts.days_ago,
151
156
  request: { signal: opts.signal },
152
157
  });
153
- // Retain this route's own assignments, but never another route's.
158
+ // Repeated locations are allowed; employee conflicts remain enforced.
154
159
  validateRouteInput(input, availability, current.id);
155
160
  const result = await this.replaceRoute({
156
161
  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;
@@ -78,6 +78,7 @@ export function routeInput(input) {
78
78
  employee_ids: vehicle.employee_ids.map((id) => id.trim()),
79
79
  })),
80
80
  start_place_id: input.start_place_id.trim(),
81
+ end_place_id: input.end_place_id?.trim() ?? "",
81
82
  destinations: input.destinations.map((destination) => ({
82
83
  tags: normalizeRouteTags(destination.tags),
83
84
  location_id: destination.location_id.trim(),
@@ -165,10 +166,17 @@ export function validateRouteDestination(destination, window) {
165
166
  });
166
167
  }
167
168
  }
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.
169
+ // Locations may be visited on several routes. Employees stay exclusive for
170
+ // the checked day; editing may retain only this route's employee assignments.
170
171
  export function availableRouteIDs(availability, kind, replacing_route_id) {
171
172
  const ids = new Set(normalizeIDs(availability.availables[kind]));
173
+ if (kind === "locations") {
174
+ for (const assignment of availability.assignations.locations) {
175
+ for (const id of normalizeIDs(assignment.entity_ids))
176
+ ids.add(id);
177
+ }
178
+ return ids;
179
+ }
172
180
  for (const assignment of availability.assignations[kind]) {
173
181
  if (assignment.route_id === replacing_route_id) {
174
182
  for (const id of normalizeIDs(assignment.entity_ids))
@@ -183,6 +191,23 @@ export function availableRouteIDs(availability, kind, replacing_route_id) {
183
191
  }
184
192
  return ids;
185
193
  }
194
+ // Group the backend's existing route references for informational display.
195
+ // Exclude the route being edited and count each route only once per entity.
196
+ export function routeAssignmentsByEntity(assignments, replacing_route_id) {
197
+ const out = new Map();
198
+ for (const assignment of assignments) {
199
+ const routeID = resolvedID(assignment.route_id);
200
+ if (!routeID || routeID === replacing_route_id)
201
+ continue;
202
+ for (const entityID of normalizeIDs(assignment.entity_ids)) {
203
+ const routes = out.get(entityID) ?? [];
204
+ if (!routes.includes(routeID))
205
+ routes.push(routeID);
206
+ out.set(entityID, routes);
207
+ }
208
+ }
209
+ return out;
210
+ }
186
211
  export function validateClientAvailabilityInput(input) {
187
212
  validateRouteTimeWindow(input);
188
213
  for (const field of ["employer_id", "location_id"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.141",
3
+ "version": "0.0.143",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,9 +17,9 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/client": "0.0.98",
20
+ "@fabriktor/client": "0.0.99",
21
21
  "@fabriktor/fx": "0.0.94",
22
- "@fabriktor/schema": "0.0.68",
22
+ "@fabriktor/schema": "0.0.69",
23
23
  "convert": "8.0.2"
24
24
  }
25
25
  }