@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.
- package/README.md +264 -0
- package/dist/core/EventBus.d.ts +14 -0
- package/dist/core/EventBus.d.ts.map +1 -0
- package/dist/core/EventBus.js +47 -0
- package/dist/core/EventBus.js.map +1 -0
- package/dist/core/HealthcareScheduler.d.ts +63 -0
- package/dist/core/HealthcareScheduler.d.ts.map +1 -0
- package/dist/core/HealthcareScheduler.js +269 -0
- package/dist/core/HealthcareScheduler.js.map +1 -0
- package/dist/core/types.d.ts +234 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/database/adapters/MockAdapter.d.ts +37 -0
- package/dist/database/adapters/MockAdapter.d.ts.map +1 -0
- package/dist/database/adapters/MockAdapter.js +518 -0
- package/dist/database/adapters/MockAdapter.js.map +1 -0
- package/dist/database/adapters/SQLiteAdapter.d.ts +40 -0
- package/dist/database/adapters/SQLiteAdapter.d.ts.map +1 -0
- package/dist/database/adapters/SQLiteAdapter.js +147 -0
- package/dist/database/adapters/SQLiteAdapter.js.map +1 -0
- package/dist/database/adapters/SupabaseAdapter.d.ts +20 -0
- package/dist/database/adapters/SupabaseAdapter.d.ts.map +1 -0
- package/dist/database/adapters/SupabaseAdapter.js +310 -0
- package/dist/database/adapters/SupabaseAdapter.js.map +1 -0
- package/dist/database/interfaces.d.ts +20 -0
- package/dist/database/interfaces.d.ts.map +1 -0
- package/dist/database/interfaces.js +3 -0
- package/dist/database/interfaces.js.map +1 -0
- package/dist/database/repositories/EventsRepository.d.ts +26 -0
- package/dist/database/repositories/EventsRepository.d.ts.map +1 -0
- package/dist/database/repositories/EventsRepository.js +91 -0
- package/dist/database/repositories/EventsRepository.js.map +1 -0
- package/dist/database/repositories/ItemsRepository.d.ts +11 -0
- package/dist/database/repositories/ItemsRepository.d.ts.map +1 -0
- package/dist/database/repositories/ItemsRepository.js +44 -0
- package/dist/database/repositories/ItemsRepository.js.map +1 -0
- package/dist/database/repositories/MedicationsRepository.d.ts +40 -0
- package/dist/database/repositories/MedicationsRepository.d.ts.map +1 -0
- package/dist/database/repositories/MedicationsRepository.js +136 -0
- package/dist/database/repositories/MedicationsRepository.js.map +1 -0
- package/dist/database/repositories/OccurrencesRepository.d.ts +25 -0
- package/dist/database/repositories/OccurrencesRepository.d.ts.map +1 -0
- package/dist/database/repositories/OccurrencesRepository.js +150 -0
- package/dist/database/repositories/OccurrencesRepository.js.map +1 -0
- package/dist/database/repositories/PlansRepository.d.ts +13 -0
- package/dist/database/repositories/PlansRepository.d.ts.map +1 -0
- package/dist/database/repositories/PlansRepository.js +76 -0
- package/dist/database/repositories/PlansRepository.js.map +1 -0
- package/dist/database/repositories/index.d.ts +6 -0
- package/dist/database/repositories/index.d.ts.map +1 -0
- package/dist/database/repositories/index.js +14 -0
- package/dist/database/repositories/index.js.map +1 -0
- package/dist/database/schema.base.d.ts +3 -0
- package/dist/database/schema.base.d.ts.map +1 -0
- package/dist/database/schema.base.js +110 -0
- package/dist/database/schema.base.js.map +1 -0
- package/dist/database/sync/interfaces.d.ts +20 -0
- package/dist/database/sync/interfaces.d.ts.map +1 -0
- package/dist/database/sync/interfaces.js +3 -0
- package/dist/database/sync/interfaces.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/adapters/MockNotificationAdapter.d.ts +18 -0
- package/dist/notifications/adapters/MockNotificationAdapter.d.ts.map +1 -0
- package/dist/notifications/adapters/MockNotificationAdapter.js +109 -0
- package/dist/notifications/adapters/MockNotificationAdapter.js.map +1 -0
- package/dist/notifications/adapters/NotifeeAdapter.d.ts +46 -0
- package/dist/notifications/adapters/NotifeeAdapter.d.ts.map +1 -0
- package/dist/notifications/adapters/NotifeeAdapter.js +479 -0
- package/dist/notifications/adapters/NotifeeAdapter.js.map +1 -0
- package/dist/notifications/interfaces.d.ts +8 -0
- package/dist/notifications/interfaces.d.ts.map +1 -0
- package/dist/notifications/interfaces.js +3 -0
- package/dist/notifications/interfaces.js.map +1 -0
- package/dist/planning/buildAgenda.d.ts +22 -0
- package/dist/planning/buildAgenda.d.ts.map +1 -0
- package/dist/planning/buildAgenda.js +148 -0
- package/dist/planning/buildAgenda.js.map +1 -0
- package/dist/planning/calculateTotalDoses.d.ts +3 -0
- package/dist/planning/calculateTotalDoses.d.ts.map +1 -0
- package/dist/planning/calculateTotalDoses.js +19 -0
- package/dist/planning/calculateTotalDoses.js.map +1 -0
- package/dist/planning/expandPlan.d.ts +3 -0
- package/dist/planning/expandPlan.d.ts.map +1 -0
- package/dist/planning/expandPlan.js +200 -0
- package/dist/planning/expandPlan.js.map +1 -0
- package/dist/planning/utils.d.ts +15 -0
- package/dist/planning/utils.d.ts.map +1 -0
- package/dist/planning/utils.js +40 -0
- package/dist/planning/utils.js.map +1 -0
- package/dist/planning/windowPlanner.d.ts +6 -0
- package/dist/planning/windowPlanner.d.ts.map +1 -0
- package/dist/planning/windowPlanner.js +18 -0
- package/dist/planning/windowPlanner.js.map +1 -0
- package/dist/plugins/InventoryPlugin.d.ts +26 -0
- package/dist/plugins/InventoryPlugin.d.ts.map +1 -0
- package/dist/plugins/InventoryPlugin.js +166 -0
- package/dist/plugins/InventoryPlugin.js.map +1 -0
- package/dist/plugins/PluginRegistry.d.ts +13 -0
- package/dist/plugins/PluginRegistry.d.ts.map +1 -0
- package/dist/plugins/PluginRegistry.js +43 -0
- package/dist/plugins/PluginRegistry.js.map +1 -0
- package/dist/plugins/SyncPluginEventDriven.d.ts +32 -0
- package/dist/plugins/SyncPluginEventDriven.d.ts.map +1 -0
- package/dist/plugins/SyncPluginEventDriven.js +609 -0
- package/dist/plugins/SyncPluginEventDriven.js.map +1 -0
- package/dist/plugins/SyncPluginPolling.d.ts +24 -0
- package/dist/plugins/SyncPluginPolling.d.ts.map +1 -0
- package/dist/plugins/SyncPluginPolling.js +266 -0
- package/dist/plugins/SyncPluginPolling.js.map +1 -0
- package/dist/plugins/gamification/GamificationPlugin.d.ts +26 -0
- package/dist/plugins/gamification/GamificationPlugin.d.ts.map +1 -0
- package/dist/plugins/gamification/GamificationPlugin.js +346 -0
- package/dist/plugins/gamification/GamificationPlugin.js.map +1 -0
- package/dist/plugins/gamification/__tests__/hysteresis.spec.d.ts +2 -0
- package/dist/plugins/gamification/__tests__/hysteresis.spec.d.ts.map +1 -0
- package/dist/plugins/gamification/__tests__/hysteresis.spec.js +22 -0
- package/dist/plugins/gamification/__tests__/hysteresis.spec.js.map +1 -0
- package/dist/plugins/gamification/index.d.ts +6 -0
- package/dist/plugins/gamification/index.d.ts.map +1 -0
- package/dist/plugins/gamification/index.js +14 -0
- package/dist/plugins/gamification/index.js.map +1 -0
- package/dist/plugins/gamification/levelsConfig.example.d.ts +17 -0
- package/dist/plugins/gamification/levelsConfig.example.d.ts.map +1 -0
- package/dist/plugins/gamification/levelsConfig.example.js +18 -0
- package/dist/plugins/gamification/levelsConfig.example.js.map +1 -0
- package/dist/plugins/gamification/levelsEngine.d.ts +19 -0
- package/dist/plugins/gamification/levelsEngine.d.ts.map +1 -0
- package/dist/plugins/gamification/levelsEngine.js +50 -0
- package/dist/plugins/gamification/levelsEngine.js.map +1 -0
- package/dist/plugins/gamification/streak.d.ts +7 -0
- package/dist/plugins/gamification/streak.d.ts.map +1 -0
- package/dist/plugins/gamification/streak.js +39 -0
- package/dist/plugins/gamification/streak.js.map +1 -0
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +28 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/interfaces.d.ts +56 -0
- package/dist/plugins/interfaces.d.ts.map +1 -0
- package/dist/plugins/interfaces.js +3 -0
- package/dist/plugins/interfaces.js.map +1 -0
- package/dist/reconciler/WindowScheduler.d.ts +29 -0
- package/dist/reconciler/WindowScheduler.d.ts.map +1 -0
- package/dist/reconciler/WindowScheduler.js +396 -0
- package/dist/reconciler/WindowScheduler.js.map +1 -0
- package/dist/services/MedicationsServiceCompat.d.ts +24 -0
- package/dist/services/MedicationsServiceCompat.d.ts.map +1 -0
- package/dist/services/MedicationsServiceCompat.js +98 -0
- package/dist/services/MedicationsServiceCompat.js.map +1 -0
- package/dist/utils/queue.d.ts +8 -0
- package/dist/utils/queue.d.ts.map +1 -0
- package/dist/utils/queue.js +28 -0
- package/dist/utils/queue.js.map +1 -0
- package/dist/utils/timeOffset.d.ts +4 -0
- package/dist/utils/timeOffset.d.ts.map +1 -0
- package/dist/utils/timeOffset.js +61 -0
- package/dist/utils/timeOffset.js.map +1 -0
- package/dist/utils/timestamp.d.ts +6 -0
- package/dist/utils/timestamp.d.ts.map +1 -0
- package/dist/utils/timestamp.js +29 -0
- package/dist/utils/timestamp.js.map +1 -0
- package/dist/utils/timezone.d.ts +4 -0
- package/dist/utils/timezone.d.ts.map +1 -0
- package/dist/utils/timezone.js +30 -0
- package/dist/utils/timezone.js.map +1 -0
- 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
|