@blackcode_sa/metaestetics-api 1.12.61 → 1.12.63

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.61",
4
+ "version": "1.12.63",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -82,6 +82,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
82
82
  if (technology.technicalDetails) {
83
83
  newTechnology.technicalDetails = technology.technicalDetails;
84
84
  }
85
+ if (technology.photoTemplate) {
86
+ newTechnology.photoTemplate = technology.photoTemplate;
87
+ }
85
88
 
86
89
  const docRef = await addDoc(this.technologiesRef, newTechnology as any);
87
90
  return { id: docRef.id, ...newTechnology };
@@ -240,6 +243,16 @@ export class TechnologyService extends BaseService implements ITechnologyService
240
243
  }
241
244
  });
242
245
 
246
+ // Handle photoTemplate: if explicitly set to null or empty string, allow it to be cleared
247
+ // If undefined, don't include it in the update (field won't change)
248
+ if ('photoTemplate' in technology) {
249
+ if (technology.photoTemplate === null || technology.photoTemplate === '') {
250
+ updateData.photoTemplate = null;
251
+ } else if (technology.photoTemplate !== undefined) {
252
+ updateData.photoTemplate = technology.photoTemplate;
253
+ }
254
+ }
255
+
243
256
  updateData.updatedAt = new Date();
244
257
 
245
258
  const docRef = doc(this.technologiesRef, id);
@@ -69,6 +69,8 @@ export interface Technology {
69
69
  benefits: TreatmentBenefitDynamic[];
70
70
  certificationRequirement: CertificationRequirement;
71
71
  documentationTemplates?: TechnologyDocumentationTemplate[];
72
+ /** Media ID of the default photo template image for this technology */
73
+ photoTemplate?: string;
72
74
  isActive: boolean;
73
75
  createdAt: Date;
74
76
  updatedAt: Date;
@@ -120,6 +120,7 @@ export const technologySchema = z.object({
120
120
  documentationTemplates: z.array(documentTemplateSchema),
121
121
  benefits: z.array(treatmentBenefitSchemaBackoffice),
122
122
  certificationRequirement: certificationRequirementSchema,
123
+ photoTemplate: z.string().url("Photo template must be a valid URL").optional(),
123
124
  isActive: z.boolean().default(true),
124
125
  });
125
126
 
@@ -14,6 +14,8 @@ import {
14
14
  startAfter,
15
15
  getDocs,
16
16
  getCountFromServer,
17
+ doc,
18
+ getDoc,
17
19
  } from 'firebase/firestore';
18
20
  import { Auth } from 'firebase/auth';
19
21
  import { FirebaseApp } from 'firebase/app';
@@ -34,8 +36,10 @@ import {
34
36
  ExtendedProcedureInfo,
35
37
  AppointmentProductMetadata,
36
38
  RecommendedProcedure,
39
+ NextStepsRecommendation,
37
40
  APPOINTMENTS_COLLECTION,
38
41
  } from '../../types/appointment';
42
+ import { PROCEDURES_COLLECTION } from '../../types/procedure';
39
43
  import {
40
44
  updateAppointmentSchema,
41
45
  searchAppointmentsSchema,
@@ -2079,4 +2083,423 @@ export class AppointmentService extends BaseService {
2079
2083
  throw error;
2080
2084
  }
2081
2085
  }
2086
+
2087
+ /**
2088
+ * Gets all next steps recommendations for a patient from their past appointments.
2089
+ * Returns recommendations with context about which appointment, practitioner, and clinic suggested them.
2090
+ *
2091
+ * @param patientId ID of the patient
2092
+ * @param options Optional parameters for filtering
2093
+ * @returns Array of next steps recommendations with context
2094
+ */
2095
+ async getPatientNextStepsRecommendations(
2096
+ patientId: string,
2097
+ options?: {
2098
+ /** Include dismissed recommendations (default: false) */
2099
+ includeDismissed?: boolean;
2100
+ /** Filter by clinic branch ID */
2101
+ clinicBranchId?: string;
2102
+ /** Filter by practitioner ID */
2103
+ practitionerId?: string;
2104
+ /** Limit the number of results */
2105
+ limit?: number;
2106
+ },
2107
+ ): Promise<NextStepsRecommendation[]> {
2108
+ try {
2109
+ console.log(
2110
+ `[APPOINTMENT_SERVICE] Getting next steps recommendations for patient: ${patientId}`,
2111
+ options,
2112
+ );
2113
+
2114
+ // Get patient profile to check dismissed recommendations
2115
+ const patientProfile = await this.patientService.getPatientProfile(patientId);
2116
+ const dismissedIds = new Set(
2117
+ patientProfile?.dismissedNextStepsRecommendations || [],
2118
+ );
2119
+
2120
+ // Get past appointments (completed appointments)
2121
+ const pastAppointments = await this.getPastPatientAppointments(patientId, {
2122
+ showCanceled: false,
2123
+ showNoShow: false,
2124
+ });
2125
+
2126
+ const recommendations: NextStepsRecommendation[] = [];
2127
+
2128
+ // Iterate through past appointments and extract recommendations
2129
+ for (const appointment of pastAppointments.appointments) {
2130
+ // Filter by clinic if specified
2131
+ if (options?.clinicBranchId && appointment.clinicBranchId !== options.clinicBranchId) {
2132
+ continue;
2133
+ }
2134
+
2135
+ // Filter by practitioner if specified
2136
+ if (options?.practitionerId && appointment.practitionerId !== options.practitionerId) {
2137
+ continue;
2138
+ }
2139
+
2140
+ // Get recommended procedures from appointment metadata
2141
+ const recommendedProcedures =
2142
+ appointment.metadata?.recommendedProcedures || [];
2143
+
2144
+ // Create NextStepsRecommendation for each recommended procedure
2145
+ for (let index = 0; index < recommendedProcedures.length; index++) {
2146
+ const recommendedProcedure = recommendedProcedures[index];
2147
+ const recommendationId = `${appointment.id}:${index}`;
2148
+
2149
+ // Skip if dismissed and not including dismissed
2150
+ if (!options?.includeDismissed && dismissedIds.has(recommendationId)) {
2151
+ continue;
2152
+ }
2153
+
2154
+ const nextStepsRecommendation: NextStepsRecommendation = {
2155
+ id: recommendationId,
2156
+ recommendedProcedure,
2157
+ appointmentId: appointment.id,
2158
+ appointmentDate: appointment.appointmentStartTime,
2159
+ practitionerId: appointment.practitionerId,
2160
+ practitionerName: appointment.practitionerInfo?.name || 'Unknown Practitioner',
2161
+ clinicBranchId: appointment.clinicBranchId,
2162
+ clinicName: appointment.clinicInfo?.name || 'Unknown Clinic',
2163
+ appointmentStatus: appointment.status,
2164
+ isDismissed: dismissedIds.has(recommendationId),
2165
+ dismissedAt: null, // We don't track when it was dismissed, just that it was
2166
+ };
2167
+
2168
+ recommendations.push(nextStepsRecommendation);
2169
+ }
2170
+ }
2171
+
2172
+ // Sort by appointment date (most recent first)
2173
+ recommendations.sort((a, b) => {
2174
+ const dateA = a.appointmentDate.toMillis();
2175
+ const dateB = b.appointmentDate.toMillis();
2176
+ return dateB - dateA;
2177
+ });
2178
+
2179
+ // Apply limit if specified
2180
+ const limitedRecommendations = options?.limit
2181
+ ? recommendations.slice(0, options.limit)
2182
+ : recommendations;
2183
+
2184
+ console.log(
2185
+ `[APPOINTMENT_SERVICE] Found ${limitedRecommendations.length} next steps recommendations for patient ${patientId}`,
2186
+ );
2187
+
2188
+ return limitedRecommendations;
2189
+ } catch (error) {
2190
+ console.error(
2191
+ `[APPOINTMENT_SERVICE] Error getting next steps recommendations for patient ${patientId}:`,
2192
+ error,
2193
+ );
2194
+ throw error;
2195
+ }
2196
+ }
2197
+
2198
+ /**
2199
+ * Dismisses a next steps recommendation for a patient.
2200
+ * This prevents the recommendation from showing up in the default view.
2201
+ *
2202
+ * @param patientId ID of the patient
2203
+ * @param recommendationId ID of the recommendation to dismiss (format: appointmentId:recommendationIndex)
2204
+ * @returns Updated patient profile
2205
+ */
2206
+ async dismissNextStepsRecommendation(
2207
+ patientId: string,
2208
+ recommendationId: string,
2209
+ ): Promise<void> {
2210
+ try {
2211
+ console.log(
2212
+ `[APPOINTMENT_SERVICE] Dismissing recommendation ${recommendationId} for patient ${patientId}`,
2213
+ );
2214
+
2215
+ // Get patient profile
2216
+ const patientProfile = await this.patientService.getPatientProfile(patientId);
2217
+ if (!patientProfile) {
2218
+ throw new Error(`Patient profile not found for patient ${patientId}`);
2219
+ }
2220
+
2221
+ // Get current dismissed recommendations
2222
+ const dismissedRecommendations =
2223
+ patientProfile.dismissedNextStepsRecommendations || [];
2224
+
2225
+ // Check if already dismissed
2226
+ if (dismissedRecommendations.includes(recommendationId)) {
2227
+ console.log(
2228
+ `[APPOINTMENT_SERVICE] Recommendation ${recommendationId} already dismissed`,
2229
+ );
2230
+ return;
2231
+ }
2232
+
2233
+ // Add to dismissed list
2234
+ const updatedDismissed = [...dismissedRecommendations, recommendationId];
2235
+
2236
+ // Update patient profile
2237
+ await this.patientService.updatePatientProfile(patientId, {
2238
+ dismissedNextStepsRecommendations: updatedDismissed,
2239
+ });
2240
+
2241
+ console.log(
2242
+ `[APPOINTMENT_SERVICE] Successfully dismissed recommendation ${recommendationId} for patient ${patientId}`,
2243
+ );
2244
+ } catch (error) {
2245
+ console.error(
2246
+ `[APPOINTMENT_SERVICE] Error dismissing recommendation for patient ${patientId}:`,
2247
+ error,
2248
+ );
2249
+ throw error;
2250
+ }
2251
+ }
2252
+
2253
+ /**
2254
+ * Undismisses a next steps recommendation for a patient.
2255
+ * This makes the recommendation visible again in the default view.
2256
+ *
2257
+ * @param patientId ID of the patient
2258
+ * @param recommendationId ID of the recommendation to undismiss (format: appointmentId:recommendationIndex)
2259
+ * @returns Updated patient profile
2260
+ */
2261
+ async undismissNextStepsRecommendation(
2262
+ patientId: string,
2263
+ recommendationId: string,
2264
+ ): Promise<void> {
2265
+ try {
2266
+ console.log(
2267
+ `[APPOINTMENT_SERVICE] Undismissing recommendation ${recommendationId} for patient ${patientId}`,
2268
+ );
2269
+
2270
+ // Get patient profile
2271
+ const patientProfile = await this.patientService.getPatientProfile(patientId);
2272
+ if (!patientProfile) {
2273
+ throw new Error(`Patient profile not found for patient ${patientId}`);
2274
+ }
2275
+
2276
+ // Get current dismissed recommendations
2277
+ const dismissedRecommendations =
2278
+ patientProfile.dismissedNextStepsRecommendations || [];
2279
+
2280
+ // Check if not dismissed
2281
+ if (!dismissedRecommendations.includes(recommendationId)) {
2282
+ console.log(
2283
+ `[APPOINTMENT_SERVICE] Recommendation ${recommendationId} is not dismissed`,
2284
+ );
2285
+ return;
2286
+ }
2287
+
2288
+ // Remove from dismissed list
2289
+ const updatedDismissed = dismissedRecommendations.filter(
2290
+ id => id !== recommendationId,
2291
+ );
2292
+
2293
+ // Update patient profile
2294
+ await this.patientService.updatePatientProfile(patientId, {
2295
+ dismissedNextStepsRecommendations: updatedDismissed,
2296
+ });
2297
+
2298
+ console.log(
2299
+ `[APPOINTMENT_SERVICE] Successfully undismissed recommendation ${recommendationId} for patient ${patientId}`,
2300
+ );
2301
+ } catch (error) {
2302
+ console.error(
2303
+ `[APPOINTMENT_SERVICE] Error undismissing recommendation for patient ${patientId}:`,
2304
+ error,
2305
+ );
2306
+ throw error;
2307
+ }
2308
+ }
2309
+
2310
+ /**
2311
+ * Gets next steps recommendations for a clinic.
2312
+ * Returns all recommendations from appointments at the specified clinic.
2313
+ * This is useful for clinic admins to see what treatments have been recommended to their patients.
2314
+ *
2315
+ * @param clinicBranchId ID of the clinic branch
2316
+ * @param options Optional parameters for filtering
2317
+ * @returns Array of next steps recommendations with context
2318
+ */
2319
+ async getClinicNextStepsRecommendations(
2320
+ clinicBranchId: string,
2321
+ options?: {
2322
+ /** Filter by patient ID */
2323
+ patientId?: string;
2324
+ /** Filter by practitioner ID */
2325
+ practitionerId?: string;
2326
+ /** Limit the number of results */
2327
+ limit?: number;
2328
+ },
2329
+ ): Promise<NextStepsRecommendation[]> {
2330
+ try {
2331
+ console.log(
2332
+ `[APPOINTMENT_SERVICE] Getting next steps recommendations for clinic: ${clinicBranchId}`,
2333
+ options,
2334
+ );
2335
+
2336
+ // Get past appointments for the clinic
2337
+ const searchParams: SearchAppointmentsParams = {
2338
+ clinicBranchId,
2339
+ patientId: options?.patientId,
2340
+ practitionerId: options?.practitionerId,
2341
+ status: AppointmentStatus.COMPLETED,
2342
+ };
2343
+
2344
+ const { appointments } = await this.searchAppointments(searchParams);
2345
+
2346
+ const recommendations: NextStepsRecommendation[] = [];
2347
+
2348
+ // Iterate through appointments and extract recommendations
2349
+ for (const appointment of appointments) {
2350
+ // Get recommended procedures from appointment metadata
2351
+ const recommendedProcedures =
2352
+ appointment.metadata?.recommendedProcedures || [];
2353
+
2354
+ // Create NextStepsRecommendation for each recommended procedure
2355
+ for (let index = 0; index < recommendedProcedures.length; index++) {
2356
+ const recommendedProcedure = recommendedProcedures[index];
2357
+ const recommendationId = `${appointment.id}:${index}`;
2358
+
2359
+ const nextStepsRecommendation: NextStepsRecommendation = {
2360
+ id: recommendationId,
2361
+ recommendedProcedure,
2362
+ appointmentId: appointment.id,
2363
+ appointmentDate: appointment.appointmentStartTime,
2364
+ practitionerId: appointment.practitionerId,
2365
+ practitionerName: appointment.practitionerInfo?.name || 'Unknown Practitioner',
2366
+ clinicBranchId: appointment.clinicBranchId,
2367
+ clinicName: appointment.clinicInfo?.name || 'Unknown Clinic',
2368
+ appointmentStatus: appointment.status,
2369
+ isDismissed: false, // Clinic view doesn't track dismissals
2370
+ dismissedAt: null,
2371
+ };
2372
+
2373
+ recommendations.push(nextStepsRecommendation);
2374
+ }
2375
+ }
2376
+
2377
+ // Sort by appointment date (most recent first)
2378
+ recommendations.sort((a, b) => {
2379
+ const dateA = a.appointmentDate.toMillis();
2380
+ const dateB = b.appointmentDate.toMillis();
2381
+ return dateB - dateA;
2382
+ });
2383
+
2384
+ // Apply limit if specified
2385
+ const limitedRecommendations = options?.limit
2386
+ ? recommendations.slice(0, options.limit)
2387
+ : recommendations;
2388
+
2389
+ console.log(
2390
+ `[APPOINTMENT_SERVICE] Found ${limitedRecommendations.length} next steps recommendations for clinic ${clinicBranchId}`,
2391
+ );
2392
+
2393
+ return limitedRecommendations;
2394
+ } catch (error) {
2395
+ console.error(
2396
+ `[APPOINTMENT_SERVICE] Error getting next steps recommendations for clinic ${clinicBranchId}:`,
2397
+ error,
2398
+ );
2399
+ throw error;
2400
+ }
2401
+ }
2402
+
2403
+ /**
2404
+ * Gets next steps recommendations from a specific appointment.
2405
+ * This is useful when viewing an appointment detail page in the clinic app
2406
+ * to see what procedures were recommended during that appointment.
2407
+ *
2408
+ * @param appointmentId ID of the appointment
2409
+ * @param options Optional parameters for filtering
2410
+ * @returns Array of next steps recommendations from that appointment
2411
+ */
2412
+ async getAppointmentNextStepsRecommendations(
2413
+ appointmentId: string,
2414
+ options?: {
2415
+ /** Filter by clinic branch ID - only show recommendations for procedures available at this clinic */
2416
+ clinicBranchId?: string;
2417
+ },
2418
+ ): Promise<NextStepsRecommendation[]> {
2419
+ try {
2420
+ console.log(
2421
+ `[APPOINTMENT_SERVICE] Getting next steps recommendations for appointment: ${appointmentId}`,
2422
+ options,
2423
+ );
2424
+
2425
+ // Get the appointment
2426
+ const appointment = await this.getAppointmentById(appointmentId);
2427
+ if (!appointment) {
2428
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
2429
+ }
2430
+
2431
+ // Get recommended procedures from appointment metadata
2432
+ const recommendedProcedures =
2433
+ appointment.metadata?.recommendedProcedures || [];
2434
+
2435
+ const recommendations: NextStepsRecommendation[] = [];
2436
+
2437
+ // If clinicBranchId is provided, we need to check which procedures are available at that clinic
2438
+ let availableProcedureIds: Set<string> | null = null;
2439
+ if (options?.clinicBranchId) {
2440
+ // Query procedures collection to get all procedure IDs available at this clinic
2441
+ const proceduresQuery = query(
2442
+ collection(this.db, PROCEDURES_COLLECTION),
2443
+ where('clinicBranchId', '==', options.clinicBranchId),
2444
+ where('isActive', '==', true),
2445
+ );
2446
+ const proceduresSnapshot = await getDocs(proceduresQuery);
2447
+ availableProcedureIds = new Set(
2448
+ proceduresSnapshot.docs.map(doc => doc.id),
2449
+ );
2450
+ console.log(
2451
+ `[APPOINTMENT_SERVICE] Found ${availableProcedureIds.size} procedures available at clinic ${options.clinicBranchId}`,
2452
+ );
2453
+ }
2454
+
2455
+ // Create NextStepsRecommendation for each recommended procedure
2456
+ for (let index = 0; index < recommendedProcedures.length; index++) {
2457
+ const recommendedProcedure = recommendedProcedures[index];
2458
+ const procedureId = recommendedProcedure.procedure.procedureId;
2459
+
2460
+ // If clinicBranchId is provided, filter to only include procedures available at that clinic
2461
+ if (options?.clinicBranchId && availableProcedureIds) {
2462
+ if (!availableProcedureIds.has(procedureId)) {
2463
+ console.log(
2464
+ `[APPOINTMENT_SERVICE] Skipping recommendation for procedure ${procedureId} - not available at clinic ${options.clinicBranchId}`,
2465
+ );
2466
+ continue;
2467
+ }
2468
+ }
2469
+
2470
+ const recommendationId = `${appointment.id}:${index}`;
2471
+
2472
+ const nextStepsRecommendation: NextStepsRecommendation = {
2473
+ id: recommendationId,
2474
+ recommendedProcedure,
2475
+ appointmentId: appointment.id,
2476
+ appointmentDate: appointment.appointmentStartTime,
2477
+ practitionerId: appointment.practitionerId,
2478
+ practitionerName: appointment.practitionerInfo?.name || 'Unknown Practitioner',
2479
+ clinicBranchId: appointment.clinicBranchId,
2480
+ clinicName: appointment.clinicInfo?.name || 'Unknown Clinic',
2481
+ appointmentStatus: appointment.status,
2482
+ isDismissed: false, // Clinic view doesn't track dismissals
2483
+ dismissedAt: null,
2484
+ };
2485
+
2486
+ recommendations.push(nextStepsRecommendation);
2487
+ }
2488
+
2489
+ console.log(
2490
+ `[APPOINTMENT_SERVICE] Found ${recommendations.length} next steps recommendations for appointment ${appointmentId}`,
2491
+ options?.clinicBranchId
2492
+ ? `(filtered to procedures available at clinic ${options.clinicBranchId})`
2493
+ : '',
2494
+ );
2495
+
2496
+ return recommendations;
2497
+ } catch (error) {
2498
+ console.error(
2499
+ `[APPOINTMENT_SERVICE] Error getting next steps recommendations for appointment ${appointmentId}:`,
2500
+ error,
2501
+ );
2502
+ throw error;
2503
+ }
2504
+ }
2082
2505
  }
@@ -451,3 +451,31 @@ export interface SearchAppointmentsParams {
451
451
 
452
452
  /** Firestore collection name */
453
453
  export const APPOINTMENTS_COLLECTION = 'appointments';
454
+
455
+ /**
456
+ * Interface for next steps recommendation with context about when and who suggested it
457
+ */
458
+ export interface NextStepsRecommendation {
459
+ /** Unique identifier for this recommendation (appointmentId + recommendationIndex) */
460
+ id: string;
461
+ /** The recommended procedure details */
462
+ recommendedProcedure: RecommendedProcedure;
463
+ /** ID of the appointment where this was recommended */
464
+ appointmentId: string;
465
+ /** Date of the appointment when this was recommended */
466
+ appointmentDate: Timestamp;
467
+ /** ID of the practitioner who made the recommendation */
468
+ practitionerId: string;
469
+ /** Name of the practitioner who made the recommendation */
470
+ practitionerName: string;
471
+ /** ID of the clinic where the appointment took place */
472
+ clinicBranchId: string;
473
+ /** Name of the clinic where the appointment took place */
474
+ clinicName: string;
475
+ /** Status of the appointment when recommendation was made */
476
+ appointmentStatus: AppointmentStatus;
477
+ /** Whether this recommendation has been dismissed by the patient */
478
+ isDismissed?: boolean;
479
+ /** When the recommendation was dismissed (if dismissed) */
480
+ dismissedAt?: Timestamp | null;
481
+ }
@@ -175,6 +175,8 @@ export interface PatientProfile {
175
175
  clinics: PatientClinic[]; // Lista klinika pacijenta
176
176
  doctorIds: string[]; // Denormalized array for querying
177
177
  clinicIds: string[]; // Denormalized array for querying
178
+ /** IDs of dismissed next steps recommendations (format: appointmentId:recommendationIndex) */
179
+ dismissedNextStepsRecommendations?: string[];
178
180
  createdAt: Timestamp;
179
181
  updatedAt: Timestamp;
180
182
  }
@@ -120,6 +120,7 @@ export const patientProfileSchema = z.object({
120
120
  clinics: z.array(patientClinicSchema),
121
121
  doctorIds: z.array(z.string()),
122
122
  clinicIds: z.array(z.string()),
123
+ dismissedNextStepsRecommendations: z.array(z.string()).optional(),
123
124
  createdAt: z.instanceof(Timestamp),
124
125
  updatedAt: z.instanceof(Timestamp),
125
126
  });