@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8
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 +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/dist/index.d.mts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +168 -6
- package/dist/index.mjs +168 -6
- package/package.json +121 -121
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +81 -81
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +67 -67
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +211 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +1043 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +14 -14
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1799 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2307 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -498
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +494 -494
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,518 +1,518 @@
|
|
|
1
|
-
# Analytics Service - Complete Usage Guide
|
|
2
|
-
|
|
3
|
-
## Table of Contents
|
|
4
|
-
1. [What We Can Do](#what-we-can-do)
|
|
5
|
-
2. [How It Works: Computed vs On-Demand](#how-it-works)
|
|
6
|
-
3. [Available Analytics](#available-analytics)
|
|
7
|
-
4. [Specific Use Cases](#specific-use-cases)
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## What We Can Do
|
|
12
|
-
|
|
13
|
-
The Analytics Service provides comprehensive insights into your clinic operations:
|
|
14
|
-
|
|
15
|
-
### 📊 **Financial Intelligence**
|
|
16
|
-
- Track total revenue, average revenue per appointment
|
|
17
|
-
- Monitor payment status (paid, unpaid, refunded)
|
|
18
|
-
- Analyze costs per patient
|
|
19
|
-
- Revenue trends over time
|
|
20
|
-
|
|
21
|
-
### 👨⚕️ **Practitioner Performance**
|
|
22
|
-
- Appointment counts and completion rates
|
|
23
|
-
- Cancellation and no-show rates per doctor
|
|
24
|
-
- Time efficiency (booked vs actual time)
|
|
25
|
-
- Revenue generation per practitioner
|
|
26
|
-
- Patient retention rates
|
|
27
|
-
|
|
28
|
-
### 🏥 **Procedure Analytics**
|
|
29
|
-
- Most popular procedures
|
|
30
|
-
- Most profitable procedures
|
|
31
|
-
- Procedure performance by category/technology
|
|
32
|
-
- Product usage per procedure
|
|
33
|
-
|
|
34
|
-
### ⏱️ **Time Management**
|
|
35
|
-
- Booked time vs actual time spent
|
|
36
|
-
- Efficiency percentages
|
|
37
|
-
- Overrun/underutilization analysis
|
|
38
|
-
- Peak hours identification
|
|
39
|
-
|
|
40
|
-
### ❌ **Cancellation & No-Show Analysis**
|
|
41
|
-
- Rates by clinic, practitioner, patient, or procedure
|
|
42
|
-
- Cancellation reasons breakdown
|
|
43
|
-
- Average cancellation lead time
|
|
44
|
-
- Patterns and trends
|
|
45
|
-
|
|
46
|
-
### 👥 **Patient Insights**
|
|
47
|
-
- Patient lifetime value
|
|
48
|
-
- Retention rates
|
|
49
|
-
- Appointment frequency
|
|
50
|
-
- Cancellation patterns per patient
|
|
51
|
-
|
|
52
|
-
### 📦 **Product Usage**
|
|
53
|
-
- Products used per appointment
|
|
54
|
-
- Revenue contribution by product
|
|
55
|
-
- Usage by procedure
|
|
56
|
-
- Quantity and pricing trends
|
|
57
|
-
|
|
58
|
-
### 🏢 **Clinic Performance**
|
|
59
|
-
- Overall clinic metrics
|
|
60
|
-
- Practitioner comparisons
|
|
61
|
-
- Procedure popularity
|
|
62
|
-
- Efficiency metrics
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## How It Works: Computed vs On-Demand
|
|
67
|
-
|
|
68
|
-
### 🔄 **Hybrid Architecture**
|
|
69
|
-
|
|
70
|
-
The service uses a **smart hybrid approach**:
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
┌─────────────────────────────────────────┐
|
|
74
|
-
│ Client Request │
|
|
75
|
-
└──────────────┬──────────────────────────┘
|
|
76
|
-
│
|
|
77
|
-
▼
|
|
78
|
-
┌─────────────────────────────────────────┐
|
|
79
|
-
│ Check Stored Analytics? │
|
|
80
|
-
└──────┬──────────────────────┬───────────┘
|
|
81
|
-
│ │
|
|
82
|
-
YES │ │ NO
|
|
83
|
-
▼ ▼
|
|
84
|
-
┌──────────────┐ ┌─────────────────────┐
|
|
85
|
-
│ Data Fresh? │ │ Calculate On-Demand │
|
|
86
|
-
│ (< 12 hours) │ │ (Query Appointments)│
|
|
87
|
-
└──┬───────┬───┘ └─────────────────────┘
|
|
88
|
-
│ │
|
|
89
|
-
YES│ │NO
|
|
90
|
-
│ │
|
|
91
|
-
▼ ▼
|
|
92
|
-
┌─────┐ ┌─────────────────────┐
|
|
93
|
-
│ ✅ │ │ Calculate On-Demand │
|
|
94
|
-
│Fast │ │ (Query Appointments)│
|
|
95
|
-
└─────┘ └─────────────────────┘
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 📦 **Pre-Computed Analytics (Recommended)**
|
|
99
|
-
|
|
100
|
-
**How it works:**
|
|
101
|
-
1. **Cloud Function runs every 12 hours** (scheduled)
|
|
102
|
-
2. Computes analytics for all clinics
|
|
103
|
-
3. Stores results in Firestore: `clinics/{clinicBranchId}/analytics/`
|
|
104
|
-
4. Client reads cached data (1 document read = instant)
|
|
105
|
-
|
|
106
|
-
**Benefits:**
|
|
107
|
-
- ⚡ **Fast**: Instant response (single document read)
|
|
108
|
-
- 💰 **Cheap**: 1 read vs hundreds/thousands
|
|
109
|
-
- 📈 **Scalable**: Works with large datasets
|
|
110
|
-
|
|
111
|
-
**Example:**
|
|
112
|
-
```typescript
|
|
113
|
-
// Automatically uses cached data if available and fresh (< 12 hours)
|
|
114
|
-
const dashboard = await analyticsService.getDashboardData(
|
|
115
|
-
{ clinicBranchId: 'clinic-123' },
|
|
116
|
-
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
117
|
-
);
|
|
118
|
-
// Returns instantly from cache! ⚡
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### 🔄 **On-Demand Calculation (Fallback)**
|
|
122
|
-
|
|
123
|
-
**When it happens:**
|
|
124
|
-
- No cached data exists yet
|
|
125
|
-
- Cached data is stale (> 12 hours old)
|
|
126
|
-
- You explicitly disable cache: `{ useCache: false }`
|
|
127
|
-
- Custom date range that doesn't match pre-computed periods
|
|
128
|
-
|
|
129
|
-
**How it works:**
|
|
130
|
-
1. Queries all appointments matching filters
|
|
131
|
-
2. Calculates metrics in real-time
|
|
132
|
-
3. Returns results
|
|
133
|
-
|
|
134
|
-
**Example:**
|
|
135
|
-
```typescript
|
|
136
|
-
// Force on-demand calculation
|
|
137
|
-
const dashboard = await analyticsService.getDashboardData(
|
|
138
|
-
{ clinicBranchId: 'clinic-123' },
|
|
139
|
-
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') },
|
|
140
|
-
{ useCache: false } // Force calculation
|
|
141
|
-
);
|
|
142
|
-
// Calculates from scratch (slower but always fresh)
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### ⚙️ **Configuration**
|
|
146
|
-
|
|
147
|
-
**Change cache freshness:**
|
|
148
|
-
```typescript
|
|
149
|
-
// Use cache if data is less than 6 hours old
|
|
150
|
-
const dashboard = await analyticsService.getDashboardData(
|
|
151
|
-
{ clinicBranchId: 'clinic-123' },
|
|
152
|
-
dateRange,
|
|
153
|
-
{ maxCacheAgeHours: 6 }
|
|
154
|
-
);
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**Change Cloud Function schedule:**
|
|
158
|
-
Edit `Cloud/functions/src/analytics/computeAnalytics.ts`:
|
|
159
|
-
```typescript
|
|
160
|
-
schedule: "every 6 hours" // or "every 24 hours", etc.
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## Available Analytics
|
|
166
|
-
|
|
167
|
-
### 1. **Practitioner Analytics** 👨⚕️
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
const metrics = await analyticsService.getPractitionerAnalytics(
|
|
171
|
-
'practitioner-id-123',
|
|
172
|
-
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Returns:
|
|
176
|
-
{
|
|
177
|
-
practitionerId: string;
|
|
178
|
-
practitionerName: string;
|
|
179
|
-
totalAppointments: number;
|
|
180
|
-
completedAppointments: number;
|
|
181
|
-
canceledAppointments: number;
|
|
182
|
-
noShowAppointments: number;
|
|
183
|
-
cancellationRate: number; // percentage
|
|
184
|
-
noShowRate: number; // percentage
|
|
185
|
-
averageBookedTime: number; // minutes
|
|
186
|
-
averageActualTime: number; // minutes
|
|
187
|
-
timeEfficiency: number; // percentage
|
|
188
|
-
totalRevenue: number;
|
|
189
|
-
averageRevenuePerAppointment: number;
|
|
190
|
-
topProcedures: Array<{ procedureId, procedureName, count, revenue }>;
|
|
191
|
-
patientRetentionRate: number;
|
|
192
|
-
uniquePatients: number;
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### 2. **Procedure Analytics** 🏥
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
// Single procedure
|
|
200
|
-
const procedure = await analyticsService.getProcedureAnalytics(
|
|
201
|
-
'procedure-id-123',
|
|
202
|
-
dateRange
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// All procedures
|
|
206
|
-
const allProcedures = await analyticsService.getProcedureAnalytics(
|
|
207
|
-
undefined,
|
|
208
|
-
dateRange
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Most popular
|
|
212
|
-
const popular = await analyticsService.getProcedurePopularity(dateRange, 10);
|
|
213
|
-
|
|
214
|
-
// Most profitable
|
|
215
|
-
const profitable = await analyticsService.getProcedureProfitability(dateRange, 10);
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### 3. **Time Efficiency** ⏱️
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
const timeMetrics = await analyticsService.getTimeEfficiencyMetrics(
|
|
222
|
-
{ clinicBranchId: 'clinic-123' },
|
|
223
|
-
dateRange
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
// Returns:
|
|
227
|
-
{
|
|
228
|
-
averageBookedDuration: number; // minutes
|
|
229
|
-
averageActualDuration: number; // minutes
|
|
230
|
-
averageEfficiency: number; // percentage
|
|
231
|
-
averageOverrun: number; // minutes
|
|
232
|
-
averageUnderutilization: number; // minutes
|
|
233
|
-
efficiencyDistribution: Array<{ range: string, count: number }>;
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### 4. **Cancellation Metrics** ❌
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
// Grouped by clinic, practitioner, patient, or procedure
|
|
241
|
-
const cancellations = await analyticsService.getCancellationMetrics(
|
|
242
|
-
'practitioner', // or 'clinic', 'patient', 'procedure'
|
|
243
|
-
dateRange
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
// Returns array of:
|
|
247
|
-
{
|
|
248
|
-
entityId: string;
|
|
249
|
-
entityName: string;
|
|
250
|
-
entityType: 'practitioner' | 'clinic' | 'patient' | 'procedure';
|
|
251
|
-
totalAppointments: number;
|
|
252
|
-
canceledAppointments: number;
|
|
253
|
-
cancellationRate: number; // percentage
|
|
254
|
-
canceledByPatient: number;
|
|
255
|
-
canceledByClinic: number;
|
|
256
|
-
averageCancellationLeadTime: number; // hours
|
|
257
|
-
cancellationReasons: Array<{ reason: string, count: number }>;
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### 5. **No-Show Metrics** 🚫
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
const noShows = await analyticsService.getNoShowMetrics(
|
|
265
|
-
'practitioner', // or 'clinic', 'patient', 'procedure'
|
|
266
|
-
dateRange
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// Returns array of:
|
|
270
|
-
{
|
|
271
|
-
entityId: string;
|
|
272
|
-
entityName: string;
|
|
273
|
-
entityType: 'practitioner' | 'clinic' | 'patient' | 'procedure';
|
|
274
|
-
totalAppointments: number;
|
|
275
|
-
noShowAppointments: number;
|
|
276
|
-
noShowRate: number; // percentage
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### 6. **Revenue Metrics** 💰
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
const revenue = await analyticsService.getRevenueMetrics(
|
|
284
|
-
{ clinicBranchId: 'clinic-123' },
|
|
285
|
-
dateRange
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
// Returns:
|
|
289
|
-
{
|
|
290
|
-
totalRevenue: number;
|
|
291
|
-
averageRevenuePerAppointment: number;
|
|
292
|
-
revenueByStatus: Record<AppointmentStatus, number>;
|
|
293
|
-
revenueByPaymentStatus: Record<PaymentStatus, number>;
|
|
294
|
-
unpaidRevenue: number;
|
|
295
|
-
refundedRevenue: number;
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### 7. **Product Usage** 📦
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
// All products
|
|
303
|
-
const products = await analyticsService.getProductUsageMetrics(
|
|
304
|
-
undefined,
|
|
305
|
-
dateRange
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
// Specific product
|
|
309
|
-
const product = await analyticsService.getProductUsageMetrics(
|
|
310
|
-
'product-id-123',
|
|
311
|
-
dateRange
|
|
312
|
-
);
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### 8. **Patient Analytics** 👥
|
|
316
|
-
|
|
317
|
-
```typescript
|
|
318
|
-
// Single patient
|
|
319
|
-
const patient = await analyticsService.getPatientAnalytics(
|
|
320
|
-
'patient-id-123',
|
|
321
|
-
dateRange
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
// All patients
|
|
325
|
-
const allPatients = await analyticsService.getPatientAnalytics(
|
|
326
|
-
undefined,
|
|
327
|
-
dateRange
|
|
328
|
-
);
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### 9. **Dashboard Data** 📊
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
const dashboard = await analyticsService.getDashboardData(
|
|
335
|
-
{ clinicBranchId: 'clinic-123' },
|
|
336
|
-
dateRange
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
// Returns comprehensive dashboard with:
|
|
340
|
-
// - Overview metrics
|
|
341
|
-
// - Top practitioners
|
|
342
|
-
// - Top procedures
|
|
343
|
-
// - Cancellation/no-show metrics
|
|
344
|
-
// - Revenue trends
|
|
345
|
-
// - Time efficiency
|
|
346
|
-
// - Top products
|
|
347
|
-
// - Recent activity
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
---
|
|
351
|
-
|
|
352
|
-
## Specific Use Cases
|
|
353
|
-
|
|
354
|
-
### Use Case 1: Patient No-Show Per Doctor 👨⚕️
|
|
355
|
-
|
|
356
|
-
**Question**: "Which patients have the highest no-show rate for each doctor?"
|
|
357
|
-
|
|
358
|
-
**Solution**: Get no-show metrics grouped by practitioner, then filter by patient if needed.
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
// Step 1: Get no-show metrics grouped by practitioner
|
|
362
|
-
const noShowByPractitioner = await analyticsService.getNoShowMetrics(
|
|
363
|
-
'practitioner',
|
|
364
|
-
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
// Returns array like:
|
|
368
|
-
// [
|
|
369
|
-
// {
|
|
370
|
-
// entityId: 'practitioner-123',
|
|
371
|
-
// entityName: 'Dr. Smith',
|
|
372
|
-
// entityType: 'practitioner',
|
|
373
|
-
// totalAppointments: 100,
|
|
374
|
-
// noShowAppointments: 15,
|
|
375
|
-
// noShowRate: 15.0
|
|
376
|
-
// },
|
|
377
|
-
// ...
|
|
378
|
-
// ]
|
|
379
|
-
|
|
380
|
-
// Step 2: For a specific practitioner, get patient-level no-shows
|
|
381
|
-
// (You can filter appointments and calculate manually, or use patient analytics)
|
|
382
|
-
|
|
383
|
-
// Get all appointments for a specific practitioner
|
|
384
|
-
const practitionerAppointments = await analyticsService['fetchAppointments'](
|
|
385
|
-
{ practitionerId: 'practitioner-123' },
|
|
386
|
-
dateRange
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
// Group no-shows by patient
|
|
390
|
-
const noShowByPatient = new Map<string, { name: string; noShows: number; total: number }>();
|
|
391
|
-
|
|
392
|
-
practitionerAppointments.forEach(appointment => {
|
|
393
|
-
const patientId = appointment.patientId;
|
|
394
|
-
const patientName = appointment.patientInfo?.fullName || 'Unknown';
|
|
395
|
-
|
|
396
|
-
if (!noShowByPatient.has(patientId)) {
|
|
397
|
-
noShowByPatient.set(patientId, { name: patientName, noShows: 0, total: 0 });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const patientData = noShowByPatient.get(patientId)!;
|
|
401
|
-
patientData.total++;
|
|
402
|
-
|
|
403
|
-
if (appointment.status === AppointmentStatus.NO_SHOW) {
|
|
404
|
-
patientData.noShows++;
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// Calculate rates
|
|
409
|
-
const patientNoShowRates = Array.from(noShowByPatient.entries()).map(([patientId, data]) => ({
|
|
410
|
-
patientId,
|
|
411
|
-
patientName: data.name,
|
|
412
|
-
totalAppointments: data.total,
|
|
413
|
-
noShowCount: data.noShows,
|
|
414
|
-
noShowRate: (data.noShows / data.total) * 100
|
|
415
|
-
})).sort((a, b) => b.noShowRate - a.noShowRate);
|
|
416
|
-
|
|
417
|
-
console.log('Patient no-show rates for this practitioner:');
|
|
418
|
-
patientNoShowRates.forEach(patient => {
|
|
419
|
-
console.log(`${patient.patientName}: ${patient.noShowRate.toFixed(1)}% (${patient.noShowCount}/${patient.total})`);
|
|
420
|
-
});
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### Use Case 2: Doctor Performance Comparison 📊
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
// Get all practitioners for a clinic
|
|
427
|
-
const practitioners = await analyticsService.getCancellationMetrics(
|
|
428
|
-
'practitioner',
|
|
429
|
-
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// Filter by clinic if needed (would need clinicBranchId in filters)
|
|
433
|
-
const clinicPractitioners = practitioners.filter(p =>
|
|
434
|
-
// Filter logic based on your needs
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
// Compare metrics
|
|
438
|
-
practitioners.forEach(practitioner => {
|
|
439
|
-
console.log(`${practitioner.entityName}:`);
|
|
440
|
-
console.log(` Cancellation Rate: ${practitioner.cancellationRate}%`);
|
|
441
|
-
console.log(` Total Appointments: ${practitioner.totalAppointments}`);
|
|
442
|
-
});
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Use Case 3: Procedure Profitability Analysis 💰
|
|
446
|
-
|
|
447
|
-
```typescript
|
|
448
|
-
// Get most profitable procedures
|
|
449
|
-
const profitable = await analyticsService.getProcedureProfitability(
|
|
450
|
-
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') },
|
|
451
|
-
10 // Top 10
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
profitable.forEach((procedure, index) => {
|
|
455
|
-
console.log(`${index + 1}. ${procedure.procedureName}`);
|
|
456
|
-
console.log(` Revenue: ${procedure.totalRevenue} ${procedure.currency || 'CHF'}`);
|
|
457
|
-
console.log(` Appointments: ${procedure.appointmentCount}`);
|
|
458
|
-
console.log(` Avg Revenue: ${procedure.averageRevenue.toFixed(2)}`);
|
|
459
|
-
});
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### Use Case 4: Time Efficiency by Doctor ⏱️
|
|
463
|
-
|
|
464
|
-
```typescript
|
|
465
|
-
// Get time efficiency for a specific practitioner
|
|
466
|
-
const practitionerMetrics = await analyticsService.getPractitionerAnalytics(
|
|
467
|
-
'practitioner-id-123',
|
|
468
|
-
dateRange
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
console.log(`Time Efficiency: ${practitionerMetrics.timeEfficiency}%`);
|
|
472
|
-
console.log(`Avg Booked Time: ${practitionerMetrics.averageBookedTime} min`);
|
|
473
|
-
console.log(`Avg Actual Time: ${practitionerMetrics.averageActualTime} min`);
|
|
474
|
-
|
|
475
|
-
// Or get overall time efficiency with filters
|
|
476
|
-
const timeMetrics = await analyticsService.getTimeEfficiencyMetrics(
|
|
477
|
-
{ practitionerId: 'practitioner-id-123' },
|
|
478
|
-
dateRange
|
|
479
|
-
);
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
### Use Case 5: Product Cost Analysis 📦
|
|
483
|
-
|
|
484
|
-
```typescript
|
|
485
|
-
// Get product usage for a specific procedure
|
|
486
|
-
const procedureAnalytics = await analyticsService.getProcedureAnalytics(
|
|
487
|
-
'procedure-id-123',
|
|
488
|
-
dateRange
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
// Check product usage
|
|
492
|
-
procedureAnalytics.productUsage.forEach(product => {
|
|
493
|
-
console.log(`${product.productName}:`);
|
|
494
|
-
console.log(` Total Quantity: ${product.totalQuantity}`);
|
|
495
|
-
console.log(` Total Revenue: ${product.totalRevenue}`);
|
|
496
|
-
console.log(` Used in ${product.usageCount} appointments`);
|
|
497
|
-
});
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
---
|
|
501
|
-
|
|
502
|
-
## Performance Tips
|
|
503
|
-
|
|
504
|
-
1. **Use pre-computed analytics** when possible (default behavior)
|
|
505
|
-
2. **Specify clinicBranchId** in filters to enable caching
|
|
506
|
-
3. **Use standard date ranges** (daily, weekly, monthly) for better cache hits
|
|
507
|
-
4. **Batch requests** when getting multiple practitioners/procedures
|
|
508
|
-
5. **Cache results** on the client side for frequently accessed dashboards
|
|
509
|
-
|
|
510
|
-
---
|
|
511
|
-
|
|
512
|
-
## Next Steps
|
|
513
|
-
|
|
514
|
-
1. **Deploy Cloud Function**: Deploy `computeAnalytics.ts` to start pre-computing
|
|
515
|
-
2. **Test with real data**: Verify analytics accuracy
|
|
516
|
-
3. **Monitor performance**: Check Cloud Function logs and Firestore usage
|
|
517
|
-
4. **Customize**: Adjust cache age and computation schedule as needed
|
|
518
|
-
|
|
1
|
+
# Analytics Service - Complete Usage Guide
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. [What We Can Do](#what-we-can-do)
|
|
5
|
+
2. [How It Works: Computed vs On-Demand](#how-it-works)
|
|
6
|
+
3. [Available Analytics](#available-analytics)
|
|
7
|
+
4. [Specific Use Cases](#specific-use-cases)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What We Can Do
|
|
12
|
+
|
|
13
|
+
The Analytics Service provides comprehensive insights into your clinic operations:
|
|
14
|
+
|
|
15
|
+
### 📊 **Financial Intelligence**
|
|
16
|
+
- Track total revenue, average revenue per appointment
|
|
17
|
+
- Monitor payment status (paid, unpaid, refunded)
|
|
18
|
+
- Analyze costs per patient
|
|
19
|
+
- Revenue trends over time
|
|
20
|
+
|
|
21
|
+
### 👨⚕️ **Practitioner Performance**
|
|
22
|
+
- Appointment counts and completion rates
|
|
23
|
+
- Cancellation and no-show rates per doctor
|
|
24
|
+
- Time efficiency (booked vs actual time)
|
|
25
|
+
- Revenue generation per practitioner
|
|
26
|
+
- Patient retention rates
|
|
27
|
+
|
|
28
|
+
### 🏥 **Procedure Analytics**
|
|
29
|
+
- Most popular procedures
|
|
30
|
+
- Most profitable procedures
|
|
31
|
+
- Procedure performance by category/technology
|
|
32
|
+
- Product usage per procedure
|
|
33
|
+
|
|
34
|
+
### ⏱️ **Time Management**
|
|
35
|
+
- Booked time vs actual time spent
|
|
36
|
+
- Efficiency percentages
|
|
37
|
+
- Overrun/underutilization analysis
|
|
38
|
+
- Peak hours identification
|
|
39
|
+
|
|
40
|
+
### ❌ **Cancellation & No-Show Analysis**
|
|
41
|
+
- Rates by clinic, practitioner, patient, or procedure
|
|
42
|
+
- Cancellation reasons breakdown
|
|
43
|
+
- Average cancellation lead time
|
|
44
|
+
- Patterns and trends
|
|
45
|
+
|
|
46
|
+
### 👥 **Patient Insights**
|
|
47
|
+
- Patient lifetime value
|
|
48
|
+
- Retention rates
|
|
49
|
+
- Appointment frequency
|
|
50
|
+
- Cancellation patterns per patient
|
|
51
|
+
|
|
52
|
+
### 📦 **Product Usage**
|
|
53
|
+
- Products used per appointment
|
|
54
|
+
- Revenue contribution by product
|
|
55
|
+
- Usage by procedure
|
|
56
|
+
- Quantity and pricing trends
|
|
57
|
+
|
|
58
|
+
### 🏢 **Clinic Performance**
|
|
59
|
+
- Overall clinic metrics
|
|
60
|
+
- Practitioner comparisons
|
|
61
|
+
- Procedure popularity
|
|
62
|
+
- Efficiency metrics
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## How It Works: Computed vs On-Demand
|
|
67
|
+
|
|
68
|
+
### 🔄 **Hybrid Architecture**
|
|
69
|
+
|
|
70
|
+
The service uses a **smart hybrid approach**:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
┌─────────────────────────────────────────┐
|
|
74
|
+
│ Client Request │
|
|
75
|
+
└──────────────┬──────────────────────────┘
|
|
76
|
+
│
|
|
77
|
+
▼
|
|
78
|
+
┌─────────────────────────────────────────┐
|
|
79
|
+
│ Check Stored Analytics? │
|
|
80
|
+
└──────┬──────────────────────┬───────────┘
|
|
81
|
+
│ │
|
|
82
|
+
YES │ │ NO
|
|
83
|
+
▼ ▼
|
|
84
|
+
┌──────────────┐ ┌─────────────────────┐
|
|
85
|
+
│ Data Fresh? │ │ Calculate On-Demand │
|
|
86
|
+
│ (< 12 hours) │ │ (Query Appointments)│
|
|
87
|
+
└──┬───────┬───┘ └─────────────────────┘
|
|
88
|
+
│ │
|
|
89
|
+
YES│ │NO
|
|
90
|
+
│ │
|
|
91
|
+
▼ ▼
|
|
92
|
+
┌─────┐ ┌─────────────────────┐
|
|
93
|
+
│ ✅ │ │ Calculate On-Demand │
|
|
94
|
+
│Fast │ │ (Query Appointments)│
|
|
95
|
+
└─────┘ └─────────────────────┘
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 📦 **Pre-Computed Analytics (Recommended)**
|
|
99
|
+
|
|
100
|
+
**How it works:**
|
|
101
|
+
1. **Cloud Function runs every 12 hours** (scheduled)
|
|
102
|
+
2. Computes analytics for all clinics
|
|
103
|
+
3. Stores results in Firestore: `clinics/{clinicBranchId}/analytics/`
|
|
104
|
+
4. Client reads cached data (1 document read = instant)
|
|
105
|
+
|
|
106
|
+
**Benefits:**
|
|
107
|
+
- ⚡ **Fast**: Instant response (single document read)
|
|
108
|
+
- 💰 **Cheap**: 1 read vs hundreds/thousands
|
|
109
|
+
- 📈 **Scalable**: Works with large datasets
|
|
110
|
+
|
|
111
|
+
**Example:**
|
|
112
|
+
```typescript
|
|
113
|
+
// Automatically uses cached data if available and fresh (< 12 hours)
|
|
114
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
115
|
+
{ clinicBranchId: 'clinic-123' },
|
|
116
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
117
|
+
);
|
|
118
|
+
// Returns instantly from cache! ⚡
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 🔄 **On-Demand Calculation (Fallback)**
|
|
122
|
+
|
|
123
|
+
**When it happens:**
|
|
124
|
+
- No cached data exists yet
|
|
125
|
+
- Cached data is stale (> 12 hours old)
|
|
126
|
+
- You explicitly disable cache: `{ useCache: false }`
|
|
127
|
+
- Custom date range that doesn't match pre-computed periods
|
|
128
|
+
|
|
129
|
+
**How it works:**
|
|
130
|
+
1. Queries all appointments matching filters
|
|
131
|
+
2. Calculates metrics in real-time
|
|
132
|
+
3. Returns results
|
|
133
|
+
|
|
134
|
+
**Example:**
|
|
135
|
+
```typescript
|
|
136
|
+
// Force on-demand calculation
|
|
137
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
138
|
+
{ clinicBranchId: 'clinic-123' },
|
|
139
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') },
|
|
140
|
+
{ useCache: false } // Force calculation
|
|
141
|
+
);
|
|
142
|
+
// Calculates from scratch (slower but always fresh)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### ⚙️ **Configuration**
|
|
146
|
+
|
|
147
|
+
**Change cache freshness:**
|
|
148
|
+
```typescript
|
|
149
|
+
// Use cache if data is less than 6 hours old
|
|
150
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
151
|
+
{ clinicBranchId: 'clinic-123' },
|
|
152
|
+
dateRange,
|
|
153
|
+
{ maxCacheAgeHours: 6 }
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Change Cloud Function schedule:**
|
|
158
|
+
Edit `Cloud/functions/src/analytics/computeAnalytics.ts`:
|
|
159
|
+
```typescript
|
|
160
|
+
schedule: "every 6 hours" // or "every 24 hours", etc.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Available Analytics
|
|
166
|
+
|
|
167
|
+
### 1. **Practitioner Analytics** 👨⚕️
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const metrics = await analyticsService.getPractitionerAnalytics(
|
|
171
|
+
'practitioner-id-123',
|
|
172
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Returns:
|
|
176
|
+
{
|
|
177
|
+
practitionerId: string;
|
|
178
|
+
practitionerName: string;
|
|
179
|
+
totalAppointments: number;
|
|
180
|
+
completedAppointments: number;
|
|
181
|
+
canceledAppointments: number;
|
|
182
|
+
noShowAppointments: number;
|
|
183
|
+
cancellationRate: number; // percentage
|
|
184
|
+
noShowRate: number; // percentage
|
|
185
|
+
averageBookedTime: number; // minutes
|
|
186
|
+
averageActualTime: number; // minutes
|
|
187
|
+
timeEfficiency: number; // percentage
|
|
188
|
+
totalRevenue: number;
|
|
189
|
+
averageRevenuePerAppointment: number;
|
|
190
|
+
topProcedures: Array<{ procedureId, procedureName, count, revenue }>;
|
|
191
|
+
patientRetentionRate: number;
|
|
192
|
+
uniquePatients: number;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 2. **Procedure Analytics** 🏥
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Single procedure
|
|
200
|
+
const procedure = await analyticsService.getProcedureAnalytics(
|
|
201
|
+
'procedure-id-123',
|
|
202
|
+
dateRange
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// All procedures
|
|
206
|
+
const allProcedures = await analyticsService.getProcedureAnalytics(
|
|
207
|
+
undefined,
|
|
208
|
+
dateRange
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Most popular
|
|
212
|
+
const popular = await analyticsService.getProcedurePopularity(dateRange, 10);
|
|
213
|
+
|
|
214
|
+
// Most profitable
|
|
215
|
+
const profitable = await analyticsService.getProcedureProfitability(dateRange, 10);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 3. **Time Efficiency** ⏱️
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const timeMetrics = await analyticsService.getTimeEfficiencyMetrics(
|
|
222
|
+
{ clinicBranchId: 'clinic-123' },
|
|
223
|
+
dateRange
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Returns:
|
|
227
|
+
{
|
|
228
|
+
averageBookedDuration: number; // minutes
|
|
229
|
+
averageActualDuration: number; // minutes
|
|
230
|
+
averageEfficiency: number; // percentage
|
|
231
|
+
averageOverrun: number; // minutes
|
|
232
|
+
averageUnderutilization: number; // minutes
|
|
233
|
+
efficiencyDistribution: Array<{ range: string, count: number }>;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 4. **Cancellation Metrics** ❌
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// Grouped by clinic, practitioner, patient, or procedure
|
|
241
|
+
const cancellations = await analyticsService.getCancellationMetrics(
|
|
242
|
+
'practitioner', // or 'clinic', 'patient', 'procedure'
|
|
243
|
+
dateRange
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Returns array of:
|
|
247
|
+
{
|
|
248
|
+
entityId: string;
|
|
249
|
+
entityName: string;
|
|
250
|
+
entityType: 'practitioner' | 'clinic' | 'patient' | 'procedure';
|
|
251
|
+
totalAppointments: number;
|
|
252
|
+
canceledAppointments: number;
|
|
253
|
+
cancellationRate: number; // percentage
|
|
254
|
+
canceledByPatient: number;
|
|
255
|
+
canceledByClinic: number;
|
|
256
|
+
averageCancellationLeadTime: number; // hours
|
|
257
|
+
cancellationReasons: Array<{ reason: string, count: number }>;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 5. **No-Show Metrics** 🚫
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const noShows = await analyticsService.getNoShowMetrics(
|
|
265
|
+
'practitioner', // or 'clinic', 'patient', 'procedure'
|
|
266
|
+
dateRange
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Returns array of:
|
|
270
|
+
{
|
|
271
|
+
entityId: string;
|
|
272
|
+
entityName: string;
|
|
273
|
+
entityType: 'practitioner' | 'clinic' | 'patient' | 'procedure';
|
|
274
|
+
totalAppointments: number;
|
|
275
|
+
noShowAppointments: number;
|
|
276
|
+
noShowRate: number; // percentage
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 6. **Revenue Metrics** 💰
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const revenue = await analyticsService.getRevenueMetrics(
|
|
284
|
+
{ clinicBranchId: 'clinic-123' },
|
|
285
|
+
dateRange
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Returns:
|
|
289
|
+
{
|
|
290
|
+
totalRevenue: number;
|
|
291
|
+
averageRevenuePerAppointment: number;
|
|
292
|
+
revenueByStatus: Record<AppointmentStatus, number>;
|
|
293
|
+
revenueByPaymentStatus: Record<PaymentStatus, number>;
|
|
294
|
+
unpaidRevenue: number;
|
|
295
|
+
refundedRevenue: number;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 7. **Product Usage** 📦
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// All products
|
|
303
|
+
const products = await analyticsService.getProductUsageMetrics(
|
|
304
|
+
undefined,
|
|
305
|
+
dateRange
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Specific product
|
|
309
|
+
const product = await analyticsService.getProductUsageMetrics(
|
|
310
|
+
'product-id-123',
|
|
311
|
+
dateRange
|
|
312
|
+
);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 8. **Patient Analytics** 👥
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// Single patient
|
|
319
|
+
const patient = await analyticsService.getPatientAnalytics(
|
|
320
|
+
'patient-id-123',
|
|
321
|
+
dateRange
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// All patients
|
|
325
|
+
const allPatients = await analyticsService.getPatientAnalytics(
|
|
326
|
+
undefined,
|
|
327
|
+
dateRange
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### 9. **Dashboard Data** 📊
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const dashboard = await analyticsService.getDashboardData(
|
|
335
|
+
{ clinicBranchId: 'clinic-123' },
|
|
336
|
+
dateRange
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Returns comprehensive dashboard with:
|
|
340
|
+
// - Overview metrics
|
|
341
|
+
// - Top practitioners
|
|
342
|
+
// - Top procedures
|
|
343
|
+
// - Cancellation/no-show metrics
|
|
344
|
+
// - Revenue trends
|
|
345
|
+
// - Time efficiency
|
|
346
|
+
// - Top products
|
|
347
|
+
// - Recent activity
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Specific Use Cases
|
|
353
|
+
|
|
354
|
+
### Use Case 1: Patient No-Show Per Doctor 👨⚕️
|
|
355
|
+
|
|
356
|
+
**Question**: "Which patients have the highest no-show rate for each doctor?"
|
|
357
|
+
|
|
358
|
+
**Solution**: Get no-show metrics grouped by practitioner, then filter by patient if needed.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// Step 1: Get no-show metrics grouped by practitioner
|
|
362
|
+
const noShowByPractitioner = await analyticsService.getNoShowMetrics(
|
|
363
|
+
'practitioner',
|
|
364
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Returns array like:
|
|
368
|
+
// [
|
|
369
|
+
// {
|
|
370
|
+
// entityId: 'practitioner-123',
|
|
371
|
+
// entityName: 'Dr. Smith',
|
|
372
|
+
// entityType: 'practitioner',
|
|
373
|
+
// totalAppointments: 100,
|
|
374
|
+
// noShowAppointments: 15,
|
|
375
|
+
// noShowRate: 15.0
|
|
376
|
+
// },
|
|
377
|
+
// ...
|
|
378
|
+
// ]
|
|
379
|
+
|
|
380
|
+
// Step 2: For a specific practitioner, get patient-level no-shows
|
|
381
|
+
// (You can filter appointments and calculate manually, or use patient analytics)
|
|
382
|
+
|
|
383
|
+
// Get all appointments for a specific practitioner
|
|
384
|
+
const practitionerAppointments = await analyticsService['fetchAppointments'](
|
|
385
|
+
{ practitionerId: 'practitioner-123' },
|
|
386
|
+
dateRange
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Group no-shows by patient
|
|
390
|
+
const noShowByPatient = new Map<string, { name: string; noShows: number; total: number }>();
|
|
391
|
+
|
|
392
|
+
practitionerAppointments.forEach(appointment => {
|
|
393
|
+
const patientId = appointment.patientId;
|
|
394
|
+
const patientName = appointment.patientInfo?.fullName || 'Unknown';
|
|
395
|
+
|
|
396
|
+
if (!noShowByPatient.has(patientId)) {
|
|
397
|
+
noShowByPatient.set(patientId, { name: patientName, noShows: 0, total: 0 });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const patientData = noShowByPatient.get(patientId)!;
|
|
401
|
+
patientData.total++;
|
|
402
|
+
|
|
403
|
+
if (appointment.status === AppointmentStatus.NO_SHOW) {
|
|
404
|
+
patientData.noShows++;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Calculate rates
|
|
409
|
+
const patientNoShowRates = Array.from(noShowByPatient.entries()).map(([patientId, data]) => ({
|
|
410
|
+
patientId,
|
|
411
|
+
patientName: data.name,
|
|
412
|
+
totalAppointments: data.total,
|
|
413
|
+
noShowCount: data.noShows,
|
|
414
|
+
noShowRate: (data.noShows / data.total) * 100
|
|
415
|
+
})).sort((a, b) => b.noShowRate - a.noShowRate);
|
|
416
|
+
|
|
417
|
+
console.log('Patient no-show rates for this practitioner:');
|
|
418
|
+
patientNoShowRates.forEach(patient => {
|
|
419
|
+
console.log(`${patient.patientName}: ${patient.noShowRate.toFixed(1)}% (${patient.noShowCount}/${patient.total})`);
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Use Case 2: Doctor Performance Comparison 📊
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// Get all practitioners for a clinic
|
|
427
|
+
const practitioners = await analyticsService.getCancellationMetrics(
|
|
428
|
+
'practitioner',
|
|
429
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') }
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Filter by clinic if needed (would need clinicBranchId in filters)
|
|
433
|
+
const clinicPractitioners = practitioners.filter(p =>
|
|
434
|
+
// Filter logic based on your needs
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Compare metrics
|
|
438
|
+
practitioners.forEach(practitioner => {
|
|
439
|
+
console.log(`${practitioner.entityName}:`);
|
|
440
|
+
console.log(` Cancellation Rate: ${practitioner.cancellationRate}%`);
|
|
441
|
+
console.log(` Total Appointments: ${practitioner.totalAppointments}`);
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Use Case 3: Procedure Profitability Analysis 💰
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// Get most profitable procedures
|
|
449
|
+
const profitable = await analyticsService.getProcedureProfitability(
|
|
450
|
+
{ start: new Date('2024-01-01'), end: new Date('2024-12-31') },
|
|
451
|
+
10 // Top 10
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
profitable.forEach((procedure, index) => {
|
|
455
|
+
console.log(`${index + 1}. ${procedure.procedureName}`);
|
|
456
|
+
console.log(` Revenue: ${procedure.totalRevenue} ${procedure.currency || 'CHF'}`);
|
|
457
|
+
console.log(` Appointments: ${procedure.appointmentCount}`);
|
|
458
|
+
console.log(` Avg Revenue: ${procedure.averageRevenue.toFixed(2)}`);
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Use Case 4: Time Efficiency by Doctor ⏱️
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// Get time efficiency for a specific practitioner
|
|
466
|
+
const practitionerMetrics = await analyticsService.getPractitionerAnalytics(
|
|
467
|
+
'practitioner-id-123',
|
|
468
|
+
dateRange
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
console.log(`Time Efficiency: ${practitionerMetrics.timeEfficiency}%`);
|
|
472
|
+
console.log(`Avg Booked Time: ${practitionerMetrics.averageBookedTime} min`);
|
|
473
|
+
console.log(`Avg Actual Time: ${practitionerMetrics.averageActualTime} min`);
|
|
474
|
+
|
|
475
|
+
// Or get overall time efficiency with filters
|
|
476
|
+
const timeMetrics = await analyticsService.getTimeEfficiencyMetrics(
|
|
477
|
+
{ practitionerId: 'practitioner-id-123' },
|
|
478
|
+
dateRange
|
|
479
|
+
);
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Use Case 5: Product Cost Analysis 📦
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// Get product usage for a specific procedure
|
|
486
|
+
const procedureAnalytics = await analyticsService.getProcedureAnalytics(
|
|
487
|
+
'procedure-id-123',
|
|
488
|
+
dateRange
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Check product usage
|
|
492
|
+
procedureAnalytics.productUsage.forEach(product => {
|
|
493
|
+
console.log(`${product.productName}:`);
|
|
494
|
+
console.log(` Total Quantity: ${product.totalQuantity}`);
|
|
495
|
+
console.log(` Total Revenue: ${product.totalRevenue}`);
|
|
496
|
+
console.log(` Used in ${product.usageCount} appointments`);
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Performance Tips
|
|
503
|
+
|
|
504
|
+
1. **Use pre-computed analytics** when possible (default behavior)
|
|
505
|
+
2. **Specify clinicBranchId** in filters to enable caching
|
|
506
|
+
3. **Use standard date ranges** (daily, weekly, monthly) for better cache hits
|
|
507
|
+
4. **Batch requests** when getting multiple practitioners/procedures
|
|
508
|
+
5. **Cache results** on the client side for frequently accessed dashboards
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Next Steps
|
|
513
|
+
|
|
514
|
+
1. **Deploy Cloud Function**: Deploy `computeAnalytics.ts` to start pre-computing
|
|
515
|
+
2. **Test with real data**: Verify analytics accuracy
|
|
516
|
+
3. **Monitor performance**: Check Cloud Function logs and Firestore usage
|
|
517
|
+
4. **Customize**: Adjust cache age and computation schedule as needed
|
|
518
|
+
|