@fabriktor/fx 0.0.150 → 0.0.152

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.
@@ -46,6 +46,9 @@ export declare function loadAvailabilityRequestContext(readers: AvailabilityRead
46
46
  }, actor: RoutesActor, data: ChatData): Promise<AvailabilityFormContext>;
47
47
  export declare function availabilitySubmission(reference: AvailabilityReference, request_id: string, ids: readonly string[]): ChatData;
48
48
  export declare function loadAvailabilityContext(readers: AvailabilityReaders, actor: RoutesActor, reference: AvailabilityReference, { request, signal }?: AvailabilityContextOptions): Promise<AvailabilityContext>;
49
+ type AvailabilityWindow = Pick<InClientAvailability, "from" | "to" | "timezone">;
50
+ export declare function isValidAvailabilityWindow(window: AvailabilityWindow): boolean;
51
+ export declare function availabilityWindowConflicts(inputs: readonly AvailabilityWindow[]): number[];
49
52
  export declare function validateAvailabilityWindows(inputs: readonly InClientAvailability[], context: AvailabilityContext, minimum?: number): void;
50
53
  export declare function loadAvailabilitySelection(readers: AvailabilityReaders & {
51
54
  routes: Pick<RoutesOperator, "findOneClientAvailability">;
@@ -53,3 +56,4 @@ export declare function loadAvailabilitySelection(readers: AvailabilityReaders &
53
56
  context: AvailabilityContext;
54
57
  windows: ClientAvailability[];
55
58
  }>;
59
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { UserTypeClient, UserTypeEmployer, } from "@fabriktor/schema";
2
2
  import { isFabriktorUser } from "../users/users.js";
3
3
  import { addressTimezone } from "../core/address.js";
4
- import { hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
4
+ import { hoursPerDay, isValidInputDate, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
5
5
  import { errValidation } from "../core/err.js";
6
6
  import { normalizeIDs, resolvedID } from "../core/id.js";
7
7
  import { newChatData } from "../chat/data.js";
@@ -46,7 +46,9 @@ const msgAvailabilityAccess = "This availability does not match the client and e
46
46
  const msgAvailabilityTimezone = "The location needs a valid saved address timezone.";
47
47
  const msgAvailabilityTimezoneChanged = "The location timezone has changed. Ask the sender to send a new form.";
48
48
  const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
49
- const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows and respect the hours supplied with the request.";
49
+ const msgAvailabilityMinimumWindows = "Choose at least {count} possible visit times.";
50
+ const msgAvailabilityWindows = "Some visit times overlap. Choose another day or change their hours.";
51
+ const msgAvailabilityOutsideHours = "Choose visit times within the working hours shown.";
50
52
  const msgAvailabilityClient = "Visit times can only be requested from clients with a Fabriktor account. Arrange external contacts’ visit times directly.";
51
53
  const msgAvailabilityData = "The availability request is invalid.";
52
54
  export function availabilityReference(data) {
@@ -178,26 +180,54 @@ export async function loadAvailabilityContext(readers, actor, reference, { reque
178
180
  timezone,
179
181
  };
180
182
  }
183
+ export function isValidAvailabilityWindow(window) {
184
+ return (isValidInputDate(window.from) &&
185
+ isValidInputDate(window.to) &&
186
+ window.from.tz === window.timezone &&
187
+ window.to.tz === window.timezone &&
188
+ sameCalendarDay(window.from, window.to) &&
189
+ minutesOfDay(window.from) < minutesOfDay(window.to));
190
+ }
191
+ // Return every affected row, including duplicates. Incomplete rows are handled
192
+ // by the inputs' required/date/time rules, not by the overlap rule.
193
+ export function availabilityWindowConflicts(inputs) {
194
+ const conflicts = new Set();
195
+ inputs.forEach((input, index) => {
196
+ if (!isValidAvailabilityWindow(input))
197
+ return;
198
+ inputs.slice(0, index).forEach((other, previous) => {
199
+ if (isValidAvailabilityWindow(other) &&
200
+ input.timezone === other.timezone &&
201
+ sameCalendarDay(input.from, other.from) &&
202
+ minutesOfDay(input.from) < minutesOfDay(other.to) &&
203
+ minutesOfDay(input.to) > minutesOfDay(other.from)) {
204
+ conflicts.add(previous);
205
+ conflicts.add(index);
206
+ }
207
+ });
208
+ });
209
+ return inputs.flatMap((_, index) => (conflicts.has(index) ? [index] : []));
210
+ }
181
211
  export function validateAvailabilityWindows(inputs, context, minimum = clientAvailabilityMinimumWindows) {
182
212
  if (inputs.length < minimum)
183
- throw errValidation({ msg: msgAvailabilityWindows });
184
- for (const [index, input] of inputs.entries()) {
213
+ throw errValidation({
214
+ msg: msgAvailabilityMinimumWindows.replace("{count}", String(minimum)),
215
+ });
216
+ for (const input of inputs) {
185
217
  validateClientAvailabilityInput(input);
186
218
  if (input.employer_id !== context.employer.id ||
187
219
  input.location_id !== context.location.id ||
188
- input.timezone !== context.timezone ||
189
- (context.working_hours &&
190
- (minutesOfDay(input.from) <
191
- context.working_hours.work_day_start_hour * minutesPerHour ||
192
- minutesOfDay(input.to) >
193
- context.working_hours.work_day_end_hour * minutesPerHour)) ||
194
- inputs
195
- .slice(0, index)
196
- .some((other) => sameCalendarDay(input.from, other.from) &&
197
- minutesOfDay(input.from) < minutesOfDay(other.to) &&
198
- minutesOfDay(input.to) > minutesOfDay(other.from)))
199
- throw errValidation({ msg: msgAvailabilityWindows });
220
+ input.timezone !== context.timezone)
221
+ throw errValidation({ msg: msgAvailabilityAccess });
222
+ if (context.working_hours &&
223
+ (minutesOfDay(input.from) <
224
+ context.working_hours.work_day_start_hour * minutesPerHour ||
225
+ minutesOfDay(input.to) >
226
+ context.working_hours.work_day_end_hour * minutesPerHour))
227
+ throw errValidation({ msg: msgAvailabilityOutsideHours });
200
228
  }
229
+ if (availabilityWindowConflicts(inputs).length)
230
+ throw errValidation({ msg: msgAvailabilityWindows });
201
231
  }
202
232
  // A chat response contains references, never authority or an appointment confirmation.
203
233
  // Reload each chosen record before preparing the response, including after a resumed batch.
@@ -29,6 +29,10 @@ export declare const RoutePlanningBlock: {
29
29
  readonly DepartureChanged: "route_departure_changed";
30
30
  readonly DestinationTimezone: "route_destination_timezone";
31
31
  readonly DestinationChanged: "route_destination_changed";
32
+ readonly DestinationOwner: "route_destination_owner";
33
+ readonly DestinationArchived: "route_destination_archived";
34
+ readonly DestinationAddress: "route_destination_address";
35
+ readonly DestinationClient: "route_destination_client";
32
36
  };
33
37
  export declare function companyRouteDeparture(user: User): RouteDeparture;
34
38
  export declare function warehouseRouteDeparture(warehouse: Warehouse, owner_id: string): RouteDeparture;
@@ -12,6 +12,10 @@ export const RoutePlanningBlock = {
12
12
  DepartureChanged: "route_departure_changed",
13
13
  DestinationTimezone: "route_destination_timezone",
14
14
  DestinationChanged: "route_destination_changed",
15
+ DestinationOwner: "route_destination_owner",
16
+ DestinationArchived: "route_destination_archived",
17
+ DestinationAddress: "route_destination_address",
18
+ DestinationClient: "route_destination_client",
15
19
  };
16
20
  export function companyRouteDeparture(user) {
17
21
  if (user.account_identity !== UserTypeEmployer ||
@@ -57,11 +61,14 @@ export async function loadRouteDeparture(readers, owner_id, reference, signal) {
57
61
  throw new PreActionBlocked(RoutePlanningBlock.Departure);
58
62
  }
59
63
  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);
64
+ if (location.owner_id !== owner_id)
65
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationOwner);
66
+ if (location.is_archived)
67
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationArchived);
68
+ if (!location.place_id?.trim())
69
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationAddress);
70
+ if (!resolvedID(location.client_id))
71
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationClient);
65
72
  if (addressTimezone(location) !== timezone)
66
73
  throw new PreActionBlocked(RoutePlanningBlock.DestinationTimezone);
67
74
  }
@@ -30,5 +30,12 @@ export type RouteAvailabilityWindow = {
30
30
  start: number;
31
31
  end: number;
32
32
  };
33
+ export declare function routeAvailabilityMatch(input: Pick<InRoute, "from" | "to" | "timezone">, availability: ClientAvailability): RouteAvailabilityWindow | undefined;
34
+ export type RouteAvailabilityOverview = {
35
+ saved: ClientAvailability[];
36
+ matching: RouteAvailabilityWindow[];
37
+ on_day: number;
38
+ };
39
+ export declare function routeAvailabilityOverview(input: Pick<InRoute, "from" | "to" | "timezone">, availability: RPRoutesAvailability, location_id: string, client_id: string): RouteAvailabilityOverview;
33
40
  export declare function routeAvailabilityWindows(input: Pick<InRoute, "from" | "to" | "timezone">, availability: RPRoutesAvailability, location_id: string, client_id: string): RouteAvailabilityWindow[];
34
41
  export {};
@@ -1,8 +1,9 @@
1
- import { isValidInputDate, minutesOfDay, sameCalendarDay, } from "../core/date.js";
1
+ import { minutesOfDay, sameCalendarDay } from "../core/date.js";
2
2
  import { errValidation } from "../core/err.js";
3
3
  import { normalizeIDs } from "../core/id.js";
4
4
  import { routeInput, validateRouteTimeWindow } from "./validation.js";
5
5
  import { validateRoutePlanLocation } from "./departure.js";
6
+ import { isValidAvailabilityWindow } from "./availability.js";
6
7
  import { PreActionBlocked } from "../core/pre-action.js";
7
8
  const routePreparationBlocked = "route_preparation";
8
9
  const msgPlanningEmployees = "Choose available employees who are not already in another vehicle.";
@@ -71,25 +72,37 @@ export function validateRouteEmployees(ids, allowed, occupied = []) {
71
72
  msg: msgPlanningEmployees,
72
73
  });
73
74
  }
74
- // Suggestions are child data in an availability response. Intersect the client's
75
- // window with the route window; never widen the hours offered by either party.
76
- export function routeAvailabilityWindows(input, availability, location_id, client_id) {
75
+ // Intersect the client's window with the route hours; never widen either.
76
+ export function routeAvailabilityMatch(input, availability) {
77
+ if (!isValidAvailabilityWindow(availability) ||
78
+ availability.timezone !== input.timezone ||
79
+ !sameCalendarDay(availability.from, input.from))
80
+ return;
81
+ const start = Math.max(minutesOfDay(availability.from), minutesOfDay(input.from));
82
+ const end = Math.min(minutesOfDay(availability.to), minutesOfDay(input.to));
83
+ return start < end ? { availability, start, end } : undefined;
84
+ }
85
+ // These are child windows returned by get_availability, not a filtered parent
86
+ // query. Keep other days visible and preserve the backend's order.
87
+ export function routeAvailabilityOverview(input, availability, location_id, client_id) {
77
88
  validateRouteTimeWindow(input);
78
- return availability.client_availabilities
89
+ const saved = availability.client_availabilities
79
90
  .flatMap((group) => group.location_id === location_id ? group.availabilities : [])
80
- .flatMap((window) => {
81
- if (window.owner_id !== client_id ||
82
- window.location_id !== location_id ||
83
- window.timezone !== input.timezone ||
84
- window.from.tz !== input.timezone ||
85
- window.to.tz !== input.timezone ||
86
- !isValidInputDate(window.from) ||
87
- !isValidInputDate(window.to) ||
88
- !sameCalendarDay(window.from, input.from) ||
89
- !sameCalendarDay(window.to, input.to))
90
- return [];
91
- const start = Math.max(minutesOfDay(window.from), minutesOfDay(input.from));
92
- const end = Math.min(minutesOfDay(window.to), minutesOfDay(input.to));
93
- return start < end ? [{ availability: window, start, end }] : [];
94
- });
91
+ .filter((window) => window.owner_id === client_id &&
92
+ window.location_id === location_id &&
93
+ window.timezone === input.timezone &&
94
+ isValidAvailabilityWindow(window));
95
+ return {
96
+ saved,
97
+ matching: saved.flatMap((window) => {
98
+ const match = routeAvailabilityMatch(input, window);
99
+ return match ? [match] : [];
100
+ }),
101
+ on_day: saved.filter((window) => sameCalendarDay(window.from, input.from))
102
+ .length,
103
+ };
104
+ }
105
+ export function routeAvailabilityWindows(input, availability, location_id, client_id) {
106
+ return routeAvailabilityOverview(input, availability, location_id, client_id)
107
+ .matching;
95
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.150",
3
+ "version": "0.0.152",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {