@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.
Files changed (44) hide show
  1. package/dist/features/openservice/init.js +4 -0
  2. package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
  3. package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
  4. package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
  5. package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +39 -0
  6. package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +310 -0
  7. package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
  8. package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
  9. package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
  10. package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
  11. package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
  12. package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
  13. package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
  14. package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
  15. package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
  16. package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +114 -0
  17. package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
  18. package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
  19. package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
  20. package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
  21. package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
  22. package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
  23. package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
  24. package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
  25. package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
  26. package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
  27. package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
  28. package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
  29. package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
  30. package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
  31. package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
  32. package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
  33. package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
  34. package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
  35. package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
  36. package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
  37. package/dist/hooks/Service_Schedule.js +54 -7
  38. package/dist/types/Service_Schedule.d.ts +6 -0
  39. package/dist/types/Service_Schedule.js +12 -0
  40. package/dist/types/Service_Schedule_Template.d.ts +3 -0
  41. package/dist/types/Service_Schedule_Template.js +6 -0
  42. package/package.json +1 -1
  43. package/schema/Service_Schedule.json +9 -0
  44. package/schema/Service_Schedule_Template.json +5 -0
@@ -5,6 +5,8 @@ const initIssuecategory_js_1 = require("./issuecategory/initIssuecategory.js");
5
5
  const initKanbanStates_js_1 = require("./kanbanState/initKanbanStates.js");
6
6
  const initMessages_js_1 = require("./messages/initMessages.js");
7
7
  const initSchedulesFeature_js_1 = require("./schedules/initSchedulesFeature.js");
8
+ const initServiceSchedulesFeature_js_1 = require("./serviceSchedules/initServiceSchedulesFeature.js");
9
+ const index_js_1 = require("./serviceTickets/alarmActions/index.js");
8
10
  const checkStatus_js_1 = require("./schedules/mappings/checkStatus.js");
9
11
  const reassignScheduleExecutions_js_1 = require("./schedules/mappings/reassignScheduleExecutions.js");
10
12
  const initTicket_js_1 = require("./ticket/initTicket.js");
@@ -20,5 +22,7 @@ async function init() {
20
22
  await (0, reassignScheduleExecutions_js_1.reassignScheduleExecutions)();
21
23
  await (0, checkStatus_js_1.checkStatus)();
22
24
  await (0, initSchedulesFeature_js_1.initSchedulesFeature)();
25
+ await (0, initServiceSchedulesFeature_js_1.initServiceSchedulesFeature)();
26
+ await (0, index_js_1.ensureAlarmTicketAction)();
23
27
  console.log("open.SERVICE feature initialized.");
24
28
  }
@@ -35,6 +35,8 @@ export type RecurrencePattern = {
35
35
  daysOfWeek?: Array<"Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday">;
36
36
  firstDayOfWeek?: "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday";
37
37
  month?: number;
38
+ /** Day of the month for `absoluteMonthly` / `absoluteYearly` patterns. */
39
+ dayOfMonth?: number;
38
40
  index?: "first" | "second" | "third" | "fourth" | "last";
39
41
  };
40
42
  export type RecurrenceRange = {
@@ -0,0 +1,50 @@
1
+ import type { GraphEventResponse } from "../../../schedules/calendarSync/types/Event.js";
2
+ import type { ServiceCadence, ServiceNotificationRule } from "../types/ServiceSyncTypes.js";
3
+ /**
4
+ * The rule id the sync owns.
5
+ *
6
+ * An Outlook reminder becomes one notification rule on the plan, and it always
7
+ * gets **this** id: the sync may replace or remove its own rule on every
8
+ * inbound change, and must never touch the rules a user wrote in the plan
9
+ * editor. A stable id also keeps the scheduler's dedup key
10
+ * (`{scheduleId, ruleId, occurrence}`) stable, so re-syncing an unchanged event
11
+ * does not notify twice.
12
+ */
13
+ export declare const OUTLOOK_REMINDER_RULE_ID = "outlook-reminder";
14
+ /** What an Outlook event says about a maintenance plan. */
15
+ export type PlanFieldsFromEvent = {
16
+ /** Plan title — the subject, minus the machine prefix if there was one. */
17
+ title: string;
18
+ /** Machine name from the subject convention, if the subject used it. */
19
+ equipmentName?: string;
20
+ cadence: ServiceCadence;
21
+ /** The event's note, kept as the description of the plan's first step. */
22
+ description?: string;
23
+ /** The event's reminder, as the sync's own notification rule. */
24
+ notification?: ServiceNotificationRule;
25
+ };
26
+ /**
27
+ * Read a calendar event as maintenance plan fields.
28
+ *
29
+ * Pure — resolving the machine and writing the plan is the poller's job.
30
+ * Returns `undefined` for an event without a usable start, which is the one
31
+ * thing a plan cannot do without.
32
+ */
33
+ export declare function eventToPlanFields(event: GraphEventResponse): PlanFieldsFromEvent | undefined;
34
+ /**
35
+ * Split `"Flex 1" - Ölwechsel` into machine and title.
36
+ *
37
+ * Same convention as v1 (separator `" - "`, quotes optional), where the prefix
38
+ * named a `Maintenance_Source`; in v2 it names the machine. Unlike v1 the
39
+ * prefix is *removed* from the title — the machine is a field of its own here,
40
+ * and repeating it in the title is what made v1's plan lists unreadable.
41
+ */
42
+ export declare function parseSubject(subject: string | undefined): {
43
+ equipmentName?: string;
44
+ title: string;
45
+ };
46
+ /**
47
+ * Put the sync's rule into a plan's rule list, replacing an older one of its
48
+ * own and leaving every user-written rule untouched.
49
+ */
50
+ export declare function mergeReminderRule(existing: ServiceNotificationRule[] | undefined, rule: ServiceNotificationRule | undefined): ServiceNotificationRule[];
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OUTLOOK_REMINDER_RULE_ID = void 0;
4
+ exports.eventToPlanFields = eventToPlanFields;
5
+ exports.parseSubject = parseSubject;
6
+ exports.mergeReminderRule = mergeReminderRule;
7
+ const outlookRecurrence_js_1 = require("./outlookRecurrence.js");
8
+ /**
9
+ * The rule id the sync owns.
10
+ *
11
+ * An Outlook reminder becomes one notification rule on the plan, and it always
12
+ * gets **this** id: the sync may replace or remove its own rule on every
13
+ * inbound change, and must never touch the rules a user wrote in the plan
14
+ * editor. A stable id also keeps the scheduler's dedup key
15
+ * (`{scheduleId, ruleId, occurrence}`) stable, so re-syncing an unchanged event
16
+ * does not notify twice.
17
+ */
18
+ exports.OUTLOOK_REMINDER_RULE_ID = "outlook-reminder";
19
+ /**
20
+ * Read a calendar event as maintenance plan fields.
21
+ *
22
+ * Pure — resolving the machine and writing the plan is the poller's job.
23
+ * Returns `undefined` for an event without a usable start, which is the one
24
+ * thing a plan cannot do without.
25
+ */
26
+ function eventToPlanFields(event) {
27
+ const start = eventStart(event);
28
+ if (!start)
29
+ return undefined;
30
+ const { equipmentName, title } = parseSubject(event.subject);
31
+ return {
32
+ title: title || "Outlook",
33
+ equipmentName,
34
+ cadence: (0, outlookRecurrence_js_1.eventToCadence)(start, event.recurrence ?? undefined),
35
+ description: event.bodyPreview?.trim() || undefined,
36
+ notification: reminderToRule(event),
37
+ };
38
+ }
39
+ /**
40
+ * The start of the series as an absolute instant.
41
+ *
42
+ * Graph returns `dateTime` in the time zone named next to it, and this server
43
+ * asks for none — so in practice it is UTC and the time of day survives, which
44
+ * matters because the plan's `dtstart` is what the frontend shows. If the event
45
+ * ever arrives in another zone, the day is kept and the time falls back to
46
+ * midnight rather than being shifted by a guess.
47
+ */
48
+ function eventStart(event) {
49
+ const raw = event.start?.dateTime;
50
+ if (!raw)
51
+ return undefined;
52
+ const zone = event.start?.timeZone;
53
+ if (!zone || /^utc$/i.test(zone)) {
54
+ // Graph omits the offset; the trailing fraction can be 7 digits long.
55
+ const normalized = raw.endsWith("Z") ? raw : `${raw.slice(0, 23)}Z`;
56
+ const parsed = new Date(normalized);
57
+ if (!Number.isNaN(parsed.getTime()))
58
+ return parsed;
59
+ }
60
+ const dateOnly = new Date(`${raw.slice(0, 10)}T00:00:00.000Z`);
61
+ return Number.isNaN(dateOnly.getTime()) ? undefined : dateOnly;
62
+ }
63
+ /**
64
+ * Split `"Flex 1" - Ölwechsel` into machine and title.
65
+ *
66
+ * Same convention as v1 (separator `" - "`, quotes optional), where the prefix
67
+ * named a `Maintenance_Source`; in v2 it names the machine. Unlike v1 the
68
+ * prefix is *removed* from the title — the machine is a field of its own here,
69
+ * and repeating it in the title is what made v1's plan lists unreadable.
70
+ */
71
+ function parseSubject(subject) {
72
+ const value = (subject ?? "").trim();
73
+ const separator = value.indexOf(" - ");
74
+ if (separator === -1)
75
+ return { title: value };
76
+ const prefix = value
77
+ .slice(0, separator)
78
+ .trim()
79
+ .replace(/^"(.*)"$/, "$1")
80
+ .trim();
81
+ const rest = value.slice(separator + 3).trim();
82
+ if (!prefix || !rest)
83
+ return { title: value };
84
+ return { equipmentName: prefix, title: rest };
85
+ }
86
+ /**
87
+ * An Outlook reminder as a notification rule.
88
+ *
89
+ * Outlook counts minutes, a plan counts days — a maintenance date is a day, not
90
+ * a minute. Anything under a day therefore becomes "on the due date" instead of
91
+ * being rounded up to a day early.
92
+ */
93
+ function reminderToRule(event) {
94
+ if (!event.isReminderOn)
95
+ return undefined;
96
+ const minutes = event.reminderMinutesBeforeStart;
97
+ if (typeof minutes !== "number" || minutes < 0)
98
+ return undefined;
99
+ const offsetDays = Math.floor(minutes / (60 * 24));
100
+ return {
101
+ id: exports.OUTLOOK_REMINDER_RULE_ID,
102
+ trigger: offsetDays > 0 ? "before" : "due",
103
+ ...(offsetDays > 0 ? { offsetDays } : {}),
104
+ assignedRoles: true,
105
+ serviceProviders: true,
106
+ };
107
+ }
108
+ /**
109
+ * Put the sync's rule into a plan's rule list, replacing an older one of its
110
+ * own and leaving every user-written rule untouched.
111
+ */
112
+ function mergeReminderRule(existing, rule) {
113
+ const others = (existing ?? []).filter((entry) => entry.id !== exports.OUTLOOK_REMINDER_RULE_ID);
114
+ return rule ? [...others, rule] : others;
115
+ }
@@ -0,0 +1,39 @@
1
+ import type { Recurrence } from "../../../schedules/calendarSync/types/Event.js";
2
+ import type { ServiceCadence } from "../types/ServiceSyncTypes.js";
3
+ /** A recurrence rule taken apart into its parts. */
4
+ export type RRuleParts = {
5
+ freq?: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
6
+ interval?: number;
7
+ byday?: string[];
8
+ bymonthday?: number[];
9
+ bymonth?: number[];
10
+ count?: number;
11
+ wkst?: string;
12
+ };
13
+ /**
14
+ * Read an RRULE string into its parts.
15
+ *
16
+ * Tolerant on purpose: an unknown key is dropped rather than throwing, because
17
+ * a rule that came from Outlook, from the editor, or from a hand-written import
18
+ * all end up here, and a single unexpected part must not stop a sync.
19
+ */
20
+ export declare function parseRRule(rrule: string | undefined): RRuleParts;
21
+ /** Write the parts back into an RRULE string, in the canonical part order. */
22
+ export declare function buildRRule(parts: RRuleParts): string;
23
+ /**
24
+ * An Outlook event's timing, as a v2 cadence.
25
+ *
26
+ * A non-recurring event becomes a one-off — the same shape the frontend's
27
+ * `oneOffCadence()` produces (`FREQ=DAILY;INTERVAL=1;COUNT=1`), so a plan
28
+ * created from a single Outlook appointment is indistinguishable from one
29
+ * created by hand.
30
+ */
31
+ export declare function eventToCadence(start: Date, recurrence?: Recurrence): ServiceCadence;
32
+ /**
33
+ * A v2 cadence as an Outlook recurrence — `undefined` for a one-off, which
34
+ * Graph expects as an event without a `recurrence` at all.
35
+ *
36
+ * `startDate` of the range is always the series start: Graph rejects a
37
+ * recurrence whose range starts on a different day than the event.
38
+ */
39
+ export declare function cadenceToRecurrence(cadence: ServiceCadence | undefined, start: Date): Recurrence | undefined;
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRRule = parseRRule;
4
+ exports.buildRRule = buildRRule;
5
+ exports.eventToCadence = eventToCadence;
6
+ exports.cadenceToRecurrence = cadenceToRecurrence;
7
+ /**
8
+ * Outlook recurrence ↔ RRULE, both directions.
9
+ *
10
+ * **Why this module exists.** v1 stored a recurrence as `{number, unit}` — every
11
+ * Outlook pattern had to be flattened into "every n days/weeks/months", and
12
+ * anything else ("last Friday of the month") silently became something else. v2
13
+ * stores an RFC 5545 rule, which is the same language Outlook's patterns are a
14
+ * subset of, so the mapping can be lossless in both directions.
15
+ *
16
+ * Everything here is **pure** — no Parse, no Graph client — because a wrong
17
+ * mapping is the one bug in a calendar sync you cannot see until someone misses
18
+ * a maintenance date. `pnpm tsx scripts/…` round-trips it (see the module test
19
+ * at the bottom of the file's docs in OPENSERVICE_V2_SCHEDULES.md).
20
+ *
21
+ * **Known asymmetry**: the plan editor's cadence UI understands
22
+ * FREQ/INTERVAL/BYDAY/BYMONTHDAY/COUNT, but not `BYMONTH` or a positional
23
+ * `BYDAY` (`+2TU`). Occurrence *maths* and the calendar are correct for those
24
+ * (`@opendash/schedule` uses a full RRULE iterator) — but a user who edits such
25
+ * a plan's recurrence in the browser will simplify it. Yearly and "nth weekday"
26
+ * series should therefore be maintained in Outlook.
27
+ */
28
+ const WEEKDAY_TO_RRULE = {
29
+ Sunday: "SU",
30
+ Monday: "MO",
31
+ Tuesday: "TU",
32
+ Wednesday: "WE",
33
+ Thursday: "TH",
34
+ Friday: "FR",
35
+ Saturday: "SA",
36
+ };
37
+ const RRULE_TO_WEEKDAY = {
38
+ SU: "Sunday",
39
+ MO: "Monday",
40
+ TU: "Tuesday",
41
+ WE: "Wednesday",
42
+ TH: "Thursday",
43
+ FR: "Friday",
44
+ SA: "Saturday",
45
+ };
46
+ /** Outlook's `index` ↔ the RRULE ordinal prefix of a `BYDAY` entry. */
47
+ const INDEX_TO_ORDINAL = {
48
+ first: 1,
49
+ second: 2,
50
+ third: 3,
51
+ fourth: 4,
52
+ last: -1,
53
+ };
54
+ const ORDINAL_TO_INDEX = {
55
+ 1: "first",
56
+ 2: "second",
57
+ 3: "third",
58
+ 4: "fourth",
59
+ [-1]: "last",
60
+ };
61
+ /**
62
+ * Read an RRULE string into its parts.
63
+ *
64
+ * Tolerant on purpose: an unknown key is dropped rather than throwing, because
65
+ * a rule that came from Outlook, from the editor, or from a hand-written import
66
+ * all end up here, and a single unexpected part must not stop a sync.
67
+ */
68
+ function parseRRule(rrule) {
69
+ const parts = {};
70
+ if (!rrule)
71
+ return parts;
72
+ for (const chunk of rrule.replace(/^RRULE:/i, "").split(";")) {
73
+ const [rawKey, rawValue] = chunk.split("=");
74
+ if (!rawKey || !rawValue)
75
+ continue;
76
+ const key = rawKey.trim().toUpperCase();
77
+ const value = rawValue.trim().toUpperCase();
78
+ switch (key) {
79
+ case "FREQ":
80
+ if (value === "DAILY" ||
81
+ value === "WEEKLY" ||
82
+ value === "MONTHLY" ||
83
+ value === "YEARLY") {
84
+ parts.freq = value;
85
+ }
86
+ break;
87
+ case "INTERVAL":
88
+ parts.interval = toPositiveInt(value);
89
+ break;
90
+ case "COUNT":
91
+ parts.count = toPositiveInt(value);
92
+ break;
93
+ case "BYDAY":
94
+ parts.byday = value.split(",").filter(Boolean);
95
+ break;
96
+ case "BYMONTHDAY":
97
+ parts.bymonthday = value.split(",").map(Number).filter(Number.isFinite);
98
+ break;
99
+ case "BYMONTH":
100
+ parts.bymonth = value.split(",").map(Number).filter(Number.isFinite);
101
+ break;
102
+ case "WKST":
103
+ parts.wkst = value;
104
+ break;
105
+ }
106
+ }
107
+ return parts;
108
+ }
109
+ /** Write the parts back into an RRULE string, in the canonical part order. */
110
+ function buildRRule(parts) {
111
+ const chunks = [`FREQ=${parts.freq ?? "DAILY"}`];
112
+ if (parts.interval && parts.interval > 1) {
113
+ chunks.push(`INTERVAL=${parts.interval}`);
114
+ }
115
+ if (parts.byday?.length)
116
+ chunks.push(`BYDAY=${parts.byday.join(",")}`);
117
+ if (parts.bymonthday?.length) {
118
+ chunks.push(`BYMONTHDAY=${parts.bymonthday.join(",")}`);
119
+ }
120
+ if (parts.bymonth?.length)
121
+ chunks.push(`BYMONTH=${parts.bymonth.join(",")}`);
122
+ if (parts.count)
123
+ chunks.push(`COUNT=${parts.count}`);
124
+ if (parts.wkst && parts.wkst !== "MO")
125
+ chunks.push(`WKST=${parts.wkst}`);
126
+ return chunks.join(";");
127
+ }
128
+ /**
129
+ * An Outlook event's timing, as a v2 cadence.
130
+ *
131
+ * A non-recurring event becomes a one-off — the same shape the frontend's
132
+ * `oneOffCadence()` produces (`FREQ=DAILY;INTERVAL=1;COUNT=1`), so a plan
133
+ * created from a single Outlook appointment is indistinguishable from one
134
+ * created by hand.
135
+ */
136
+ function eventToCadence(start, recurrence) {
137
+ const dtstart = start.toISOString();
138
+ if (!recurrence?.pattern) {
139
+ return { kind: "rrule", rrule: "FREQ=DAILY;INTERVAL=1;COUNT=1", dtstart };
140
+ }
141
+ const { pattern, range } = recurrence;
142
+ const parts = { interval: pattern.interval };
143
+ switch (pattern.type) {
144
+ case "daily":
145
+ parts.freq = "DAILY";
146
+ break;
147
+ case "weekly":
148
+ parts.freq = "WEEKLY";
149
+ parts.byday = (pattern.daysOfWeek ?? [])
150
+ .map((day) => WEEKDAY_TO_RRULE[day])
151
+ .filter(Boolean);
152
+ if (pattern.firstDayOfWeek) {
153
+ parts.wkst = WEEKDAY_TO_RRULE[pattern.firstDayOfWeek];
154
+ }
155
+ break;
156
+ case "absoluteMonthly":
157
+ parts.freq = "MONTHLY";
158
+ parts.bymonthday = [pattern.dayOfMonth ?? start.getUTCDate()];
159
+ break;
160
+ case "relativeMonthly":
161
+ parts.freq = "MONTHLY";
162
+ parts.byday = positionalByDay(pattern);
163
+ break;
164
+ case "absoluteYearly":
165
+ parts.freq = "YEARLY";
166
+ parts.bymonth = [pattern.month ?? start.getUTCMonth() + 1];
167
+ parts.bymonthday = [pattern.dayOfMonth ?? start.getUTCDate()];
168
+ break;
169
+ case "relativeYearly":
170
+ parts.freq = "YEARLY";
171
+ parts.bymonth = [pattern.month ?? start.getUTCMonth() + 1];
172
+ parts.byday = positionalByDay(pattern);
173
+ break;
174
+ }
175
+ // A weekly series with no explicit days repeats on the start's weekday.
176
+ if (parts.freq === "WEEKLY" && !parts.byday?.length) {
177
+ parts.byday = [rruleWeekday(start)];
178
+ }
179
+ const cadence = {
180
+ kind: "rrule",
181
+ rrule: buildRRule(parts),
182
+ dtstart,
183
+ };
184
+ if (range?.type === "numbered") {
185
+ cadence.rrule = buildRRule({
186
+ ...parts,
187
+ count: range.numberOfOccurrences,
188
+ });
189
+ }
190
+ else if (range?.type === "endDate" && range.endDate) {
191
+ // Outlook's end date is a plain day and inclusive — the series may still
192
+ // occur on it, so the boundary is the end of that day.
193
+ cadence.until = endOfDay(range.endDate).toISOString();
194
+ }
195
+ return cadence;
196
+ }
197
+ /**
198
+ * A v2 cadence as an Outlook recurrence — `undefined` for a one-off, which
199
+ * Graph expects as an event without a `recurrence` at all.
200
+ *
201
+ * `startDate` of the range is always the series start: Graph rejects a
202
+ * recurrence whose range starts on a different day than the event.
203
+ */
204
+ function cadenceToRecurrence(cadence, start) {
205
+ if (cadence?.kind !== "rrule" || !cadence.rrule)
206
+ return undefined;
207
+ const parts = parseRRule(cadence.rrule);
208
+ // One occurrence is not a series — and Outlook has no way to say "once".
209
+ if (parts.count === 1)
210
+ return undefined;
211
+ const pattern = recurrencePattern(parts, start);
212
+ if (!pattern)
213
+ return undefined;
214
+ return { pattern, range: recurrenceRange(parts, cadence, start) };
215
+ }
216
+ function recurrencePattern(parts, start) {
217
+ const interval = parts.interval && parts.interval > 0 ? parts.interval : 1;
218
+ const positional = parts.byday?.map(splitByDay).find((day) => !!day.ordinal);
219
+ switch (parts.freq ?? "DAILY") {
220
+ case "DAILY":
221
+ return { type: "daily", interval };
222
+ case "WEEKLY": {
223
+ const daysOfWeek = (parts.byday ?? [])
224
+ .map((entry) => RRULE_TO_WEEKDAY[splitByDay(entry).weekday])
225
+ .filter(Boolean);
226
+ return {
227
+ type: "weekly",
228
+ interval,
229
+ daysOfWeek: daysOfWeek.length
230
+ ? daysOfWeek
231
+ : [RRULE_TO_WEEKDAY[rruleWeekday(start)]],
232
+ firstDayOfWeek: RRULE_TO_WEEKDAY[parts.wkst ?? "MO"] ?? "Monday",
233
+ };
234
+ }
235
+ case "MONTHLY":
236
+ if (positional) {
237
+ return {
238
+ type: "relativeMonthly",
239
+ interval,
240
+ daysOfWeek: [RRULE_TO_WEEKDAY[positional.weekday]],
241
+ index: ORDINAL_TO_INDEX[positional.ordinal] ?? "first",
242
+ };
243
+ }
244
+ return {
245
+ type: "absoluteMonthly",
246
+ interval,
247
+ dayOfMonth: parts.bymonthday?.[0] ?? start.getUTCDate(),
248
+ };
249
+ case "YEARLY": {
250
+ const month = parts.bymonth?.[0] ?? start.getUTCMonth() + 1;
251
+ if (positional) {
252
+ return {
253
+ type: "relativeYearly",
254
+ interval,
255
+ month,
256
+ daysOfWeek: [RRULE_TO_WEEKDAY[positional.weekday]],
257
+ index: ORDINAL_TO_INDEX[positional.ordinal] ?? "first",
258
+ };
259
+ }
260
+ return {
261
+ type: "absoluteYearly",
262
+ interval,
263
+ month,
264
+ dayOfMonth: parts.bymonthday?.[0] ?? start.getUTCDate(),
265
+ };
266
+ }
267
+ }
268
+ return undefined;
269
+ }
270
+ function recurrenceRange(parts, cadence, start) {
271
+ const startDate = graphDate(start);
272
+ if (parts.count && parts.count > 1) {
273
+ return { type: "numbered", startDate, numberOfOccurrences: parts.count };
274
+ }
275
+ if (cadence.until) {
276
+ return { type: "endDate", startDate, endDate: graphDate(cadence.until) };
277
+ }
278
+ return { type: "noEnd", startDate };
279
+ }
280
+ /** `["+2TU"]` for "second Tuesday" — the RRULE spelling of Outlook's `index`. */
281
+ function positionalByDay(pattern) {
282
+ const weekday = WEEKDAY_TO_RRULE[pattern.daysOfWeek?.[0] ?? "Monday"];
283
+ const ordinal = INDEX_TO_ORDINAL[pattern.index ?? "first"] ?? 1;
284
+ return [`${ordinal > 0 ? "+" : ""}${ordinal}${weekday}`];
285
+ }
286
+ /** `"+2TU"` → `{ ordinal: 2, weekday: "TU" }`, `"TU"` → `{ weekday: "TU" }`. */
287
+ function splitByDay(entry) {
288
+ const match = /^([+-]?\d+)?([A-Z]{2})$/.exec(entry.trim().toUpperCase());
289
+ if (!match)
290
+ return { weekday: "MO" };
291
+ return {
292
+ ordinal: match[1] ? Number(match[1]) : undefined,
293
+ weekday: match[2],
294
+ };
295
+ }
296
+ function rruleWeekday(date) {
297
+ return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][date.getUTCDay()];
298
+ }
299
+ /** Graph wants a plain `YYYY-MM-DD` in a recurrence range. */
300
+ function graphDate(value) {
301
+ const date = value instanceof Date ? value : new Date(value);
302
+ return date.toISOString().slice(0, 10);
303
+ }
304
+ function endOfDay(day) {
305
+ return new Date(`${day.slice(0, 10)}T23:59:59.999Z`);
306
+ }
307
+ function toPositiveInt(value) {
308
+ const parsed = Number(value);
309
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
310
+ }
@@ -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 {};