@fabriktor/fx 0.0.149 → 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.
- package/dist/src/chat/data.js +10 -5
- package/dist/src/chat/forms.d.ts +7 -0
- package/dist/src/chat/forms.js +24 -0
- package/dist/src/jobs/jobs.js +7 -0
- package/dist/src/routes/availability.d.ts +21 -5
- package/dist/src/routes/availability.js +125 -27
- package/dist/src/routes/planning.d.ts +7 -0
- package/dist/src/routes/planning.js +33 -20
- package/package.json +1 -1
package/dist/src/chat/data.js
CHANGED
|
@@ -14,19 +14,24 @@ export function newChatData(type, payload) {
|
|
|
14
14
|
type,
|
|
15
15
|
payload,
|
|
16
16
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
17
|
+
// JSON snapshots also accept reactive UI objects without structuredClone's
|
|
18
|
+
// Proxy failure. Validation and size limits are shared with sending.
|
|
19
|
+
return JSON.parse(serializeChatData(data));
|
|
20
20
|
}
|
|
21
21
|
export function encodeChatContent(text, data) {
|
|
22
22
|
if (!data)
|
|
23
23
|
return text;
|
|
24
|
-
if (!text.trim() || text.includes(chatDataMarker)
|
|
24
|
+
if (!text.trim() || text.includes(chatDataMarker))
|
|
25
|
+
throw errValidation({ msg: msgChatData });
|
|
26
|
+
return text.trim() + chatDataMarker + serializeChatData(data);
|
|
27
|
+
}
|
|
28
|
+
function serializeChatData(data) {
|
|
29
|
+
if (!validChatData(data))
|
|
25
30
|
throw errValidation({ msg: msgChatData });
|
|
26
31
|
const value = JSON.stringify(data);
|
|
27
32
|
if (new TextEncoder().encode(value).length > chatDataMaximumBytes)
|
|
28
33
|
throw errValidation({ msg: msgChatData });
|
|
29
|
-
return
|
|
34
|
+
return value;
|
|
30
35
|
}
|
|
31
36
|
export function decodeChatContent(value) {
|
|
32
37
|
const boundary = value.indexOf(chatDataMarker);
|
package/dist/src/chat/forms.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { User } from "@fabriktor/schema";
|
|
2
2
|
import type { EmploymentActor } from "../contacts/scope.js";
|
|
3
|
+
import { type ChatData, type ChatJSON } from "./data.js";
|
|
3
4
|
export type ChatFormRecipient = Pick<User, "id" | "account_identity">;
|
|
4
5
|
export type ChatFormPolicy = {
|
|
5
6
|
readonly sender_modes: readonly User["active_mode"][];
|
|
6
7
|
readonly recipient_modes: readonly User["active_mode"][];
|
|
7
8
|
readonly allow_group: boolean;
|
|
8
9
|
};
|
|
10
|
+
export type ChatFormContextDefinition<T extends ChatJSON> = {
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly read: (value: ChatJSON | undefined) => T | undefined;
|
|
13
|
+
};
|
|
14
|
+
export declare function newChatFormData<T extends ChatJSON>(type: string, payload: ChatData["payload"], definition: ChatFormContextDefinition<T>, context: T): ChatData;
|
|
15
|
+
export declare function readChatFormContext<T extends ChatJSON>(data: ChatData, definition: ChatFormContextDefinition<T>): T;
|
|
9
16
|
export declare function canSendChatForm(policy: ChatFormPolicy, actor: EmploymentActor, recipients: readonly ChatFormRecipient[]): boolean;
|
package/dist/src/chat/forms.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
|
+
import { errValidation } from "../core/err.js";
|
|
1
2
|
import { normalizeIDs, resolvedID } from "../core/id.js";
|
|
3
|
+
import { newChatData } from "./data.js";
|
|
2
4
|
import { isFabriktorUser, isOtherUserID, userSupportsMode, } from "../users/users.js";
|
|
5
|
+
const msgFormContext = "The form details are missing or unsupported. Ask the sender to send a new form.";
|
|
6
|
+
export function newChatFormData(type, payload, definition, context) {
|
|
7
|
+
const value = definition.read(context);
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
throw errValidation({ msg: msgFormContext });
|
|
10
|
+
return newChatData(type, {
|
|
11
|
+
...payload,
|
|
12
|
+
form_context: { version: definition.version, data: value },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function readChatFormContext(data, definition) {
|
|
16
|
+
const context = data.payload.form_context;
|
|
17
|
+
if (context &&
|
|
18
|
+
typeof context === "object" &&
|
|
19
|
+
!Array.isArray(context) &&
|
|
20
|
+
context.version === definition.version) {
|
|
21
|
+
const value = definition.read(context.data);
|
|
22
|
+
if (value !== undefined)
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
throw errValidation({ msg: msgFormContext });
|
|
26
|
+
}
|
|
3
27
|
// Recipients are all other members of the conversation, resolved before use.
|
|
4
28
|
// Sender modes apply now; recipient modes describe what their identity supports.
|
|
5
29
|
// Do not depend on the recipient's current mode, which Public() omits.
|
package/dist/src/jobs/jobs.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runPreActionCheck } from "../core/pre-action.js";
|
|
|
12
12
|
import { runOwnerSearch, runSearch, withOwnerQuery, } from "../core/search.js";
|
|
13
13
|
const msgJobIDRequired = "Job ID is required.";
|
|
14
14
|
const msgLocationIDRequired = "Location ID is required.";
|
|
15
|
+
const msgLocationClientRequired = "Choose a client for this location.";
|
|
15
16
|
const msgOwnerIDRequired = "Owner ID is required.";
|
|
16
17
|
const msgMemberIDRequired = "Member ID is required.";
|
|
17
18
|
const msgJobLocationMismatch = "Job location does not match the provided parent location.";
|
|
@@ -59,9 +60,11 @@ export class JobsFX {
|
|
|
59
60
|
this.now = opts.now ?? (() => new globalThis.Date());
|
|
60
61
|
}
|
|
61
62
|
async createLocation(opts) {
|
|
63
|
+
const client_id = requiredID(opts.in.client_id, "client_id", msgLocationClientRequired);
|
|
62
64
|
const out = await this.jobs.insertOneLocation({
|
|
63
65
|
in: {
|
|
64
66
|
...opts.in,
|
|
67
|
+
client_id,
|
|
65
68
|
is_archived: false,
|
|
66
69
|
current_members: normalizeIDs(opts.in.current_members),
|
|
67
70
|
},
|
|
@@ -72,8 +75,12 @@ export class JobsFX {
|
|
|
72
75
|
async updateLocation(opts) {
|
|
73
76
|
const owner_id = requiredID(opts.owner_id, "owner_id", msgOwnerIDRequired);
|
|
74
77
|
const id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
78
|
+
const client_id = requiredID(opts.in.client_id === undefined
|
|
79
|
+
? opts.location.client_id
|
|
80
|
+
: opts.in.client_id, "client_id", msgLocationClientRequired);
|
|
75
81
|
const input = {
|
|
76
82
|
...opts.in,
|
|
83
|
+
client_id,
|
|
77
84
|
...locationAddressInput(opts.location),
|
|
78
85
|
};
|
|
79
86
|
if (opts.in.current_members !== undefined) {
|
|
@@ -2,7 +2,7 @@ import type { JobsOperator, RoutesOperator, UsersOperator } from "@fabriktor/cli
|
|
|
2
2
|
import { type ClientAvailability, type InClientAvailability, type Location, type PrefCompany, type User } from "@fabriktor/schema";
|
|
3
3
|
import type { PreferencesFXOperator } from "../preferences/preferences.js";
|
|
4
4
|
import { type ChatData } from "../chat/data.js";
|
|
5
|
-
import { type ChatFormRecipient } from "../chat/forms.js";
|
|
5
|
+
import { type ChatFormContextDefinition, type ChatFormRecipient } from "../chat/forms.js";
|
|
6
6
|
import { type RoutesActor } from "./workspace.js";
|
|
7
7
|
export declare const clientAvailabilityMinimumWindows = 3;
|
|
8
8
|
export declare const availabilityRequestPolicy: {
|
|
@@ -21,23 +21,39 @@ export type AvailabilityContext = {
|
|
|
21
21
|
location: Location;
|
|
22
22
|
employer: User;
|
|
23
23
|
client: User;
|
|
24
|
-
|
|
24
|
+
working_hours?: AvailabilityWorkingHours;
|
|
25
25
|
timezone: string;
|
|
26
26
|
};
|
|
27
27
|
export type AvailabilityReaders = {
|
|
28
28
|
jobs: Pick<JobsOperator, "findOneLocation">;
|
|
29
29
|
users: Pick<UsersOperator, "findOneUser">;
|
|
30
|
-
|
|
30
|
+
};
|
|
31
|
+
export type AvailabilityWorkingHours = Pick<PrefCompany, "work_day_start_hour" | "work_day_end_hour">;
|
|
32
|
+
export type AvailabilityFormContext = {
|
|
33
|
+
working_hours: AvailabilityWorkingHours;
|
|
34
|
+
timezone: Location["address_timezone"];
|
|
35
|
+
};
|
|
36
|
+
export declare const availabilityFormContextDefinition: ChatFormContextDefinition<AvailabilityFormContext>;
|
|
37
|
+
export type AvailabilityContextOptions = {
|
|
38
|
+
request?: ChatData;
|
|
39
|
+
signal?: AbortSignal;
|
|
31
40
|
};
|
|
32
41
|
export declare function availabilityReference(data: ChatData): AvailabilityReference | undefined;
|
|
33
42
|
export declare function availabilityRequest(location: Location, actor: RoutesActor): ChatData;
|
|
34
43
|
export declare function canSendAvailabilityRequest(data: ChatData, actor: RoutesActor, recipients: readonly ChatFormRecipient[]): boolean;
|
|
44
|
+
export declare function loadAvailabilityRequestContext(readers: AvailabilityReaders & {
|
|
45
|
+
preferences: Pick<PreferencesFXOperator, "getPrefCompany">;
|
|
46
|
+
}, actor: RoutesActor, data: ChatData): Promise<AvailabilityFormContext>;
|
|
35
47
|
export declare function availabilitySubmission(reference: AvailabilityReference, request_id: string, ids: readonly string[]): ChatData;
|
|
36
|
-
export declare function loadAvailabilityContext(readers: AvailabilityReaders, actor: RoutesActor, reference: AvailabilityReference, signal?:
|
|
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[];
|
|
37
52
|
export declare function validateAvailabilityWindows(inputs: readonly InClientAvailability[], context: AvailabilityContext, minimum?: number): void;
|
|
38
53
|
export declare function loadAvailabilitySelection(readers: AvailabilityReaders & {
|
|
39
54
|
routes: Pick<RoutesOperator, "findOneClientAvailability">;
|
|
40
|
-
}, actor: RoutesActor, reference: AvailabilityReference, ids: readonly string[],
|
|
55
|
+
}, actor: RoutesActor, reference: AvailabilityReference, ids: readonly string[], options?: AvailabilityContextOptions): Promise<{
|
|
41
56
|
context: AvailabilityContext;
|
|
42
57
|
windows: ClientAvailability[];
|
|
43
58
|
}>;
|
|
59
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
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";
|
|
8
|
-
import { canSendChatForm, } from "../chat/forms.js";
|
|
8
|
+
import { canSendChatForm, readChatFormContext, } from "../chat/forms.js";
|
|
9
9
|
import { routeCanPlan } from "./workspace.js";
|
|
10
10
|
import { validateClientAvailabilityInput } from "./validation.js";
|
|
11
11
|
export const clientAvailabilityMinimumWindows = 3;
|
|
@@ -18,10 +18,37 @@ export const RoutesChatDataType = {
|
|
|
18
18
|
AvailabilityRequest: "routes.availability.request",
|
|
19
19
|
AvailabilitySubmitted: "routes.availability.submitted",
|
|
20
20
|
};
|
|
21
|
+
export const availabilityFormContextDefinition = {
|
|
22
|
+
version: 1,
|
|
23
|
+
read: (value) => {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
return;
|
|
26
|
+
const { working_hours, timezone } = value;
|
|
27
|
+
if (!working_hours ||
|
|
28
|
+
typeof working_hours !== "object" ||
|
|
29
|
+
Array.isArray(working_hours) ||
|
|
30
|
+
typeof timezone !== "string" ||
|
|
31
|
+
!timezone.trim())
|
|
32
|
+
return;
|
|
33
|
+
const { work_day_start_hour: start, work_day_end_hour: end } = working_hours;
|
|
34
|
+
if (typeof start !== "number" ||
|
|
35
|
+
typeof end !== "number" ||
|
|
36
|
+
![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
|
|
37
|
+
start >= end)
|
|
38
|
+
return;
|
|
39
|
+
return {
|
|
40
|
+
timezone,
|
|
41
|
+
working_hours: { work_day_start_hour: start, work_day_end_hour: end },
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
21
45
|
const msgAvailabilityAccess = "This availability does not match the client and employer of the location.";
|
|
22
46
|
const msgAvailabilityTimezone = "The location needs a valid saved address timezone.";
|
|
47
|
+
const msgAvailabilityTimezoneChanged = "The location timezone has changed. Ask the sender to send a new form.";
|
|
23
48
|
const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
|
|
24
|
-
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.";
|
|
25
52
|
const msgAvailabilityClient = "Visit times can only be requested from clients with a Fabriktor account. Arrange external contacts’ visit times directly.";
|
|
26
53
|
const msgAvailabilityData = "The availability request is invalid.";
|
|
27
54
|
export function availabilityReference(data) {
|
|
@@ -69,6 +96,30 @@ export function canSendAvailabilityRequest(data, actor, recipients) {
|
|
|
69
96
|
canSendChatForm(availabilityRequestPolicy, actor, recipients) &&
|
|
70
97
|
reference.client_id === recipients[0].id);
|
|
71
98
|
}
|
|
99
|
+
// Registry pre-action: only the employer preparing the request reads their
|
|
100
|
+
// preferences. Transfer the two hours, never the complete preferences record.
|
|
101
|
+
export async function loadAvailabilityRequestContext(readers, actor, data) {
|
|
102
|
+
const reference = availabilityReference(data);
|
|
103
|
+
if (data.type !== RoutesChatDataType.AvailabilityRequest ||
|
|
104
|
+
!reference ||
|
|
105
|
+
!routeCanPlan(actor) ||
|
|
106
|
+
reference.employer_id !== actor.owner_id)
|
|
107
|
+
throw errValidation({ msg: msgAvailabilityAccess });
|
|
108
|
+
const [context, company] = await Promise.all([
|
|
109
|
+
loadAvailabilityContext(readers, actor, reference),
|
|
110
|
+
readers.preferences.getPrefCompany({ owner_id: actor.owner_id }),
|
|
111
|
+
]);
|
|
112
|
+
const supplied = availabilityFormContextDefinition.read({
|
|
113
|
+
timezone: context.timezone,
|
|
114
|
+
working_hours: {
|
|
115
|
+
work_day_start_hour: company.work_day_start_hour,
|
|
116
|
+
work_day_end_hour: company.work_day_end_hour,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (!supplied)
|
|
120
|
+
throw errValidation({ msg: msgAvailabilityHours });
|
|
121
|
+
return supplied;
|
|
122
|
+
}
|
|
72
123
|
export function availabilitySubmission(reference, request_id, ids) {
|
|
73
124
|
const data = newChatData(RoutesChatDataType.AvailabilitySubmitted, {
|
|
74
125
|
...reference,
|
|
@@ -79,7 +130,19 @@ export function availabilitySubmission(reference, request_id, ids) {
|
|
|
79
130
|
throw errValidation({ msg: msgAvailabilityData });
|
|
80
131
|
return data;
|
|
81
132
|
}
|
|
82
|
-
export async function loadAvailabilityContext(readers, actor, reference, signal) {
|
|
133
|
+
export async function loadAvailabilityContext(readers, actor, reference, { request, signal } = {}) {
|
|
134
|
+
const supplied = request
|
|
135
|
+
? readChatFormContext(request, availabilityFormContextDefinition)
|
|
136
|
+
: undefined;
|
|
137
|
+
if (request) {
|
|
138
|
+
const requestReference = availabilityReference(request);
|
|
139
|
+
if (request.type !== RoutesChatDataType.AvailabilityRequest ||
|
|
140
|
+
!requestReference ||
|
|
141
|
+
requestReference.employer_id !== reference.employer_id ||
|
|
142
|
+
requestReference.client_id !== reference.client_id ||
|
|
143
|
+
requestReference.location_id !== reference.location_id)
|
|
144
|
+
throw errValidation({ msg: msgAvailabilityAccess });
|
|
145
|
+
}
|
|
83
146
|
const location = (await readers.jobs.findOneLocation({ id: reference.location_id, signal })).value;
|
|
84
147
|
if (!location ||
|
|
85
148
|
location.id !== reference.location_id ||
|
|
@@ -90,10 +153,9 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
|
|
|
90
153
|
? actor.owner_id !== reference.client_id
|
|
91
154
|
: !routeCanPlan(actor) || actor.owner_id !== reference.employer_id))
|
|
92
155
|
throw errValidation({ msg: msgAvailabilityAccess });
|
|
93
|
-
const [result, clientResult
|
|
156
|
+
const [result, clientResult] = await Promise.all([
|
|
94
157
|
readers.users.findOneUser({ id: reference.employer_id, signal }),
|
|
95
158
|
readers.users.findOneUser({ id: reference.client_id, signal }),
|
|
96
|
-
readers.preferences.getPrefCompany({ owner_id: reference.employer_id }),
|
|
97
159
|
]);
|
|
98
160
|
const employer = result.value;
|
|
99
161
|
if (!employer ||
|
|
@@ -108,41 +170,77 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
|
|
|
108
170
|
const timezone = addressTimezone(location);
|
|
109
171
|
if (!timezone)
|
|
110
172
|
throw errValidation({ msg: msgAvailabilityTimezone });
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
173
|
+
if (supplied && supplied.timezone !== timezone)
|
|
174
|
+
throw errValidation({ msg: msgAvailabilityTimezoneChanged });
|
|
175
|
+
return {
|
|
176
|
+
location,
|
|
177
|
+
employer,
|
|
178
|
+
client,
|
|
179
|
+
working_hours: supplied?.working_hours,
|
|
180
|
+
timezone,
|
|
181
|
+
};
|
|
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] : []));
|
|
116
210
|
}
|
|
117
211
|
export function validateAvailabilityWindows(inputs, context, minimum = clientAvailabilityMinimumWindows) {
|
|
118
212
|
if (inputs.length < minimum)
|
|
119
|
-
throw errValidation({
|
|
120
|
-
|
|
213
|
+
throw errValidation({
|
|
214
|
+
msg: msgAvailabilityMinimumWindows.replace("{count}", String(minimum)),
|
|
215
|
+
});
|
|
216
|
+
for (const input of inputs) {
|
|
121
217
|
validateClientAvailabilityInput(input);
|
|
122
218
|
if (input.employer_id !== context.employer.id ||
|
|
123
219
|
input.location_id !== context.location.id ||
|
|
124
|
-
input.timezone !== context.timezone
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
minutesOfDay(input.
|
|
128
|
-
context.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
minutesOfDay(input.from) < minutesOfDay(other.to) &&
|
|
133
|
-
minutesOfDay(input.to) > minutesOfDay(other.from)))
|
|
134
|
-
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 });
|
|
135
228
|
}
|
|
229
|
+
if (availabilityWindowConflicts(inputs).length)
|
|
230
|
+
throw errValidation({ msg: msgAvailabilityWindows });
|
|
136
231
|
}
|
|
137
232
|
// A chat response contains references, never authority or an appointment confirmation.
|
|
138
233
|
// Reload each chosen record before preparing the response, including after a resumed batch.
|
|
139
|
-
export async function loadAvailabilitySelection(readers, actor, reference, ids,
|
|
234
|
+
export async function loadAvailabilitySelection(readers, actor, reference, ids, options = {}) {
|
|
140
235
|
if (ids.length < clientAvailabilityMinimumWindows ||
|
|
141
236
|
normalizeIDs(ids).length !== ids.length)
|
|
142
237
|
throw errValidation({ msg: msgAvailabilityData });
|
|
143
|
-
const context = await loadAvailabilityContext(readers, actor, reference,
|
|
238
|
+
const context = await loadAvailabilityContext(readers, actor, reference, options);
|
|
144
239
|
const windows = await Promise.all(ids.map(async (id) => {
|
|
145
|
-
const window = (await readers.routes.findOneClientAvailability({
|
|
240
|
+
const window = (await readers.routes.findOneClientAvailability({
|
|
241
|
+
id,
|
|
242
|
+
signal: options.signal,
|
|
243
|
+
})).value;
|
|
146
244
|
if (!window ||
|
|
147
245
|
window.id !== id ||
|
|
148
246
|
window.owner_id !== reference.client_id)
|
|
@@ -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
|
}
|