@fabriktor/fx 0.0.139 → 0.0.141

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.
Files changed (37) hide show
  1. package/dist/src/chat/data.d.ts +23 -0
  2. package/dist/src/chat/data.js +76 -0
  3. package/dist/src/core/address.d.ts +2 -0
  4. package/dist/src/core/address.js +6 -0
  5. package/dist/src/core/date.d.ts +6 -0
  6. package/dist/src/core/date.js +27 -0
  7. package/dist/src/core/measurement.d.ts +15 -0
  8. package/dist/src/core/measurement.js +45 -0
  9. package/dist/src/core/server-clock.d.ts +10 -0
  10. package/dist/src/core/server-clock.js +36 -0
  11. package/dist/src/fx.d.ts +5 -1
  12. package/dist/src/fx.js +6 -1
  13. package/dist/src/index.d.ts +12 -0
  14. package/dist/src/index.js +12 -0
  15. package/dist/src/routes/arrival.d.ts +16 -0
  16. package/dist/src/routes/arrival.js +50 -0
  17. package/dist/src/routes/availability.d.ts +35 -0
  18. package/dist/src/routes/availability.js +132 -0
  19. package/dist/src/routes/departure.d.ts +45 -0
  20. package/dist/src/routes/departure.js +98 -0
  21. package/dist/src/routes/planning.d.ts +34 -0
  22. package/dist/src/routes/planning.js +94 -0
  23. package/dist/src/routes/punch.d.ts +9 -0
  24. package/dist/src/routes/punch.js +23 -0
  25. package/dist/src/routes/routes.d.ts +45 -12
  26. package/dist/src/routes/routes.js +223 -139
  27. package/dist/src/routes/tracking.d.ts +59 -0
  28. package/dist/src/routes/tracking.js +178 -0
  29. package/dist/src/routes/validation.d.ts +13 -0
  30. package/dist/src/routes/validation.js +227 -0
  31. package/dist/src/routes/workspace.d.ts +74 -0
  32. package/dist/src/routes/workspace.js +212 -0
  33. package/dist/src/timesheets/timesheets.d.ts +3 -1
  34. package/dist/src/timesheets/timesheets.js +3 -0
  35. package/dist/src/timesheets/workspace.d.ts +9 -1
  36. package/dist/src/timesheets/workspace.js +50 -8
  37. package/package.json +4 -3
@@ -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
+ }
@@ -0,0 +1,34 @@
1
+ import type { ClientAvailability, InDate, InRoute, Location, PrefCompany, RPRoutesAvailability, Route } from "@fabriktor/schema";
2
+ import { PreActionBlocked } from "../core/pre-action.js";
3
+ declare const routePreparationBlocked = "route_preparation";
4
+ export declare const RouteMutationBlock: {
5
+ readonly Busy: "route_mutation_busy";
6
+ readonly Changed: "route_mutation_changed";
7
+ readonly Unconfirmed: "route_mutation_unconfirmed";
8
+ };
9
+ export declare function routePlanChanged(current: Route, input: InRoute): boolean;
10
+ export declare class RoutePreparationBlocked extends PreActionBlocked<typeof routePreparationBlocked> {
11
+ readonly cause: unknown;
12
+ constructor(cause: unknown);
13
+ }
14
+ export declare const routePlanningDefaults: {
15
+ readonly days_ago: 0;
16
+ readonly duration_in_minutes: 0;
17
+ readonly consider_traffic: false;
18
+ readonly start_early: false;
19
+ readonly override_itinerary: false;
20
+ };
21
+ export declare function newRoutePlan(opts: {
22
+ day: InDate;
23
+ company: Pick<PrefCompany, "work_day_start_hour" | "work_day_end_hour">;
24
+ start_place_id: string;
25
+ }): InRoute;
26
+ export declare function routeDestination(location: Location, owner_id: string, timezone: string): InRoute["destinations"][number];
27
+ export declare function validateRouteEmployees(ids: readonly string[], allowed: readonly string[], occupied?: readonly string[]): void;
28
+ export type RouteAvailabilityWindow = {
29
+ availability: ClientAvailability;
30
+ start: number;
31
+ end: number;
32
+ };
33
+ export declare function routeAvailabilityWindows(input: Pick<InRoute, "from" | "to" | "timezone">, availability: RPRoutesAvailability, location_id: string, client_id: string): RouteAvailabilityWindow[];
34
+ export {};
@@ -0,0 +1,94 @@
1
+ import { isValidInputDate, minutesOfDay, sameCalendarDay, } from "../core/date.js";
2
+ import { errValidation } from "../core/err.js";
3
+ import { normalizeIDs } from "../core/id.js";
4
+ import { routeInput, validateRouteTimeWindow } from "./validation.js";
5
+ import { validateRoutePlanLocation } from "./departure.js";
6
+ import { PreActionBlocked } from "../core/pre-action.js";
7
+ const routePreparationBlocked = "route_preparation";
8
+ const msgPlanningEmployees = "Choose available employees who are not already in another vehicle.";
9
+ export const RouteMutationBlock = {
10
+ Busy: "route_mutation_busy",
11
+ Changed: "route_mutation_changed",
12
+ Unconfirmed: "route_mutation_unconfirmed",
13
+ };
14
+ // Compare only editable fields. Vehicle IDs and itineraries belong to Go.
15
+ export function routePlanChanged(current, input) {
16
+ return (input.override_itinerary ||
17
+ JSON.stringify({
18
+ ...routeInput(current),
19
+ override_itinerary: false,
20
+ }) !== JSON.stringify({ ...routeInput(input), override_itinerary: false }));
21
+ }
22
+ export class RoutePreparationBlocked extends PreActionBlocked {
23
+ cause;
24
+ constructor(cause) {
25
+ super(routePreparationBlocked);
26
+ this.cause = cause;
27
+ }
28
+ }
29
+ export const routePlanningDefaults = {
30
+ days_ago: 0,
31
+ duration_in_minutes: 0,
32
+ consider_traffic: false,
33
+ start_early: false,
34
+ override_itinerary: false,
35
+ };
36
+ export function newRoutePlan(opts) {
37
+ return {
38
+ timezone: opts.day.tz,
39
+ from: { ...opts.day, h: opts.company.work_day_start_hour, min: 0 },
40
+ to: { ...opts.day, h: opts.company.work_day_end_hour, min: 0 },
41
+ start_place_id: opts.start_place_id,
42
+ vehicles: [],
43
+ destinations: [],
44
+ consider_traffic: routePlanningDefaults.consider_traffic,
45
+ start_early: routePlanningDefaults.start_early,
46
+ override_itinerary: routePlanningDefaults.override_itinerary,
47
+ };
48
+ }
49
+ export function routeDestination(location, owner_id, timezone) {
50
+ validateRoutePlanLocation(location, owner_id, timezone);
51
+ return {
52
+ location_id: location.id,
53
+ client_id: location.client_id,
54
+ place_id: location.place_id,
55
+ tags: [],
56
+ duration_in_minutes: routePlanningDefaults.duration_in_minutes,
57
+ use_time_window: false,
58
+ time_window_start_hour_in_minutes: 0,
59
+ time_window_end_hour_in_minutes: 0,
60
+ };
61
+ }
62
+ export function validateRouteEmployees(ids, allowed, occupied = []) {
63
+ const available = new Set(allowed);
64
+ const used = new Set(occupied);
65
+ if (!ids.length ||
66
+ normalizeIDs(ids).length !== ids.length ||
67
+ ids.some((id) => !available.has(id) || used.has(id)))
68
+ throw errValidation({
69
+ field: "vehicles",
70
+ msg: msgPlanningEmployees,
71
+ });
72
+ }
73
+ // Suggestions are child data in an availability response. Intersect the client's
74
+ // window with the route window; never widen the hours offered by either party.
75
+ export function routeAvailabilityWindows(input, availability, location_id, client_id) {
76
+ validateRouteTimeWindow(input);
77
+ return availability.client_availabilities
78
+ .flatMap((group) => group.location_id === location_id ? group.availabilities : [])
79
+ .flatMap((window) => {
80
+ if (window.owner_id !== client_id ||
81
+ window.location_id !== location_id ||
82
+ window.timezone !== input.timezone ||
83
+ window.from.tz !== input.timezone ||
84
+ window.to.tz !== input.timezone ||
85
+ !isValidInputDate(window.from) ||
86
+ !isValidInputDate(window.to) ||
87
+ !sameCalendarDay(window.from, input.from) ||
88
+ !sameCalendarDay(window.to, input.to))
89
+ return [];
90
+ const start = Math.max(minutesOfDay(window.from), minutesOfDay(input.from));
91
+ const end = Math.min(minutesOfDay(window.to), minutesOfDay(input.to));
92
+ return start < end ? [{ availability: window, start, end }] : [];
93
+ });
94
+ }
@@ -0,0 +1,9 @@
1
+ import type { JobsOperator, RoutesOperator } from "@fabriktor/client";
2
+ import { type Location } from "@fabriktor/schema";
3
+ import { type RoutesActor } from "./workspace.js";
4
+ export declare function loadRoutePunchLocation(routes: RoutesOperator, jobs: JobsOperator, opts: {
5
+ actor: RoutesActor;
6
+ route_id: string;
7
+ vehicle_id: string;
8
+ location_id: string;
9
+ }): Promise<Location>;
@@ -0,0 +1,23 @@
1
+ import { UserTypeEmployee } from "@fabriktor/schema";
2
+ import { errValidation } from "../core/err.js";
3
+ import { routeCanTrack } from "./workspace.js";
4
+ const msgRoutePunch = "This destination is no longer available on the assigned route.";
5
+ export async function loadRoutePunchLocation(routes, jobs, opts) {
6
+ const route = (await routes.findOneRoute({ id: opts.route_id })).value;
7
+ if (!route ||
8
+ opts.actor.active_mode !== UserTypeEmployee ||
9
+ !routeCanTrack(opts.actor, route, opts.vehicle_id, opts.actor.owner_id) ||
10
+ !route.itinerary.routes
11
+ .find((v) => v.vehicle_id === opts.vehicle_id)
12
+ ?.stops.some((s) => s.location_id === opts.location_id))
13
+ throw errValidation({ msg: msgRoutePunch });
14
+ const location = (await jobs.findOneLocation({ id: opts.location_id })).value;
15
+ const destination = route.destinations.find((value) => value.location_id === opts.location_id);
16
+ if (!location ||
17
+ location.is_archived ||
18
+ location.owner_id !== route.owner_id ||
19
+ !destination ||
20
+ location.place_id !== destination.place_id)
21
+ throw errValidation({ msg: msgRoutePunch });
22
+ return location;
23
+ }
@@ -1,5 +1,8 @@
1
+ import { type RouteDepartureReference, type RoutePlanningReaders } from "./departure.js";
1
2
  import type { RoutesOperator } from "@fabriktor/client";
2
- import type { ClientAvailability, Route, RouteTeam, Tag, Tracking } from "@fabriktor/schema";
3
+ import type { ClientAvailability, InRoute, Route, RouteTeam, RPRoutesAvailability, Tag, Tracking } from "@fabriktor/schema";
4
+ import { RoutesWorkspace, type PrepareRouteOptions, type RoutesActor } from "./workspace.js";
5
+ import type { MultiCreateCheckpoint, MultiCreateAttempt } from "../core/multi-create.js";
3
6
  import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
4
7
  type RouteInsertOptions = Parameters<RoutesOperator["insertOneRoute"]>[0];
5
8
  type RouteReplaceOptions = Parameters<RoutesOperator["replaceOneRoute"]>[0];
@@ -11,34 +14,55 @@ type TrackingInsertOptions = Parameters<RoutesOperator["insertOneTracking"]>[0];
11
14
  type TrackingReplaceOptions = Parameters<RoutesOperator["replaceOneTracking"]>[0];
12
15
  type RouteTeamInsertOptions = Parameters<RoutesOperator["insertOneRouteTeam"]>[0];
13
16
  type RouteTeamReplaceOptions = Parameters<RoutesOperator["replaceOneRouteTeam"]>[0];
14
- type RouteInsert = RouteInsertOptions["in"];
15
17
  type RouteAvailability = NonNullable<Awaited<ReturnType<RoutesOperator["getAvailability"]>>["value"]>;
16
- export type CreateRouteOptions = {
17
- in: RouteInsert;
18
+ export type CreateRouteOptions = RouteInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created"> & {
18
19
  availability: RouteAvailability;
19
20
  };
20
21
  export type CreateValidatedRouteResult = Awaited<ReturnType<RoutesOperator["insertOneRoute"]>>;
21
22
  export type CreateRouteResult = CreateValidatedRouteResult;
22
- export type ReplaceRouteOptions = RouteReplaceOptions;
23
- export type CreateTagOptions = TagInsertOptions;
23
+ export type CreatePlannedRouteOptions = Omit<PrepareRouteOptions, "request"> & {
24
+ attempt: MultiCreateAttempt;
25
+ departure: RouteDepartureReference;
26
+ };
27
+ export type ReplaceRouteOptions = RouteReplaceOptions & {
28
+ availability?: RPRoutesAvailability;
29
+ expected?: Route;
30
+ before_request?: () => void;
31
+ };
32
+ export type RouteMutationOptions = {
33
+ actor: RoutesActor;
34
+ expected: Route;
35
+ signal?: AbortSignal;
36
+ before_request?: () => void;
37
+ };
38
+ export type ReplacePlannedRouteOptions = RouteMutationOptions & {
39
+ in: InRoute;
40
+ days_ago: PrepareRouteOptions["days_ago"];
41
+ departure: RouteDepartureReference;
42
+ };
43
+ export type CreateTagOptions = TagInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
24
44
  export type ReplaceTagOptions = TagReplaceOptions;
25
45
  export type SearchTagsOptions = OwnerSearchOptions;
26
46
  export type SearchTagsPage = SearchPage<Tag>;
27
- export type CreateClientAvailabilityOptions = ClientAvailabilityInsertOptions;
28
- export type ReplaceClientAvailabilityOptions = ClientAvailabilityReplaceOptions;
29
- export type CreateTrackingOptions = TrackingInsertOptions;
30
- export type ReplaceTrackingOptions = TrackingReplaceOptions;
31
- export type CreateRouteTeamOptions = RouteTeamInsertOptions;
47
+ export type CreateClientAvailabilityOptions = ClientAvailabilityInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
48
+ export type ReplaceClientAvailabilityOptions = ClientAvailabilityReplaceOptions & Pick<MultiCreateCheckpoint, "before_request">;
49
+ export type CreateTrackingOptions = TrackingInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
50
+ export type ReplaceTrackingOptions = TrackingReplaceOptions & Pick<MultiCreateCheckpoint, "before_request">;
51
+ export type CreateRouteTeamOptions = RouteTeamInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
32
52
  export type ReplaceRouteTeamOptions = RouteTeamReplaceOptions;
33
53
  export type SearchRouteTeamsOptions = OwnerSearchOptions;
34
54
  export type SearchRouteTeamsPage = SearchPage<RouteTeam>;
35
- export type RoutesFXOptions = {
55
+ export type RoutesFXOptions = RoutePlanningReaders & {
36
56
  routes: RoutesOperator;
37
57
  };
38
58
  export interface RoutesFXOperator {
59
+ readonly workspace: RoutesWorkspace;
39
60
  createRoute(opts: CreateRouteOptions): Promise<Route>;
61
+ createPlannedRoute(opts: CreatePlannedRouteOptions): Promise<Route>;
40
62
  createValidatedRoute(opts: CreateRouteOptions): Promise<CreateValidatedRouteResult>;
41
63
  replaceRoute(opts: ReplaceRouteOptions): Promise<Route>;
64
+ replacePlannedRoute(opts: ReplacePlannedRouteOptions): Promise<Route>;
65
+ deletePlannedRoute(opts: RouteMutationOptions): Promise<void>;
42
66
  createTag(opts: CreateTagOptions): Promise<Tag>;
43
67
  replaceTag(opts: ReplaceTagOptions): Promise<Tag>;
44
68
  searchTags(opts: SearchTagsOptions): Promise<SearchTagsPage>;
@@ -51,11 +75,20 @@ export interface RoutesFXOperator {
51
75
  searchRouteTeams(opts: SearchRouteTeamsOptions): Promise<SearchRouteTeamsPage>;
52
76
  }
53
77
  export declare class RoutesFX implements RoutesFXOperator {
78
+ private readonly opts;
54
79
  private routes;
80
+ private routeMutationPending;
81
+ readonly workspace: RoutesWorkspace;
55
82
  constructor(opts: RoutesFXOptions);
56
83
  createRoute(opts: CreateRouteOptions): Promise<Route>;
84
+ createPlannedRoute(opts: CreatePlannedRouteOptions): Promise<Route>;
57
85
  createValidatedRoute(opts: CreateRouteOptions): Promise<CreateValidatedRouteResult>;
58
86
  replaceRoute(opts: ReplaceRouteOptions): Promise<Route>;
87
+ replacePlannedRoute(opts: ReplacePlannedRouteOptions): Promise<Route>;
88
+ deletePlannedRoute(opts: RouteMutationOptions): Promise<void>;
89
+ private checkedRoute;
90
+ private requireCurrentRoute;
91
+ private withRouteMutation;
59
92
  createTag(opts: CreateTagOptions): Promise<Tag>;
60
93
  replaceTag(opts: ReplaceTagOptions): Promise<Tag>;
61
94
  searchTags(opts: SearchTagsOptions): Promise<SearchTagsPage>;