@openinc/parse-server-opendash 4.1.8 → 4.2.1
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/acl/assignmentAcl.d.ts +30 -0
- package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.js +85 -0
- package/dist/features/openservice/serviceSchedules/acl/index.d.ts +1 -0
- package/dist/features/openservice/serviceSchedules/acl/index.js +6 -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 +59 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +353 -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 +122 -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/features/permissions/types/Permissions.d.ts +1 -2
- package/dist/features/permissions/types/Permissions.js +8 -2
- 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 +89 -16
- package/dist/hooks/Service_Schedule_Template.js +28 -7
- package/dist/hooks/Service_Ticket.js +2 -2
- package/dist/types/Service_Schedule.d.ts +12 -0
- package/dist/types/Service_Schedule.js +21 -0
- package/dist/types/Service_Schedule_Execution.d.ts +6 -3
- package/dist/types/Service_Schedule_Execution.js +6 -0
- package/dist/types/Service_Schedule_Template.d.ts +7 -0
- package/dist/types/Service_Schedule_Template.js +12 -0
- package/package.json +5 -4
- package/schema/Service_Schedule.json +17 -0
- package/schema/Service_Schedule_Execution.json +5 -0
- package/schema/Service_Schedule_Template.json +9 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +0 -41
- package/dist/features/ruleset/applyRuleSet.js +0 -81
package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRRule = parseRRule;
|
|
4
|
+
exports.buildRRule = buildRRule;
|
|
5
|
+
exports.eventToCadence = eventToCadence;
|
|
6
|
+
exports.calendarUnsupportedReason = calendarUnsupportedReason;
|
|
7
|
+
exports.cadenceToRecurrence = cadenceToRecurrence;
|
|
8
|
+
/**
|
|
9
|
+
* Outlook recurrence ↔ RRULE, both directions.
|
|
10
|
+
*
|
|
11
|
+
* **Why this module exists.** v1 stored a recurrence as `{number, unit}` — every
|
|
12
|
+
* Outlook pattern had to be flattened into "every n days/weeks/months", and
|
|
13
|
+
* anything else ("last Friday of the month") silently became something else. v2
|
|
14
|
+
* stores an RFC 5545 rule, which is the same language Outlook's patterns are a
|
|
15
|
+
* subset of, so the mapping can be lossless in both directions.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is **pure** — no Parse, no Graph client — because a wrong
|
|
18
|
+
* mapping is the one bug in a calendar sync you cannot see until someone misses
|
|
19
|
+
* a maintenance date. `pnpm tsx scripts/…` round-trips it (see the module test
|
|
20
|
+
* at the bottom of the file's docs in OPENSERVICE_V2_SCHEDULES.md).
|
|
21
|
+
*
|
|
22
|
+
* **Where the two vocabularies differ**: Outlook's patterns are a subset of the
|
|
23
|
+
* RRULE grammar. A positional weekday (`BYDAY=+2TU`) maps to `relativeMonthly`
|
|
24
|
+
* and back, but `BYSETPOS` over several weekdays ("last workday"), several
|
|
25
|
+
* days of the month, or several months do not exist there at all —
|
|
26
|
+
* {@link calendarUnsupportedReason} names them, and the sync skips such plans
|
|
27
|
+
* instead of approximating a maintenance date.
|
|
28
|
+
*/
|
|
29
|
+
const WEEKDAY_TO_RRULE = {
|
|
30
|
+
Sunday: "SU",
|
|
31
|
+
Monday: "MO",
|
|
32
|
+
Tuesday: "TU",
|
|
33
|
+
Wednesday: "WE",
|
|
34
|
+
Thursday: "TH",
|
|
35
|
+
Friday: "FR",
|
|
36
|
+
Saturday: "SA",
|
|
37
|
+
};
|
|
38
|
+
const RRULE_TO_WEEKDAY = {
|
|
39
|
+
SU: "Sunday",
|
|
40
|
+
MO: "Monday",
|
|
41
|
+
TU: "Tuesday",
|
|
42
|
+
WE: "Wednesday",
|
|
43
|
+
TH: "Thursday",
|
|
44
|
+
FR: "Friday",
|
|
45
|
+
SA: "Saturday",
|
|
46
|
+
};
|
|
47
|
+
/** Outlook's `index` ↔ the RRULE ordinal prefix of a `BYDAY` entry. */
|
|
48
|
+
const INDEX_TO_ORDINAL = {
|
|
49
|
+
first: 1,
|
|
50
|
+
second: 2,
|
|
51
|
+
third: 3,
|
|
52
|
+
fourth: 4,
|
|
53
|
+
last: -1,
|
|
54
|
+
};
|
|
55
|
+
const ORDINAL_TO_INDEX = {
|
|
56
|
+
1: "first",
|
|
57
|
+
2: "second",
|
|
58
|
+
3: "third",
|
|
59
|
+
4: "fourth",
|
|
60
|
+
[-1]: "last",
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Read an RRULE string into its parts.
|
|
64
|
+
*
|
|
65
|
+
* Tolerant on purpose: an unknown key is dropped rather than throwing, because
|
|
66
|
+
* a rule that came from Outlook, from the editor, or from a hand-written import
|
|
67
|
+
* all end up here, and a single unexpected part must not stop a sync.
|
|
68
|
+
*/
|
|
69
|
+
function parseRRule(rrule) {
|
|
70
|
+
const parts = {};
|
|
71
|
+
if (!rrule)
|
|
72
|
+
return parts;
|
|
73
|
+
for (const chunk of rrule.replace(/^RRULE:/i, "").split(";")) {
|
|
74
|
+
const [rawKey, rawValue] = chunk.split("=");
|
|
75
|
+
if (!rawKey || !rawValue)
|
|
76
|
+
continue;
|
|
77
|
+
const key = rawKey.trim().toUpperCase();
|
|
78
|
+
const value = rawValue.trim().toUpperCase();
|
|
79
|
+
switch (key) {
|
|
80
|
+
case "FREQ":
|
|
81
|
+
if (value === "DAILY" ||
|
|
82
|
+
value === "WEEKLY" ||
|
|
83
|
+
value === "MONTHLY" ||
|
|
84
|
+
value === "YEARLY") {
|
|
85
|
+
parts.freq = value;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case "INTERVAL":
|
|
89
|
+
parts.interval = toPositiveInt(value);
|
|
90
|
+
break;
|
|
91
|
+
case "COUNT":
|
|
92
|
+
parts.count = toPositiveInt(value);
|
|
93
|
+
break;
|
|
94
|
+
case "BYDAY":
|
|
95
|
+
parts.byday = value.split(",").filter(Boolean);
|
|
96
|
+
break;
|
|
97
|
+
case "BYMONTHDAY":
|
|
98
|
+
parts.bymonthday = value.split(",").map(Number).filter(Number.isFinite);
|
|
99
|
+
break;
|
|
100
|
+
case "BYMONTH":
|
|
101
|
+
parts.bymonth = value.split(",").map(Number).filter(Number.isFinite);
|
|
102
|
+
break;
|
|
103
|
+
case "WKST":
|
|
104
|
+
parts.wkst = value;
|
|
105
|
+
break;
|
|
106
|
+
case "BYSETPOS": {
|
|
107
|
+
const parsed = Number(value);
|
|
108
|
+
if (Number.isFinite(parsed) && parsed !== 0)
|
|
109
|
+
parts.bysetpos = parsed;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
116
|
+
/** Write the parts back into an RRULE string, in the canonical part order. */
|
|
117
|
+
function buildRRule(parts) {
|
|
118
|
+
const chunks = [`FREQ=${parts.freq ?? "DAILY"}`];
|
|
119
|
+
if (parts.interval && parts.interval > 1) {
|
|
120
|
+
chunks.push(`INTERVAL=${parts.interval}`);
|
|
121
|
+
}
|
|
122
|
+
if (parts.byday?.length)
|
|
123
|
+
chunks.push(`BYDAY=${parts.byday.join(",")}`);
|
|
124
|
+
if (parts.bymonthday?.length) {
|
|
125
|
+
chunks.push(`BYMONTHDAY=${parts.bymonthday.join(",")}`);
|
|
126
|
+
}
|
|
127
|
+
if (parts.bymonth?.length)
|
|
128
|
+
chunks.push(`BYMONTH=${parts.bymonth.join(",")}`);
|
|
129
|
+
if (parts.bysetpos)
|
|
130
|
+
chunks.push(`BYSETPOS=${parts.bysetpos}`);
|
|
131
|
+
if (parts.count)
|
|
132
|
+
chunks.push(`COUNT=${parts.count}`);
|
|
133
|
+
if (parts.wkst && parts.wkst !== "MO")
|
|
134
|
+
chunks.push(`WKST=${parts.wkst}`);
|
|
135
|
+
return chunks.join(";");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* An Outlook event's timing, as a v2 cadence.
|
|
139
|
+
*
|
|
140
|
+
* A non-recurring event becomes a one-off — the same shape the frontend's
|
|
141
|
+
* `oneOffCadence()` produces (`FREQ=DAILY;INTERVAL=1;COUNT=1`), so a plan
|
|
142
|
+
* created from a single Outlook appointment is indistinguishable from one
|
|
143
|
+
* created by hand.
|
|
144
|
+
*/
|
|
145
|
+
function eventToCadence(start, recurrence) {
|
|
146
|
+
const dtstart = start.toISOString();
|
|
147
|
+
if (!recurrence?.pattern) {
|
|
148
|
+
return { kind: "rrule", rrule: "FREQ=DAILY;INTERVAL=1;COUNT=1", dtstart };
|
|
149
|
+
}
|
|
150
|
+
const { pattern, range } = recurrence;
|
|
151
|
+
const parts = { interval: pattern.interval };
|
|
152
|
+
switch (pattern.type) {
|
|
153
|
+
case "daily":
|
|
154
|
+
parts.freq = "DAILY";
|
|
155
|
+
break;
|
|
156
|
+
case "weekly":
|
|
157
|
+
parts.freq = "WEEKLY";
|
|
158
|
+
parts.byday = (pattern.daysOfWeek ?? [])
|
|
159
|
+
.map((day) => WEEKDAY_TO_RRULE[day])
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
if (pattern.firstDayOfWeek) {
|
|
162
|
+
parts.wkst = WEEKDAY_TO_RRULE[pattern.firstDayOfWeek];
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case "absoluteMonthly":
|
|
166
|
+
parts.freq = "MONTHLY";
|
|
167
|
+
parts.bymonthday = [pattern.dayOfMonth ?? start.getUTCDate()];
|
|
168
|
+
break;
|
|
169
|
+
case "relativeMonthly":
|
|
170
|
+
parts.freq = "MONTHLY";
|
|
171
|
+
parts.byday = positionalByDay(pattern);
|
|
172
|
+
break;
|
|
173
|
+
case "absoluteYearly":
|
|
174
|
+
parts.freq = "YEARLY";
|
|
175
|
+
parts.bymonth = [pattern.month ?? start.getUTCMonth() + 1];
|
|
176
|
+
parts.bymonthday = [pattern.dayOfMonth ?? start.getUTCDate()];
|
|
177
|
+
break;
|
|
178
|
+
case "relativeYearly":
|
|
179
|
+
parts.freq = "YEARLY";
|
|
180
|
+
parts.bymonth = [pattern.month ?? start.getUTCMonth() + 1];
|
|
181
|
+
parts.byday = positionalByDay(pattern);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
// A weekly series with no explicit days repeats on the start's weekday.
|
|
185
|
+
if (parts.freq === "WEEKLY" && !parts.byday?.length) {
|
|
186
|
+
parts.byday = [rruleWeekday(start)];
|
|
187
|
+
}
|
|
188
|
+
const cadence = {
|
|
189
|
+
kind: "rrule",
|
|
190
|
+
rrule: buildRRule(parts),
|
|
191
|
+
dtstart,
|
|
192
|
+
};
|
|
193
|
+
if (range?.type === "numbered") {
|
|
194
|
+
cadence.rrule = buildRRule({
|
|
195
|
+
...parts,
|
|
196
|
+
count: range.numberOfOccurrences,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else if (range?.type === "endDate" && range.endDate) {
|
|
200
|
+
// Outlook's end date is a plain day and inclusive — the series may still
|
|
201
|
+
// occur on it, so the boundary is the end of that day.
|
|
202
|
+
cadence.until = endOfDay(range.endDate).toISOString();
|
|
203
|
+
}
|
|
204
|
+
return cadence;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Why this cadence cannot be written to a calendar — `undefined` when it can.
|
|
208
|
+
*
|
|
209
|
+
* Outlook's recurrence vocabulary is a **subset** of RFC 5545, and the gap is
|
|
210
|
+
* not cosmetic: a rule we cannot express would have to be approximated, and an
|
|
211
|
+
* approximated maintenance date in someone's calendar is worse than no entry
|
|
212
|
+
* at all. The sync therefore leaves such plans alone instead of guessing (see
|
|
213
|
+
* `syncPlanToCalendar`).
|
|
214
|
+
*/
|
|
215
|
+
function calendarUnsupportedReason(cadence) {
|
|
216
|
+
if (cadence?.kind !== "rrule" || !cadence.rrule)
|
|
217
|
+
return undefined;
|
|
218
|
+
const parts = parseRRule(cadence.rrule);
|
|
219
|
+
const days = parts.byday?.map(splitByDay) ?? [];
|
|
220
|
+
if (parts.bysetpos !== undefined && days.length > 1) {
|
|
221
|
+
return `BYSETPOS with ${days.length} weekdays has no Outlook equivalent`;
|
|
222
|
+
}
|
|
223
|
+
if (parts.byday && parts.byday.length > 1 && parts.freq !== "WEEKLY") {
|
|
224
|
+
return `several weekdays are only expressible for a weekly series (FREQ=${parts.freq})`;
|
|
225
|
+
}
|
|
226
|
+
if (parts.bymonthday && parts.bymonthday.length > 1) {
|
|
227
|
+
return "several days of the month have no Outlook equivalent";
|
|
228
|
+
}
|
|
229
|
+
if (parts.bymonth && parts.bymonth.length > 1) {
|
|
230
|
+
return "several months have no Outlook equivalent";
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A v2 cadence as an Outlook recurrence — `undefined` for a one-off, which
|
|
236
|
+
* Graph expects as an event without a `recurrence` at all.
|
|
237
|
+
*
|
|
238
|
+
* `startDate` of the range is always the series start: Graph rejects a
|
|
239
|
+
* recurrence whose range starts on a different day than the event.
|
|
240
|
+
*/
|
|
241
|
+
function cadenceToRecurrence(cadence, start) {
|
|
242
|
+
if (cadence?.kind !== "rrule" || !cadence.rrule)
|
|
243
|
+
return undefined;
|
|
244
|
+
const parts = parseRRule(cadence.rrule);
|
|
245
|
+
// One occurrence is not a series — and Outlook has no way to say "once".
|
|
246
|
+
if (parts.count === 1)
|
|
247
|
+
return undefined;
|
|
248
|
+
const pattern = recurrencePattern(parts, start);
|
|
249
|
+
if (!pattern)
|
|
250
|
+
return undefined;
|
|
251
|
+
return { pattern, range: recurrenceRange(parts, cadence, start) };
|
|
252
|
+
}
|
|
253
|
+
function recurrencePattern(parts, start) {
|
|
254
|
+
const interval = parts.interval && parts.interval > 0 ? parts.interval : 1;
|
|
255
|
+
const days = parts.byday?.map(splitByDay) ?? [];
|
|
256
|
+
// A position comes either as an ordinal on the weekday (`+2TU`) or as
|
|
257
|
+
// BYSETPOS next to a single weekday — both are one Outlook `index`.
|
|
258
|
+
const positional = days.find((day) => !!day.ordinal) ??
|
|
259
|
+
(parts.bysetpos !== undefined && days.length === 1
|
|
260
|
+
? { ordinal: parts.bysetpos, weekday: days[0].weekday }
|
|
261
|
+
: undefined);
|
|
262
|
+
switch (parts.freq ?? "DAILY") {
|
|
263
|
+
case "DAILY":
|
|
264
|
+
return { type: "daily", interval };
|
|
265
|
+
case "WEEKLY": {
|
|
266
|
+
const daysOfWeek = (parts.byday ?? [])
|
|
267
|
+
.map((entry) => RRULE_TO_WEEKDAY[splitByDay(entry).weekday])
|
|
268
|
+
.filter(Boolean);
|
|
269
|
+
return {
|
|
270
|
+
type: "weekly",
|
|
271
|
+
interval,
|
|
272
|
+
daysOfWeek: daysOfWeek.length
|
|
273
|
+
? daysOfWeek
|
|
274
|
+
: [RRULE_TO_WEEKDAY[rruleWeekday(start)]],
|
|
275
|
+
firstDayOfWeek: RRULE_TO_WEEKDAY[parts.wkst ?? "MO"] ?? "Monday",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
case "MONTHLY":
|
|
279
|
+
if (positional) {
|
|
280
|
+
return {
|
|
281
|
+
type: "relativeMonthly",
|
|
282
|
+
interval,
|
|
283
|
+
daysOfWeek: [RRULE_TO_WEEKDAY[positional.weekday]],
|
|
284
|
+
index: ORDINAL_TO_INDEX[positional.ordinal] ?? "first",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
type: "absoluteMonthly",
|
|
289
|
+
interval,
|
|
290
|
+
dayOfMonth: parts.bymonthday?.[0] ?? start.getUTCDate(),
|
|
291
|
+
};
|
|
292
|
+
case "YEARLY": {
|
|
293
|
+
const month = parts.bymonth?.[0] ?? start.getUTCMonth() + 1;
|
|
294
|
+
if (positional) {
|
|
295
|
+
return {
|
|
296
|
+
type: "relativeYearly",
|
|
297
|
+
interval,
|
|
298
|
+
month,
|
|
299
|
+
daysOfWeek: [RRULE_TO_WEEKDAY[positional.weekday]],
|
|
300
|
+
index: ORDINAL_TO_INDEX[positional.ordinal] ?? "first",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
type: "absoluteYearly",
|
|
305
|
+
interval,
|
|
306
|
+
month,
|
|
307
|
+
dayOfMonth: parts.bymonthday?.[0] ?? start.getUTCDate(),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
function recurrenceRange(parts, cadence, start) {
|
|
314
|
+
const startDate = graphDate(start);
|
|
315
|
+
if (parts.count && parts.count > 1) {
|
|
316
|
+
return { type: "numbered", startDate, numberOfOccurrences: parts.count };
|
|
317
|
+
}
|
|
318
|
+
if (cadence.until) {
|
|
319
|
+
return { type: "endDate", startDate, endDate: graphDate(cadence.until) };
|
|
320
|
+
}
|
|
321
|
+
return { type: "noEnd", startDate };
|
|
322
|
+
}
|
|
323
|
+
/** `["+2TU"]` for "second Tuesday" — the RRULE spelling of Outlook's `index`. */
|
|
324
|
+
function positionalByDay(pattern) {
|
|
325
|
+
const weekday = WEEKDAY_TO_RRULE[pattern.daysOfWeek?.[0] ?? "Monday"];
|
|
326
|
+
const ordinal = INDEX_TO_ORDINAL[pattern.index ?? "first"] ?? 1;
|
|
327
|
+
return [`${ordinal > 0 ? "+" : ""}${ordinal}${weekday}`];
|
|
328
|
+
}
|
|
329
|
+
/** `"+2TU"` → `{ ordinal: 2, weekday: "TU" }`, `"TU"` → `{ weekday: "TU" }`. */
|
|
330
|
+
function splitByDay(entry) {
|
|
331
|
+
const match = /^([+-]?\d+)?([A-Z]{2})$/.exec(entry.trim().toUpperCase());
|
|
332
|
+
if (!match)
|
|
333
|
+
return { weekday: "MO" };
|
|
334
|
+
return {
|
|
335
|
+
ordinal: match[1] ? Number(match[1]) : undefined,
|
|
336
|
+
weekday: match[2],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function rruleWeekday(date) {
|
|
340
|
+
return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][date.getUTCDay()];
|
|
341
|
+
}
|
|
342
|
+
/** Graph wants a plain `YYYY-MM-DD` in a recurrence range. */
|
|
343
|
+
function graphDate(value) {
|
|
344
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
345
|
+
return date.toISOString().slice(0, 10);
|
|
346
|
+
}
|
|
347
|
+
function endOfDay(day) {
|
|
348
|
+
return new Date(`${day.slice(0, 10)}T23:59:59.999Z`);
|
|
349
|
+
}
|
|
350
|
+
function toPositiveInt(value) {
|
|
351
|
+
const parsed = Number(value);
|
|
352
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
353
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GraphEvent } from "../../../schedules/calendarSync/types/Event.js";
|
|
2
|
+
import type { ServiceCalendarEventSnapshot } from "../service/serviceCalendarMeta.js";
|
|
3
|
+
import type { ServiceCadence, ServiceNotificationRule } from "../types/ServiceSyncTypes.js";
|
|
4
|
+
/** A step of the plan, as far as the calendar entry shows it. */
|
|
5
|
+
type PlanStep = {
|
|
6
|
+
order?: number;
|
|
7
|
+
title?: string;
|
|
8
|
+
};
|
|
9
|
+
export type PlanEventInput = {
|
|
10
|
+
title?: string;
|
|
11
|
+
cadence?: ServiceCadence;
|
|
12
|
+
steps?: PlanStep[];
|
|
13
|
+
notifications?: ServiceNotificationRule[];
|
|
14
|
+
/** Machine name — becomes the subject prefix. */
|
|
15
|
+
equipmentName?: string;
|
|
16
|
+
/** The event as it currently stands in Outlook, if it exists already. */
|
|
17
|
+
snapshot?: ServiceCalendarEventSnapshot;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A maintenance plan as a calendar event.
|
|
21
|
+
*
|
|
22
|
+
* Returns `undefined` for a plan that has no date to put in a calendar — a
|
|
23
|
+
* manual, sensor- or meter-driven plan has no due date until something happens,
|
|
24
|
+
* and inventing one would show maintenance that nobody scheduled.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planToEvent(plan: PlanEventInput): GraphEvent | undefined;
|
|
27
|
+
export {};
|
|
@@ -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
|
+
}
|