@blackcode_sa/metaestetics-api 1.12.67 → 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.
Files changed (47) hide show
  1. package/dist/admin/index.d.mts +801 -2
  2. package/dist/admin/index.d.ts +801 -2
  3. package/dist/admin/index.js +2332 -153
  4. package/dist/admin/index.mjs +2321 -153
  5. package/dist/backoffice/index.d.mts +40 -0
  6. package/dist/backoffice/index.d.ts +40 -0
  7. package/dist/backoffice/index.js +118 -18
  8. package/dist/backoffice/index.mjs +118 -20
  9. package/dist/index.d.mts +1097 -2
  10. package/dist/index.d.ts +1097 -2
  11. package/dist/index.js +4224 -2091
  12. package/dist/index.mjs +3941 -1821
  13. package/package.json +1 -1
  14. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +140 -0
  15. package/src/admin/analytics/analytics.admin.service.ts +278 -0
  16. package/src/admin/analytics/index.ts +2 -0
  17. package/src/admin/index.ts +6 -0
  18. package/src/backoffice/services/README.md +17 -0
  19. package/src/backoffice/services/analytics.service.proposal.md +863 -0
  20. package/src/backoffice/services/analytics.service.summary.md +143 -0
  21. package/src/backoffice/services/category.service.ts +49 -6
  22. package/src/backoffice/services/subcategory.service.ts +50 -6
  23. package/src/backoffice/services/technology.service.ts +53 -6
  24. package/src/services/analytics/ARCHITECTURE.md +199 -0
  25. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -0
  26. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -0
  27. package/src/services/analytics/QUICK_START.md +393 -0
  28. package/src/services/analytics/README.md +287 -0
  29. package/src/services/analytics/SUMMARY.md +141 -0
  30. package/src/services/analytics/USAGE_GUIDE.md +518 -0
  31. package/src/services/analytics/analytics-cloud.service.ts +222 -0
  32. package/src/services/analytics/analytics.service.ts +1632 -0
  33. package/src/services/analytics/index.ts +3 -0
  34. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -0
  35. package/src/services/analytics/utils/cost-calculation.utils.ts +154 -0
  36. package/src/services/analytics/utils/grouping.utils.ts +394 -0
  37. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -0
  38. package/src/services/analytics/utils/time-calculation.utils.ts +186 -0
  39. package/src/services/appointment/appointment.service.ts +50 -6
  40. package/src/services/index.ts +1 -0
  41. package/src/services/procedure/procedure.service.ts +3 -3
  42. package/src/types/analytics/analytics.types.ts +500 -0
  43. package/src/types/analytics/grouped-analytics.types.ts +148 -0
  44. package/src/types/analytics/index.ts +4 -0
  45. package/src/types/analytics/stored-analytics.types.ts +137 -0
  46. package/src/types/index.ts +3 -0
  47. package/src/types/notifications/index.ts +21 -0
@@ -0,0 +1,347 @@
1
+ import { Firestore, Timestamp, doc, getDoc } from 'firebase/firestore';
2
+ import {
3
+ ANALYTICS_COLLECTION,
4
+ PRACTITIONER_ANALYTICS_SUBCOLLECTION,
5
+ PROCEDURE_ANALYTICS_SUBCOLLECTION,
6
+ CLINIC_ANALYTICS_SUBCOLLECTION,
7
+ DASHBOARD_ANALYTICS_SUBCOLLECTION,
8
+ TIME_EFFICIENCY_ANALYTICS_SUBCOLLECTION,
9
+ CANCELLATION_ANALYTICS_SUBCOLLECTION,
10
+ NO_SHOW_ANALYTICS_SUBCOLLECTION,
11
+ REVENUE_ANALYTICS_SUBCOLLECTION,
12
+ AnalyticsPeriod,
13
+ ReadStoredAnalyticsOptions,
14
+ StoredPractitionerAnalytics,
15
+ StoredProcedureAnalytics,
16
+ StoredClinicAnalytics,
17
+ StoredDashboardAnalytics,
18
+ StoredTimeEfficiencyMetrics,
19
+ StoredCancellationMetrics,
20
+ StoredNoShowMetrics,
21
+ StoredRevenueMetrics,
22
+ } from '../../../types/analytics';
23
+ import { CLINICS_COLLECTION } from '../../../types/clinic';
24
+
25
+ /**
26
+ * Checks if stored analytics data is fresh enough to use
27
+ *
28
+ * @param computedAt - Timestamp when analytics were computed
29
+ * @param maxAgeHours - Maximum age in hours
30
+ * @returns True if data is fresh enough
31
+ */
32
+ export function isAnalyticsDataFresh(computedAt: Timestamp, maxAgeHours: number): boolean {
33
+ const now = Timestamp.now();
34
+ const ageMs = now.toMillis() - computedAt.toMillis();
35
+ const ageHours = ageMs / (1000 * 60 * 60);
36
+ return ageHours <= maxAgeHours;
37
+ }
38
+
39
+ /**
40
+ * Reads stored analytics document
41
+ *
42
+ * @param db - Firestore instance
43
+ * @param clinicBranchId - Clinic branch ID
44
+ * @param subcollection - Analytics subcollection name
45
+ * @param documentId - Document ID
46
+ * @param period - Analytics period
47
+ * @returns Stored analytics document or null
48
+ */
49
+ async function readStoredAnalytics<T>(
50
+ db: Firestore,
51
+ clinicBranchId: string,
52
+ subcollection: string,
53
+ documentId: string,
54
+ period: AnalyticsPeriod,
55
+ ): Promise<T | null> {
56
+ try {
57
+ const docRef = doc(
58
+ db,
59
+ CLINICS_COLLECTION,
60
+ clinicBranchId,
61
+ ANALYTICS_COLLECTION,
62
+ subcollection,
63
+ period,
64
+ documentId,
65
+ );
66
+
67
+ const docSnap = await getDoc(docRef);
68
+ if (!docSnap.exists()) {
69
+ return null;
70
+ }
71
+
72
+ return docSnap.data() as T;
73
+ } catch (error) {
74
+ console.error(`[StoredAnalytics] Error reading ${subcollection}/${period}/${documentId}:`, error);
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Reads stored practitioner analytics
81
+ */
82
+ export async function readStoredPractitionerAnalytics(
83
+ db: Firestore,
84
+ clinicBranchId: string,
85
+ practitionerId: string,
86
+ options: ReadStoredAnalyticsOptions = {},
87
+ ): Promise<StoredPractitionerAnalytics | null> {
88
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
89
+
90
+ if (!useCache) {
91
+ return null;
92
+ }
93
+
94
+ const stored = await readStoredAnalytics<StoredPractitionerAnalytics>(
95
+ db,
96
+ clinicBranchId,
97
+ PRACTITIONER_ANALYTICS_SUBCOLLECTION,
98
+ practitionerId,
99
+ period,
100
+ );
101
+
102
+ if (!stored) {
103
+ return null;
104
+ }
105
+
106
+ // Check if data is fresh enough
107
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
108
+ return null;
109
+ }
110
+
111
+ return stored;
112
+ }
113
+
114
+ /**
115
+ * Reads stored procedure analytics
116
+ */
117
+ export async function readStoredProcedureAnalytics(
118
+ db: Firestore,
119
+ clinicBranchId: string,
120
+ procedureId: string,
121
+ options: ReadStoredAnalyticsOptions = {},
122
+ ): Promise<StoredProcedureAnalytics | null> {
123
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
124
+
125
+ if (!useCache) {
126
+ return null;
127
+ }
128
+
129
+ const stored = await readStoredAnalytics<StoredProcedureAnalytics>(
130
+ db,
131
+ clinicBranchId,
132
+ PROCEDURE_ANALYTICS_SUBCOLLECTION,
133
+ procedureId,
134
+ period,
135
+ );
136
+
137
+ if (!stored) {
138
+ return null;
139
+ }
140
+
141
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
142
+ return null;
143
+ }
144
+
145
+ return stored;
146
+ }
147
+
148
+ /**
149
+ * Reads stored clinic analytics
150
+ */
151
+ export async function readStoredClinicAnalytics(
152
+ db: Firestore,
153
+ clinicBranchId: string,
154
+ options: ReadStoredAnalyticsOptions = {},
155
+ ): Promise<StoredClinicAnalytics | null> {
156
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
157
+
158
+ if (!useCache) {
159
+ return null;
160
+ }
161
+
162
+ const stored = await readStoredAnalytics<StoredClinicAnalytics>(
163
+ db,
164
+ clinicBranchId,
165
+ CLINIC_ANALYTICS_SUBCOLLECTION,
166
+ 'current',
167
+ period,
168
+ );
169
+
170
+ if (!stored) {
171
+ return null;
172
+ }
173
+
174
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
175
+ return null;
176
+ }
177
+
178
+ return stored;
179
+ }
180
+
181
+ /**
182
+ * Reads stored dashboard analytics
183
+ */
184
+ export async function readStoredDashboardAnalytics(
185
+ db: Firestore,
186
+ clinicBranchId: string,
187
+ options: ReadStoredAnalyticsOptions = {},
188
+ ): Promise<StoredDashboardAnalytics | null> {
189
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
190
+
191
+ if (!useCache) {
192
+ return null;
193
+ }
194
+
195
+ const stored = await readStoredAnalytics<StoredDashboardAnalytics>(
196
+ db,
197
+ clinicBranchId,
198
+ DASHBOARD_ANALYTICS_SUBCOLLECTION,
199
+ 'current',
200
+ period,
201
+ );
202
+
203
+ if (!stored) {
204
+ return null;
205
+ }
206
+
207
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
208
+ return null;
209
+ }
210
+
211
+ return stored;
212
+ }
213
+
214
+ /**
215
+ * Reads stored time efficiency metrics
216
+ */
217
+ export async function readStoredTimeEfficiencyMetrics(
218
+ db: Firestore,
219
+ clinicBranchId: string,
220
+ options: ReadStoredAnalyticsOptions = {},
221
+ ): Promise<StoredTimeEfficiencyMetrics | null> {
222
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
223
+
224
+ if (!useCache) {
225
+ return null;
226
+ }
227
+
228
+ const stored = await readStoredAnalytics<StoredTimeEfficiencyMetrics>(
229
+ db,
230
+ clinicBranchId,
231
+ TIME_EFFICIENCY_ANALYTICS_SUBCOLLECTION,
232
+ 'current',
233
+ period,
234
+ );
235
+
236
+ if (!stored) {
237
+ return null;
238
+ }
239
+
240
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
241
+ return null;
242
+ }
243
+
244
+ return stored;
245
+ }
246
+
247
+ /**
248
+ * Reads stored revenue metrics
249
+ */
250
+ export async function readStoredRevenueMetrics(
251
+ db: Firestore,
252
+ clinicBranchId: string,
253
+ options: ReadStoredAnalyticsOptions = {},
254
+ ): Promise<StoredRevenueMetrics | null> {
255
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
256
+
257
+ if (!useCache) {
258
+ return null;
259
+ }
260
+
261
+ const stored = await readStoredAnalytics<StoredRevenueMetrics>(
262
+ db,
263
+ clinicBranchId,
264
+ REVENUE_ANALYTICS_SUBCOLLECTION,
265
+ 'current',
266
+ period,
267
+ );
268
+
269
+ if (!stored) {
270
+ return null;
271
+ }
272
+
273
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
274
+ return null;
275
+ }
276
+
277
+ return stored;
278
+ }
279
+
280
+ /**
281
+ * Reads stored cancellation metrics
282
+ */
283
+ export async function readStoredCancellationMetrics(
284
+ db: Firestore,
285
+ clinicBranchId: string,
286
+ entityType: string,
287
+ options: ReadStoredAnalyticsOptions = {},
288
+ ): Promise<StoredCancellationMetrics | null> {
289
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
290
+
291
+ if (!useCache) {
292
+ return null;
293
+ }
294
+
295
+ const stored = await readStoredAnalytics<StoredCancellationMetrics>(
296
+ db,
297
+ clinicBranchId,
298
+ CANCELLATION_ANALYTICS_SUBCOLLECTION,
299
+ entityType,
300
+ period,
301
+ );
302
+
303
+ if (!stored) {
304
+ return null;
305
+ }
306
+
307
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
308
+ return null;
309
+ }
310
+
311
+ return stored;
312
+ }
313
+
314
+ /**
315
+ * Reads stored no-show metrics
316
+ */
317
+ export async function readStoredNoShowMetrics(
318
+ db: Firestore,
319
+ clinicBranchId: string,
320
+ entityType: string,
321
+ options: ReadStoredAnalyticsOptions = {},
322
+ ): Promise<StoredNoShowMetrics | null> {
323
+ const { useCache = true, maxCacheAgeHours = 12, period = 'all_time' } = options;
324
+
325
+ if (!useCache) {
326
+ return null;
327
+ }
328
+
329
+ const stored = await readStoredAnalytics<StoredNoShowMetrics>(
330
+ db,
331
+ clinicBranchId,
332
+ NO_SHOW_ANALYTICS_SUBCOLLECTION,
333
+ entityType,
334
+ period,
335
+ );
336
+
337
+ if (!stored) {
338
+ return null;
339
+ }
340
+
341
+ if (!isAnalyticsDataFresh(stored.metadata.computedAt, maxCacheAgeHours)) {
342
+ return null;
343
+ }
344
+
345
+ return stored;
346
+ }
347
+
@@ -0,0 +1,186 @@
1
+ import { Appointment, AppointmentStatus } from '../../../types/appointment';
2
+ import { Timestamp } from 'firebase/firestore';
3
+
4
+ /**
5
+ * Calculates time efficiency metrics for an appointment
6
+ *
7
+ * @param appointment - The appointment to calculate metrics for
8
+ * @returns Time efficiency data or null if insufficient data
9
+ */
10
+ export function calculateTimeEfficiency(appointment: Appointment): {
11
+ bookedDuration: number; // minutes
12
+ actualDuration: number; // minutes
13
+ efficiency: number; // percentage
14
+ overrun: number; // minutes (positive if actual > booked)
15
+ underutilization: number; // minutes (positive if booked > actual)
16
+ } | null {
17
+ const startTime = appointment.appointmentStartTime;
18
+ const endTime = appointment.appointmentEndTime;
19
+
20
+ if (!startTime || !endTime) {
21
+ return null;
22
+ }
23
+
24
+ // Calculate booked duration in minutes
25
+ const bookedDurationMs = endTime.toMillis() - startTime.toMillis();
26
+ const bookedDuration = Math.round(bookedDurationMs / (1000 * 60));
27
+
28
+ // Use actual duration if available, otherwise use booked duration
29
+ const actualDuration = appointment.actualDurationMinutes || bookedDuration;
30
+
31
+ // Calculate efficiency percentage
32
+ const efficiency = bookedDuration > 0 ? (actualDuration / bookedDuration) * 100 : 100;
33
+
34
+ // Calculate overrun (positive if actual > booked)
35
+ const overrun = actualDuration > bookedDuration ? actualDuration - bookedDuration : 0;
36
+
37
+ // Calculate underutilization (positive if booked > actual)
38
+ const underutilization = bookedDuration > actualDuration ? bookedDuration - actualDuration : 0;
39
+
40
+ return {
41
+ bookedDuration,
42
+ actualDuration,
43
+ efficiency,
44
+ overrun,
45
+ underutilization,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Calculates average time metrics from an array of appointments
51
+ *
52
+ * @param appointments - Array of appointments
53
+ * @returns Average time metrics
54
+ */
55
+ export function calculateAverageTimeMetrics(appointments: Appointment[]): {
56
+ averageBookedDuration: number;
57
+ averageActualDuration: number;
58
+ averageEfficiency: number;
59
+ totalOverrun: number;
60
+ totalUnderutilization: number;
61
+ averageOverrun: number;
62
+ averageUnderutilization: number;
63
+ appointmentsWithActualTime: number;
64
+ } {
65
+ if (appointments.length === 0) {
66
+ return {
67
+ averageBookedDuration: 0,
68
+ averageActualDuration: 0,
69
+ averageEfficiency: 0,
70
+ totalOverrun: 0,
71
+ totalUnderutilization: 0,
72
+ averageOverrun: 0,
73
+ averageUnderutilization: 0,
74
+ appointmentsWithActualTime: 0,
75
+ };
76
+ }
77
+
78
+ let totalBookedDuration = 0;
79
+ let totalActualDuration = 0;
80
+ let totalOverrun = 0;
81
+ let totalUnderutilization = 0;
82
+ let appointmentsWithActualTime = 0;
83
+
84
+ appointments.forEach(appointment => {
85
+ const timeData = calculateTimeEfficiency(appointment);
86
+ if (timeData) {
87
+ totalBookedDuration += timeData.bookedDuration;
88
+ totalActualDuration += timeData.actualDuration;
89
+ totalOverrun += timeData.overrun;
90
+ totalUnderutilization += timeData.underutilization;
91
+
92
+ if (appointment.actualDurationMinutes !== undefined) {
93
+ appointmentsWithActualTime++;
94
+ }
95
+ }
96
+ });
97
+
98
+ const count = appointments.length;
99
+ const averageBookedDuration = count > 0 ? totalBookedDuration / count : 0;
100
+ const averageActualDuration = count > 0 ? totalActualDuration / count : 0;
101
+ const averageEfficiency = averageBookedDuration > 0 ? (averageActualDuration / averageBookedDuration) * 100 : 0;
102
+ const averageOverrun = count > 0 ? totalOverrun / count : 0;
103
+ const averageUnderutilization = count > 0 ? totalUnderutilization / count : 0;
104
+
105
+ return {
106
+ averageBookedDuration: Math.round(averageBookedDuration),
107
+ averageActualDuration: Math.round(averageActualDuration),
108
+ averageEfficiency: Math.round(averageEfficiency * 100) / 100,
109
+ totalOverrun,
110
+ totalUnderutilization,
111
+ averageOverrun: Math.round(averageOverrun),
112
+ averageUnderutilization: Math.round(averageUnderutilization),
113
+ appointmentsWithActualTime,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Groups appointments by efficiency ranges
119
+ *
120
+ * @param appointments - Array of appointments
121
+ * @returns Distribution of appointments by efficiency range
122
+ */
123
+ export function calculateEfficiencyDistribution(appointments: Appointment[]): Array<{
124
+ range: string;
125
+ count: number;
126
+ percentage: number;
127
+ }> {
128
+ const ranges = [
129
+ { label: '0-50%', min: 0, max: 50 },
130
+ { label: '50-75%', min: 50, max: 75 },
131
+ { label: '75-100%', min: 75, max: 100 },
132
+ { label: '100%+', min: 100, max: Infinity },
133
+ ];
134
+
135
+ const distribution = ranges.map(range => ({
136
+ range: range.label,
137
+ count: 0,
138
+ percentage: 0,
139
+ }));
140
+
141
+ let validCount = 0;
142
+
143
+ appointments.forEach(appointment => {
144
+ const timeData = calculateTimeEfficiency(appointment);
145
+ if (timeData) {
146
+ validCount++;
147
+ const efficiency = timeData.efficiency;
148
+
149
+ for (let i = 0; i < ranges.length; i++) {
150
+ if (efficiency >= ranges[i].min && efficiency < ranges[i].max) {
151
+ distribution[i].count++;
152
+ break;
153
+ }
154
+ }
155
+ }
156
+ });
157
+
158
+ // Calculate percentages
159
+ if (validCount > 0) {
160
+ distribution.forEach(item => {
161
+ item.percentage = Math.round((item.count / validCount) * 100 * 100) / 100;
162
+ });
163
+ }
164
+
165
+ return distribution;
166
+ }
167
+
168
+ /**
169
+ * Calculates cancellation lead time in hours
170
+ *
171
+ * @param appointment - The appointment to calculate lead time for
172
+ * @returns Lead time in hours, or null if insufficient data
173
+ */
174
+ export function calculateCancellationLeadTime(appointment: Appointment): number | null {
175
+ if (!appointment.cancellationTime || !appointment.appointmentStartTime) {
176
+ return null;
177
+ }
178
+
179
+ const cancellationTime = appointment.cancellationTime.toMillis();
180
+ const appointmentTime = appointment.appointmentStartTime.toMillis();
181
+ const diffMs = appointmentTime - cancellationTime;
182
+
183
+ // Return positive hours (time before appointment)
184
+ return Math.max(0, diffMs / (1000 * 60 * 60));
185
+ }
186
+
@@ -2123,10 +2123,58 @@ export class AppointmentService extends BaseService {
2123
2123
  showNoShow: false,
2124
2124
  });
2125
2125
 
2126
+ // Also get appointments that have recommendations but might not be COMPLETED yet
2127
+ // (e.g., doctor finalized but appointment status is still CONFIRMED/CHECKED_IN)
2128
+ // Get all patient appointments that are past their end time
2129
+ const now = new Date();
2130
+ const allPastAppointments = await this.getPatientAppointments(patientId, {
2131
+ endDate: now,
2132
+ status: [
2133
+ AppointmentStatus.COMPLETED,
2134
+ AppointmentStatus.CONFIRMED,
2135
+ AppointmentStatus.CHECKED_IN,
2136
+ AppointmentStatus.IN_PROGRESS,
2137
+ ],
2138
+ });
2139
+
2140
+ // Filter to only include appointments that are past their end time AND have recommendations
2141
+ const appointmentsWithRecommendations = allPastAppointments.appointments.filter(
2142
+ appointment => {
2143
+ const endTime = appointment.appointmentEndTime?.toMillis
2144
+ ? appointment.appointmentEndTime.toMillis()
2145
+ : appointment.appointmentEndTime?.seconds
2146
+ ? appointment.appointmentEndTime.seconds * 1000
2147
+ : null;
2148
+
2149
+ if (!endTime) return false;
2150
+
2151
+ const isPastEndTime = endTime < now.getTime();
2152
+ const hasRecommendations =
2153
+ (appointment.metadata?.recommendedProcedures?.length || 0) > 0;
2154
+
2155
+ return isPastEndTime && hasRecommendations;
2156
+ },
2157
+ );
2158
+
2159
+ // Combine and deduplicate by appointment ID
2160
+ const allAppointmentsMap = new Map<string, Appointment>();
2161
+
2162
+ // Add completed appointments
2163
+ pastAppointments.appointments.forEach(apt => {
2164
+ allAppointmentsMap.set(apt.id, apt);
2165
+ });
2166
+
2167
+ // Add appointments with recommendations (will overwrite if duplicate)
2168
+ appointmentsWithRecommendations.forEach(apt => {
2169
+ allAppointmentsMap.set(apt.id, apt);
2170
+ });
2171
+
2172
+ const allAppointments = Array.from(allAppointmentsMap.values());
2173
+
2126
2174
  const recommendations: NextStepsRecommendation[] = [];
2127
2175
 
2128
- // Iterate through past appointments and extract recommendations
2129
- for (const appointment of pastAppointments.appointments) {
2176
+ // Iterate through all appointments and extract recommendations
2177
+ for (const appointment of allAppointments) {
2130
2178
  // Filter by clinic if specified
2131
2179
  if (options?.clinicBranchId && appointment.clinicBranchId !== options.clinicBranchId) {
2132
2180
  continue;
@@ -2181,10 +2229,6 @@ export class AppointmentService extends BaseService {
2181
2229
  ? recommendations.slice(0, options.limit)
2182
2230
  : recommendations;
2183
2231
 
2184
- console.log(
2185
- `[APPOINTMENT_SERVICE] Found ${limitedRecommendations.length} next steps recommendations for patient ${patientId}`,
2186
- );
2187
-
2188
2232
  return limitedRecommendations;
2189
2233
  } catch (error) {
2190
2234
  console.error(
@@ -1,3 +1,4 @@
1
+ export * from "./analytics";
1
2
  export * from "./appointment";
2
3
  export * from "./auth";
3
4
  export * from "./calendar";
@@ -1497,9 +1497,9 @@ export class ProcedureService extends BaseService {
1497
1497
  // Get references to related entities (Category, Subcategory, Technology)
1498
1498
  // For consultation, we don't need a product
1499
1499
  const [category, subcategory, technology] = await Promise.all([
1500
- this.categoryService.getById(data.categoryId),
1501
- this.subcategoryService.getById(data.categoryId, data.subcategoryId),
1502
- this.technologyService.getById(data.technologyId),
1500
+ this.categoryService.getByIdInternal(data.categoryId),
1501
+ this.subcategoryService.getByIdInternal(data.categoryId, data.subcategoryId),
1502
+ this.technologyService.getByIdInternal(data.technologyId),
1503
1503
  ]);
1504
1504
 
1505
1505
  if (!category || !subcategory || !technology) {