@fabriktor/fx 0.0.150 → 0.0.151
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
|
|
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({
|
|
184
|
-
|
|
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
|
-
(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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.
|
|
@@ -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 {
|
|
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
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
+
const saved = availability.client_availabilities
|
|
79
90
|
.flatMap((group) => group.location_id === location_id ? group.availabilities : [])
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
}
|