@blackcode_sa/metaestetics-api 1.12.68 → 1.12.69
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 +34 -1
- package/dist/admin/index.d.ts +34 -1
- package/dist/admin/index.js +104 -0
- package/dist/admin/index.mjs +104 -0
- package/dist/index.d.mts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +128 -16
- package/dist/index.mjs +128 -16
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +140 -0
- package/src/backoffice/services/README.md +17 -0
- package/src/backoffice/services/analytics.service.proposal.md +859 -0
- package/src/backoffice/services/analytics.service.summary.md +143 -0
- package/src/services/appointment/appointment.service.ts +59 -6
- package/src/services/procedure/procedure.service.ts +117 -4
- package/src/types/notifications/index.ts +21 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Analytics Service - Executive Summary
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document provides a high-level summary of the proposed Analytics Service for the Clinic Admin app. The full detailed proposal can be found in [analytics.service.proposal.md](./analytics.service.proposal.md).
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
The Analytics Service will provide comprehensive financial and operational intelligence to clinic administrators, enabling data-driven decision making across:
|
|
10
|
+
|
|
11
|
+
- **Practitioner Performance**: Track doctor efficiency, revenue generation, and patient satisfaction
|
|
12
|
+
- **Procedure Analytics**: Understand procedure popularity, profitability, and product usage
|
|
13
|
+
- **Financial Intelligence**: Monitor revenue, costs, payment status, and trends
|
|
14
|
+
- **Operational Efficiency**: Analyze time utilization, cancellations, and no-shows
|
|
15
|
+
- **Patient Insights**: Track patient lifetime value, retention, and behavior patterns
|
|
16
|
+
|
|
17
|
+
## Key Data Sources
|
|
18
|
+
|
|
19
|
+
The service will aggregate data from:
|
|
20
|
+
|
|
21
|
+
1. **Appointments Collection**: Primary source for all appointment-related metrics
|
|
22
|
+
2. **Procedures Collection**: Procedure metadata and categorization
|
|
23
|
+
3. **Practitioners Collection**: Doctor information and associations
|
|
24
|
+
4. **Patients Collection**: Patient profiles and history
|
|
25
|
+
5. **Clinics Collection**: Clinic and branch information
|
|
26
|
+
6. **Products Collection**: Product usage and pricing data
|
|
27
|
+
|
|
28
|
+
## Core Analytics Categories
|
|
29
|
+
|
|
30
|
+
### 1. Practitioner Analytics
|
|
31
|
+
- Total and completed appointments
|
|
32
|
+
- Cancellation and no-show rates
|
|
33
|
+
- Time efficiency (booked vs actual)
|
|
34
|
+
- Revenue generation
|
|
35
|
+
- Patient retention
|
|
36
|
+
|
|
37
|
+
### 2. Procedure Analytics
|
|
38
|
+
- Appointment counts and popularity
|
|
39
|
+
- Revenue and profitability
|
|
40
|
+
- Cancellation rates
|
|
41
|
+
- Product usage patterns
|
|
42
|
+
- Performance by category/technology
|
|
43
|
+
|
|
44
|
+
### 3. Time Analytics
|
|
45
|
+
- Booked vs actual time comparison
|
|
46
|
+
- Efficiency percentages
|
|
47
|
+
- Overrun/underutilization analysis
|
|
48
|
+
- Peak hours and trends
|
|
49
|
+
|
|
50
|
+
### 4. Cancellation & No-Show Analytics
|
|
51
|
+
- Rates by clinic, practitioner, patient, procedure
|
|
52
|
+
- Cancellation reasons breakdown
|
|
53
|
+
- Patterns and trends
|
|
54
|
+
- Lead time analysis
|
|
55
|
+
|
|
56
|
+
### 5. Financial Analytics
|
|
57
|
+
- Total and average revenue
|
|
58
|
+
- Cost per patient/appointment
|
|
59
|
+
- Payment status breakdown
|
|
60
|
+
- Revenue trends
|
|
61
|
+
- Product cost analysis
|
|
62
|
+
|
|
63
|
+
### 6. Product Usage Analytics
|
|
64
|
+
- Products used per appointment
|
|
65
|
+
- Revenue contribution
|
|
66
|
+
- Usage by procedure/zone
|
|
67
|
+
- Quantity and pricing trends
|
|
68
|
+
|
|
69
|
+
### 7. Patient Analytics
|
|
70
|
+
- Lifetime value
|
|
71
|
+
- Retention rates
|
|
72
|
+
- Appointment frequency
|
|
73
|
+
- Cancellation patterns
|
|
74
|
+
|
|
75
|
+
### 8. Clinic Analytics
|
|
76
|
+
- Overall performance metrics
|
|
77
|
+
- Practitioner comparisons
|
|
78
|
+
- Procedure popularity
|
|
79
|
+
- Efficiency metrics
|
|
80
|
+
|
|
81
|
+
## Proposed Service Structure
|
|
82
|
+
|
|
83
|
+
The `AnalyticsService` will extend `BaseService` and provide methods organized by analytics category:
|
|
84
|
+
|
|
85
|
+
- `getPractitionerAnalytics()` - Comprehensive practitioner metrics
|
|
86
|
+
- `getProcedureAnalytics()` - Procedure performance data
|
|
87
|
+
- `getTimeEfficiencyMetrics()` - Time utilization analysis
|
|
88
|
+
- `getCancellationMetrics()` - Cancellation insights
|
|
89
|
+
- `getRevenueMetrics()` - Financial intelligence
|
|
90
|
+
- `getProductUsageMetrics()` - Product analytics
|
|
91
|
+
- `getPatientAnalytics()` - Patient insights
|
|
92
|
+
- `getClinicAnalytics()` - Clinic performance
|
|
93
|
+
- `getDashboardData()` - Comprehensive dashboard aggregation
|
|
94
|
+
|
|
95
|
+
## Key Calculations
|
|
96
|
+
|
|
97
|
+
### Cost Calculation Priority
|
|
98
|
+
1. `metadata.finalbilling.finalPrice` (if available)
|
|
99
|
+
2. Sum of `metadata.zonesData` subtotals
|
|
100
|
+
3. `appointment.cost` (fallback)
|
|
101
|
+
|
|
102
|
+
### Time Efficiency
|
|
103
|
+
```
|
|
104
|
+
efficiency = (actualDuration / bookedDuration) * 100
|
|
105
|
+
overrun = actualDuration > bookedDuration ? difference : 0
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Cancellation Rate
|
|
109
|
+
```
|
|
110
|
+
cancellationRate = (canceledCount / totalAppointments) * 100
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Implementation Phases
|
|
114
|
+
|
|
115
|
+
1. **Phase 1**: Core infrastructure and utilities
|
|
116
|
+
2. **Phase 2**: Basic metrics (practitioner, procedure, financial)
|
|
117
|
+
3. **Phase 3**: Advanced analytics (time, products, patients)
|
|
118
|
+
4. **Phase 4**: Dashboard and trend analysis
|
|
119
|
+
|
|
120
|
+
## Performance Considerations
|
|
121
|
+
|
|
122
|
+
- Firestore composite indexes for query optimization
|
|
123
|
+
- Pagination for large datasets
|
|
124
|
+
- Caching strategy for frequently accessed metrics
|
|
125
|
+
- Batch processing for complex aggregations
|
|
126
|
+
- Server-side calculations via Cloud Functions
|
|
127
|
+
|
|
128
|
+
## Next Steps
|
|
129
|
+
|
|
130
|
+
1. Review and approve the detailed proposal
|
|
131
|
+
2. Create TypeScript type definitions
|
|
132
|
+
3. Implement core service structure
|
|
133
|
+
4. Build utility functions for calculations
|
|
134
|
+
5. Implement metrics methods incrementally
|
|
135
|
+
6. Add comprehensive tests
|
|
136
|
+
7. Create API documentation
|
|
137
|
+
8. Integrate with Clinic Admin app
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
- **Full Proposal**: [analytics.service.proposal.md](./analytics.service.proposal.md)
|
|
142
|
+
- **Service README**: [README.md](./README.md)
|
|
143
|
+
|
|
@@ -1833,6 +1833,12 @@ export class AppointmentService extends BaseService {
|
|
|
1833
1833
|
finalizationNotes: null,
|
|
1834
1834
|
};
|
|
1835
1835
|
|
|
1836
|
+
// Update payment status if billing data exists but status is NOT_APPLICABLE
|
|
1837
|
+
// This handles cases where appointment was created with price 0 but billing was added later
|
|
1838
|
+
const shouldUpdatePaymentStatus =
|
|
1839
|
+
finalbilling.finalPrice > 0 &&
|
|
1840
|
+
appointment.paymentStatus === PaymentStatus.NOT_APPLICABLE;
|
|
1841
|
+
|
|
1836
1842
|
const updateData: UpdateAppointmentData = {
|
|
1837
1843
|
metadata: {
|
|
1838
1844
|
selectedZones: currentMetadata.selectedZones,
|
|
@@ -1848,6 +1854,9 @@ export class AppointmentService extends BaseService {
|
|
|
1848
1854
|
finalbilling,
|
|
1849
1855
|
finalizationNotes: currentMetadata.finalizationNotes,
|
|
1850
1856
|
},
|
|
1857
|
+
...(shouldUpdatePaymentStatus && {
|
|
1858
|
+
paymentStatus: PaymentStatus.UNPAID,
|
|
1859
|
+
}),
|
|
1851
1860
|
updatedAt: serverTimestamp(),
|
|
1852
1861
|
};
|
|
1853
1862
|
|
|
@@ -2123,10 +2132,58 @@ export class AppointmentService extends BaseService {
|
|
|
2123
2132
|
showNoShow: false,
|
|
2124
2133
|
});
|
|
2125
2134
|
|
|
2135
|
+
// Also get appointments that have recommendations but might not be COMPLETED yet
|
|
2136
|
+
// (e.g., doctor finalized but appointment status is still CONFIRMED/CHECKED_IN)
|
|
2137
|
+
// Get all patient appointments that are past their end time
|
|
2138
|
+
const now = new Date();
|
|
2139
|
+
const allPastAppointments = await this.getPatientAppointments(patientId, {
|
|
2140
|
+
endDate: now,
|
|
2141
|
+
status: [
|
|
2142
|
+
AppointmentStatus.COMPLETED,
|
|
2143
|
+
AppointmentStatus.CONFIRMED,
|
|
2144
|
+
AppointmentStatus.CHECKED_IN,
|
|
2145
|
+
AppointmentStatus.IN_PROGRESS,
|
|
2146
|
+
],
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
// Filter to only include appointments that are past their end time AND have recommendations
|
|
2150
|
+
const appointmentsWithRecommendations = allPastAppointments.appointments.filter(
|
|
2151
|
+
appointment => {
|
|
2152
|
+
const endTime = appointment.appointmentEndTime?.toMillis
|
|
2153
|
+
? appointment.appointmentEndTime.toMillis()
|
|
2154
|
+
: appointment.appointmentEndTime?.seconds
|
|
2155
|
+
? appointment.appointmentEndTime.seconds * 1000
|
|
2156
|
+
: null;
|
|
2157
|
+
|
|
2158
|
+
if (!endTime) return false;
|
|
2159
|
+
|
|
2160
|
+
const isPastEndTime = endTime < now.getTime();
|
|
2161
|
+
const hasRecommendations =
|
|
2162
|
+
(appointment.metadata?.recommendedProcedures?.length || 0) > 0;
|
|
2163
|
+
|
|
2164
|
+
return isPastEndTime && hasRecommendations;
|
|
2165
|
+
},
|
|
2166
|
+
);
|
|
2167
|
+
|
|
2168
|
+
// Combine and deduplicate by appointment ID
|
|
2169
|
+
const allAppointmentsMap = new Map<string, Appointment>();
|
|
2170
|
+
|
|
2171
|
+
// Add completed appointments
|
|
2172
|
+
pastAppointments.appointments.forEach(apt => {
|
|
2173
|
+
allAppointmentsMap.set(apt.id, apt);
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
// Add appointments with recommendations (will overwrite if duplicate)
|
|
2177
|
+
appointmentsWithRecommendations.forEach(apt => {
|
|
2178
|
+
allAppointmentsMap.set(apt.id, apt);
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
const allAppointments = Array.from(allAppointmentsMap.values());
|
|
2182
|
+
|
|
2126
2183
|
const recommendations: NextStepsRecommendation[] = [];
|
|
2127
2184
|
|
|
2128
|
-
// Iterate through
|
|
2129
|
-
for (const appointment of
|
|
2185
|
+
// Iterate through all appointments and extract recommendations
|
|
2186
|
+
for (const appointment of allAppointments) {
|
|
2130
2187
|
// Filter by clinic if specified
|
|
2131
2188
|
if (options?.clinicBranchId && appointment.clinicBranchId !== options.clinicBranchId) {
|
|
2132
2189
|
continue;
|
|
@@ -2181,10 +2238,6 @@ export class AppointmentService extends BaseService {
|
|
|
2181
2238
|
? recommendations.slice(0, options.limit)
|
|
2182
2239
|
: recommendations;
|
|
2183
2240
|
|
|
2184
|
-
console.log(
|
|
2185
|
-
`[APPOINTMENT_SERVICE] Found ${limitedRecommendations.length} next steps recommendations for patient ${patientId}`,
|
|
2186
|
-
);
|
|
2187
|
-
|
|
2188
2241
|
return limitedRecommendations;
|
|
2189
2242
|
} catch (error) {
|
|
2190
2243
|
console.error(
|
|
@@ -1088,6 +1088,14 @@ export class ProcedureService extends BaseService {
|
|
|
1088
1088
|
console.log('[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search');
|
|
1089
1089
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1090
1090
|
const constraints = getBaseConstraints();
|
|
1091
|
+
|
|
1092
|
+
// Check if we have nested field filters that might conflict with orderBy
|
|
1093
|
+
const hasNestedFilters = !!(filters.procedureTechnology || filters.procedureCategory || filters.procedureSubcategory);
|
|
1094
|
+
|
|
1095
|
+
if (hasNestedFilters) {
|
|
1096
|
+
console.log('[PROCEDURE_SERVICE] Strategy 1: Has nested filters, will apply client-side after query');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1091
1099
|
constraints.push(where('nameLower', '>=', searchTerm));
|
|
1092
1100
|
constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
|
|
1093
1101
|
constraints.push(orderBy('nameLower'));
|
|
@@ -1105,9 +1113,15 @@ export class ProcedureService extends BaseService {
|
|
|
1105
1113
|
|
|
1106
1114
|
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1107
1115
|
const querySnapshot = await getDocs(q);
|
|
1108
|
-
|
|
1116
|
+
let procedures = querySnapshot.docs.map(
|
|
1109
1117
|
doc => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1110
1118
|
);
|
|
1119
|
+
|
|
1120
|
+
// Apply client-side filters for nested fields if needed
|
|
1121
|
+
if (hasNestedFilters) {
|
|
1122
|
+
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1111
1125
|
const lastDoc =
|
|
1112
1126
|
querySnapshot.docs.length > 0
|
|
1113
1127
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
@@ -1131,6 +1145,14 @@ export class ProcedureService extends BaseService {
|
|
|
1131
1145
|
console.log('[PROCEDURE_SERVICE] Strategy 2: Trying name field search');
|
|
1132
1146
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1133
1147
|
const constraints = getBaseConstraints();
|
|
1148
|
+
|
|
1149
|
+
// Check if we have nested field filters that might conflict with orderBy
|
|
1150
|
+
const hasNestedFilters = !!(filters.procedureTechnology || filters.procedureCategory || filters.procedureSubcategory);
|
|
1151
|
+
|
|
1152
|
+
if (hasNestedFilters) {
|
|
1153
|
+
console.log('[PROCEDURE_SERVICE] Strategy 2: Has nested filters, will apply client-side after query');
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1134
1156
|
constraints.push(where('name', '>=', searchTerm));
|
|
1135
1157
|
constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
|
|
1136
1158
|
constraints.push(orderBy('name'));
|
|
@@ -1148,9 +1170,15 @@ export class ProcedureService extends BaseService {
|
|
|
1148
1170
|
|
|
1149
1171
|
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1150
1172
|
const querySnapshot = await getDocs(q);
|
|
1151
|
-
|
|
1173
|
+
let procedures = querySnapshot.docs.map(
|
|
1152
1174
|
doc => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1153
1175
|
);
|
|
1176
|
+
|
|
1177
|
+
// Apply client-side filters for nested fields if needed
|
|
1178
|
+
if (hasNestedFilters) {
|
|
1179
|
+
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1154
1182
|
const lastDoc =
|
|
1155
1183
|
querySnapshot.docs.length > 0
|
|
1156
1184
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
@@ -1169,11 +1197,66 @@ export class ProcedureService extends BaseService {
|
|
|
1169
1197
|
}
|
|
1170
1198
|
|
|
1171
1199
|
// Strategy 3: orderBy createdAt with client-side filtering
|
|
1200
|
+
// NOTE: This strategy excludes nested field filters (technology.id, category.id, subcategory.id)
|
|
1201
|
+
// from Firestore query because Firestore doesn't support orderBy on different field
|
|
1202
|
+
// when using where on nested fields without a composite index.
|
|
1203
|
+
// These filters are applied client-side instead.
|
|
1172
1204
|
try {
|
|
1173
1205
|
console.log(
|
|
1174
1206
|
'[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering',
|
|
1207
|
+
{
|
|
1208
|
+
procedureTechnology: filters.procedureTechnology,
|
|
1209
|
+
hasTechnologyFilter: !!filters.procedureTechnology,
|
|
1210
|
+
},
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
// Build constraints WITHOUT nested field filters (these will be applied client-side)
|
|
1214
|
+
const constraints: QueryConstraint[] = [];
|
|
1215
|
+
|
|
1216
|
+
// Active status filter
|
|
1217
|
+
if (filters.isActive !== undefined) {
|
|
1218
|
+
constraints.push(where('isActive', '==', filters.isActive));
|
|
1219
|
+
} else {
|
|
1220
|
+
constraints.push(where('isActive', '==', true));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Only include non-nested field filters in Firestore query
|
|
1224
|
+
if (filters.procedureFamily) {
|
|
1225
|
+
constraints.push(where('family', '==', filters.procedureFamily));
|
|
1226
|
+
}
|
|
1227
|
+
if (filters.practitionerId) {
|
|
1228
|
+
constraints.push(where('practitionerId', '==', filters.practitionerId));
|
|
1229
|
+
}
|
|
1230
|
+
if (filters.clinicId) {
|
|
1231
|
+
constraints.push(where('clinicBranchId', '==', filters.clinicId));
|
|
1232
|
+
}
|
|
1233
|
+
if (filters.minPrice !== undefined) {
|
|
1234
|
+
constraints.push(where('price', '>=', filters.minPrice));
|
|
1235
|
+
}
|
|
1236
|
+
if (filters.maxPrice !== undefined) {
|
|
1237
|
+
constraints.push(where('price', '<=', filters.maxPrice));
|
|
1238
|
+
}
|
|
1239
|
+
if (filters.minRating !== undefined) {
|
|
1240
|
+
constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
|
|
1241
|
+
}
|
|
1242
|
+
if (filters.maxRating !== undefined) {
|
|
1243
|
+
constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
|
|
1244
|
+
}
|
|
1245
|
+
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
1246
|
+
const benefitIdsToMatch = filters.treatmentBenefits;
|
|
1247
|
+
constraints.push(where('treatmentBenefitIds', 'array-contains-any', benefitIdsToMatch));
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// NOTE: We intentionally EXCLUDE these nested field filters from Firestore query:
|
|
1251
|
+
// - filters.procedureTechnology (technology.id)
|
|
1252
|
+
// - filters.procedureCategory (category.id)
|
|
1253
|
+
// - filters.procedureSubcategory (subcategory.id)
|
|
1254
|
+
// These will be applied client-side in applyInMemoryFilters
|
|
1255
|
+
|
|
1256
|
+
console.log(
|
|
1257
|
+
'[PROCEDURE_SERVICE] Strategy 3 Firestore constraints (nested filters excluded):',
|
|
1258
|
+
constraints.map(c => (c as any).fieldPath || 'unknown'),
|
|
1175
1259
|
);
|
|
1176
|
-
const constraints = getBaseConstraints();
|
|
1177
1260
|
constraints.push(orderBy('createdAt', 'desc'));
|
|
1178
1261
|
|
|
1179
1262
|
if (filters.lastDoc) {
|
|
@@ -1194,7 +1277,20 @@ export class ProcedureService extends BaseService {
|
|
|
1194
1277
|
);
|
|
1195
1278
|
|
|
1196
1279
|
// Apply all client-side filters using centralized function
|
|
1280
|
+
console.log('[PROCEDURE_SERVICE] Before applyInMemoryFilters (Strategy 3):', {
|
|
1281
|
+
procedureCount: procedures.length,
|
|
1282
|
+
procedureTechnology: filters.procedureTechnology,
|
|
1283
|
+
filtersObject: {
|
|
1284
|
+
procedureTechnology: filters.procedureTechnology,
|
|
1285
|
+
procedureFamily: filters.procedureFamily,
|
|
1286
|
+
procedureCategory: filters.procedureCategory,
|
|
1287
|
+
procedureSubcategory: filters.procedureSubcategory,
|
|
1288
|
+
},
|
|
1289
|
+
});
|
|
1197
1290
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1291
|
+
console.log('[PROCEDURE_SERVICE] After applyInMemoryFilters (Strategy 3):', {
|
|
1292
|
+
procedureCount: procedures.length,
|
|
1293
|
+
});
|
|
1198
1294
|
|
|
1199
1295
|
const lastDoc =
|
|
1200
1296
|
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
@@ -1265,6 +1361,14 @@ export class ProcedureService extends BaseService {
|
|
|
1265
1361
|
): (Procedure & { distance?: number })[] {
|
|
1266
1362
|
let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
|
|
1267
1363
|
|
|
1364
|
+
// Debug: Log what filters we received
|
|
1365
|
+
console.log('[PROCEDURE_SERVICE] applyInMemoryFilters called:', {
|
|
1366
|
+
procedureCount: procedures.length,
|
|
1367
|
+
procedureTechnology: filters.procedureTechnology,
|
|
1368
|
+
hasTechnologyFilter: !!filters.procedureTechnology,
|
|
1369
|
+
allFilterKeys: Object.keys(filters).filter(k => filters[k] !== undefined && filters[k] !== null),
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1268
1372
|
// Name search filter
|
|
1269
1373
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1270
1374
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
@@ -1348,12 +1452,21 @@ export class ProcedureService extends BaseService {
|
|
|
1348
1452
|
|
|
1349
1453
|
// Technology filtering
|
|
1350
1454
|
if (filters.procedureTechnology) {
|
|
1455
|
+
const beforeCount = filteredProcedures.length;
|
|
1351
1456
|
filteredProcedures = filteredProcedures.filter(
|
|
1352
1457
|
procedure => procedure.technology?.id === filters.procedureTechnology,
|
|
1353
1458
|
);
|
|
1354
1459
|
console.log(
|
|
1355
|
-
`[PROCEDURE_SERVICE] Applied technology filter,
|
|
1460
|
+
`[PROCEDURE_SERVICE] Applied technology filter (${filters.procedureTechnology}), before: ${beforeCount}, after: ${filteredProcedures.length}`,
|
|
1356
1461
|
);
|
|
1462
|
+
// Log sample technology IDs for debugging
|
|
1463
|
+
if (beforeCount > filteredProcedures.length) {
|
|
1464
|
+
const filteredOut = procedures
|
|
1465
|
+
.filter(p => p.technology?.id !== filters.procedureTechnology)
|
|
1466
|
+
.slice(0, 3)
|
|
1467
|
+
.map(p => ({ id: p.id, techId: p.technology?.id, name: p.name }));
|
|
1468
|
+
console.log('[PROCEDURE_SERVICE] Filtered out sample procedures:', filteredOut);
|
|
1469
|
+
}
|
|
1357
1470
|
}
|
|
1358
1471
|
|
|
1359
1472
|
// Practitioner filtering
|
|
@@ -21,6 +21,7 @@ export enum NotificationType {
|
|
|
21
21
|
|
|
22
22
|
// --- Patient Engagement ---
|
|
23
23
|
REVIEW_REQUEST = "reviewRequest", // Request for patient review post-appointment
|
|
24
|
+
PROCEDURE_RECOMMENDATION = "procedureRecommendation", // Doctor recommended a procedure for follow-up
|
|
24
25
|
|
|
25
26
|
// --- Payment Related (Examples) ---
|
|
26
27
|
PAYMENT_DUE = "paymentDue",
|
|
@@ -231,6 +232,25 @@ export interface ReviewRequestNotification extends BaseNotification {
|
|
|
231
232
|
procedureName?: string;
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Notification for when a doctor recommends a procedure for follow-up.
|
|
237
|
+
* Example: "Dr. Smith recommended [Procedure Name] for you. Suggested timeframe: in 2 weeks"
|
|
238
|
+
*/
|
|
239
|
+
export interface ProcedureRecommendationNotification extends BaseNotification {
|
|
240
|
+
notificationType: NotificationType.PROCEDURE_RECOMMENDATION;
|
|
241
|
+
appointmentId: string; // The appointment where recommendation was made
|
|
242
|
+
recommendationId: string; // Format: `${appointmentId}:${index}`
|
|
243
|
+
procedureId: string;
|
|
244
|
+
procedureName: string;
|
|
245
|
+
practitionerName: string;
|
|
246
|
+
clinicName: string;
|
|
247
|
+
note?: string; // Doctor's note about the recommendation
|
|
248
|
+
timeframe: {
|
|
249
|
+
value: number;
|
|
250
|
+
unit: 'day' | 'week' | 'month' | 'year';
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
/**
|
|
235
255
|
* Generic notification for direct messages or announcements.
|
|
236
256
|
*/
|
|
@@ -261,5 +281,6 @@ export type Notification =
|
|
|
261
281
|
| FormReminderNotification
|
|
262
282
|
| FormSubmissionConfirmationNotification
|
|
263
283
|
| ReviewRequestNotification
|
|
284
|
+
| ProcedureRecommendationNotification
|
|
264
285
|
| GeneralMessageNotification
|
|
265
286
|
| PaymentConfirmationNotification;
|