@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
|
@@ -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
|
+
}
|
package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts
ADDED
|
@@ -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;
|
package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js
ADDED
|
@@ -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 {};
|