@multisystemsuite/timezone-engine-scheduler 1.0.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/index.cjs +155 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var dateFns = require('date-fns');
|
|
4
|
+
var dateFnsTz = require('date-fns-tz');
|
|
5
|
+
var timezoneEngineCore = require('@multisystemsuite/timezone-engine-core');
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
function parseTimeOnDate(date, time, timezone) {
|
|
9
|
+
const parsed = dateFns.parse(time, "HH:mm", date);
|
|
10
|
+
return dateFnsTz.fromZonedTime(parsed, timezone);
|
|
11
|
+
}
|
|
12
|
+
var SchedulerEngine = class {
|
|
13
|
+
config;
|
|
14
|
+
holidays = [];
|
|
15
|
+
shifts = [];
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
updateConfig(config) {
|
|
20
|
+
this.config = { ...this.config, ...config };
|
|
21
|
+
}
|
|
22
|
+
registerHoliday(holiday) {
|
|
23
|
+
this.holidays.push(holiday);
|
|
24
|
+
}
|
|
25
|
+
registerShift(shift) {
|
|
26
|
+
this.shifts.push(shift);
|
|
27
|
+
}
|
|
28
|
+
/** Check if a date falls on a registered holiday */
|
|
29
|
+
isHoliday(date, timezone) {
|
|
30
|
+
const zoned = dateFnsTz.toZonedTime(date, timezone);
|
|
31
|
+
const dateStr = zoned.toISOString().slice(0, 10);
|
|
32
|
+
return this.holidays.some((h) => h.date === dateStr && h.timezone === timezone);
|
|
33
|
+
}
|
|
34
|
+
/** Check if time is within business hours */
|
|
35
|
+
isWithinBusinessHours(date, timezone, hours) {
|
|
36
|
+
const bh = hours ?? this.config.businessHours;
|
|
37
|
+
const zoned = dateFnsTz.toZonedTime(date, timezone);
|
|
38
|
+
const day = zoned.getDay();
|
|
39
|
+
if (!bh.days.includes(day)) return bh;
|
|
40
|
+
parseTimeOnDate(zoned, bh.start, timezone);
|
|
41
|
+
parseTimeOnDate(zoned, bh.end, timezone);
|
|
42
|
+
return bh;
|
|
43
|
+
}
|
|
44
|
+
isBusinessHour(date, timezone, hours) {
|
|
45
|
+
const bh = hours ?? this.config.businessHours;
|
|
46
|
+
const zoned = dateFnsTz.toZonedTime(date, timezone);
|
|
47
|
+
const day = zoned.getDay();
|
|
48
|
+
if (!bh.days.includes(day)) return false;
|
|
49
|
+
if (this.isHoliday(date, timezone)) return false;
|
|
50
|
+
const start = parseTimeOnDate(zoned, bh.start, timezone);
|
|
51
|
+
const end = parseTimeOnDate(zoned, bh.end, timezone);
|
|
52
|
+
return dateFns.isWithinInterval(zoned, { start, end });
|
|
53
|
+
}
|
|
54
|
+
/** Calculate meeting overlap across participants */
|
|
55
|
+
calculateMeetingOverlap(slots) {
|
|
56
|
+
if (slots.length < 2) {
|
|
57
|
+
return { hasOverlap: false, overlapDurationMinutes: 0, participants: slots.map((s) => s.timezone) };
|
|
58
|
+
}
|
|
59
|
+
const intervals = slots.map((s) => ({ start: s.start, end: s.end }));
|
|
60
|
+
let hasOverlap = false;
|
|
61
|
+
let overlapStart;
|
|
62
|
+
let overlapEnd;
|
|
63
|
+
for (let i = 0; i < intervals.length - 1; i++) {
|
|
64
|
+
for (let j = i + 1; j < intervals.length; j++) {
|
|
65
|
+
const a = intervals[i];
|
|
66
|
+
const b = intervals[j];
|
|
67
|
+
if (a && b && dateFns.areIntervalsOverlapping(a, b)) {
|
|
68
|
+
hasOverlap = true;
|
|
69
|
+
const start = a.start > b.start ? a.start : b.start;
|
|
70
|
+
const end = a.end < b.end ? a.end : b.end;
|
|
71
|
+
if (!overlapStart || start > overlapStart) overlapStart = start;
|
|
72
|
+
if (!overlapEnd || end < overlapEnd) overlapEnd = end;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const duration = overlapStart && overlapEnd ? (overlapEnd.getTime() - overlapStart.getTime()) / 6e4 : 0;
|
|
77
|
+
const result = {
|
|
78
|
+
hasOverlap,
|
|
79
|
+
overlapDurationMinutes: duration,
|
|
80
|
+
participants: slots.map((s) => s.timezone)
|
|
81
|
+
};
|
|
82
|
+
if (overlapStart) result.overlapStart = overlapStart;
|
|
83
|
+
if (overlapEnd) result.overlapEnd = overlapEnd;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/** Find available meeting slots across timezones */
|
|
87
|
+
findAvailableSlots(participantTimezones, date, durationMinutes) {
|
|
88
|
+
const duration = durationMinutes ?? this.config.slotDurationMinutes;
|
|
89
|
+
const { startHourUTC, endHourUTC, overlapHours } = timezoneEngineCore.findCommonWorkingHours(
|
|
90
|
+
participantTimezones,
|
|
91
|
+
parseInt(this.config.businessHours.start.split(":")[0] ?? "9", 10),
|
|
92
|
+
parseInt(this.config.businessHours.end.split(":")[0] ?? "17", 10),
|
|
93
|
+
date
|
|
94
|
+
);
|
|
95
|
+
if (overlapHours <= 0) return [];
|
|
96
|
+
const slots = [];
|
|
97
|
+
const startMs = date.getTime();
|
|
98
|
+
const slotStart = new Date(startMs);
|
|
99
|
+
slotStart.setUTCHours(Math.floor(startHourUTC), startHourUTC % 1 * 60, 0, 0);
|
|
100
|
+
let current = slotStart;
|
|
101
|
+
const endTime = new Date(startMs);
|
|
102
|
+
endTime.setUTCHours(Math.floor(endHourUTC), endHourUTC % 1 * 60, 0, 0);
|
|
103
|
+
while (current < endTime) {
|
|
104
|
+
const slotEnd = dateFns.addMinutes(current, duration);
|
|
105
|
+
if (slotEnd <= endTime) {
|
|
106
|
+
slots.push({
|
|
107
|
+
start: current,
|
|
108
|
+
end: slotEnd,
|
|
109
|
+
timezone: this.config.defaultTimezone
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
current = dateFns.addMinutes(current, duration + (this.config.bufferMinutes ?? 0));
|
|
113
|
+
}
|
|
114
|
+
return slots;
|
|
115
|
+
}
|
|
116
|
+
/** Smart meeting planner — suggest best slot */
|
|
117
|
+
suggestMeetingTime(participantTimezones, date, durationMinutes) {
|
|
118
|
+
const slots = this.findAvailableSlots(participantTimezones, date, durationMinutes);
|
|
119
|
+
return slots[0] ?? null;
|
|
120
|
+
}
|
|
121
|
+
/** Global workforce shift support */
|
|
122
|
+
getActiveShifts(date) {
|
|
123
|
+
const day = date.getDay();
|
|
124
|
+
return this.shifts.filter((shift) => {
|
|
125
|
+
const zoned = dateFnsTz.toZonedTime(date, shift.timezone);
|
|
126
|
+
return shift.days.includes(zoned.getDay()) || shift.days.includes(day);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Payroll timezone — compute shift hours in UTC */
|
|
130
|
+
computeShiftHoursUTC(shift, date) {
|
|
131
|
+
const zoned = dateFnsTz.toZonedTime(date, shift.timezone);
|
|
132
|
+
const start = parseTimeOnDate(zoned, shift.startTime, shift.timezone);
|
|
133
|
+
const end = parseTimeOnDate(zoned, shift.endTime, shift.timezone);
|
|
134
|
+
return { start: dateFnsTz.fromZonedTime(start, shift.timezone), end: dateFnsTz.fromZonedTime(end, shift.timezone) };
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function createSchedulerEngine(config) {
|
|
138
|
+
return new SchedulerEngine(config);
|
|
139
|
+
}
|
|
140
|
+
var defaultSchedulerConfig = {
|
|
141
|
+
defaultTimezone: "UTC",
|
|
142
|
+
businessHours: {
|
|
143
|
+
start: "09:00",
|
|
144
|
+
end: "17:00",
|
|
145
|
+
days: [1, 2, 3, 4, 5]
|
|
146
|
+
},
|
|
147
|
+
slotDurationMinutes: 30,
|
|
148
|
+
bufferMinutes: 15
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
exports.SchedulerEngine = SchedulerEngine;
|
|
152
|
+
exports.createSchedulerEngine = createSchedulerEngine;
|
|
153
|
+
exports.defaultSchedulerConfig = defaultSchedulerConfig;
|
|
154
|
+
//# sourceMappingURL=index.cjs.map
|
|
155
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["parse","fromZonedTime","toZonedTime","isWithinInterval","areIntervalsOverlapping","findCommonWorkingHours","addMinutes"],"mappings":"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","file":"index.cjs","sourcesContent":["import { addMinutes, isWithinInterval, parse, areIntervalsOverlapping } from \"date-fns\";\nimport { toZonedTime, fromZonedTime } from \"date-fns-tz\";\nimport type {\n SchedulerConfig,\n MeetingSlot,\n MeetingOverlapResult,\n ShiftDefinition,\n HolidayDefinition,\n BusinessHours,\n} from \"@multisystemsuite/timezone-engine-shared-types\";\nimport { findCommonWorkingHours } from \"@multisystemsuite/timezone-engine-core\";\n\nfunction parseTimeOnDate(date: Date, time: string, timezone: string): Date {\n const parsed = parse(time, \"HH:mm\", date);\n return fromZonedTime(parsed, timezone);\n}\n\n/** Enterprise scheduler engine */\nexport class SchedulerEngine {\n private config: SchedulerConfig;\n private holidays: HolidayDefinition[] = [];\n private shifts: ShiftDefinition[] = [];\n\n constructor(config: SchedulerConfig) {\n this.config = config;\n }\n\n updateConfig(config: Partial<SchedulerConfig>): void {\n this.config = { ...this.config, ...config };\n }\n\n registerHoliday(holiday: HolidayDefinition): void {\n this.holidays.push(holiday);\n }\n\n registerShift(shift: ShiftDefinition): void {\n this.shifts.push(shift);\n }\n\n /** Check if a date falls on a registered holiday */\n isHoliday(date: Date, timezone: string): boolean {\n const zoned = toZonedTime(date, timezone);\n const dateStr = zoned.toISOString().slice(0, 10);\n return this.holidays.some((h) => h.date === dateStr && h.timezone === timezone);\n }\n\n /** Check if time is within business hours */\n isWithinBusinessHours(date: Date, timezone: string, hours?: BusinessHours): BusinessHours {\n const bh = hours ?? this.config.businessHours;\n const zoned = toZonedTime(date, timezone);\n const day = zoned.getDay();\n if (!bh.days.includes(day)) return bh;\n\n const start = parseTimeOnDate(zoned, bh.start, timezone);\n const end = parseTimeOnDate(zoned, bh.end, timezone);\n\n return bh;\n }\n\n isBusinessHour(date: Date, timezone: string, hours?: BusinessHours): boolean {\n const bh = hours ?? this.config.businessHours;\n const zoned = toZonedTime(date, timezone);\n const day = zoned.getDay();\n if (!bh.days.includes(day)) return false;\n if (this.isHoliday(date, timezone)) return false;\n\n const start = parseTimeOnDate(zoned, bh.start, timezone);\n const end = parseTimeOnDate(zoned, bh.end, timezone);\n return isWithinInterval(zoned, { start, end });\n }\n\n /** Calculate meeting overlap across participants */\n calculateMeetingOverlap(slots: MeetingSlot[]): MeetingOverlapResult {\n if (slots.length < 2) {\n return { hasOverlap: false, overlapDurationMinutes: 0, participants: slots.map((s) => s.timezone) };\n }\n\n const intervals = slots.map((s) => ({ start: s.start, end: s.end }));\n let hasOverlap = false;\n let overlapStart: Date | undefined;\n let overlapEnd: Date | undefined;\n\n for (let i = 0; i < intervals.length - 1; i++) {\n for (let j = i + 1; j < intervals.length; j++) {\n const a = intervals[i];\n const b = intervals[j];\n if (a && b && areIntervalsOverlapping(a, b)) {\n hasOverlap = true;\n const start = a.start > b.start ? a.start : b.start;\n const end = a.end < b.end ? a.end : b.end;\n if (!overlapStart || start > overlapStart) overlapStart = start;\n if (!overlapEnd || end < overlapEnd) overlapEnd = end;\n }\n }\n }\n\n const duration =\n overlapStart && overlapEnd\n ? (overlapEnd.getTime() - overlapStart.getTime()) / 60_000\n : 0;\n\n const result: MeetingOverlapResult = {\n hasOverlap,\n overlapDurationMinutes: duration,\n participants: slots.map((s) => s.timezone),\n };\n\n if (overlapStart) result.overlapStart = overlapStart;\n if (overlapEnd) result.overlapEnd = overlapEnd;\n\n return result;\n }\n\n /** Find available meeting slots across timezones */\n findAvailableSlots(\n participantTimezones: string[],\n date: Date,\n durationMinutes?: number,\n ): MeetingSlot[] {\n const duration = durationMinutes ?? this.config.slotDurationMinutes;\n const { startHourUTC, endHourUTC, overlapHours } = findCommonWorkingHours(\n participantTimezones,\n parseInt(this.config.businessHours.start.split(\":\")[0] ?? \"9\", 10),\n parseInt(this.config.businessHours.end.split(\":\")[0] ?? \"17\", 10),\n date,\n );\n\n if (overlapHours <= 0) return [];\n\n const slots: MeetingSlot[] = [];\n const startMs = date.getTime();\n const slotStart = new Date(startMs);\n slotStart.setUTCHours(Math.floor(startHourUTC), (startHourUTC % 1) * 60, 0, 0);\n\n let current = slotStart;\n const endTime = new Date(startMs);\n endTime.setUTCHours(Math.floor(endHourUTC), (endHourUTC % 1) * 60, 0, 0);\n\n while (current < endTime) {\n const slotEnd = addMinutes(current, duration);\n if (slotEnd <= endTime) {\n slots.push({\n start: current,\n end: slotEnd,\n timezone: this.config.defaultTimezone,\n });\n }\n current = addMinutes(current, duration + (this.config.bufferMinutes ?? 0));\n }\n\n return slots;\n }\n\n /** Smart meeting planner — suggest best slot */\n suggestMeetingTime(\n participantTimezones: string[],\n date: Date,\n durationMinutes?: number,\n ): MeetingSlot | null {\n const slots = this.findAvailableSlots(participantTimezones, date, durationMinutes);\n return slots[0] ?? null;\n }\n\n /** Global workforce shift support */\n getActiveShifts(date: Date): ShiftDefinition[] {\n const day = date.getDay();\n return this.shifts.filter((shift) => {\n const zoned = toZonedTime(date, shift.timezone);\n return shift.days.includes(zoned.getDay()) || shift.days.includes(day);\n });\n }\n\n /** Payroll timezone — compute shift hours in UTC */\n computeShiftHoursUTC(shift: ShiftDefinition, date: Date): { start: Date; end: Date } {\n const zoned = toZonedTime(date, shift.timezone);\n const start = parseTimeOnDate(zoned, shift.startTime, shift.timezone);\n const end = parseTimeOnDate(zoned, shift.endTime, shift.timezone);\n return { start: fromZonedTime(start, shift.timezone), end: fromZonedTime(end, shift.timezone) };\n }\n}\n\nexport function createSchedulerEngine(config: SchedulerConfig): SchedulerEngine {\n return new SchedulerEngine(config);\n}\n\nexport const defaultSchedulerConfig: SchedulerConfig = {\n defaultTimezone: \"UTC\",\n businessHours: {\n start: \"09:00\",\n end: \"17:00\",\n days: [1, 2, 3, 4, 5],\n },\n slotDurationMinutes: 30,\n bufferMinutes: 15,\n};\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SchedulerConfig, HolidayDefinition, ShiftDefinition, BusinessHours, MeetingSlot, MeetingOverlapResult } from '@multisystemsuite/timezone-engine-shared-types';
|
|
2
|
+
|
|
3
|
+
/** Enterprise scheduler engine */
|
|
4
|
+
declare class SchedulerEngine {
|
|
5
|
+
private config;
|
|
6
|
+
private holidays;
|
|
7
|
+
private shifts;
|
|
8
|
+
constructor(config: SchedulerConfig);
|
|
9
|
+
updateConfig(config: Partial<SchedulerConfig>): void;
|
|
10
|
+
registerHoliday(holiday: HolidayDefinition): void;
|
|
11
|
+
registerShift(shift: ShiftDefinition): void;
|
|
12
|
+
/** Check if a date falls on a registered holiday */
|
|
13
|
+
isHoliday(date: Date, timezone: string): boolean;
|
|
14
|
+
/** Check if time is within business hours */
|
|
15
|
+
isWithinBusinessHours(date: Date, timezone: string, hours?: BusinessHours): BusinessHours;
|
|
16
|
+
isBusinessHour(date: Date, timezone: string, hours?: BusinessHours): boolean;
|
|
17
|
+
/** Calculate meeting overlap across participants */
|
|
18
|
+
calculateMeetingOverlap(slots: MeetingSlot[]): MeetingOverlapResult;
|
|
19
|
+
/** Find available meeting slots across timezones */
|
|
20
|
+
findAvailableSlots(participantTimezones: string[], date: Date, durationMinutes?: number): MeetingSlot[];
|
|
21
|
+
/** Smart meeting planner — suggest best slot */
|
|
22
|
+
suggestMeetingTime(participantTimezones: string[], date: Date, durationMinutes?: number): MeetingSlot | null;
|
|
23
|
+
/** Global workforce shift support */
|
|
24
|
+
getActiveShifts(date: Date): ShiftDefinition[];
|
|
25
|
+
/** Payroll timezone — compute shift hours in UTC */
|
|
26
|
+
computeShiftHoursUTC(shift: ShiftDefinition, date: Date): {
|
|
27
|
+
start: Date;
|
|
28
|
+
end: Date;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
declare function createSchedulerEngine(config: SchedulerConfig): SchedulerEngine;
|
|
32
|
+
declare const defaultSchedulerConfig: SchedulerConfig;
|
|
33
|
+
|
|
34
|
+
export { SchedulerEngine, createSchedulerEngine, defaultSchedulerConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SchedulerConfig, HolidayDefinition, ShiftDefinition, BusinessHours, MeetingSlot, MeetingOverlapResult } from '@multisystemsuite/timezone-engine-shared-types';
|
|
2
|
+
|
|
3
|
+
/** Enterprise scheduler engine */
|
|
4
|
+
declare class SchedulerEngine {
|
|
5
|
+
private config;
|
|
6
|
+
private holidays;
|
|
7
|
+
private shifts;
|
|
8
|
+
constructor(config: SchedulerConfig);
|
|
9
|
+
updateConfig(config: Partial<SchedulerConfig>): void;
|
|
10
|
+
registerHoliday(holiday: HolidayDefinition): void;
|
|
11
|
+
registerShift(shift: ShiftDefinition): void;
|
|
12
|
+
/** Check if a date falls on a registered holiday */
|
|
13
|
+
isHoliday(date: Date, timezone: string): boolean;
|
|
14
|
+
/** Check if time is within business hours */
|
|
15
|
+
isWithinBusinessHours(date: Date, timezone: string, hours?: BusinessHours): BusinessHours;
|
|
16
|
+
isBusinessHour(date: Date, timezone: string, hours?: BusinessHours): boolean;
|
|
17
|
+
/** Calculate meeting overlap across participants */
|
|
18
|
+
calculateMeetingOverlap(slots: MeetingSlot[]): MeetingOverlapResult;
|
|
19
|
+
/** Find available meeting slots across timezones */
|
|
20
|
+
findAvailableSlots(participantTimezones: string[], date: Date, durationMinutes?: number): MeetingSlot[];
|
|
21
|
+
/** Smart meeting planner — suggest best slot */
|
|
22
|
+
suggestMeetingTime(participantTimezones: string[], date: Date, durationMinutes?: number): MeetingSlot | null;
|
|
23
|
+
/** Global workforce shift support */
|
|
24
|
+
getActiveShifts(date: Date): ShiftDefinition[];
|
|
25
|
+
/** Payroll timezone — compute shift hours in UTC */
|
|
26
|
+
computeShiftHoursUTC(shift: ShiftDefinition, date: Date): {
|
|
27
|
+
start: Date;
|
|
28
|
+
end: Date;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
declare function createSchedulerEngine(config: SchedulerConfig): SchedulerEngine;
|
|
32
|
+
declare const defaultSchedulerConfig: SchedulerConfig;
|
|
33
|
+
|
|
34
|
+
export { SchedulerEngine, createSchedulerEngine, defaultSchedulerConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isWithinInterval, areIntervalsOverlapping, addMinutes, parse } from 'date-fns';
|
|
2
|
+
import { toZonedTime, fromZonedTime } from 'date-fns-tz';
|
|
3
|
+
import { findCommonWorkingHours } from '@multisystemsuite/timezone-engine-core';
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function parseTimeOnDate(date, time, timezone) {
|
|
7
|
+
const parsed = parse(time, "HH:mm", date);
|
|
8
|
+
return fromZonedTime(parsed, timezone);
|
|
9
|
+
}
|
|
10
|
+
var SchedulerEngine = class {
|
|
11
|
+
config;
|
|
12
|
+
holidays = [];
|
|
13
|
+
shifts = [];
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
updateConfig(config) {
|
|
18
|
+
this.config = { ...this.config, ...config };
|
|
19
|
+
}
|
|
20
|
+
registerHoliday(holiday) {
|
|
21
|
+
this.holidays.push(holiday);
|
|
22
|
+
}
|
|
23
|
+
registerShift(shift) {
|
|
24
|
+
this.shifts.push(shift);
|
|
25
|
+
}
|
|
26
|
+
/** Check if a date falls on a registered holiday */
|
|
27
|
+
isHoliday(date, timezone) {
|
|
28
|
+
const zoned = toZonedTime(date, timezone);
|
|
29
|
+
const dateStr = zoned.toISOString().slice(0, 10);
|
|
30
|
+
return this.holidays.some((h) => h.date === dateStr && h.timezone === timezone);
|
|
31
|
+
}
|
|
32
|
+
/** Check if time is within business hours */
|
|
33
|
+
isWithinBusinessHours(date, timezone, hours) {
|
|
34
|
+
const bh = hours ?? this.config.businessHours;
|
|
35
|
+
const zoned = toZonedTime(date, timezone);
|
|
36
|
+
const day = zoned.getDay();
|
|
37
|
+
if (!bh.days.includes(day)) return bh;
|
|
38
|
+
parseTimeOnDate(zoned, bh.start, timezone);
|
|
39
|
+
parseTimeOnDate(zoned, bh.end, timezone);
|
|
40
|
+
return bh;
|
|
41
|
+
}
|
|
42
|
+
isBusinessHour(date, timezone, hours) {
|
|
43
|
+
const bh = hours ?? this.config.businessHours;
|
|
44
|
+
const zoned = toZonedTime(date, timezone);
|
|
45
|
+
const day = zoned.getDay();
|
|
46
|
+
if (!bh.days.includes(day)) return false;
|
|
47
|
+
if (this.isHoliday(date, timezone)) return false;
|
|
48
|
+
const start = parseTimeOnDate(zoned, bh.start, timezone);
|
|
49
|
+
const end = parseTimeOnDate(zoned, bh.end, timezone);
|
|
50
|
+
return isWithinInterval(zoned, { start, end });
|
|
51
|
+
}
|
|
52
|
+
/** Calculate meeting overlap across participants */
|
|
53
|
+
calculateMeetingOverlap(slots) {
|
|
54
|
+
if (slots.length < 2) {
|
|
55
|
+
return { hasOverlap: false, overlapDurationMinutes: 0, participants: slots.map((s) => s.timezone) };
|
|
56
|
+
}
|
|
57
|
+
const intervals = slots.map((s) => ({ start: s.start, end: s.end }));
|
|
58
|
+
let hasOverlap = false;
|
|
59
|
+
let overlapStart;
|
|
60
|
+
let overlapEnd;
|
|
61
|
+
for (let i = 0; i < intervals.length - 1; i++) {
|
|
62
|
+
for (let j = i + 1; j < intervals.length; j++) {
|
|
63
|
+
const a = intervals[i];
|
|
64
|
+
const b = intervals[j];
|
|
65
|
+
if (a && b && areIntervalsOverlapping(a, b)) {
|
|
66
|
+
hasOverlap = true;
|
|
67
|
+
const start = a.start > b.start ? a.start : b.start;
|
|
68
|
+
const end = a.end < b.end ? a.end : b.end;
|
|
69
|
+
if (!overlapStart || start > overlapStart) overlapStart = start;
|
|
70
|
+
if (!overlapEnd || end < overlapEnd) overlapEnd = end;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const duration = overlapStart && overlapEnd ? (overlapEnd.getTime() - overlapStart.getTime()) / 6e4 : 0;
|
|
75
|
+
const result = {
|
|
76
|
+
hasOverlap,
|
|
77
|
+
overlapDurationMinutes: duration,
|
|
78
|
+
participants: slots.map((s) => s.timezone)
|
|
79
|
+
};
|
|
80
|
+
if (overlapStart) result.overlapStart = overlapStart;
|
|
81
|
+
if (overlapEnd) result.overlapEnd = overlapEnd;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/** Find available meeting slots across timezones */
|
|
85
|
+
findAvailableSlots(participantTimezones, date, durationMinutes) {
|
|
86
|
+
const duration = durationMinutes ?? this.config.slotDurationMinutes;
|
|
87
|
+
const { startHourUTC, endHourUTC, overlapHours } = findCommonWorkingHours(
|
|
88
|
+
participantTimezones,
|
|
89
|
+
parseInt(this.config.businessHours.start.split(":")[0] ?? "9", 10),
|
|
90
|
+
parseInt(this.config.businessHours.end.split(":")[0] ?? "17", 10),
|
|
91
|
+
date
|
|
92
|
+
);
|
|
93
|
+
if (overlapHours <= 0) return [];
|
|
94
|
+
const slots = [];
|
|
95
|
+
const startMs = date.getTime();
|
|
96
|
+
const slotStart = new Date(startMs);
|
|
97
|
+
slotStart.setUTCHours(Math.floor(startHourUTC), startHourUTC % 1 * 60, 0, 0);
|
|
98
|
+
let current = slotStart;
|
|
99
|
+
const endTime = new Date(startMs);
|
|
100
|
+
endTime.setUTCHours(Math.floor(endHourUTC), endHourUTC % 1 * 60, 0, 0);
|
|
101
|
+
while (current < endTime) {
|
|
102
|
+
const slotEnd = addMinutes(current, duration);
|
|
103
|
+
if (slotEnd <= endTime) {
|
|
104
|
+
slots.push({
|
|
105
|
+
start: current,
|
|
106
|
+
end: slotEnd,
|
|
107
|
+
timezone: this.config.defaultTimezone
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
current = addMinutes(current, duration + (this.config.bufferMinutes ?? 0));
|
|
111
|
+
}
|
|
112
|
+
return slots;
|
|
113
|
+
}
|
|
114
|
+
/** Smart meeting planner — suggest best slot */
|
|
115
|
+
suggestMeetingTime(participantTimezones, date, durationMinutes) {
|
|
116
|
+
const slots = this.findAvailableSlots(participantTimezones, date, durationMinutes);
|
|
117
|
+
return slots[0] ?? null;
|
|
118
|
+
}
|
|
119
|
+
/** Global workforce shift support */
|
|
120
|
+
getActiveShifts(date) {
|
|
121
|
+
const day = date.getDay();
|
|
122
|
+
return this.shifts.filter((shift) => {
|
|
123
|
+
const zoned = toZonedTime(date, shift.timezone);
|
|
124
|
+
return shift.days.includes(zoned.getDay()) || shift.days.includes(day);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Payroll timezone — compute shift hours in UTC */
|
|
128
|
+
computeShiftHoursUTC(shift, date) {
|
|
129
|
+
const zoned = toZonedTime(date, shift.timezone);
|
|
130
|
+
const start = parseTimeOnDate(zoned, shift.startTime, shift.timezone);
|
|
131
|
+
const end = parseTimeOnDate(zoned, shift.endTime, shift.timezone);
|
|
132
|
+
return { start: fromZonedTime(start, shift.timezone), end: fromZonedTime(end, shift.timezone) };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function createSchedulerEngine(config) {
|
|
136
|
+
return new SchedulerEngine(config);
|
|
137
|
+
}
|
|
138
|
+
var defaultSchedulerConfig = {
|
|
139
|
+
defaultTimezone: "UTC",
|
|
140
|
+
businessHours: {
|
|
141
|
+
start: "09:00",
|
|
142
|
+
end: "17:00",
|
|
143
|
+
days: [1, 2, 3, 4, 5]
|
|
144
|
+
},
|
|
145
|
+
slotDurationMinutes: 30,
|
|
146
|
+
bufferMinutes: 15
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export { SchedulerEngine, createSchedulerEngine, defaultSchedulerConfig };
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { addMinutes, isWithinInterval, parse, areIntervalsOverlapping } from \"date-fns\";\nimport { toZonedTime, fromZonedTime } from \"date-fns-tz\";\nimport type {\n SchedulerConfig,\n MeetingSlot,\n MeetingOverlapResult,\n ShiftDefinition,\n HolidayDefinition,\n BusinessHours,\n} from \"@multisystemsuite/timezone-engine-shared-types\";\nimport { findCommonWorkingHours } from \"@multisystemsuite/timezone-engine-core\";\n\nfunction parseTimeOnDate(date: Date, time: string, timezone: string): Date {\n const parsed = parse(time, \"HH:mm\", date);\n return fromZonedTime(parsed, timezone);\n}\n\n/** Enterprise scheduler engine */\nexport class SchedulerEngine {\n private config: SchedulerConfig;\n private holidays: HolidayDefinition[] = [];\n private shifts: ShiftDefinition[] = [];\n\n constructor(config: SchedulerConfig) {\n this.config = config;\n }\n\n updateConfig(config: Partial<SchedulerConfig>): void {\n this.config = { ...this.config, ...config };\n }\n\n registerHoliday(holiday: HolidayDefinition): void {\n this.holidays.push(holiday);\n }\n\n registerShift(shift: ShiftDefinition): void {\n this.shifts.push(shift);\n }\n\n /** Check if a date falls on a registered holiday */\n isHoliday(date: Date, timezone: string): boolean {\n const zoned = toZonedTime(date, timezone);\n const dateStr = zoned.toISOString().slice(0, 10);\n return this.holidays.some((h) => h.date === dateStr && h.timezone === timezone);\n }\n\n /** Check if time is within business hours */\n isWithinBusinessHours(date: Date, timezone: string, hours?: BusinessHours): BusinessHours {\n const bh = hours ?? this.config.businessHours;\n const zoned = toZonedTime(date, timezone);\n const day = zoned.getDay();\n if (!bh.days.includes(day)) return bh;\n\n const start = parseTimeOnDate(zoned, bh.start, timezone);\n const end = parseTimeOnDate(zoned, bh.end, timezone);\n\n return bh;\n }\n\n isBusinessHour(date: Date, timezone: string, hours?: BusinessHours): boolean {\n const bh = hours ?? this.config.businessHours;\n const zoned = toZonedTime(date, timezone);\n const day = zoned.getDay();\n if (!bh.days.includes(day)) return false;\n if (this.isHoliday(date, timezone)) return false;\n\n const start = parseTimeOnDate(zoned, bh.start, timezone);\n const end = parseTimeOnDate(zoned, bh.end, timezone);\n return isWithinInterval(zoned, { start, end });\n }\n\n /** Calculate meeting overlap across participants */\n calculateMeetingOverlap(slots: MeetingSlot[]): MeetingOverlapResult {\n if (slots.length < 2) {\n return { hasOverlap: false, overlapDurationMinutes: 0, participants: slots.map((s) => s.timezone) };\n }\n\n const intervals = slots.map((s) => ({ start: s.start, end: s.end }));\n let hasOverlap = false;\n let overlapStart: Date | undefined;\n let overlapEnd: Date | undefined;\n\n for (let i = 0; i < intervals.length - 1; i++) {\n for (let j = i + 1; j < intervals.length; j++) {\n const a = intervals[i];\n const b = intervals[j];\n if (a && b && areIntervalsOverlapping(a, b)) {\n hasOverlap = true;\n const start = a.start > b.start ? a.start : b.start;\n const end = a.end < b.end ? a.end : b.end;\n if (!overlapStart || start > overlapStart) overlapStart = start;\n if (!overlapEnd || end < overlapEnd) overlapEnd = end;\n }\n }\n }\n\n const duration =\n overlapStart && overlapEnd\n ? (overlapEnd.getTime() - overlapStart.getTime()) / 60_000\n : 0;\n\n const result: MeetingOverlapResult = {\n hasOverlap,\n overlapDurationMinutes: duration,\n participants: slots.map((s) => s.timezone),\n };\n\n if (overlapStart) result.overlapStart = overlapStart;\n if (overlapEnd) result.overlapEnd = overlapEnd;\n\n return result;\n }\n\n /** Find available meeting slots across timezones */\n findAvailableSlots(\n participantTimezones: string[],\n date: Date,\n durationMinutes?: number,\n ): MeetingSlot[] {\n const duration = durationMinutes ?? this.config.slotDurationMinutes;\n const { startHourUTC, endHourUTC, overlapHours } = findCommonWorkingHours(\n participantTimezones,\n parseInt(this.config.businessHours.start.split(\":\")[0] ?? \"9\", 10),\n parseInt(this.config.businessHours.end.split(\":\")[0] ?? \"17\", 10),\n date,\n );\n\n if (overlapHours <= 0) return [];\n\n const slots: MeetingSlot[] = [];\n const startMs = date.getTime();\n const slotStart = new Date(startMs);\n slotStart.setUTCHours(Math.floor(startHourUTC), (startHourUTC % 1) * 60, 0, 0);\n\n let current = slotStart;\n const endTime = new Date(startMs);\n endTime.setUTCHours(Math.floor(endHourUTC), (endHourUTC % 1) * 60, 0, 0);\n\n while (current < endTime) {\n const slotEnd = addMinutes(current, duration);\n if (slotEnd <= endTime) {\n slots.push({\n start: current,\n end: slotEnd,\n timezone: this.config.defaultTimezone,\n });\n }\n current = addMinutes(current, duration + (this.config.bufferMinutes ?? 0));\n }\n\n return slots;\n }\n\n /** Smart meeting planner — suggest best slot */\n suggestMeetingTime(\n participantTimezones: string[],\n date: Date,\n durationMinutes?: number,\n ): MeetingSlot | null {\n const slots = this.findAvailableSlots(participantTimezones, date, durationMinutes);\n return slots[0] ?? null;\n }\n\n /** Global workforce shift support */\n getActiveShifts(date: Date): ShiftDefinition[] {\n const day = date.getDay();\n return this.shifts.filter((shift) => {\n const zoned = toZonedTime(date, shift.timezone);\n return shift.days.includes(zoned.getDay()) || shift.days.includes(day);\n });\n }\n\n /** Payroll timezone — compute shift hours in UTC */\n computeShiftHoursUTC(shift: ShiftDefinition, date: Date): { start: Date; end: Date } {\n const zoned = toZonedTime(date, shift.timezone);\n const start = parseTimeOnDate(zoned, shift.startTime, shift.timezone);\n const end = parseTimeOnDate(zoned, shift.endTime, shift.timezone);\n return { start: fromZonedTime(start, shift.timezone), end: fromZonedTime(end, shift.timezone) };\n }\n}\n\nexport function createSchedulerEngine(config: SchedulerConfig): SchedulerEngine {\n return new SchedulerEngine(config);\n}\n\nexport const defaultSchedulerConfig: SchedulerConfig = {\n defaultTimezone: \"UTC\",\n businessHours: {\n start: \"09:00\",\n end: \"17:00\",\n days: [1, 2, 3, 4, 5],\n },\n slotDurationMinutes: 30,\n bufferMinutes: 15,\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@multisystemsuite/timezone-engine-scheduler",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Enterprise scheduling engine for @multisystemsuite/timezone-engine",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "MultiSystemSuite",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/multisystemsuite/timezone-engine.git",
|
|
10
|
+
"directory": "packages/timezone-scheduler"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"date-fns": "^4.1.0",
|
|
37
|
+
"date-fns-tz": "^3.2.0",
|
|
38
|
+
"@multisystemsuite/timezone-engine-core": "1.0.0",
|
|
39
|
+
"@multisystemsuite/timezone-engine-shared-types": "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"rimraf": "^6.0.1",
|
|
43
|
+
"tsup": "^8.3.5",
|
|
44
|
+
"typescript": "^5.7.2",
|
|
45
|
+
"vitest": "^2.1.8"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch",
|
|
50
|
+
"clean": "rimraf dist",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"lint": "eslint src",
|
|
53
|
+
"lint:fix": "eslint src --fix",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:coverage": "vitest run --coverage"
|
|
56
|
+
}
|
|
57
|
+
}
|