@openinc/parse-server-opendash 4.1.8 → 4.2.0
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/features/openservice/init.js +4 -0
- package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +39 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +310 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +114 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
- package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
- package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
- package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
- package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
- package/dist/hooks/Service_Schedule.js +54 -7
- package/dist/types/Service_Schedule.d.ts +6 -0
- package/dist/types/Service_Schedule.js +12 -0
- package/dist/types/Service_Schedule_Template.d.ts +3 -0
- package/dist/types/Service_Schedule_Template.js +6 -0
- package/package.json +1 -1
- package/schema/Service_Schedule.json +9 -0
- package/schema/Service_Schedule_Template.json +5 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.planToEvent = planToEvent;
|
|
7
|
+
const dayjs_1 = __importDefault(require("dayjs"));
|
|
8
|
+
const outlookRecurrence_js_1 = require("./outlookRecurrence.js");
|
|
9
|
+
/**
|
|
10
|
+
* A maintenance plan as a calendar event.
|
|
11
|
+
*
|
|
12
|
+
* Returns `undefined` for a plan that has no date to put in a calendar — a
|
|
13
|
+
* manual, sensor- or meter-driven plan has no due date until something happens,
|
|
14
|
+
* and inventing one would show maintenance that nobody scheduled.
|
|
15
|
+
*/
|
|
16
|
+
function planToEvent(plan) {
|
|
17
|
+
const start = cadenceStart(plan.cadence);
|
|
18
|
+
if (!start)
|
|
19
|
+
return undefined;
|
|
20
|
+
const timed = timeOfDay(plan, start);
|
|
21
|
+
const event = {
|
|
22
|
+
subject: eventSubject(plan),
|
|
23
|
+
body: { contentType: "Text", content: stepsAsText(plan.steps) },
|
|
24
|
+
start: graphStart(start, timed),
|
|
25
|
+
end: graphEnd(start, timed),
|
|
26
|
+
isAllDay: timed ? undefined : true,
|
|
27
|
+
isReminderOn: false,
|
|
28
|
+
};
|
|
29
|
+
const reminder = reminderMinutes(plan.notifications);
|
|
30
|
+
if (reminder !== undefined) {
|
|
31
|
+
event.isReminderOn = true;
|
|
32
|
+
event.reminderMinutesBeforeStart = reminder;
|
|
33
|
+
}
|
|
34
|
+
const recurrence = (0, outlookRecurrence_js_1.cadenceToRecurrence)(plan.cadence, start);
|
|
35
|
+
if (recurrence)
|
|
36
|
+
event.recurrence = recurrence;
|
|
37
|
+
return event;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The instant the series starts.
|
|
41
|
+
*
|
|
42
|
+
* Only an RRULE cadence has one. `until` is not consulted here: it bounds the
|
|
43
|
+
* series (the recurrence range does that), it does not start it.
|
|
44
|
+
*/
|
|
45
|
+
function cadenceStart(cadence) {
|
|
46
|
+
if (cadence?.kind !== "rrule" || !cadence.dtstart)
|
|
47
|
+
return undefined;
|
|
48
|
+
const start = new Date(cadence.dtstart);
|
|
49
|
+
return Number.isNaN(start.getTime()) ? undefined : start;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The time of day the event should keep, as `{hour, minute}` — or `undefined`
|
|
53
|
+
* for an all-day event.
|
|
54
|
+
*
|
|
55
|
+
* Two sources, in this order:
|
|
56
|
+
*
|
|
57
|
+
* 1. the plan's own `dtstart`, if it names a time (not UTC midnight) — the plan
|
|
58
|
+
* is the leading side for anything the plan can express;
|
|
59
|
+
* 2. otherwise the time the **event** has in Outlook, so a user who dragged a
|
|
60
|
+
* maintenance appointment to 9:00 keeps it. A plan whose due date shifts
|
|
61
|
+
* then moves the appointment to the new *day* at the same time — which is
|
|
62
|
+
* where v1 went wrong: it kept the whole original start and quietly ignored
|
|
63
|
+
* the new date.
|
|
64
|
+
*/
|
|
65
|
+
function timeOfDay(plan, start) {
|
|
66
|
+
if (start.getUTCHours() !== 0 || start.getUTCMinutes() !== 0) {
|
|
67
|
+
return { hour: start.getUTCHours(), minute: start.getUTCMinutes() };
|
|
68
|
+
}
|
|
69
|
+
const snapshot = plan.snapshot;
|
|
70
|
+
if (!snapshot || snapshot.isAllDay || !snapshot.start?.dateTime) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const previous = new Date(snapshot.start.dateTime.endsWith("Z")
|
|
74
|
+
? snapshot.start.dateTime
|
|
75
|
+
: `${snapshot.start.dateTime.slice(0, 23)}Z`);
|
|
76
|
+
if (Number.isNaN(previous.getTime()))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (previous.getUTCHours() === 0 && previous.getUTCMinutes() === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return { hour: previous.getUTCHours(), minute: previous.getUTCMinutes() };
|
|
82
|
+
}
|
|
83
|
+
function graphStart(start, timed) {
|
|
84
|
+
if (!timed) {
|
|
85
|
+
return { dateTime: dateOnly(start), timeZone: "UTC" };
|
|
86
|
+
}
|
|
87
|
+
return { dateTime: atTime(start, timed), timeZone: "UTC" };
|
|
88
|
+
}
|
|
89
|
+
function graphEnd(start, timed) {
|
|
90
|
+
if (!timed) {
|
|
91
|
+
// An all-day event ends on the following day — Graph's half-open interval.
|
|
92
|
+
return {
|
|
93
|
+
dateTime: dateOnly((0, dayjs_1.default)(start).add(1, "day").toDate()),
|
|
94
|
+
timeZone: "UTC",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const end = (0, dayjs_1.default)(atTime(start, timed)).add(1, "hour");
|
|
98
|
+
return { dateTime: end.format("YYYY-MM-DDTHH:mm:ss"), timeZone: "UTC" };
|
|
99
|
+
}
|
|
100
|
+
function dateOnly(date) {
|
|
101
|
+
return date.toISOString().slice(0, 10);
|
|
102
|
+
}
|
|
103
|
+
function atTime(date, timed) {
|
|
104
|
+
return `${dateOnly(date)}T${pad(timed.hour)}:${pad(timed.minute)}:00`;
|
|
105
|
+
}
|
|
106
|
+
function pad(value) {
|
|
107
|
+
return String(value).padStart(2, "0");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `"Flex 1" - Ölwechsel` — the convention the inbound direction reads back
|
|
111
|
+
* ([`parseSubject`](./eventToPlan.ts)), so a plan created here and edited in
|
|
112
|
+
* Outlook keeps its machine.
|
|
113
|
+
*/
|
|
114
|
+
function eventSubject(plan) {
|
|
115
|
+
const title = plan.title?.trim() || "Wartung";
|
|
116
|
+
return plan.equipmentName ? `"${plan.equipmentName}" - ${title}` : title;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The checklist as the event's note.
|
|
120
|
+
*
|
|
121
|
+
* The point of the appointment is that someone reads it in Outlook and knows
|
|
122
|
+
* what to bring — a title alone rarely says that.
|
|
123
|
+
*/
|
|
124
|
+
function stepsAsText(steps) {
|
|
125
|
+
const lines = [...(steps ?? [])]
|
|
126
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
127
|
+
.map((step) => step.title?.trim())
|
|
128
|
+
.filter((title) => !!title)
|
|
129
|
+
.map((title) => `• ${title}`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The plan's earliest warning as Outlook reminder minutes.
|
|
134
|
+
*
|
|
135
|
+
* A plan can warn several groups at different times; a calendar event has one
|
|
136
|
+
* reminder. The earliest one is the useful one — it is the moment the work
|
|
137
|
+
* first needs attention. `0` (reminder on the due date) is deliberate, not
|
|
138
|
+
* "no reminder": `undefined` means the plan warns nobody.
|
|
139
|
+
*/
|
|
140
|
+
function reminderMinutes(notifications) {
|
|
141
|
+
if (!notifications?.length)
|
|
142
|
+
return undefined;
|
|
143
|
+
const offsets = notifications.map((rule) => rule.trigger === "before" ? Math.max(0, rule.offsetDays ?? 0) : 0);
|
|
144
|
+
return Math.max(...offsets) * 24 * 60;
|
|
145
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { eventToPlanFields, mergeReminderRule, OUTLOOK_REMINDER_RULE_ID, parseSubject, } from "./functions/eventToPlan.js";
|
|
2
|
+
export { buildRRule, cadenceToRecurrence, eventToCadence, parseRRule, } from "./functions/outlookRecurrence.js";
|
|
3
|
+
export { planToEvent } from "./functions/planToEvent.js";
|
|
4
|
+
export { findMetaForEvent, findMetaForPlan, SERVICE_CALENDAR_SYNC_CONTEXT, ServiceCalendarMetaConfig, } from "./service/serviceCalendarMeta.js";
|
|
5
|
+
export { CALENDAR_SYNC_CONTEXT_FLAG, ServiceCalendarSyncPoller, } from "./service/ServiceCalendarSyncPoller.js";
|
|
6
|
+
export { connectionForTenant, syncPlanToCalendar, } from "./service/syncPlanToCalendar.js";
|
|
7
|
+
export { OUTLOOK_CONFIG_KEY } from "./types/ServiceSyncTypes.js";
|
|
8
|
+
export type { ServiceCadence, ServiceNotificationRule, ServiceSyncConfigValues, } from "./types/ServiceSyncTypes.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OUTLOOK_CONFIG_KEY = exports.syncPlanToCalendar = exports.connectionForTenant = exports.ServiceCalendarSyncPoller = exports.CALENDAR_SYNC_CONTEXT_FLAG = exports.ServiceCalendarMetaConfig = exports.SERVICE_CALENDAR_SYNC_CONTEXT = exports.findMetaForPlan = exports.findMetaForEvent = exports.planToEvent = exports.parseRRule = exports.eventToCadence = exports.cadenceToRecurrence = exports.buildRRule = exports.parseSubject = exports.OUTLOOK_REMINDER_RULE_ID = exports.mergeReminderRule = exports.eventToPlanFields = void 0;
|
|
4
|
+
var eventToPlan_js_1 = require("./functions/eventToPlan.js");
|
|
5
|
+
Object.defineProperty(exports, "eventToPlanFields", { enumerable: true, get: function () { return eventToPlan_js_1.eventToPlanFields; } });
|
|
6
|
+
Object.defineProperty(exports, "mergeReminderRule", { enumerable: true, get: function () { return eventToPlan_js_1.mergeReminderRule; } });
|
|
7
|
+
Object.defineProperty(exports, "OUTLOOK_REMINDER_RULE_ID", { enumerable: true, get: function () { return eventToPlan_js_1.OUTLOOK_REMINDER_RULE_ID; } });
|
|
8
|
+
Object.defineProperty(exports, "parseSubject", { enumerable: true, get: function () { return eventToPlan_js_1.parseSubject; } });
|
|
9
|
+
var outlookRecurrence_js_1 = require("./functions/outlookRecurrence.js");
|
|
10
|
+
Object.defineProperty(exports, "buildRRule", { enumerable: true, get: function () { return outlookRecurrence_js_1.buildRRule; } });
|
|
11
|
+
Object.defineProperty(exports, "cadenceToRecurrence", { enumerable: true, get: function () { return outlookRecurrence_js_1.cadenceToRecurrence; } });
|
|
12
|
+
Object.defineProperty(exports, "eventToCadence", { enumerable: true, get: function () { return outlookRecurrence_js_1.eventToCadence; } });
|
|
13
|
+
Object.defineProperty(exports, "parseRRule", { enumerable: true, get: function () { return outlookRecurrence_js_1.parseRRule; } });
|
|
14
|
+
var planToEvent_js_1 = require("./functions/planToEvent.js");
|
|
15
|
+
Object.defineProperty(exports, "planToEvent", { enumerable: true, get: function () { return planToEvent_js_1.planToEvent; } });
|
|
16
|
+
var serviceCalendarMeta_js_1 = require("./service/serviceCalendarMeta.js");
|
|
17
|
+
Object.defineProperty(exports, "findMetaForEvent", { enumerable: true, get: function () { return serviceCalendarMeta_js_1.findMetaForEvent; } });
|
|
18
|
+
Object.defineProperty(exports, "findMetaForPlan", { enumerable: true, get: function () { return serviceCalendarMeta_js_1.findMetaForPlan; } });
|
|
19
|
+
Object.defineProperty(exports, "SERVICE_CALENDAR_SYNC_CONTEXT", { enumerable: true, get: function () { return serviceCalendarMeta_js_1.SERVICE_CALENDAR_SYNC_CONTEXT; } });
|
|
20
|
+
Object.defineProperty(exports, "ServiceCalendarMetaConfig", { enumerable: true, get: function () { return serviceCalendarMeta_js_1.ServiceCalendarMetaConfig; } });
|
|
21
|
+
var ServiceCalendarSyncPoller_js_1 = require("./service/ServiceCalendarSyncPoller.js");
|
|
22
|
+
Object.defineProperty(exports, "CALENDAR_SYNC_CONTEXT_FLAG", { enumerable: true, get: function () { return ServiceCalendarSyncPoller_js_1.CALENDAR_SYNC_CONTEXT_FLAG; } });
|
|
23
|
+
Object.defineProperty(exports, "ServiceCalendarSyncPoller", { enumerable: true, get: function () { return ServiceCalendarSyncPoller_js_1.ServiceCalendarSyncPoller; } });
|
|
24
|
+
var syncPlanToCalendar_js_1 = require("./service/syncPlanToCalendar.js");
|
|
25
|
+
Object.defineProperty(exports, "connectionForTenant", { enumerable: true, get: function () { return syncPlanToCalendar_js_1.connectionForTenant; } });
|
|
26
|
+
Object.defineProperty(exports, "syncPlanToCalendar", { enumerable: true, get: function () { return syncPlanToCalendar_js_1.syncPlanToCalendar; } });
|
|
27
|
+
var ServiceSyncTypes_js_1 = require("./types/ServiceSyncTypes.js");
|
|
28
|
+
Object.defineProperty(exports, "OUTLOOK_CONFIG_KEY", { enumerable: true, get: function () { return ServiceSyncTypes_js_1.OUTLOOK_CONFIG_KEY; } });
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Config } from "../../../../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Marker on a save that came from the calendar.
|
|
4
|
+
*
|
|
5
|
+
* The outbound side (`syncPlanToCalendar`, called from the `Service_Schedule`
|
|
6
|
+
* afterSave hook) checks it and stays quiet — otherwise every inbound change
|
|
7
|
+
* would be written straight back to Outlook, produce a new `changeKey`, come
|
|
8
|
+
* back through the delta feed as a foreign change, and loop forever. The stored
|
|
9
|
+
* `changeKey` is the second line of defence; this is the first, and unlike v1's
|
|
10
|
+
* `!master` check it does not disable outbound sync for every other
|
|
11
|
+
* master-key writer (the scheduler pausing a plan, a migration, a script).
|
|
12
|
+
*/
|
|
13
|
+
export declare const CALENDAR_SYNC_CONTEXT_FLAG = "fromCalendarSync";
|
|
14
|
+
/**
|
|
15
|
+
* Pulls Outlook changes into open.SERVICE **v2** maintenance plans.
|
|
16
|
+
*
|
|
17
|
+
* One poller per configured connection, driven by the Graph *delta* feed: the
|
|
18
|
+
* first call reads the whole calendar, each later call only what changed, and
|
|
19
|
+
* the cursor lives in the Config row so a restart resumes instead of
|
|
20
|
+
* re-importing. The v1 poller does the same for `Maintenance_Schedule` from the
|
|
21
|
+
* same connection — the two keep separate cursors (`delta_link` vs
|
|
22
|
+
* `delta_link_v2`) so neither consumes the other's changes.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ServiceCalendarSyncPoller {
|
|
25
|
+
private timer;
|
|
26
|
+
private config;
|
|
27
|
+
constructor(config: Config);
|
|
28
|
+
private get values();
|
|
29
|
+
private get manager();
|
|
30
|
+
start(intervalMs?: number): this;
|
|
31
|
+
stop(): void;
|
|
32
|
+
private poll;
|
|
33
|
+
private processEvent;
|
|
34
|
+
private createPlan;
|
|
35
|
+
private updatePlan;
|
|
36
|
+
/**
|
|
37
|
+
* A deleted appointment archives the plan — it never destroys it.
|
|
38
|
+
*
|
|
39
|
+
* Executions document that work happened; deleting the plan they point at
|
|
40
|
+
* would strand that history (the `beforeDelete` hook refuses it, too). The
|
|
41
|
+
* meta entry goes, so the same plan can be linked to a new event later.
|
|
42
|
+
*/
|
|
43
|
+
private handleRemoved;
|
|
44
|
+
/** The machine named in the subject, else the configured fallback. */
|
|
45
|
+
private resolveEquipment;
|
|
46
|
+
private findEquipmentByLabel;
|
|
47
|
+
/**
|
|
48
|
+
* The cursor is re-read from the Config row on every tick, and written back
|
|
49
|
+
* after it: the row is also the admin form's storage, so it can change
|
|
50
|
+
* underneath us.
|
|
51
|
+
*/
|
|
52
|
+
private loadDeltaLink;
|
|
53
|
+
private saveDeltaLink;
|
|
54
|
+
}
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ServiceCalendarSyncPoller = exports.CALENDAR_SYNC_CONTEXT_FLAG = void 0;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
9
|
+
const CalendarManager_js_1 = require("../../../schedules/calendarSync/service/CalendarManager.js");
|
|
10
|
+
const index_js_1 = require("../../../../../types/index.js");
|
|
11
|
+
const eventToPlan_js_1 = require("../functions/eventToPlan.js");
|
|
12
|
+
const serviceCalendarMeta_js_1 = require("./serviceCalendarMeta.js");
|
|
13
|
+
/**
|
|
14
|
+
* Marker on a save that came from the calendar.
|
|
15
|
+
*
|
|
16
|
+
* The outbound side (`syncPlanToCalendar`, called from the `Service_Schedule`
|
|
17
|
+
* afterSave hook) checks it and stays quiet — otherwise every inbound change
|
|
18
|
+
* would be written straight back to Outlook, produce a new `changeKey`, come
|
|
19
|
+
* back through the delta feed as a foreign change, and loop forever. The stored
|
|
20
|
+
* `changeKey` is the second line of defence; this is the first, and unlike v1's
|
|
21
|
+
* `!master` check it does not disable outbound sync for every other
|
|
22
|
+
* master-key writer (the scheduler pausing a plan, a migration, a script).
|
|
23
|
+
*/
|
|
24
|
+
exports.CALENDAR_SYNC_CONTEXT_FLAG = "fromCalendarSync";
|
|
25
|
+
/**
|
|
26
|
+
* Pulls Outlook changes into open.SERVICE **v2** maintenance plans.
|
|
27
|
+
*
|
|
28
|
+
* One poller per configured connection, driven by the Graph *delta* feed: the
|
|
29
|
+
* first call reads the whole calendar, each later call only what changed, and
|
|
30
|
+
* the cursor lives in the Config row so a restart resumes instead of
|
|
31
|
+
* re-importing. The v1 poller does the same for `Maintenance_Schedule` from the
|
|
32
|
+
* same connection — the two keep separate cursors (`delta_link` vs
|
|
33
|
+
* `delta_link_v2`) so neither consumes the other's changes.
|
|
34
|
+
*/
|
|
35
|
+
class ServiceCalendarSyncPoller {
|
|
36
|
+
timer = null;
|
|
37
|
+
config;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
}
|
|
41
|
+
get values() {
|
|
42
|
+
return JSON.parse(this.config.get("value"));
|
|
43
|
+
}
|
|
44
|
+
get manager() {
|
|
45
|
+
const values = this.values;
|
|
46
|
+
return new CalendarManager_js_1.CalendarManager({
|
|
47
|
+
tenantId: values.tenant_id,
|
|
48
|
+
clientID: values.client_id,
|
|
49
|
+
clientSecret: values.client_secret,
|
|
50
|
+
userMail: values.user_email,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
start(intervalMs = 5 * 60 * 1000) {
|
|
54
|
+
const valid = this.manager.isValid;
|
|
55
|
+
if (valid !== true) {
|
|
56
|
+
console.error(`[ServiceCalendarSync] Not started — invalid configuration: ${valid}`);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
this.poll().catch(console.error);
|
|
60
|
+
this.timer = setInterval(() => this.poll().catch(console.error), intervalMs);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
stop() {
|
|
64
|
+
if (this.timer)
|
|
65
|
+
clearInterval(this.timer);
|
|
66
|
+
this.timer = null;
|
|
67
|
+
}
|
|
68
|
+
async poll() {
|
|
69
|
+
const result = await this.manager.fetchDelta(await this.loadDeltaLink());
|
|
70
|
+
if (!result) {
|
|
71
|
+
console.error("[ServiceCalendarSync] Could not fetch the calendar delta");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const event of result.events) {
|
|
75
|
+
try {
|
|
76
|
+
await this.processEvent(event);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// One unusable event must not cost us the whole batch — and not the
|
|
80
|
+
// delta cursor either, or it would be retried forever.
|
|
81
|
+
console.error("[ServiceCalendarSync] Event could not be processed:", event.id, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await this.saveDeltaLink(result.deltaLink);
|
|
85
|
+
}
|
|
86
|
+
async processEvent(event) {
|
|
87
|
+
if (event["@removed"]) {
|
|
88
|
+
await this.handleRemoved(event.id);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// An "exception" is a single moved or edited occurrence of a series. The
|
|
92
|
+
// plan's own `cadenceExceptions` could hold that, but Graph does not tell
|
|
93
|
+
// us *which* occurrence was moved in a form we can map back reliably — and
|
|
94
|
+
// a wrong exception silently moves a maintenance date. The series master
|
|
95
|
+
// carries the recurrence; exceptions are left to Outlook.
|
|
96
|
+
if (event.type === "exception")
|
|
97
|
+
return;
|
|
98
|
+
const meta = await (0, serviceCalendarMeta_js_1.findMetaForEvent)(event.id);
|
|
99
|
+
if (!meta) {
|
|
100
|
+
// Delta payloads are partial — fetch the whole event before creating.
|
|
101
|
+
const full = await this.manager.fetchEvent(event.id);
|
|
102
|
+
if (full)
|
|
103
|
+
await this.createPlan(full);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (event.changeKey &&
|
|
107
|
+
(0, serviceCalendarMeta_js_1.metaValues)(meta)?.microsoftCalendarChangeKey === event.changeKey) {
|
|
108
|
+
return; // Our own write, echoed back by the delta feed.
|
|
109
|
+
}
|
|
110
|
+
await this.updatePlan(meta, event);
|
|
111
|
+
}
|
|
112
|
+
async createPlan(event) {
|
|
113
|
+
const fields = (0, eventToPlan_js_1.eventToPlanFields)(event);
|
|
114
|
+
if (!fields) {
|
|
115
|
+
console.warn(`[ServiceCalendarSync] Event ${event.id} has no usable start — skipped`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const tenant = this.config.get("tenant");
|
|
119
|
+
const equipment = await this.resolveEquipment(fields.equipmentName, tenant);
|
|
120
|
+
const plan = new index_js_1.Service_Schedule();
|
|
121
|
+
plan.set("title", fields.title);
|
|
122
|
+
plan.set("active", true);
|
|
123
|
+
plan.set("tenant", tenant);
|
|
124
|
+
plan.set("cadence", fields.cadence);
|
|
125
|
+
if (equipment)
|
|
126
|
+
plan.set("equipment", equipment);
|
|
127
|
+
// A plan needs at least one step to be executable. The event's note becomes
|
|
128
|
+
// its description, so what someone wrote in Outlook is not lost — the
|
|
129
|
+
// checklist proper is then built in the plan editor.
|
|
130
|
+
plan.set("steps", [
|
|
131
|
+
{
|
|
132
|
+
id: (0, crypto_1.randomUUID)(),
|
|
133
|
+
order: 0,
|
|
134
|
+
title: fields.title,
|
|
135
|
+
...(fields.description ? { description: fields.description } : {}),
|
|
136
|
+
required: false,
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
plan.set("notifications", (0, eventToPlan_js_1.mergeReminderRule)([], fields.notification));
|
|
140
|
+
await plan.save(null, {
|
|
141
|
+
useMasterKey: true,
|
|
142
|
+
context: { [exports.CALENDAR_SYNC_CONTEXT_FLAG]: true },
|
|
143
|
+
});
|
|
144
|
+
await (0, serviceCalendarMeta_js_1.saveMeta)(plan.id, event.id, event.changeKey, tenant, event);
|
|
145
|
+
console.log(`[ServiceCalendarSync] Created plan ${plan.id} from event ${event.id}` +
|
|
146
|
+
(equipment ? ` (machine ${equipment.id})` : " (no machine)"));
|
|
147
|
+
}
|
|
148
|
+
async updatePlan(meta, event) {
|
|
149
|
+
const planId = meta.get("entryObjectId");
|
|
150
|
+
if (!planId)
|
|
151
|
+
return;
|
|
152
|
+
// Delta payloads are partial: without the full event, an unchanged subject
|
|
153
|
+
// would arrive as `undefined` and wipe the plan's title.
|
|
154
|
+
const full = (await this.manager.fetchEvent(event.id)) ?? event;
|
|
155
|
+
const fields = (0, eventToPlan_js_1.eventToPlanFields)(full);
|
|
156
|
+
if (!fields)
|
|
157
|
+
return;
|
|
158
|
+
const plan = await new node_1.default.Query(index_js_1.Service_Schedule)
|
|
159
|
+
.include("equipment")
|
|
160
|
+
.get(planId, { useMasterKey: true });
|
|
161
|
+
plan.set("title", fields.title);
|
|
162
|
+
plan.set("cadence", fields.cadence);
|
|
163
|
+
plan.set("notifications", (0, eventToPlan_js_1.mergeReminderRule)(plan.get("notifications"), fields.notification));
|
|
164
|
+
// The machine only changes if the subject actually names a different one —
|
|
165
|
+
// a subject without the prefix leaves the existing assignment alone rather
|
|
166
|
+
// than falling back to the configured default and moving the plan.
|
|
167
|
+
if (fields.equipmentName) {
|
|
168
|
+
const equipment = await this.findEquipmentByLabel(fields.equipmentName, this.config.get("tenant"));
|
|
169
|
+
if (equipment)
|
|
170
|
+
plan.set("equipment", equipment);
|
|
171
|
+
}
|
|
172
|
+
await plan.save(null, {
|
|
173
|
+
useMasterKey: true,
|
|
174
|
+
context: { [exports.CALENDAR_SYNC_CONTEXT_FLAG]: true },
|
|
175
|
+
});
|
|
176
|
+
if (event.changeKey)
|
|
177
|
+
await (0, serviceCalendarMeta_js_1.updateChangeKey)(meta, event.changeKey, full);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A deleted appointment archives the plan — it never destroys it.
|
|
181
|
+
*
|
|
182
|
+
* Executions document that work happened; deleting the plan they point at
|
|
183
|
+
* would strand that history (the `beforeDelete` hook refuses it, too). The
|
|
184
|
+
* meta entry goes, so the same plan can be linked to a new event later.
|
|
185
|
+
*/
|
|
186
|
+
async handleRemoved(eventId) {
|
|
187
|
+
const meta = await (0, serviceCalendarMeta_js_1.findMetaForEvent)(eventId);
|
|
188
|
+
if (!meta)
|
|
189
|
+
return;
|
|
190
|
+
const planId = meta.get("entryObjectId");
|
|
191
|
+
if (planId) {
|
|
192
|
+
try {
|
|
193
|
+
const plan = await new node_1.default.Query(index_js_1.Service_Schedule).get(planId, {
|
|
194
|
+
useMasterKey: true,
|
|
195
|
+
});
|
|
196
|
+
plan.set("active", false);
|
|
197
|
+
plan.set("deletedAt", new Date());
|
|
198
|
+
await plan.save(null, {
|
|
199
|
+
useMasterKey: true,
|
|
200
|
+
context: { [exports.CALENDAR_SYNC_CONTEXT_FLAG]: true },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error(`[ServiceCalendarSync] Could not archive plan ${planId} of deleted event ${eventId}:`, error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
await meta.destroy({ useMasterKey: true });
|
|
208
|
+
}
|
|
209
|
+
/** The machine named in the subject, else the configured fallback. */
|
|
210
|
+
async resolveEquipment(label, tenant) {
|
|
211
|
+
if (label) {
|
|
212
|
+
const found = await this.findEquipmentByLabel(label, tenant);
|
|
213
|
+
if (found)
|
|
214
|
+
return found;
|
|
215
|
+
console.warn(`[ServiceCalendarSync] No machine named "${label}" — falling back to the default`);
|
|
216
|
+
}
|
|
217
|
+
const defaultId = this.values.default_equipment_id;
|
|
218
|
+
if (!defaultId)
|
|
219
|
+
return undefined;
|
|
220
|
+
return ((await new node_1.default.Query(index_js_1.Service_Equipment)
|
|
221
|
+
.equalTo("objectId", defaultId)
|
|
222
|
+
.first({ useMasterKey: true })) ?? undefined);
|
|
223
|
+
}
|
|
224
|
+
async findEquipmentByLabel(label, tenant) {
|
|
225
|
+
const query = new node_1.default.Query(index_js_1.Service_Equipment).equalTo("label", label);
|
|
226
|
+
if (tenant)
|
|
227
|
+
query.equalTo("tenant", tenant);
|
|
228
|
+
query.notEqualTo("deleted", true);
|
|
229
|
+
return (await query.first({ useMasterKey: true })) ?? undefined;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The cursor is re-read from the Config row on every tick, and written back
|
|
233
|
+
* after it: the row is also the admin form's storage, so it can change
|
|
234
|
+
* underneath us.
|
|
235
|
+
*/
|
|
236
|
+
async loadDeltaLink() {
|
|
237
|
+
this.config = await this.config.fetch({ useMasterKey: true });
|
|
238
|
+
return this.values.delta_link_v2;
|
|
239
|
+
}
|
|
240
|
+
async saveDeltaLink(link) {
|
|
241
|
+
this.config = await this.config.fetch({ useMasterKey: true });
|
|
242
|
+
const values = this.values;
|
|
243
|
+
values.delta_link_v2 = link;
|
|
244
|
+
this.config.set("value", JSON.stringify(values));
|
|
245
|
+
await this.config.save(null, { useMasterKey: true });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.ServiceCalendarSyncPoller = ServiceCalendarSyncPoller;
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MetaConfig } from "../../../../../types/custom/MetaField.js";
|
|
2
|
+
import { Meta_Entry, Tenant } from "../../../../../types/index.js";
|
|
3
|
+
import type { GraphEventResponse } from "../../../schedules/calendarSync/types/Event.js";
|
|
4
|
+
/**
|
|
5
|
+
* Which calendar event belongs to which v2 maintenance plan.
|
|
6
|
+
*
|
|
7
|
+
* Same mechanism as v1 (a `Meta_Entry` per plan), but a **separate context**:
|
|
8
|
+
* one Outlook connection serves both generations, and a v1 entry pointing at a
|
|
9
|
+
* `Maintenance_Schedule` must never be mistaken for a v2 one pointing at an
|
|
10
|
+
* `OD3_Service_Schedule`. The stored `changeKey` is what keeps the two sides
|
|
11
|
+
* from echoing each other — see `ServiceCalendarSyncPoller`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SERVICE_CALENDAR_SYNC_CONTEXT: string;
|
|
14
|
+
/**
|
|
15
|
+
* The last known state of the event, as far as the sync needs it.
|
|
16
|
+
*
|
|
17
|
+
* Kept so an outbound update can preserve what the user set in Outlook and this
|
|
18
|
+
* side has no field for — the event's times above all: a plan knows a due
|
|
19
|
+
* *date*, and writing that back must not turn someone's 9:00 appointment into
|
|
20
|
+
* an all-day block.
|
|
21
|
+
*/
|
|
22
|
+
export interface ServiceCalendarEventSnapshot {
|
|
23
|
+
id: string;
|
|
24
|
+
subject?: string;
|
|
25
|
+
start: {
|
|
26
|
+
dateTime: string;
|
|
27
|
+
timeZone: string;
|
|
28
|
+
};
|
|
29
|
+
end: {
|
|
30
|
+
dateTime: string;
|
|
31
|
+
timeZone: string;
|
|
32
|
+
};
|
|
33
|
+
isAllDay?: boolean;
|
|
34
|
+
isCancelled?: boolean;
|
|
35
|
+
recurrence?: GraphEventResponse["recurrence"];
|
|
36
|
+
}
|
|
37
|
+
export interface ServiceCalendarMetaValues {
|
|
38
|
+
microsoftCalendarEventId: string;
|
|
39
|
+
microsoftCalendarChangeKey?: string;
|
|
40
|
+
microsoftCalendarEvent?: ServiceCalendarEventSnapshot;
|
|
41
|
+
}
|
|
42
|
+
/** Shown by the meta plugin when the entry is inspected in the frontend. */
|
|
43
|
+
export declare const ServiceCalendarMetaConfig: MetaConfig;
|
|
44
|
+
export declare function findMetaForPlan(scheduleId: string): Promise<Meta_Entry | null>;
|
|
45
|
+
export declare function findMetaForEvent(eventId: string): Promise<Meta_Entry | null>;
|
|
46
|
+
export declare function saveMeta(scheduleId: string, eventId: string, changeKey?: string, tenant?: Tenant, event?: GraphEventResponse): Promise<Meta_Entry>;
|
|
47
|
+
/**
|
|
48
|
+
* Remember the `changeKey` of the version we just wrote.
|
|
49
|
+
*
|
|
50
|
+
* This is the loop breaker: the delta feed will report our own write back to
|
|
51
|
+
* us, and the poller recognises it by this key. Losing it means one harmless
|
|
52
|
+
* extra round trip; writing the wrong one means an endless echo.
|
|
53
|
+
*/
|
|
54
|
+
export declare function updateChangeKey(entry: Meta_Entry, changeKey: string, event?: GraphEventResponse): Promise<void>;
|
|
55
|
+
export declare function metaValues(entry: Meta_Entry): ServiceCalendarMetaValues | undefined;
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ServiceCalendarMetaConfig = exports.SERVICE_CALENDAR_SYNC_CONTEXT = void 0;
|
|
7
|
+
exports.findMetaForPlan = findMetaForPlan;
|
|
8
|
+
exports.findMetaForEvent = findMetaForEvent;
|
|
9
|
+
exports.saveMeta = saveMeta;
|
|
10
|
+
exports.updateChangeKey = updateChangeKey;
|
|
11
|
+
exports.metaValues = metaValues;
|
|
12
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
13
|
+
const index_js_1 = require("../../../../../types/index.js");
|
|
14
|
+
/**
|
|
15
|
+
* Which calendar event belongs to which v2 maintenance plan.
|
|
16
|
+
*
|
|
17
|
+
* Same mechanism as v1 (a `Meta_Entry` per plan), but a **separate context**:
|
|
18
|
+
* one Outlook connection serves both generations, and a v1 entry pointing at a
|
|
19
|
+
* `Maintenance_Schedule` must never be mistaken for a v2 one pointing at an
|
|
20
|
+
* `OD3_Service_Schedule`. The stored `changeKey` is what keeps the two sides
|
|
21
|
+
* from echoing each other — see `ServiceCalendarSyncPoller`.
|
|
22
|
+
*/
|
|
23
|
+
exports.SERVICE_CALENDAR_SYNC_CONTEXT = JSON.stringify([
|
|
24
|
+
"service",
|
|
25
|
+
"schedule",
|
|
26
|
+
"sync",
|
|
27
|
+
"microsoft",
|
|
28
|
+
]);
|
|
29
|
+
/** Shown by the meta plugin when the entry is inspected in the frontend. */
|
|
30
|
+
exports.ServiceCalendarMetaConfig = {
|
|
31
|
+
fields: [
|
|
32
|
+
{
|
|
33
|
+
name: "microsoftCalendarEventId",
|
|
34
|
+
type: "string",
|
|
35
|
+
label: "Microsoft Calendar Event ID",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "microsoftCalendarChangeKey",
|
|
39
|
+
type: "string",
|
|
40
|
+
label: "Microsoft Calendar Change Key",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
function toSnapshot(event) {
|
|
45
|
+
return {
|
|
46
|
+
id: event.id,
|
|
47
|
+
subject: event.subject,
|
|
48
|
+
start: event.start,
|
|
49
|
+
end: event.end,
|
|
50
|
+
isAllDay: event.isAllDay,
|
|
51
|
+
isCancelled: event.isCancelled,
|
|
52
|
+
recurrence: event.recurrence ?? undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function findMetaForPlan(scheduleId) {
|
|
56
|
+
return ((await new node_1.default.Query(index_js_1.Meta_Entry)
|
|
57
|
+
.equalTo("context", exports.SERVICE_CALENDAR_SYNC_CONTEXT)
|
|
58
|
+
.equalTo("entryClassName", index_js_1.Service_Schedule.className)
|
|
59
|
+
.equalTo("entryObjectId", scheduleId)
|
|
60
|
+
.first({ useMasterKey: true })) ?? null);
|
|
61
|
+
}
|
|
62
|
+
async function findMetaForEvent(eventId) {
|
|
63
|
+
return ((await new node_1.default.Query(index_js_1.Meta_Entry)
|
|
64
|
+
.equalTo("context", exports.SERVICE_CALENDAR_SYNC_CONTEXT)
|
|
65
|
+
.equalTo("entryClassName", index_js_1.Service_Schedule.className)
|
|
66
|
+
// Dot access into the `values` Object column — Mongo resolves it, the
|
|
67
|
+
// generated type only knows top-level columns.
|
|
68
|
+
// @ts-expect-error
|
|
69
|
+
.equalTo("values.microsoftCalendarEventId", eventId)
|
|
70
|
+
.first({ useMasterKey: true })) ?? null);
|
|
71
|
+
}
|
|
72
|
+
async function saveMeta(scheduleId, eventId, changeKey, tenant, event) {
|
|
73
|
+
let entry = await findMetaForPlan(scheduleId);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
entry = new index_js_1.Meta_Entry();
|
|
76
|
+
entry.set("context", exports.SERVICE_CALENDAR_SYNC_CONTEXT);
|
|
77
|
+
entry.set("entryClassName", index_js_1.Service_Schedule.className);
|
|
78
|
+
entry.set("entryObjectId", scheduleId);
|
|
79
|
+
entry.set("tenant", tenant);
|
|
80
|
+
entry.set("config", {});
|
|
81
|
+
}
|
|
82
|
+
const values = {
|
|
83
|
+
microsoftCalendarEventId: eventId,
|
|
84
|
+
};
|
|
85
|
+
if (changeKey)
|
|
86
|
+
values.microsoftCalendarChangeKey = changeKey;
|
|
87
|
+
if (event)
|
|
88
|
+
values.microsoftCalendarEvent = toSnapshot(event);
|
|
89
|
+
entry.set("values", values);
|
|
90
|
+
return entry.save(null, { useMasterKey: true });
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Remember the `changeKey` of the version we just wrote.
|
|
94
|
+
*
|
|
95
|
+
* This is the loop breaker: the delta feed will report our own write back to
|
|
96
|
+
* us, and the poller recognises it by this key. Losing it means one harmless
|
|
97
|
+
* extra round trip; writing the wrong one means an endless echo.
|
|
98
|
+
*/
|
|
99
|
+
async function updateChangeKey(entry, changeKey, event) {
|
|
100
|
+
entry.set("values", {
|
|
101
|
+
...entry.get("values"),
|
|
102
|
+
microsoftCalendarChangeKey: changeKey,
|
|
103
|
+
...(event ? { microsoftCalendarEvent: toSnapshot(event) } : {}),
|
|
104
|
+
});
|
|
105
|
+
await entry.save(null, { useMasterKey: true });
|
|
106
|
+
}
|
|
107
|
+
function metaValues(entry) {
|
|
108
|
+
return entry.get("values");
|
|
109
|
+
}
|