@aleonnet/healthcare-scheduler 0.1.6

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 (170) hide show
  1. package/README.md +264 -0
  2. package/dist/core/EventBus.d.ts +14 -0
  3. package/dist/core/EventBus.d.ts.map +1 -0
  4. package/dist/core/EventBus.js +47 -0
  5. package/dist/core/EventBus.js.map +1 -0
  6. package/dist/core/HealthcareScheduler.d.ts +63 -0
  7. package/dist/core/HealthcareScheduler.d.ts.map +1 -0
  8. package/dist/core/HealthcareScheduler.js +269 -0
  9. package/dist/core/HealthcareScheduler.js.map +1 -0
  10. package/dist/core/types.d.ts +234 -0
  11. package/dist/core/types.d.ts.map +1 -0
  12. package/dist/core/types.js +3 -0
  13. package/dist/core/types.js.map +1 -0
  14. package/dist/database/adapters/MockAdapter.d.ts +37 -0
  15. package/dist/database/adapters/MockAdapter.d.ts.map +1 -0
  16. package/dist/database/adapters/MockAdapter.js +518 -0
  17. package/dist/database/adapters/MockAdapter.js.map +1 -0
  18. package/dist/database/adapters/SQLiteAdapter.d.ts +40 -0
  19. package/dist/database/adapters/SQLiteAdapter.d.ts.map +1 -0
  20. package/dist/database/adapters/SQLiteAdapter.js +147 -0
  21. package/dist/database/adapters/SQLiteAdapter.js.map +1 -0
  22. package/dist/database/adapters/SupabaseAdapter.d.ts +20 -0
  23. package/dist/database/adapters/SupabaseAdapter.d.ts.map +1 -0
  24. package/dist/database/adapters/SupabaseAdapter.js +310 -0
  25. package/dist/database/adapters/SupabaseAdapter.js.map +1 -0
  26. package/dist/database/interfaces.d.ts +20 -0
  27. package/dist/database/interfaces.d.ts.map +1 -0
  28. package/dist/database/interfaces.js +3 -0
  29. package/dist/database/interfaces.js.map +1 -0
  30. package/dist/database/repositories/EventsRepository.d.ts +26 -0
  31. package/dist/database/repositories/EventsRepository.d.ts.map +1 -0
  32. package/dist/database/repositories/EventsRepository.js +91 -0
  33. package/dist/database/repositories/EventsRepository.js.map +1 -0
  34. package/dist/database/repositories/ItemsRepository.d.ts +11 -0
  35. package/dist/database/repositories/ItemsRepository.d.ts.map +1 -0
  36. package/dist/database/repositories/ItemsRepository.js +44 -0
  37. package/dist/database/repositories/ItemsRepository.js.map +1 -0
  38. package/dist/database/repositories/MedicationsRepository.d.ts +40 -0
  39. package/dist/database/repositories/MedicationsRepository.d.ts.map +1 -0
  40. package/dist/database/repositories/MedicationsRepository.js +136 -0
  41. package/dist/database/repositories/MedicationsRepository.js.map +1 -0
  42. package/dist/database/repositories/OccurrencesRepository.d.ts +25 -0
  43. package/dist/database/repositories/OccurrencesRepository.d.ts.map +1 -0
  44. package/dist/database/repositories/OccurrencesRepository.js +150 -0
  45. package/dist/database/repositories/OccurrencesRepository.js.map +1 -0
  46. package/dist/database/repositories/PlansRepository.d.ts +13 -0
  47. package/dist/database/repositories/PlansRepository.d.ts.map +1 -0
  48. package/dist/database/repositories/PlansRepository.js +76 -0
  49. package/dist/database/repositories/PlansRepository.js.map +1 -0
  50. package/dist/database/repositories/index.d.ts +6 -0
  51. package/dist/database/repositories/index.d.ts.map +1 -0
  52. package/dist/database/repositories/index.js +14 -0
  53. package/dist/database/repositories/index.js.map +1 -0
  54. package/dist/database/schema.base.d.ts +3 -0
  55. package/dist/database/schema.base.d.ts.map +1 -0
  56. package/dist/database/schema.base.js +110 -0
  57. package/dist/database/schema.base.js.map +1 -0
  58. package/dist/database/sync/interfaces.d.ts +20 -0
  59. package/dist/database/sync/interfaces.d.ts.map +1 -0
  60. package/dist/database/sync/interfaces.js +3 -0
  61. package/dist/database/sync/interfaces.js.map +1 -0
  62. package/dist/index.d.ts +27 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +58 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/notifications/adapters/MockNotificationAdapter.d.ts +18 -0
  67. package/dist/notifications/adapters/MockNotificationAdapter.d.ts.map +1 -0
  68. package/dist/notifications/adapters/MockNotificationAdapter.js +109 -0
  69. package/dist/notifications/adapters/MockNotificationAdapter.js.map +1 -0
  70. package/dist/notifications/adapters/NotifeeAdapter.d.ts +46 -0
  71. package/dist/notifications/adapters/NotifeeAdapter.d.ts.map +1 -0
  72. package/dist/notifications/adapters/NotifeeAdapter.js +479 -0
  73. package/dist/notifications/adapters/NotifeeAdapter.js.map +1 -0
  74. package/dist/notifications/interfaces.d.ts +8 -0
  75. package/dist/notifications/interfaces.d.ts.map +1 -0
  76. package/dist/notifications/interfaces.js +3 -0
  77. package/dist/notifications/interfaces.js.map +1 -0
  78. package/dist/planning/buildAgenda.d.ts +22 -0
  79. package/dist/planning/buildAgenda.d.ts.map +1 -0
  80. package/dist/planning/buildAgenda.js +148 -0
  81. package/dist/planning/buildAgenda.js.map +1 -0
  82. package/dist/planning/calculateTotalDoses.d.ts +3 -0
  83. package/dist/planning/calculateTotalDoses.d.ts.map +1 -0
  84. package/dist/planning/calculateTotalDoses.js +19 -0
  85. package/dist/planning/calculateTotalDoses.js.map +1 -0
  86. package/dist/planning/expandPlan.d.ts +3 -0
  87. package/dist/planning/expandPlan.d.ts.map +1 -0
  88. package/dist/planning/expandPlan.js +200 -0
  89. package/dist/planning/expandPlan.js.map +1 -0
  90. package/dist/planning/utils.d.ts +15 -0
  91. package/dist/planning/utils.d.ts.map +1 -0
  92. package/dist/planning/utils.js +40 -0
  93. package/dist/planning/utils.js.map +1 -0
  94. package/dist/planning/windowPlanner.d.ts +6 -0
  95. package/dist/planning/windowPlanner.d.ts.map +1 -0
  96. package/dist/planning/windowPlanner.js +18 -0
  97. package/dist/planning/windowPlanner.js.map +1 -0
  98. package/dist/plugins/InventoryPlugin.d.ts +26 -0
  99. package/dist/plugins/InventoryPlugin.d.ts.map +1 -0
  100. package/dist/plugins/InventoryPlugin.js +166 -0
  101. package/dist/plugins/InventoryPlugin.js.map +1 -0
  102. package/dist/plugins/PluginRegistry.d.ts +13 -0
  103. package/dist/plugins/PluginRegistry.d.ts.map +1 -0
  104. package/dist/plugins/PluginRegistry.js +43 -0
  105. package/dist/plugins/PluginRegistry.js.map +1 -0
  106. package/dist/plugins/SyncPluginEventDriven.d.ts +32 -0
  107. package/dist/plugins/SyncPluginEventDriven.d.ts.map +1 -0
  108. package/dist/plugins/SyncPluginEventDriven.js +609 -0
  109. package/dist/plugins/SyncPluginEventDriven.js.map +1 -0
  110. package/dist/plugins/SyncPluginPolling.d.ts +24 -0
  111. package/dist/plugins/SyncPluginPolling.d.ts.map +1 -0
  112. package/dist/plugins/SyncPluginPolling.js +266 -0
  113. package/dist/plugins/SyncPluginPolling.js.map +1 -0
  114. package/dist/plugins/gamification/GamificationPlugin.d.ts +26 -0
  115. package/dist/plugins/gamification/GamificationPlugin.d.ts.map +1 -0
  116. package/dist/plugins/gamification/GamificationPlugin.js +346 -0
  117. package/dist/plugins/gamification/GamificationPlugin.js.map +1 -0
  118. package/dist/plugins/gamification/__tests__/hysteresis.spec.d.ts +2 -0
  119. package/dist/plugins/gamification/__tests__/hysteresis.spec.d.ts.map +1 -0
  120. package/dist/plugins/gamification/__tests__/hysteresis.spec.js +22 -0
  121. package/dist/plugins/gamification/__tests__/hysteresis.spec.js.map +1 -0
  122. package/dist/plugins/gamification/index.d.ts +6 -0
  123. package/dist/plugins/gamification/index.d.ts.map +1 -0
  124. package/dist/plugins/gamification/index.js +14 -0
  125. package/dist/plugins/gamification/index.js.map +1 -0
  126. package/dist/plugins/gamification/levelsConfig.example.d.ts +17 -0
  127. package/dist/plugins/gamification/levelsConfig.example.d.ts.map +1 -0
  128. package/dist/plugins/gamification/levelsConfig.example.js +18 -0
  129. package/dist/plugins/gamification/levelsConfig.example.js.map +1 -0
  130. package/dist/plugins/gamification/levelsEngine.d.ts +19 -0
  131. package/dist/plugins/gamification/levelsEngine.d.ts.map +1 -0
  132. package/dist/plugins/gamification/levelsEngine.js +50 -0
  133. package/dist/plugins/gamification/levelsEngine.js.map +1 -0
  134. package/dist/plugins/gamification/streak.d.ts +7 -0
  135. package/dist/plugins/gamification/streak.d.ts.map +1 -0
  136. package/dist/plugins/gamification/streak.js +39 -0
  137. package/dist/plugins/gamification/streak.js.map +1 -0
  138. package/dist/plugins/index.d.ts +8 -0
  139. package/dist/plugins/index.d.ts.map +1 -0
  140. package/dist/plugins/index.js +28 -0
  141. package/dist/plugins/index.js.map +1 -0
  142. package/dist/plugins/interfaces.d.ts +56 -0
  143. package/dist/plugins/interfaces.d.ts.map +1 -0
  144. package/dist/plugins/interfaces.js +3 -0
  145. package/dist/plugins/interfaces.js.map +1 -0
  146. package/dist/reconciler/WindowScheduler.d.ts +29 -0
  147. package/dist/reconciler/WindowScheduler.d.ts.map +1 -0
  148. package/dist/reconciler/WindowScheduler.js +396 -0
  149. package/dist/reconciler/WindowScheduler.js.map +1 -0
  150. package/dist/services/MedicationsServiceCompat.d.ts +24 -0
  151. package/dist/services/MedicationsServiceCompat.d.ts.map +1 -0
  152. package/dist/services/MedicationsServiceCompat.js +98 -0
  153. package/dist/services/MedicationsServiceCompat.js.map +1 -0
  154. package/dist/utils/queue.d.ts +8 -0
  155. package/dist/utils/queue.d.ts.map +1 -0
  156. package/dist/utils/queue.js +28 -0
  157. package/dist/utils/queue.js.map +1 -0
  158. package/dist/utils/timeOffset.d.ts +4 -0
  159. package/dist/utils/timeOffset.d.ts.map +1 -0
  160. package/dist/utils/timeOffset.js +61 -0
  161. package/dist/utils/timeOffset.js.map +1 -0
  162. package/dist/utils/timestamp.d.ts +6 -0
  163. package/dist/utils/timestamp.d.ts.map +1 -0
  164. package/dist/utils/timestamp.js +29 -0
  165. package/dist/utils/timestamp.js.map +1 -0
  166. package/dist/utils/timezone.d.ts +4 -0
  167. package/dist/utils/timezone.d.ts.map +1 -0
  168. package/dist/utils/timezone.js +30 -0
  169. package/dist/utils/timezone.js.map +1 -0
  170. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # @healthcare-scheduler/core
2
+
3
+ Production-ready healthcare scheduling library for medication reminders, appointments, and health-related notifications with OS-level integration.
4
+
5
+ ## Status
6
+
7
+ - **169/169 tests passing**
8
+ - **25 test suites** (unit, integration, validation)
9
+ - **100% TypeScript** (strict mode)
10
+ - **6 scheduling patterns** validated
11
+
12
+ ## Features
13
+
14
+ - **📅 Flexible Scheduling**: 6 schedule types (TIMES, WEEKLY, INTERVAL, CYCLIC, DAYS_INTERVAL, PARTS_OF_DAY)
15
+ - **🔔 OS Notifications**: Native notification scheduling with automatic reconciliation
16
+ - **💾 Offline-First**: SQLite local storage
17
+ - **📊 Adherence Tracking**: Materialized daily adherence metrics
18
+ - **🎮 Gamification**: Streak calculation and level progression
19
+ - **🔌 Extensible**: Event-driven plugin system
20
+ - **🧪 Test Coverage**: Comprehensive validation suite
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @healthcare-scheduler/core
26
+ # Peer dependency (if using React Native)
27
+ npm install expo-sqlite
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```typescript
33
+ import { HealthcareScheduler, MockAdapter, MockNotificationAdapter } from '@healthcare-scheduler/core';
34
+
35
+ const scheduler = new HealthcareScheduler({
36
+ storage: new MockAdapter(), // or SQLiteAdapter for production
37
+ notificationDriver: new MockNotificationAdapter(), // or NotifeeAdapter (see below)
38
+ windowDays: 14,
39
+ maxPendingNotifications: 45,
40
+ enableGrouping: false
41
+ });
42
+
43
+ await scheduler.bootstrap();
44
+
45
+ // Create medication plan
46
+ const planId = await scheduler.createPlan({
47
+ item: {
48
+ type: 'MED',
49
+ name: 'Losartana 50mg',
50
+ meta: { concentration: '50mg' }
51
+ },
52
+ schedule: {
53
+ kind: 'TIMES',
54
+ times: ['08:00', '20:00']
55
+ },
56
+ startsAt: new Date().toISOString()
57
+ });
58
+
59
+ // Get today's medication schedule
60
+ const today = await scheduler.getTodayOccurrences();
61
+
62
+ // Record medication taken
63
+ await scheduler.recordEvent(occurrenceId, 'TAKEN', { source: 'user' });
64
+ ```
65
+
66
+ ## Schedule Types
67
+
68
+ ### 1. TIMES (Fixed times per day)
69
+ ```typescript
70
+ { kind: 'TIMES', times: ['08:00', '14:00', '20:00'] }
71
+ ```
72
+
73
+ ### 2. WEEKLY (Weekday-specific schedule)
74
+ ```typescript
75
+ {
76
+ kind: 'WEEKLY',
77
+ lines: [
78
+ { id: '1', daysOfWeek: [1,2,3,4,5], time: '08:00', qty: 1 }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ ### 3. INTERVAL (Every N hours)
84
+ ```typescript
85
+ {
86
+ kind: 'INTERVAL',
87
+ intervalH: 8,
88
+ firstDoseAt: '2025-01-01T08:00:00.000Z'
89
+ }
90
+ ```
91
+
92
+ ### 4. CYCLIC (Take/pause cycles, e.g. contraceptives)
93
+ ```typescript
94
+ {
95
+ kind: 'CYCLIC',
96
+ takeDays: 21,
97
+ pauseDays: 7,
98
+ times: ['21:00'],
99
+ cycleAnchorISO: '2025-01-01T00:00:00.000Z'
100
+ }
101
+ ```
102
+
103
+ ### 5. DAYS_INTERVAL (Every N days)
104
+ ```typescript
105
+ {
106
+ kind: 'DAYS_INTERVAL',
107
+ daysInterval: 7,
108
+ times: ['09:00'],
109
+ anchorDateISO: '2025-01-01T00:00:00.000Z'
110
+ }
111
+ ```
112
+
113
+ ### 6. PARTS_OF_DAY (Morning/Afternoon/Evening)
114
+ ```typescript
115
+ {
116
+ kind: 'PARTS_OF_DAY',
117
+ parts: [
118
+ { code: 'morning', label: 'Manhã', time: '08:00', qty: 1 },
119
+ { code: 'night', label: 'Noite', time: '21:00', qty: 1 }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ ## Gamification Plugin
125
+
126
+ ```typescript
127
+ import { GamificationPlugin } from '@healthcare-scheduler/core';
128
+
129
+ const gamification = new GamificationPlugin();
130
+ scheduler.registerPlugin(gamification);
131
+
132
+ await scheduler.bootstrap();
133
+
134
+ // Get current level and streak
135
+ const plugin = scheduler.getPlugin<GamificationPlugin>('gamification');
136
+ const levels = await plugin.getCurrentLevelsState();
137
+ // => { streak: 5, levelId: 'bronze', levelName: 'Bronze', progress: 0.4, ... }
138
+
139
+ // Get daily adherence
140
+ const adherence = await plugin.getDayAdherence(new Date());
141
+ // => { date: '2025-10-11', total: 4, done: 3, pending: 1, percent: 75 }
142
+ ```
143
+
144
+ ## Production Usage with NotifeeAdapter
145
+
146
+ For React Native production apps, use `NotifeeAdapter` with Platform information:
147
+
148
+ ```typescript
149
+ import { Platform } from 'react-native';
150
+ import {
151
+ HealthcareScheduler,
152
+ SQLiteAdapter,
153
+ NotifeeAdapter,
154
+ GamificationPlugin
155
+ } from '@healthcare-scheduler/core';
156
+
157
+ // Initialize storage
158
+ const storage = await SQLiteAdapter.connect({
159
+ dbName: 'healthcare.db',
160
+ autoApplySchema: true
161
+ });
162
+
163
+ // Initialize notifications with Platform info
164
+ const notificationDriver = new NotifeeAdapter({
165
+ platform: Platform // Required for Android/iOS specific behavior
166
+ });
167
+
168
+ // Create scheduler
169
+ const scheduler = new HealthcareScheduler({
170
+ storage,
171
+ notificationDriver,
172
+ windowDays: 14,
173
+ maxPendingNotifications: 45
174
+ });
175
+
176
+ // Register plugins
177
+ scheduler.registerPlugin(new GamificationPlugin());
178
+
179
+ // Bootstrap
180
+ await scheduler.bootstrap();
181
+ ```
182
+
183
+ **Note**: `NotifeeAdapter` requires `platform` to be passed to avoid dynamic imports of `react-native`, ensuring compatibility with React Native's New Architecture and preventing deprecated API warnings.
184
+
185
+ ## Architecture
186
+
187
+ ```
188
+ src/
189
+ ├── core/ # Main API, EventBus, types
190
+ ├── database/ # Storage abstraction, repositories
191
+ │ ├── repositories/ # ItemsRepo, PlansRepo, OccurrencesRepo, EventsRepo
192
+ │ └── adapters/ # MockAdapter, SQLiteAdapter (peer dep)
193
+ ├── planning/ # Schedule expansion engine
194
+ ├── reconciler/ # DB ↔ OS notification sync
195
+ ├── notifications/ # Notification driver abstraction
196
+ ├── plugins/ # Extensible plugin system
197
+ │ └── gamification/ # Adherence tracking & levels
198
+ └── utils/ # Queue, timezone helpers
199
+ ```
200
+
201
+ ## Events
202
+
203
+ ```typescript
204
+ scheduler.on('plans:changed', ({ planIds }) => {
205
+ console.log('Plans updated:', planIds);
206
+ });
207
+
208
+ scheduler.on('occurrences:changed', ({ occurrenceIds }) => {
209
+ console.log('Occurrences changed:', occurrenceIds);
210
+ });
211
+
212
+ scheduler.on('window:filled', ({ count }) => {
213
+ console.log('Notifications scheduled:', count);
214
+ });
215
+ ```
216
+
217
+ Note:
218
+ - Window fill and reconciliation are asynchronous and triggered by events (`plans:changed`, `occurrences:changed`). Wait for `window:filled` in tests or after batch operations; there is no public `updateWindow` method.
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ # Install
224
+ npm install
225
+
226
+ # Run tests
227
+ npm test
228
+
229
+ # Run specific test suite
230
+ npm test -- tests/validation/scenarios/scenario1_weekly.test.ts
231
+
232
+ # Build
233
+ npm run build
234
+
235
+ # Lint
236
+ npm run lint
237
+ ```
238
+
239
+ ## Testing
240
+
241
+ The library includes comprehensive validation:
242
+
243
+ - **Unit tests**: Planning engine, repositories, window scheduler
244
+ - **Integration tests**: Full CRUD cycles
245
+ - **Validation scenarios**: 6 real-world medication schedules
246
+ - **Plugin tests**: Gamification, streak calculation
247
+
248
+ All tests run in-memory using MockAdapter (no database required).
249
+
250
+ ## Documentation
251
+
252
+ - [Implementation Plan](./docs/healthcare-scheduler-library.plan.md)
253
+ - [Executive Summary](./docs/EXECUTIVE_SUMMARY.md)
254
+ - [Developer Guide](./docs/README_DEV.md)
255
+
256
+ ## License
257
+
258
+ MIT
259
+
260
+ ## Credits
261
+
262
+ Alessandro Barbosa.
263
+
264
+ Built with TypeScript, tested with Jest, validated against real-world use cases.
@@ -0,0 +1,14 @@
1
+ type EventCallback<T = any> = (payload: T) => void;
2
+ export declare class EventBus {
3
+ private listeners;
4
+ constructor();
5
+ on<T = any>(eventType: string, callback: EventCallback<T>): () => void;
6
+ off<T = any>(eventType: string, callback: EventCallback<T>): void;
7
+ emit<T = any>(eventType: string, payload: T): void;
8
+ clearEventType(eventType: string): void;
9
+ clearAll(): void;
10
+ listenerCount(eventType: string): number;
11
+ }
12
+ export declare const globalEventBus: EventBus;
13
+ export default globalEventBus;
14
+ //# sourceMappingURL=EventBus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventBus.d.ts","sourceRoot":"","sources":["../../src/core/EventBus.ts"],"names":[],"mappings":"AAMA,KAAK,aAAa,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;AAEnD,qBAAa,QAAQ;IACnB,OAAO,CAAC,SAAS,CAAkC;;IAYnD,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAetE,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI;IAYjE,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI;IAiBlD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAOvC,QAAQ,IAAI,IAAI;IAShB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAGzC;AAGD,eAAO,MAAM,cAAc,UAAiB,CAAC;AAG7C,eAAe,cAAc,CAAC"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.globalEventBus = exports.EventBus = void 0;
4
+ class EventBus {
5
+ constructor() {
6
+ this.listeners = new Map();
7
+ }
8
+ on(eventType, callback) {
9
+ if (!this.listeners.has(eventType)) {
10
+ this.listeners.set(eventType, new Set());
11
+ }
12
+ this.listeners.get(eventType).add(callback);
13
+ return () => this.off(eventType, callback);
14
+ }
15
+ off(eventType, callback) {
16
+ const callbacks = this.listeners.get(eventType);
17
+ if (callbacks) {
18
+ callbacks.delete(callback);
19
+ }
20
+ }
21
+ emit(eventType, payload) {
22
+ const callbacks = this.listeners.get(eventType);
23
+ if (callbacks) {
24
+ for (const callback of callbacks) {
25
+ try {
26
+ callback(payload);
27
+ }
28
+ catch (error) {
29
+ console.error(`Error in event listener for "${eventType}":`, error);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ clearEventType(eventType) {
35
+ this.listeners.delete(eventType);
36
+ }
37
+ clearAll() {
38
+ this.listeners.clear();
39
+ }
40
+ listenerCount(eventType) {
41
+ return this.listeners.get(eventType)?.size ?? 0;
42
+ }
43
+ }
44
+ exports.EventBus = EventBus;
45
+ exports.globalEventBus = new EventBus();
46
+ exports.default = exports.globalEventBus;
47
+ //# sourceMappingURL=EventBus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventBus.js","sourceRoot":"","sources":["../../src/core/EventBus.ts"],"names":[],"mappings":";;;AAQA,MAAa,QAAQ;IAGnB;QACE,IAAI,CAAC,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;IAC7B,CAAC;IAQD,EAAE,CAAU,SAAiB,EAAE,QAA0B;QACvD,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAG7C,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAOD,GAAG,CAAU,SAAiB,EAAE,QAA0B;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAChD,IAAI,SAAS,EAAE,CAAC;YACd,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAOD,IAAI,CAAU,SAAiB,EAAE,OAAU;QACzC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAChD,IAAI,SAAS,EAAE,CAAC;YACd,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBACjC,IAAI,CAAC;oBACH,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACpB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,KAAK,CAAC,gCAAgC,SAAS,IAAI,EAAE,KAAK,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAMD,cAAc,CAAC,SAAiB;QAC9B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;IAKD,QAAQ;QACN,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAOD,aAAa,CAAC,SAAiB;QAC7B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC;IAClD,CAAC;CACF;AA5ED,4BA4EC;AAGY,QAAA,cAAc,GAAG,IAAI,QAAQ,EAAE,CAAC;AAG7C,kBAAe,sBAAc,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { EventBus } from './EventBus';
2
+ import { SchedulerConfig, Item, ScheduleRule, Occurrence, EventAction, SchedulerPlugin } from './types';
3
+ import { ItemsRepository, PlansRepository, EventsRepository, MedicationsRepository } from '../database/repositories';
4
+ import { WindowScheduler } from '../reconciler/WindowScheduler';
5
+ import type { PluginInterface } from '../plugins/interfaces';
6
+ export declare class HealthcareScheduler {
7
+ private eventBus;
8
+ private storage;
9
+ private notificationDriver;
10
+ private config;
11
+ private itemsRepo;
12
+ private plansRepo;
13
+ private occurrencesRepo;
14
+ private eventsRepo;
15
+ private medicationsRepo;
16
+ private windowScheduler;
17
+ private plugins;
18
+ private pluginRegistry;
19
+ constructor(config: SchedulerConfig);
20
+ bootstrap(): Promise<void>;
21
+ fillWindow(): Promise<void>;
22
+ private resolveGroupOccurrencesFromPayload;
23
+ use(plugin: SchedulerPlugin): this;
24
+ registerPlugin(plugin: PluginInterface): this;
25
+ getPlugin<T extends PluginInterface>(name: string): T | undefined;
26
+ createPlan(options: {
27
+ item: Omit<Item, 'id' | 'created_at'>;
28
+ schedule: ScheduleRule;
29
+ startsAt: string;
30
+ endsAt?: string;
31
+ }): Promise<string>;
32
+ updatePlan(planId: string, updates: {
33
+ schedule?: ScheduleRule;
34
+ endsAt?: string;
35
+ }): Promise<void>;
36
+ endPlan(planId: string): Promise<void>;
37
+ deletePlan(planId: string): Promise<void>;
38
+ getOccurrences(options: {
39
+ days: number;
40
+ limit?: number;
41
+ }): Promise<Occurrence[]>;
42
+ getTodayOccurrences(): Promise<Occurrence[]>;
43
+ recordEvent(occurrenceId: string, action: EventAction, meta?: {
44
+ source?: 'notif' | 'screen';
45
+ note?: string;
46
+ payload?: Record<string, any>;
47
+ }): Promise<void>;
48
+ on<T = any>(eventType: string, callback: (payload: T) => void): () => void;
49
+ emit<T = any>(eventType: string, payload: T): void;
50
+ private setupEventHandlers;
51
+ getItemsRepository(): ItemsRepository;
52
+ getPlansRepository(): PlansRepository;
53
+ getMedicationsRepository(): MedicationsRepository;
54
+ getEventBus(): EventBus;
55
+ getConfig(): typeof this.config;
56
+ getWindowScheduler(): WindowScheduler;
57
+ getEventsRepository(): EventsRepository;
58
+ cleanupOldRecords(daysOld?: number): Promise<{
59
+ occurrences: number;
60
+ events: number;
61
+ }>;
62
+ }
63
+ //# sourceMappingURL=HealthcareScheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealthcareScheduler.d.ts","sourceRoot":"","sources":["../../src/core/HealthcareScheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HealthcareScheduler = void 0;
4
+ const EventBus_1 = require("./EventBus");
5
+ const timestamp_1 = require("../utils/timestamp");
6
+ const repositories_1 = require("../database/repositories");
7
+ const WindowScheduler_1 = require("../reconciler/WindowScheduler");
8
+ const PluginRegistry_1 = require("../plugins/PluginRegistry");
9
+ class HealthcareScheduler {
10
+ constructor(config) {
11
+ this.plugins = [];
12
+ this.storage = config.storage;
13
+ this.notificationDriver = config.notificationDriver;
14
+ this.eventBus = new EventBus_1.EventBus();
15
+ this.config = {
16
+ windowDays: config.windowDays || 14,
17
+ maxPendingNotifications: config.maxPendingNotifications || 45,
18
+ enableGrouping: config.enableGrouping || false,
19
+ enableInventory: config.enableInventory || false,
20
+ timezone: config.timezone || 'America/Sao_Paulo'
21
+ };
22
+ this.itemsRepo = new repositories_1.ItemsRepository(this.storage);
23
+ this.plansRepo = new repositories_1.PlansRepository(this.storage);
24
+ this.occurrencesRepo = new repositories_1.OccurrencesRepository(this.storage);
25
+ this.eventsRepo = new repositories_1.EventsRepository(this.storage, this.eventBus);
26
+ this.medicationsRepo = new repositories_1.MedicationsRepository(this.storage);
27
+ this.windowScheduler = new WindowScheduler_1.WindowScheduler(this.plansRepo, this.occurrencesRepo, this.notificationDriver, this.eventBus);
28
+ this.pluginRegistry = new PluginRegistry_1.PluginRegistry();
29
+ this.setupEventHandlers();
30
+ }
31
+ async bootstrap() {
32
+ console.log('🚀 Bootstrapping HealthcareScheduler...');
33
+ const hasPermissions = await this.notificationDriver.ensurePermissions();
34
+ if (!hasPermissions) {
35
+ console.warn('⚠️ Notification permissions not granted');
36
+ }
37
+ if (this.notificationDriver.createChannelsAndCategories) {
38
+ await this.notificationDriver.createChannelsAndCategories();
39
+ }
40
+ try {
41
+ this.notificationDriver.registerHandlers(async (evt) => {
42
+ const type = evt?.type;
43
+ const occId = evt?.occurrenceId;
44
+ const payload = evt?.payload || {};
45
+ const occurrenceIds = Array.isArray(payload?.occurrenceIds) ? payload.occurrenceIds : [];
46
+ const groupKey = typeof payload?.groupKey === 'string' ? payload.groupKey : null;
47
+ const slotISO = typeof payload?.slotISO === 'string' ? payload.slotISO : null;
48
+ const itemType = typeof payload?.itemType === 'string' ? payload.itemType : null;
49
+ if (type === 'TAKEN' || type === 'SKIPPED' || type === 'SNOOZED') {
50
+ let handledBatch = false;
51
+ if (occurrenceIds && occurrenceIds.length > 0) {
52
+ try {
53
+ await this.eventsRepo.recordEventsBatch(occurrenceIds, type, { source: 'notif', payload });
54
+ handledBatch = true;
55
+ }
56
+ catch (e) {
57
+ console.error('❌ Error batch-recording events from notif:', e);
58
+ }
59
+ }
60
+ else if (groupKey || slotISO) {
61
+ try {
62
+ const ids = await this.resolveGroupOccurrencesFromPayload(groupKey, slotISO, itemType || 'MED');
63
+ if (ids.length > 0) {
64
+ await this.eventsRepo.recordEventsBatch(ids, type, { source: 'notif', payload });
65
+ handledBatch = true;
66
+ }
67
+ }
68
+ catch (e) {
69
+ console.error('❌ Error resolving group occurrences from notif:', e);
70
+ }
71
+ }
72
+ if (!handledBatch && occId) {
73
+ try {
74
+ await this.eventsRepo.recordEvent(occId, type, { source: 'notif', payload });
75
+ }
76
+ catch { }
77
+ }
78
+ return;
79
+ }
80
+ if (occId && (type === 'TAPPED' || type === 'DISMISSED')) {
81
+ try {
82
+ await this.eventsRepo.recordEvent(occId, type, { source: 'notif', payload });
83
+ }
84
+ catch { }
85
+ return;
86
+ }
87
+ });
88
+ }
89
+ catch (e) {
90
+ console.warn('⚠️ Failed to register notification handlers:', e);
91
+ }
92
+ for (const plugin of this.plugins) {
93
+ if (plugin.onBootstrap) {
94
+ await plugin.onBootstrap(this);
95
+ }
96
+ }
97
+ await this.pluginRegistry.bootstrapAll({
98
+ storage: this.storage,
99
+ eventBus: this.eventBus,
100
+ config: this.config
101
+ });
102
+ console.log('✅ HealthcareScheduler bootstrapped');
103
+ }
104
+ async fillWindow() {
105
+ await this.windowScheduler.updateWindow({ days: this.config.windowDays });
106
+ }
107
+ async resolveGroupOccurrencesFromPayload(groupKey, slotISO, itemType) {
108
+ const legacyMatch = groupKey ? /^([A-Z]+):HH:(\d{2}):(\d{2})$/.exec(groupKey) : null;
109
+ if (legacyMatch) {
110
+ const hh = Number(legacyMatch[2]);
111
+ const mm = Number(legacyMatch[3]);
112
+ const today = await this.occurrencesRepo.getToday(new Date());
113
+ return today
114
+ .filter(o => o.kind === itemType && (0, timestamp_1.parseUTC)(o.scheduled_at).getHours() === hh && (0, timestamp_1.parseUTC)(o.scheduled_at).getMinutes() === mm)
115
+ .map(o => o.id);
116
+ }
117
+ if (slotISO) {
118
+ const slot = new Date(slotISO);
119
+ const sameDayOccs = await this.occurrencesRepo.getToday(slot);
120
+ return sameDayOccs
121
+ .filter(o => o.kind === itemType && Math.abs((0, timestamp_1.parseUTC)(o.scheduled_at).getTime() - slot.getTime()) < 60 * 1000)
122
+ .map(o => o.id);
123
+ }
124
+ return [];
125
+ }
126
+ use(plugin) {
127
+ this.plugins.push(plugin);
128
+ console.log(`🔌 Plugin registered: ${plugin.name}`);
129
+ return this;
130
+ }
131
+ registerPlugin(plugin) {
132
+ this.pluginRegistry.register(plugin);
133
+ console.log(`🔌 Plugin registered: ${plugin.name}`);
134
+ return this;
135
+ }
136
+ getPlugin(name) {
137
+ return this.pluginRegistry.get(name);
138
+ }
139
+ async createPlan(options) {
140
+ const itemId = `item:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`;
141
+ const item = {
142
+ id: itemId,
143
+ ...options.item
144
+ };
145
+ await this.itemsRepo.upsert(item);
146
+ const planId = `plan:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`;
147
+ const plan = {
148
+ id: planId,
149
+ item_id: itemId,
150
+ schedule_rule: options.schedule,
151
+ tz: this.config.timezone,
152
+ starts_at: options.startsAt,
153
+ ends_at: options.endsAt
154
+ };
155
+ await this.plansRepo.upsert(plan);
156
+ this.eventBus.emit('plans:changed', { planId });
157
+ return planId;
158
+ }
159
+ async updatePlan(planId, updates) {
160
+ const plan = await this.plansRepo.findById(planId);
161
+ if (!plan) {
162
+ throw new Error(`Plan ${planId} not found`);
163
+ }
164
+ const updatedPlan = {
165
+ ...plan,
166
+ schedule_rule: updates.schedule || plan.schedule_rule,
167
+ ends_at: updates.endsAt !== undefined ? updates.endsAt : plan.ends_at
168
+ };
169
+ await this.plansRepo.upsert(updatedPlan);
170
+ this.eventBus.emit('plans:changed', { planId });
171
+ }
172
+ async endPlan(planId) {
173
+ const plan = await this.plansRepo.findById(planId);
174
+ if (!plan) {
175
+ throw new Error(`Plan ${planId} not found`);
176
+ }
177
+ const updatedPlan = {
178
+ ...plan,
179
+ ends_at: new Date().toISOString()
180
+ };
181
+ await this.plansRepo.upsert(updatedPlan);
182
+ this.eventBus.emit('plans:changed', { planId });
183
+ }
184
+ async deletePlan(planId) {
185
+ await this.plansRepo.delete(planId);
186
+ this.eventBus.emit('plans:changed', { planId });
187
+ }
188
+ async getOccurrences(options) {
189
+ if (options.days === 1) {
190
+ return this.occurrencesRepo.getToday(new Date());
191
+ }
192
+ return this.occurrencesRepo.getUpcomingWindow({
193
+ days: options.days - 1,
194
+ limit: options.limit || 50,
195
+ pastDays: 0
196
+ });
197
+ }
198
+ async getTodayOccurrences() {
199
+ return this.occurrencesRepo.getToday(new Date());
200
+ }
201
+ async recordEvent(occurrenceId, action, meta) {
202
+ await this.eventsRepo.recordEvent(occurrenceId, action, meta || {});
203
+ }
204
+ on(eventType, callback) {
205
+ return this.eventBus.on(eventType, callback);
206
+ }
207
+ emit(eventType, payload) {
208
+ this.eventBus.emit(eventType, payload);
209
+ }
210
+ setupEventHandlers() {
211
+ this.eventBus.on('plans:changed', () => {
212
+ void (async () => {
213
+ console.log('📅 Plans changed, updating window...');
214
+ try {
215
+ await this.windowScheduler.updateWindow({ days: this.config.windowDays });
216
+ }
217
+ catch (error) {
218
+ console.error('❌ Error updating window:', error);
219
+ }
220
+ })();
221
+ });
222
+ let reconciling = false;
223
+ this.eventBus.on('occurrences:changed', () => {
224
+ void (async () => {
225
+ if (reconciling)
226
+ return;
227
+ reconciling = true;
228
+ console.log('🔄 Occurrences changed, reconciling...');
229
+ try {
230
+ await this.windowScheduler.reconcileWithOS();
231
+ }
232
+ catch (error) {
233
+ console.error('❌ Error reconciling:', error);
234
+ }
235
+ finally {
236
+ reconciling = false;
237
+ }
238
+ })();
239
+ });
240
+ }
241
+ getItemsRepository() {
242
+ return this.itemsRepo;
243
+ }
244
+ getPlansRepository() {
245
+ return this.plansRepo;
246
+ }
247
+ getMedicationsRepository() {
248
+ return this.medicationsRepo;
249
+ }
250
+ getEventBus() {
251
+ return this.eventBus;
252
+ }
253
+ getConfig() {
254
+ return { ...this.config };
255
+ }
256
+ getWindowScheduler() {
257
+ return this.windowScheduler;
258
+ }
259
+ getEventsRepository() {
260
+ return this.eventsRepo;
261
+ }
262
+ async cleanupOldRecords(daysOld = 90) {
263
+ const occurrences = await this.occurrencesRepo.cleanupOldOccurrences(daysOld);
264
+ const events = await this.eventsRepo.cleanupOldEvents(daysOld);
265
+ return { occurrences, events };
266
+ }
267
+ }
268
+ exports.HealthcareScheduler = HealthcareScheduler;
269
+ //# sourceMappingURL=HealthcareScheduler.js.map