@blackcode_sa/metaestetics-api 1.12.68 → 1.13.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/admin/index.d.mts +801 -2
- package/dist/admin/index.d.ts +801 -2
- package/dist/admin/index.js +2332 -153
- package/dist/admin/index.mjs +2321 -153
- package/dist/index.d.mts +1057 -2
- package/dist/index.d.ts +1057 -2
- package/dist/index.js +4150 -2117
- package/dist/index.mjs +3832 -1810
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +140 -0
- package/src/admin/analytics/analytics.admin.service.ts +278 -0
- package/src/admin/analytics/index.ts +2 -0
- package/src/admin/index.ts +6 -0
- package/src/backoffice/services/README.md +17 -0
- package/src/backoffice/services/analytics.service.proposal.md +863 -0
- package/src/backoffice/services/analytics.service.summary.md +143 -0
- package/src/services/analytics/ARCHITECTURE.md +199 -0
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -0
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -0
- package/src/services/analytics/QUICK_START.md +393 -0
- package/src/services/analytics/README.md +287 -0
- package/src/services/analytics/SUMMARY.md +141 -0
- package/src/services/analytics/USAGE_GUIDE.md +518 -0
- package/src/services/analytics/analytics-cloud.service.ts +222 -0
- package/src/services/analytics/analytics.service.ts +1632 -0
- package/src/services/analytics/index.ts +3 -0
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -0
- package/src/services/analytics/utils/cost-calculation.utils.ts +154 -0
- package/src/services/analytics/utils/grouping.utils.ts +394 -0
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -0
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -0
- package/src/services/appointment/appointment.service.ts +50 -6
- package/src/services/index.ts +1 -0
- package/src/types/analytics/analytics.types.ts +500 -0
- package/src/types/analytics/grouped-analytics.types.ts +148 -0
- package/src/types/analytics/index.ts +4 -0
- package/src/types/analytics/stored-analytics.types.ts +137 -0
- package/src/types/index.ts +3 -0
- package/src/types/notifications/index.ts +21 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Analytics Service - Quick Start Guide
|
|
2
|
+
|
|
3
|
+
## 🚀 What We Can Do
|
|
4
|
+
|
|
5
|
+
The Analytics Service provides **comprehensive insights** into your clinic operations:
|
|
6
|
+
|
|
7
|
+
- **Financial Intelligence**: Revenue, costs, payment tracking
|
|
8
|
+
- **Practitioner Performance**: Doctor efficiency, cancellations, revenue
|
|
9
|
+
- **Procedure Analytics**: Popularity, profitability, product usage
|
|
10
|
+
- **Time Management**: Booked vs actual time, efficiency
|
|
11
|
+
- **Cancellation & No-Show Analysis**: Rates by clinic/doctor/patient/procedure
|
|
12
|
+
- **Patient Insights**: Lifetime value, retention, behavior
|
|
13
|
+
- **Product Usage**: Usage patterns, revenue contribution
|
|
14
|
+
- **Clinic Performance**: Overall metrics and comparisons
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🔄 How It Works: Computed vs On-Demand
|
|
19
|
+
|
|
20
|
+
### **Pre-Computed Analytics** (Default - Recommended) ⚡
|
|
21
|
+
|
|
22
|
+
**How:**
|
|
23
|
+
1. Cloud Function runs **every 12 hours**
|
|
24
|
+
2. Computes analytics for all clinics
|
|
25
|
+
3. Stores in Firestore: `clinics/{clinicBranchId}/analytics/`
|
|
26
|
+
4. Client reads cached data (instant!)
|
|
27
|
+
|
|
28
|
+
**Benefits:**
|
|
29
|
+
- ⚡ **Fast**: 1 document read = instant response
|
|
30
|
+
- 💰 **Cheap**: 1 read vs hundreds/thousands
|
|
31
|
+
- 📈 **Scalable**: Works with large datasets
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
```typescript
|
|
35
|
+
// Automatically uses cached data if fresh (< 12 hours)
|
|
36
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
37
|
+
{ clinicBranchId: 'clinic-123' },
|
|
38
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
39
|
+
);
|
|
40
|
+
// Returns instantly! ⚡
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### **On-Demand Calculation** (Fallback) 🔄
|
|
44
|
+
|
|
45
|
+
**When:**
|
|
46
|
+
- No cached data exists
|
|
47
|
+
- Cache is stale (> 12 hours)
|
|
48
|
+
- You set `useCache: false`
|
|
49
|
+
- Custom date ranges
|
|
50
|
+
|
|
51
|
+
**How:**
|
|
52
|
+
1. Queries all appointments
|
|
53
|
+
2. Calculates metrics in real-time
|
|
54
|
+
3. Returns results
|
|
55
|
+
|
|
56
|
+
**Example:**
|
|
57
|
+
```typescript
|
|
58
|
+
// Force on-demand calculation
|
|
59
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
60
|
+
{ clinicBranchId: 'clinic-123' },
|
|
61
|
+
dateRange,
|
|
62
|
+
{ useCache: false } // Force calculation
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📊 Available Analytics
|
|
69
|
+
|
|
70
|
+
### 1. **Practitioner Analytics** 👨⚕️
|
|
71
|
+
```typescript
|
|
72
|
+
const metrics = await analyticsService.getPractitionerAnalytics(
|
|
73
|
+
'practitioner-id',
|
|
74
|
+
dateRange
|
|
75
|
+
);
|
|
76
|
+
// Returns: appointments, cancellations, no-shows, revenue, time efficiency, etc.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. **Procedure Analytics** 🏥
|
|
80
|
+
```typescript
|
|
81
|
+
// Single procedure
|
|
82
|
+
const procedure = await analyticsService.getProcedureAnalytics('procedure-id', dateRange);
|
|
83
|
+
|
|
84
|
+
// All procedures
|
|
85
|
+
const all = await analyticsService.getProcedureAnalytics(undefined, dateRange);
|
|
86
|
+
|
|
87
|
+
// Most popular
|
|
88
|
+
const popular = await analyticsService.getProcedurePopularity(dateRange, 10);
|
|
89
|
+
|
|
90
|
+
// Most profitable
|
|
91
|
+
const profitable = await analyticsService.getProcedureProfitability(dateRange, 10);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. **Time Efficiency** ⏱️
|
|
95
|
+
```typescript
|
|
96
|
+
const time = await analyticsService.getTimeEfficiencyMetrics(
|
|
97
|
+
{ clinicBranchId: 'clinic-123' },
|
|
98
|
+
dateRange
|
|
99
|
+
);
|
|
100
|
+
// Returns: efficiency %, overrun, underutilization, etc.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. **Cancellation Metrics** ❌
|
|
104
|
+
```typescript
|
|
105
|
+
const cancellations = await analyticsService.getCancellationMetrics(
|
|
106
|
+
'practitioner', // or 'clinic', 'patient', 'procedure'
|
|
107
|
+
dateRange
|
|
108
|
+
);
|
|
109
|
+
// Returns array with cancellation rates, reasons, lead times
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 5. **No-Show Metrics** 🚫
|
|
113
|
+
```typescript
|
|
114
|
+
const noShows = await analyticsService.getNoShowMetrics(
|
|
115
|
+
'practitioner', // or 'clinic', 'patient', 'procedure'
|
|
116
|
+
dateRange
|
|
117
|
+
);
|
|
118
|
+
// Returns array with no-show rates per entity
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 6. **Revenue Metrics** 💰
|
|
122
|
+
```typescript
|
|
123
|
+
const revenue = await analyticsService.getRevenueMetrics(
|
|
124
|
+
{ clinicBranchId: 'clinic-123' },
|
|
125
|
+
dateRange
|
|
126
|
+
);
|
|
127
|
+
// Returns: total revenue, by status, by payment status, unpaid, etc.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 7. **Product Usage** 📦
|
|
131
|
+
```typescript
|
|
132
|
+
const products = await analyticsService.getProductUsageMetrics(
|
|
133
|
+
undefined, // or 'product-id'
|
|
134
|
+
dateRange
|
|
135
|
+
);
|
|
136
|
+
// Returns: usage counts, revenue, quantities per product
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 8. **Patient Analytics** 👥
|
|
140
|
+
```typescript
|
|
141
|
+
const patient = await analyticsService.getPatientAnalytics(
|
|
142
|
+
'patient-id', // or undefined for all
|
|
143
|
+
dateRange
|
|
144
|
+
);
|
|
145
|
+
// Returns: lifetime value, retention, frequency, etc.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 9. **Dashboard Data** 📊
|
|
149
|
+
```typescript
|
|
150
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
151
|
+
{ clinicBranchId: 'clinic-123' },
|
|
152
|
+
dateRange
|
|
153
|
+
);
|
|
154
|
+
// Returns: comprehensive dashboard with all metrics
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 🎯 Specific Use Case: Patient No-Show Per Doctor
|
|
160
|
+
|
|
161
|
+
### **Question**: "Which patients have the highest no-show rate for each doctor?"
|
|
162
|
+
|
|
163
|
+
### **Solution 1: No-Show Rates by Practitioner** ✅
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Get no-show metrics grouped by practitioner (doctor)
|
|
167
|
+
const noShowByPractitioner = await analyticsService.getNoShowMetrics(
|
|
168
|
+
'practitioner',
|
|
169
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Returns array:
|
|
173
|
+
// [
|
|
174
|
+
// {
|
|
175
|
+
// entityId: 'practitioner-123',
|
|
176
|
+
// entityName: 'Dr. Smith',
|
|
177
|
+
// entityType: 'practitioner',
|
|
178
|
+
// totalAppointments: 100,
|
|
179
|
+
// noShowAppointments: 15,
|
|
180
|
+
// noShowRate: 15.0 // 15%
|
|
181
|
+
// },
|
|
182
|
+
// ...
|
|
183
|
+
// ]
|
|
184
|
+
|
|
185
|
+
// Sort by no-show rate (highest first)
|
|
186
|
+
const sorted = noShowByPractitioner
|
|
187
|
+
.sort((a, b) => b.noShowRate - a.noShowRate);
|
|
188
|
+
|
|
189
|
+
console.log('Doctors ranked by no-show rate:');
|
|
190
|
+
sorted.forEach((doctor, index) => {
|
|
191
|
+
console.log(`${index + 1}. ${doctor.entityName}: ${doctor.noShowRate}%`);
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### **Solution 2: Patient No-Shows for a Specific Doctor** ✅
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Step 1: Get all appointments for a specific practitioner
|
|
199
|
+
const practitionerId = 'practitioner-123';
|
|
200
|
+
const dateRange = { start: new Date('2024-01-01'), end: new Date('2024-12-31') };
|
|
201
|
+
|
|
202
|
+
// Get practitioner analytics (includes no-show count)
|
|
203
|
+
const practitionerMetrics = await analyticsService.getPractitionerAnalytics(
|
|
204
|
+
practitionerId,
|
|
205
|
+
dateRange
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
console.log(`Dr. ${practitionerMetrics.practitionerName}:`);
|
|
209
|
+
console.log(` Total Appointments: ${practitionerMetrics.totalAppointments}`);
|
|
210
|
+
console.log(` No-Shows: ${practitionerMetrics.noShowAppointments}`);
|
|
211
|
+
console.log(` No-Show Rate: ${practitionerMetrics.noShowRate}%`);
|
|
212
|
+
|
|
213
|
+
// Step 2: Get patient-level breakdown for this doctor
|
|
214
|
+
// (You can filter appointments manually or use patient analytics)
|
|
215
|
+
|
|
216
|
+
// Get patient analytics filtered by practitioner
|
|
217
|
+
const allPatients = await analyticsService.getPatientAnalytics(undefined, dateRange);
|
|
218
|
+
|
|
219
|
+
// Filter patients who had appointments with this practitioner
|
|
220
|
+
// and calculate their no-show rates
|
|
221
|
+
const patientsWithThisDoctor = allPatients
|
|
222
|
+
.filter(patient => {
|
|
223
|
+
// Check if patient had appointments with this practitioner
|
|
224
|
+
// (You'd need to query appointments or enhance patient analytics)
|
|
225
|
+
return true; // Placeholder
|
|
226
|
+
})
|
|
227
|
+
.sort((a, b) => b.noShowRate - a.noShowRate);
|
|
228
|
+
|
|
229
|
+
console.log('\nPatients with highest no-show rates for this doctor:');
|
|
230
|
+
patientsWithThisDoctor.slice(0, 10).forEach((patient, index) => {
|
|
231
|
+
console.log(`${index + 1}. ${patient.patientName}: ${patient.noShowRate}%`);
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### **Solution 3: Cross-Analysis (No-Shows by Patient AND Doctor)** ✅
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Get no-shows grouped by patient
|
|
239
|
+
const noShowByPatient = await analyticsService.getNoShowMetrics(
|
|
240
|
+
'patient',
|
|
241
|
+
dateRange
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Get no-shows grouped by practitioner
|
|
245
|
+
const noShowByPractitioner = await analyticsService.getNoShowMetrics(
|
|
246
|
+
'practitioner',
|
|
247
|
+
dateRange
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// For detailed cross-analysis, you can:
|
|
251
|
+
// 1. Get all appointments
|
|
252
|
+
// 2. Filter no-shows
|
|
253
|
+
// 3. Group by practitioner AND patient
|
|
254
|
+
const appointments = await analyticsService['fetchAppointments'](
|
|
255
|
+
undefined,
|
|
256
|
+
dateRange
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const noShows = appointments.filter(a => a.status === AppointmentStatus.NO_SHOW);
|
|
260
|
+
|
|
261
|
+
// Create a map: practitioner -> patients -> no-show count
|
|
262
|
+
const practitionerPatientMap = new Map<
|
|
263
|
+
string,
|
|
264
|
+
Map<string, { patientName: string; noShowCount: number; totalAppointments: number }>
|
|
265
|
+
>();
|
|
266
|
+
|
|
267
|
+
appointments.forEach(appointment => {
|
|
268
|
+
const practitionerId = appointment.practitionerId;
|
|
269
|
+
const patientId = appointment.patientId;
|
|
270
|
+
const patientName = appointment.patientInfo?.fullName || 'Unknown';
|
|
271
|
+
|
|
272
|
+
if (!practitionerPatientMap.has(practitionerId)) {
|
|
273
|
+
practitionerPatientMap.set(practitionerId, new Map());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const patientMap = practitionerPatientMap.get(practitionerId)!;
|
|
277
|
+
if (!patientMap.has(patientId)) {
|
|
278
|
+
patientMap.set(patientId, { patientName, noShowCount: 0, totalAppointments: 0 });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const patientData = patientMap.get(patientId)!;
|
|
282
|
+
patientData.totalAppointments++;
|
|
283
|
+
|
|
284
|
+
if (appointment.status === AppointmentStatus.NO_SHOW) {
|
|
285
|
+
patientData.noShowCount++;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Display results
|
|
290
|
+
practitionerPatientMap.forEach((patientMap, practitionerId) => {
|
|
291
|
+
const practitioner = appointments.find(a => a.practitionerId === practitionerId);
|
|
292
|
+
const practitionerName = practitioner?.practitionerInfo?.name || 'Unknown';
|
|
293
|
+
|
|
294
|
+
console.log(`\n${practitionerName}:`);
|
|
295
|
+
|
|
296
|
+
const patientRates = Array.from(patientMap.entries())
|
|
297
|
+
.map(([patientId, data]) => ({
|
|
298
|
+
patientId,
|
|
299
|
+
patientName: data.patientName,
|
|
300
|
+
noShowRate: (data.noShowCount / data.totalAppointments) * 100,
|
|
301
|
+
noShowCount: data.noShowCount,
|
|
302
|
+
totalAppointments: data.totalAppointments,
|
|
303
|
+
}))
|
|
304
|
+
.sort((a, b) => b.noShowRate - a.noShowRate);
|
|
305
|
+
|
|
306
|
+
patientRates.forEach(patient => {
|
|
307
|
+
console.log(` ${patient.patientName}: ${patient.noShowRate.toFixed(1)}% (${patient.noShowCount}/${patient.totalAppointments})`);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 📝 Quick Examples
|
|
315
|
+
|
|
316
|
+
### Example 1: Doctor Performance Dashboard
|
|
317
|
+
```typescript
|
|
318
|
+
const practitionerMetrics = await analyticsService.getPractitionerAnalytics(
|
|
319
|
+
'practitioner-id',
|
|
320
|
+
dateRange
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
console.log(`Doctor: ${practitionerMetrics.practitionerName}`);
|
|
324
|
+
console.log(`Appointments: ${practitionerMetrics.totalAppointments}`);
|
|
325
|
+
console.log(`Cancellation Rate: ${practitionerMetrics.cancellationRate}%`);
|
|
326
|
+
console.log(`No-Show Rate: ${practitionerMetrics.noShowRate}%`);
|
|
327
|
+
console.log(`Time Efficiency: ${practitionerMetrics.timeEfficiency}%`);
|
|
328
|
+
console.log(`Total Revenue: ${practitionerMetrics.totalRevenue} ${practitionerMetrics.currency}`);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Example 2: Top Procedures by Revenue
|
|
332
|
+
```typescript
|
|
333
|
+
const topProcedures = await analyticsService.getProcedureProfitability(
|
|
334
|
+
dateRange,
|
|
335
|
+
10
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
topProcedures.forEach((procedure, index) => {
|
|
339
|
+
console.log(`${index + 1}. ${procedure.procedureName}`);
|
|
340
|
+
console.log(` Revenue: ${procedure.totalRevenue}`);
|
|
341
|
+
console.log(` Appointments: ${procedure.appointmentCount}`);
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Example 3: Clinic Comparison
|
|
346
|
+
```typescript
|
|
347
|
+
const clinic1 = await analyticsService.getClinicAnalytics('clinic-1', dateRange);
|
|
348
|
+
const clinic2 = await analyticsService.getClinicAnalytics('clinic-2', dateRange);
|
|
349
|
+
|
|
350
|
+
console.log('Clinic Comparison:');
|
|
351
|
+
console.log(`Clinic 1 Revenue: ${clinic1.totalRevenue}`);
|
|
352
|
+
console.log(`Clinic 2 Revenue: ${clinic2.totalRevenue}`);
|
|
353
|
+
console.log(`Clinic 1 Cancellation Rate: ${clinic1.cancellationRate}%`);
|
|
354
|
+
console.log(`Clinic 2 Cancellation Rate: ${clinic2.cancellationRate}%`);
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 🎛️ Configuration
|
|
360
|
+
|
|
361
|
+
### Change Cache Freshness
|
|
362
|
+
```typescript
|
|
363
|
+
// Use cache if data is less than 6 hours old
|
|
364
|
+
const metrics = await analyticsService.getPractitionerAnalytics(
|
|
365
|
+
practitionerId,
|
|
366
|
+
dateRange,
|
|
367
|
+
{ maxCacheAgeHours: 6 }
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Force On-Demand Calculation
|
|
372
|
+
```typescript
|
|
373
|
+
const metrics = await analyticsService.getPractitionerAnalytics(
|
|
374
|
+
practitionerId,
|
|
375
|
+
dateRange,
|
|
376
|
+
{ useCache: false }
|
|
377
|
+
);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Change Cloud Function Schedule
|
|
381
|
+
Edit `Cloud/functions/src/analytics/computeAnalytics.ts`:
|
|
382
|
+
```typescript
|
|
383
|
+
schedule: "every 6 hours" // or "every 24 hours"
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 📚 Full Documentation
|
|
389
|
+
|
|
390
|
+
- **Usage Guide**: `USAGE_GUIDE.md` - Complete guide with all use cases
|
|
391
|
+
- **Architecture**: `ARCHITECTURE.md` - How computed vs on-demand works
|
|
392
|
+
- **README**: `README.md` - API reference
|
|
393
|
+
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Analytics Service
|
|
2
|
+
|
|
3
|
+
Comprehensive financial and analytical intelligence service for the Clinic Admin app. Provides insights about doctors, procedures, appointments, patients, products, and clinic operations.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `AnalyticsService` extends `BaseService` and provides methods to analyze appointment data, calculate metrics, and generate reports for clinic administration.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Initialization
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { AnalyticsService } from '@blackcode_sa/metaestetics-api';
|
|
15
|
+
import { AppointmentService } from '@blackcode_sa/metaestetics-api';
|
|
16
|
+
|
|
17
|
+
// Initialize dependencies
|
|
18
|
+
const appointmentService = new AppointmentService(db, auth, app, ...);
|
|
19
|
+
|
|
20
|
+
// Initialize analytics service
|
|
21
|
+
const analyticsService = new AnalyticsService(db, auth, app, appointmentService);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Available Methods
|
|
25
|
+
|
|
26
|
+
### ⚡ **Grouped Analytics** (NEW!)
|
|
27
|
+
|
|
28
|
+
All analytics now support **consistent grouping** by clinic, practitioner, procedure, patient, or technology:
|
|
29
|
+
|
|
30
|
+
- `getRevenueMetricsByEntity(groupBy, dateRange?, filters?)` - Revenue by entity
|
|
31
|
+
- `getProductUsageMetricsByEntity(groupBy, dateRange?, filters?)` - Product usage by entity
|
|
32
|
+
- `getTimeEfficiencyMetricsByEntity(groupBy, dateRange?, filters?)` - Time efficiency by entity
|
|
33
|
+
- `getPatientBehaviorMetricsByEntity(groupBy, dateRange?, filters?)` - Patient behavior by entity
|
|
34
|
+
- `getCancellationMetrics(groupBy, dateRange?)` - Cancellations by entity (existing)
|
|
35
|
+
- `getNoShowMetrics(groupBy, dateRange?)` - No-shows by entity (existing)
|
|
36
|
+
|
|
37
|
+
**Technology Grouping**: Groups all procedures using the same technology across all doctors (e.g., all Botox treatments regardless of which doctor performed them).
|
|
38
|
+
|
|
39
|
+
**See [GROUPED_ANALYTICS.md](./GROUPED_ANALYTICS.md) for complete guide and examples.**
|
|
40
|
+
|
|
41
|
+
### Practitioner Analytics
|
|
42
|
+
|
|
43
|
+
#### `getPractitionerAnalytics(practitionerId, dateRange?)`
|
|
44
|
+
|
|
45
|
+
Get comprehensive performance metrics for a practitioner.
|
|
46
|
+
|
|
47
|
+
**Returns**: `PractitionerAnalytics`
|
|
48
|
+
|
|
49
|
+
**Example**:
|
|
50
|
+
```typescript
|
|
51
|
+
const metrics = await analyticsService.getPractitionerAnalytics(
|
|
52
|
+
'practitioner-id-123',
|
|
53
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
console.log(`Total appointments: ${metrics.totalAppointments}`);
|
|
57
|
+
console.log(`Cancellation rate: ${metrics.cancellationRate}%`);
|
|
58
|
+
console.log(`Total revenue: ${metrics.totalRevenue} ${metrics.currency}`);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Procedure Analytics
|
|
62
|
+
|
|
63
|
+
#### `getProcedureAnalytics(procedureId?, dateRange?)`
|
|
64
|
+
|
|
65
|
+
Get performance metrics for one or all procedures.
|
|
66
|
+
|
|
67
|
+
**Returns**: `ProcedureAnalytics | ProcedureAnalytics[]`
|
|
68
|
+
|
|
69
|
+
**Example**:
|
|
70
|
+
```typescript
|
|
71
|
+
// Get analytics for a specific procedure
|
|
72
|
+
const procedureMetrics = await analyticsService.getProcedureAnalytics(
|
|
73
|
+
'procedure-id-123',
|
|
74
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Get analytics for all procedures
|
|
78
|
+
const allProcedures = await analyticsService.getProcedureAnalytics(
|
|
79
|
+
undefined,
|
|
80
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
81
|
+
);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### `getProcedurePopularity(dateRange?, limit?)`
|
|
85
|
+
|
|
86
|
+
Get the most popular procedures by appointment count.
|
|
87
|
+
|
|
88
|
+
**Returns**: `ProcedurePopularity[]`
|
|
89
|
+
|
|
90
|
+
**Example**:
|
|
91
|
+
```typescript
|
|
92
|
+
const topProcedures = await analyticsService.getProcedurePopularity(
|
|
93
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') },
|
|
94
|
+
10 // Top 10 procedures
|
|
95
|
+
);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `getProcedureProfitability(dateRange?, limit?)`
|
|
99
|
+
|
|
100
|
+
Get the most profitable procedures by revenue.
|
|
101
|
+
|
|
102
|
+
**Returns**: `ProcedureProfitability[]`
|
|
103
|
+
|
|
104
|
+
**Example**:
|
|
105
|
+
```typescript
|
|
106
|
+
const profitableProcedures = await analyticsService.getProcedureProfitability(
|
|
107
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') },
|
|
108
|
+
10
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Time Efficiency Analytics
|
|
113
|
+
|
|
114
|
+
#### `getTimeEfficiencyMetrics(filters?, dateRange?)`
|
|
115
|
+
|
|
116
|
+
Analyze booked time vs actual time spent on appointments.
|
|
117
|
+
|
|
118
|
+
**Returns**: `TimeEfficiencyMetrics`
|
|
119
|
+
|
|
120
|
+
**Example**:
|
|
121
|
+
```typescript
|
|
122
|
+
const timeMetrics = await analyticsService.getTimeEfficiencyMetrics(
|
|
123
|
+
{ clinicBranchId: 'clinic-123' },
|
|
124
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
console.log(`Average efficiency: ${timeMetrics.averageEfficiency}%`);
|
|
128
|
+
console.log(`Average overrun: ${timeMetrics.averageOverrun} minutes`);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Cancellation & No-Show Analytics
|
|
132
|
+
|
|
133
|
+
#### `getCancellationMetrics(groupBy, dateRange?)`
|
|
134
|
+
|
|
135
|
+
Get cancellation metrics grouped by clinic, practitioner, patient, or procedure.
|
|
136
|
+
|
|
137
|
+
**Returns**: `CancellationMetrics | CancellationMetrics[]`
|
|
138
|
+
|
|
139
|
+
**Example**:
|
|
140
|
+
```typescript
|
|
141
|
+
// Get cancellations by clinic
|
|
142
|
+
const clinicCancellations = await analyticsService.getCancellationMetrics(
|
|
143
|
+
'clinic',
|
|
144
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Get cancellations by practitioner
|
|
148
|
+
const practitionerCancellations = await analyticsService.getCancellationMetrics(
|
|
149
|
+
'practitioner',
|
|
150
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `getNoShowMetrics(groupBy, dateRange?)`
|
|
155
|
+
|
|
156
|
+
Get no-show metrics grouped by clinic, practitioner, patient, or procedure.
|
|
157
|
+
|
|
158
|
+
**Returns**: `NoShowMetrics | NoShowMetrics[]`
|
|
159
|
+
|
|
160
|
+
**Example**:
|
|
161
|
+
```typescript
|
|
162
|
+
const noShowMetrics = await analyticsService.getNoShowMetrics(
|
|
163
|
+
'patient',
|
|
164
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Financial Analytics
|
|
169
|
+
|
|
170
|
+
#### `getRevenueMetrics(filters?, dateRange?)`
|
|
171
|
+
|
|
172
|
+
Get comprehensive revenue metrics.
|
|
173
|
+
|
|
174
|
+
**Returns**: `RevenueMetrics`
|
|
175
|
+
|
|
176
|
+
**Example**:
|
|
177
|
+
```typescript
|
|
178
|
+
const revenue = await analyticsService.getRevenueMetrics(
|
|
179
|
+
{ clinicBranchId: 'clinic-123' },
|
|
180
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
console.log(`Total revenue: ${revenue.totalRevenue} ${revenue.currency}`);
|
|
184
|
+
console.log(`Unpaid revenue: ${revenue.unpaidRevenue}`);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Product Usage Analytics
|
|
188
|
+
|
|
189
|
+
#### `getProductUsageMetrics(productId?, dateRange?)`
|
|
190
|
+
|
|
191
|
+
Get product usage metrics for one or all products.
|
|
192
|
+
|
|
193
|
+
**Returns**: `ProductUsageMetrics | ProductUsageMetrics[]`
|
|
194
|
+
|
|
195
|
+
**Example**:
|
|
196
|
+
```typescript
|
|
197
|
+
// Get usage for a specific product
|
|
198
|
+
const productMetrics = await analyticsService.getProductUsageMetrics(
|
|
199
|
+
'product-id-123',
|
|
200
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Get usage for all products
|
|
204
|
+
const allProducts = await analyticsService.getProductUsageMetrics(
|
|
205
|
+
undefined,
|
|
206
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Patient Analytics
|
|
211
|
+
|
|
212
|
+
#### `getPatientAnalytics(patientId?, dateRange?)`
|
|
213
|
+
|
|
214
|
+
Get analytics for one or all patients.
|
|
215
|
+
|
|
216
|
+
**Returns**: `PatientAnalytics | PatientAnalytics[]`
|
|
217
|
+
|
|
218
|
+
**Example**:
|
|
219
|
+
```typescript
|
|
220
|
+
const patientMetrics = await analyticsService.getPatientAnalytics(
|
|
221
|
+
'patient-id-123',
|
|
222
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
console.log(`Lifetime value: ${patientMetrics.lifetimeValue}`);
|
|
226
|
+
console.log(`Average days between appointments: ${patientMetrics.averageDaysBetweenAppointments}`);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Dashboard Analytics
|
|
230
|
+
|
|
231
|
+
#### `getDashboardData(filters?, dateRange?)`
|
|
232
|
+
|
|
233
|
+
Get comprehensive dashboard data aggregation.
|
|
234
|
+
|
|
235
|
+
**Returns**: `DashboardAnalytics`
|
|
236
|
+
|
|
237
|
+
**Example**:
|
|
238
|
+
```typescript
|
|
239
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
240
|
+
{ clinicBranchId: 'clinic-123' },
|
|
241
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
console.log(`Total appointments: ${dashboard.overview.totalAppointments}`);
|
|
245
|
+
console.log(`Total revenue: ${dashboard.overview.totalRevenue}`);
|
|
246
|
+
console.log(`Top practitioners:`, dashboard.practitionerMetrics);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Cost Calculation Priority
|
|
250
|
+
|
|
251
|
+
The service calculates appointment costs using the following priority:
|
|
252
|
+
|
|
253
|
+
1. **`metadata.finalbilling.finalPrice`** - If available, uses the final billing price
|
|
254
|
+
2. **`metadata.zonesData`** - Calculates from zone item subtotals
|
|
255
|
+
3. **`appointment.cost`** - Falls back to base appointment cost
|
|
256
|
+
|
|
257
|
+
## Time Efficiency Calculation
|
|
258
|
+
|
|
259
|
+
Time efficiency is calculated as:
|
|
260
|
+
- **Booked Duration**: `appointmentEndTime - appointmentStartTime` (minutes)
|
|
261
|
+
- **Actual Duration**: `actualDurationMinutes` or booked duration if not available
|
|
262
|
+
- **Efficiency**: `(actualDuration / bookedDuration) * 100` (percentage)
|
|
263
|
+
- **Overrun**: Positive difference if actual > booked
|
|
264
|
+
- **Underutilization**: Positive difference if booked > actual
|
|
265
|
+
|
|
266
|
+
## Status Filtering
|
|
267
|
+
|
|
268
|
+
The service filters appointments by status:
|
|
269
|
+
|
|
270
|
+
- **Completed**: `AppointmentStatus.COMPLETED`
|
|
271
|
+
- **Canceled**: `CANCELED_PATIENT`, `CANCELED_CLINIC`, `CANCELED_PATIENT_RESCHEDULED`
|
|
272
|
+
- **No-Show**: `AppointmentStatus.NO_SHOW`
|
|
273
|
+
- **Active**: All statuses except canceled and no-show
|
|
274
|
+
|
|
275
|
+
## Performance Considerations
|
|
276
|
+
|
|
277
|
+
- The service uses the `AppointmentService` to query appointments efficiently
|
|
278
|
+
- Large date ranges may require pagination (to be implemented)
|
|
279
|
+
- Consider caching dashboard data for frequently accessed metrics
|
|
280
|
+
- Use specific filters to reduce query scope when possible
|
|
281
|
+
|
|
282
|
+
## Related Documentation
|
|
283
|
+
|
|
284
|
+
- [Analytics Types](../../types/analytics/analytics.types.ts)
|
|
285
|
+
- [Appointment Service](../appointment/README.md)
|
|
286
|
+
- [Full Proposal](../../backoffice/services/analytics.service.proposal.md)
|
|
287
|
+
|