@aleonnet/healthcare-scheduler
v0.1.49
Published
Healthcare scheduling and medication reminder system
Downloads
740
Maintainers
Readme
@aleonnet/healthcare-scheduler
Healthcare scheduling library for medication reminders, appointments, and health-related notifications with OS-level integration.
Status
- 211/217 tests passing (6 skipped - real DB/Supabase)
- 33/34 test suites (1 real Supabase suite skipped por padrão)
- 100% TypeScript (strict mode)
- 6 scheduling patterns validated
Supabase Sync (multi-tenant)
- Contrato userId: O app é a fonte de verdade. Passar
syncNow({ userId })em toda chamada; a lib nunca resolve userId internamente. - Core upserts are scoped by tenant:
onConflict: 'user_id,id' initialLastSyncAt— restauralastSyncAtde storage persistente (evita cold-start loop)- Fallback
since: 90 dias (não epoch) em cold start / reinstall - Migration (preserves data):
docs/SUPABASE_MIGRATION_MULTI_TENANT.sql - Real Supabase E2E tests are opt-in:
SUPABASE_TEST=1 npm run test:supabase - Aggregated tables (
adherence_daily,levels_state) are derived locally; Supabase sync usa merge não-degradante (linhas remotas não podem reduzirtotal/done/percent/streakquando comparadas ao agregado local). - Timestamp conflict checks (
updated_at) are normalized to UTC before comparison, preventing false last-write-wins decisions when local SQLite timestamps have no timezone suffix.
Features
- 📅 Flexible Scheduling: 6 schedule types (TIMES, WEEKLY, INTERVAL, CYCLIC, DAYS_INTERVAL, PARTS_OF_DAY)
- 🔔 OS Notifications: Native notification scheduling with automatic reconciliation
- 💾 Offline-First: SQLite local storage
- 📊 Adherence Tracking: Materialized daily adherence metrics
- 🎮 Gamification: Streak calculation and level progression
- 🔌 Extensible: Event-driven plugin system
- 🧪 Test Coverage: Comprehensive validation suite
Installation
npm install @aleonnet/healthcare-scheduler
# Peer dependency (if using React Native)
npm install expo-sqliteQuick Start
import { HealthcareScheduler, MockAdapter, MockNotificationAdapter } from '@aleonnet/healthcare-scheduler';
const scheduler = new HealthcareScheduler({
storage: new MockAdapter(), // or SQLiteAdapter for production
notificationDriver: new MockNotificationAdapter(), // or NotifeeAdapter (see below)
windowDays: 14,
maxPendingNotifications: 45,
enableGrouping: false
});
await scheduler.bootstrap();
// Create medication plan
const planId = await scheduler.createPlan({
item: {
type: 'MED',
name: 'Losartana 50mg',
meta: { concentration: '50mg' }
},
schedule: {
kind: 'TIMES',
times: ['08:00', '20:00']
},
startsAt: new Date().toISOString()
});
// Get today's medication schedule
const today = await scheduler.getTodayOccurrences();
// Record medication taken
await scheduler.recordEvent(occurrenceId, 'TAKEN', { source: 'user' });Schedule Types
1. TIMES (Fixed times per day)
{ kind: 'TIMES', times: ['08:00', '14:00', '20:00'] }2. WEEKLY (Weekday-specific schedule)
{
kind: 'WEEKLY',
lines: [
{ id: '1', daysOfWeek: [1,2,3,4,5], time: '08:00', qty: 1 }
]
}3. INTERVAL (Every N hours)
{
kind: 'INTERVAL',
intervalH: 8,
firstDoseAt: '2025-01-01T08:00:00.000Z'
}4. CYCLIC (Take/pause cycles, e.g. contraceptives)
{
kind: 'CYCLIC',
takeDays: 21,
pauseDays: 7,
times: ['21:00'],
cycleAnchorISO: '2025-01-01T00:00:00.000Z'
}5. DAYS_INTERVAL (Every N days)
{
kind: 'DAYS_INTERVAL',
daysInterval: 7,
times: ['09:00'],
anchorDateISO: '2025-01-01T00:00:00.000Z'
}6. PARTS_OF_DAY (Morning/Afternoon/Evening)
{
kind: 'PARTS_OF_DAY',
parts: [
{ code: 'morning', label: 'Manhã', time: '08:00', qty: 1 },
{ code: 'night', label: 'Noite', time: '21:00', qty: 1 }
]
}Gamification Plugin
import { GamificationPlugin } from '@aleonnet/healthcare-scheduler';
const config = {
levels: [
{ id: 'novice', name: 'Novato', range: [0, 6], tolerance: 0 },
{ id: 'bronze', name: 'Bronze', range: [7, 13], tolerance: 1 },
{ id: 'silver', name: 'Prata', range: [14, 29], tolerance: 2 },
{ id: 'gold', name: 'Ouro', range: [30, 59], tolerance: 3 },
{ id: 'diamond', name: 'Diamante',range: [60, 9999], tolerance: 4 },
],
rules: [],
streakThresholdPercent: 80,
recomputeWindowDays: 30,
streakCalculationWindowDays: 90,
streakFallbackMode: 'fallback', // 'fallback' | 'reset'
};
scheduler.registerPlugin(new GamificationPlugin(config));
await scheduler.bootstrap();
// Get current level and streak
const plugin = scheduler.getPlugin<GamificationPlugin>('gamification');
const levels = await plugin.getCurrentLevelsState();
// => { streak: 5, levelId: 'bronze', levelName: 'Bronze', progress: 0.4, ... }
// Get daily adherence
const adherence = await plugin.getDayAdherence(new Date());
// => { date: '2025-10-11', total: 4, done: 3, pending: 1, percent: 75 }Streak Tolerance
Levels have tolerance (grace period) for missed days:
- Within tolerance: Streak freezes, level preserved
- Exceeds tolerance (
streakFallbackMode):'fallback': Streak falls to ceiling of previous level, then resumes progressively with new good days'reset': Streak falls to 0 (original behavior)
Production Usage with NotifeeAdapter
For React Native production apps, use NotifeeAdapter with Platform information:
import { Platform } from 'react-native';
import {
HealthcareScheduler,
SQLiteAdapter,
NotifeeAdapter,
GamificationPlugin
} from '@aleonnet/healthcare-scheduler';
// Initialize storage
const storage = await SQLiteAdapter.connect({
dbName: 'healthcare.db',
autoApplySchema: true
});
// Initialize notifications with Platform info
const notificationDriver = new NotifeeAdapter({
platform: Platform // Required for Android/iOS specific behavior
});
// Create scheduler
const scheduler = new HealthcareScheduler({
storage,
notificationDriver,
windowDays: 14,
maxPendingNotifications: 45
});
// Register plugins
scheduler.registerPlugin(new GamificationPlugin());
// Bootstrap
await scheduler.bootstrap();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.
Handling Significant Time/Timezone Changes (DST, travel)
When the device timezone/offset changes, OS timestamp-based triggers fire at the same absolute epoch, which may shift local times. To guarantee exact local times, re-build the window and re-schedule notifications:
// iOS: observe UIApplicationSignificantTimeChange
// Android: observe ACTION_TIMEZONE_CHANGED / ACTION_TIME_CHANGED
await scheduler.handleSignificantTimeChange({ horizonDays: 2 });What it does:
- Rebuilds the local window using current timezone.
- Removes stale
PENDINGoccurrences in a local border window (past + future bounds), keeping non-pending history untouched. - Cancels all scheduled notifications.
- Reconciles with the OS scheduling upcoming items again, including a 1-day past check to clean border cases.
References:
- Apple UIApplication.significantTimeChangeNotification — https://developer.apple.com/documentation/uikit/uiapplication/1622943-significanttimechangenotification
- Android ACTION_TIMEZONE_CHANGED / ACTION_TIME_CHANGED — https://developer.android.com/reference/android/content/Intent#ACTION_TIMEZONE_CHANGED
- Notifee timestamp triggers — https://notifee.app/react-native/docs/trigger-notifications#timestamp-trigger
Architecture
src/
├── core/ # Main API, EventBus, types
├── database/ # Storage abstraction, repositories
│ ├── repositories/ # ItemsRepo, PlansRepo, OccurrencesRepo, EventsRepo
│ └── adapters/ # MockAdapter, SQLiteAdapter (peer dep)
├── planning/ # Schedule expansion engine
├── reconciler/ # DB ↔ OS notification sync
├── notifications/ # Notification driver abstraction
├── plugins/ # Extensible plugin system
│ └── gamification/ # Adherence tracking & levels
└── utils/ # Queue, timezone helpersEvents
scheduler.on('plans:changed', ({ planIds }) => {
console.log('Plans updated:', planIds);
});
scheduler.on('occurrences:changed', ({ occurrenceIds }) => {
console.log('Occurrences changed:', occurrenceIds);
});
scheduler.on('window:filled', ({ count }) => {
console.log('Notifications scheduled:', count);
});Note:
- Window fill and reconciliation are asynchronous and triggered by events (
plans:changed,occurrences:changed). Wait forwindow:filledin tests or after batch operations; there is no publicupdateWindowmethod.
Development
# Install
npm install
# Run tests
npm test
# Run specific test suite
npm test -- tests/validation/scenarios/scenario1_weekly.test.ts
# Build
npm run build
# Lint
npm run lintTesting
The library includes comprehensive validation:
- Unit tests: Planning engine, repositories, window scheduler
- Integration tests: Full CRUD cycles
- Validation scenarios: 6 real-world medication schedules
- Plugin tests: Gamification, streak calculation
- Regression coverage: bad-days accumulation across daily recomputes (
64 -> 72, preventing inflated77) - Sync regression coverage: UTC normalization for
updated_atconflict resolution (local timestamp sem timezone vs remoto com+00)
All tests run in-memory using MockAdapter (no database required).
Documentation
License
MIT
Credits
Alessandro Barbosa.
Built with TypeScript, tested with Jest, validated against real-world use cases.
