@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.
- package/dist/src/core/address.d.ts +2 -0
- package/dist/src/core/address.js +6 -0
- package/dist/src/core/measurement.d.ts +15 -0
- package/dist/src/core/measurement.js +45 -0
- package/dist/src/fx.js +3 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/routes/availability.js +5 -4
- package/dist/src/routes/departure.d.ts +45 -0
- package/dist/src/routes/departure.js +98 -0
- package/dist/src/routes/planning.d.ts +1 -1
- package/dist/src/routes/planning.js +4 -11
- package/dist/src/routes/routes.d.ts +5 -1
- package/dist/src/routes/routes.js +23 -1
- package/dist/src/routes/validation.d.ts +1 -0
- package/dist/src/routes/validation.js +26 -2
- package/dist/src/routes/workspace.js +2 -1
- package/package.json +3 -2
|
@@ -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
package/dist/src/index.d.ts
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";
|
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 {
|
|
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
|
|
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 =
|
|
86
|
-
if (!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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
169
|
-
//
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.142",
|
|
4
4
|
"description": "",
|
|
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
|
}
|