@go-avro/avro-js 0.0.45 → 0.0.46

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.
@@ -35,6 +35,21 @@ export declare class Job {
35
35
  getOverdueLabel: () => string;
36
36
  getStatus(): "PENDING_CUSTOMER" | "PENDING_COMPANY" | "ACTIVE" | "ARCHIVED" | "DRAFT" | "PENDING_PAYMENT" | "PENDING_ACTIVATION";
37
37
  getStatusLabel(): "Active" | "Draft" | "Archived" | "Pending Activation" | "Customer Approval" | "Company Approval" | "Awaiting Payment";
38
+ /**
39
+ * Returns the sum of `service` (in seconds) for the tasks attached to a
40
+ * given routeJob. Use this to compute predicted completion time:
41
+ * `routeJob.getPredictedArrival() + job.getServiceDuration(routeJob)`.
42
+ */
43
+ getServiceDuration: (routeJob: RouteJob) => number;
44
+ /**
45
+ * Returns the most relevant event for the job in its current state:
46
+ * the active `current_event` if one exists, otherwise the
47
+ * `last_completed_event`. Falls back to `null`.
48
+ *
49
+ * Most callers historically used `last_completed_event ?? current_event`
50
+ * which inverted the precedence — prefer the live event over a stale one.
51
+ */
52
+ getRouteEvent: () => _Event | null;
38
53
  portionDone: (route: Route) => number;
39
54
  isDone: (route: Route) => boolean;
40
55
  }
@@ -21,6 +21,27 @@ export class Job {
21
21
  }, activeTasks[0]);
22
22
  return mostOverdueTask.getOverdueLabel?.() ?? 'N/A';
23
23
  };
24
+ /**
25
+ * Returns the sum of `service` (in seconds) for the tasks attached to a
26
+ * given routeJob. Use this to compute predicted completion time:
27
+ * `routeJob.getPredictedArrival() + job.getServiceDuration(routeJob)`.
28
+ */
29
+ this.getServiceDuration = (routeJob) => {
30
+ return routeJob.tasks
31
+ .map((tid) => this.tasks.find((t) => t.id === tid)?.service ?? 0)
32
+ .reduce((a, b) => a + b, 0);
33
+ };
34
+ /**
35
+ * Returns the most relevant event for the job in its current state:
36
+ * the active `current_event` if one exists, otherwise the
37
+ * `last_completed_event`. Falls back to `null`.
38
+ *
39
+ * Most callers historically used `last_completed_event ?? current_event`
40
+ * which inverted the precedence — prefer the live event over a stale one.
41
+ */
42
+ this.getRouteEvent = () => {
43
+ return this.current_event ?? this.last_completed_event ?? null;
44
+ };
24
45
  this.portionDone = (route) => {
25
46
  if (!this.tasks || this.tasks.length === 0) {
26
47
  return 0;
@@ -1,4 +1,5 @@
1
1
  import { RouteJob } from '../../types/api/RouteJob';
2
+ import type { Job } from '../../types/api/Job';
2
3
  export declare const FrequencyType: {
3
4
  readonly ONCE: "ONCE";
4
5
  readonly DAILY: "DAILY";
@@ -33,4 +34,16 @@ export declare class Route {
33
34
  constructor(init?: Partial<Route>);
34
35
  getNextOccurrences: (count?: number) => (Date | null)[];
35
36
  getNextOccurrence: (after?: Date) => Date | null;
37
+ /**
38
+ * Computes how far ahead/behind schedule the route is, in seconds, based on
39
+ * stops that have already been completed. Positive = behind schedule,
40
+ * negative = ahead of schedule.
41
+ *
42
+ * The offset is taken from the most recently completed stop (largest
43
+ * `time_ended`), not the last stop in route order. This is what consumers
44
+ * want: "given the latest completed work, how off-schedule are we?".
45
+ *
46
+ * Returns `null` if no stop in this route has a usable completion event.
47
+ */
48
+ getScheduleOffset: (jobs: Job[]) => number | null;
36
49
  }
@@ -43,6 +43,38 @@ export class Route {
43
43
  }
44
44
  return next_occurrence;
45
45
  };
46
+ /**
47
+ * Computes how far ahead/behind schedule the route is, in seconds, based on
48
+ * stops that have already been completed. Positive = behind schedule,
49
+ * negative = ahead of schedule.
50
+ *
51
+ * The offset is taken from the most recently completed stop (largest
52
+ * `time_ended`), not the last stop in route order. This is what consumers
53
+ * want: "given the latest completed work, how off-schedule are we?".
54
+ *
55
+ * Returns `null` if no stop in this route has a usable completion event.
56
+ */
57
+ this.getScheduleOffset = (jobs) => {
58
+ let latestEnd = -Infinity;
59
+ let offset = null;
60
+ for (const rj of this.jobs) {
61
+ const job = jobs.find((j) => j.id === rj.job_id);
62
+ if (!job || !job.isDone(this))
63
+ continue;
64
+ // Offset semantics specifically want a *completed* event, not an
65
+ // in-progress one. Use last_completed_event directly rather than
66
+ // getRouteEvent() (which prefers current_event for display).
67
+ const actualEnd = job.last_completed_event?.time_ended;
68
+ if (typeof actualEnd !== 'number' || actualEnd <= 0)
69
+ continue;
70
+ if (actualEnd <= latestEnd)
71
+ continue;
72
+ const predictedCompletion = rj.getPredictedArrival() + job.getServiceDuration(rj);
73
+ latestEnd = actualEnd;
74
+ offset = actualEnd - predictedCompletion;
75
+ }
76
+ return offset;
77
+ };
46
78
  Object.assign(this, init);
47
79
  if (init?.jobs) {
48
80
  this.jobs = init.jobs.map((j) => new RouteJob(j));
@@ -12,4 +12,11 @@ declare module '../../types/api/RouteJob' {
12
12
  }
13
13
  export declare class RouteJob {
14
14
  constructor(init?: Partial<RouteJob>);
15
+ /**
16
+ * Returns the arrival time the UI should show for this stop:
17
+ * `scheduled_arrival_time` if set (>= 0), otherwise the optimizer's
18
+ * `estimated_arrival_time`. The sentinel value -1 means "no manual
19
+ * schedule, fall back to the optimizer estimate".
20
+ */
21
+ getPredictedArrival: () => number;
15
22
  }
@@ -1,5 +1,16 @@
1
1
  export class RouteJob {
2
2
  constructor(init) {
3
+ /**
4
+ * Returns the arrival time the UI should show for this stop:
5
+ * `scheduled_arrival_time` if set (>= 0), otherwise the optimizer's
6
+ * `estimated_arrival_time`. The sentinel value -1 means "no manual
7
+ * schedule, fall back to the optimizer estimate".
8
+ */
9
+ this.getPredictedArrival = () => {
10
+ return this.scheduled_arrival_time === -1
11
+ ? this.estimated_arrival_time
12
+ : this.scheduled_arrival_time;
13
+ };
3
14
  Object.assign(this, init);
4
15
  }
5
16
  }
@@ -102,12 +102,15 @@ export class Task {
102
102
  if (!this.status || this.status !== TaskStatus.ACTIVE) {
103
103
  return true;
104
104
  }
105
- if (this.frequency > 0) {
106
- return (this.last_completed_event?.time_ended ?? 0) > route.start_time;
107
- }
108
- else {
109
- return this.last_completed_event?.time_ended ? true : false;
110
- }
105
+ // Cutoff: midnight (local time) of the day route.start_time falls on.
106
+ // Counts work completed at any point during the route's day (including
107
+ // an early-start autostart that finishes a few minutes before
108
+ // route.start_time) while still rejecting stale events from previous
109
+ // occurrences. Applies to both recurring and one-shot tasks.
110
+ const routeDay = new Date(route.start_time * 1000);
111
+ routeDay.setHours(0, 0, 0, 0);
112
+ const cutoff = Math.floor(routeDay.getTime() / 1000);
113
+ return (this.last_completed_event?.time_ended ?? 0) >= cutoff;
111
114
  };
112
115
  Object.assign(this, init);
113
116
  if (init?.prepayments) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-avro/avro-js",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "description": "JS client for Avro backend integration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",