@blackcode_sa/metaestetics-api 1.12.20 → 1.12.22

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.
@@ -40,6 +40,16 @@ export class ReviewService extends BaseService {
40
40
  appointmentId: string,
41
41
  ): Promise<Review> {
42
42
  try {
43
+ console.log('🔍 ReviewService.createReview - Input data:', {
44
+ appointmentId,
45
+ hasClinicReview: !!data.clinicReview,
46
+ hasPractitionerReview: !!data.practitionerReview,
47
+ hasProcedureReview: !!data.procedureReview,
48
+ practitionerId: data.practitionerReview?.practitionerId,
49
+ clinicId: data.clinicReview?.clinicId,
50
+ procedureId: data.procedureReview?.procedureId,
51
+ });
52
+
43
53
  // Validate input data
44
54
  const validatedData = createReviewSchema.parse(data);
45
55
 
@@ -132,6 +142,13 @@ export class ReviewService extends BaseService {
132
142
  updatedAt: serverTimestamp(),
133
143
  });
134
144
 
145
+ console.log('✅ ReviewService.createReview - Review saved to Firestore:', {
146
+ reviewId,
147
+ practitionerId: review.practitionerReview?.practitionerId,
148
+ clinicId: review.clinicReview?.clinicId,
149
+ procedureId: review.procedureReview?.procedureId,
150
+ });
151
+
135
152
  // Note: Related entity updates (clinic, practitioner, procedure) are now handled
136
153
  // by cloud functions through the ReviewsAggregationService
137
154
 
@@ -145,19 +162,80 @@ export class ReviewService extends BaseService {
145
162
  }
146
163
 
147
164
  /**
148
- * Gets a review by ID
165
+ * Gets a review by ID with enhanced entity names
149
166
  * @param reviewId The ID of the review to get
150
- * @returns The review if found, null otherwise
167
+ * @returns The review with entity names if found, null otherwise
151
168
  */
152
169
  async getReview(reviewId: string): Promise<Review | null> {
170
+ console.log('🔍 ReviewService.getReview - Getting review:', reviewId);
171
+
153
172
  const docRef = doc(this.db, REVIEWS_COLLECTION, reviewId);
154
173
  const docSnap = await getDoc(docRef);
155
174
 
156
175
  if (!docSnap.exists()) {
176
+ console.log('❌ ReviewService.getReview - Review not found:', reviewId);
157
177
  return null;
158
178
  }
159
179
 
160
- return docSnap.data() as Review;
180
+ const review = { ...docSnap.data(), id: reviewId } as Review;
181
+
182
+ try {
183
+ // Fetch the associated appointment to enhance with entity names
184
+ const appointmentDoc = await getDoc(
185
+ doc(this.db, APPOINTMENTS_COLLECTION, review.appointmentId),
186
+ );
187
+
188
+ if (appointmentDoc.exists()) {
189
+ const appointment = appointmentDoc.data() as Appointment;
190
+
191
+ // Create enhanced review with entity names
192
+ const enhancedReview = { ...review };
193
+
194
+ if (enhancedReview.clinicReview && appointment.clinicInfo) {
195
+ enhancedReview.clinicReview = {
196
+ ...enhancedReview.clinicReview,
197
+ clinicName: appointment.clinicInfo.name,
198
+ };
199
+ }
200
+
201
+ if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
202
+ enhancedReview.practitionerReview = {
203
+ ...enhancedReview.practitionerReview,
204
+ practitionerName: appointment.practitionerInfo.name,
205
+ };
206
+ }
207
+
208
+ if (enhancedReview.procedureReview && appointment.procedureInfo) {
209
+ enhancedReview.procedureReview = {
210
+ ...enhancedReview.procedureReview,
211
+ procedureName: appointment.procedureInfo.name,
212
+ };
213
+ }
214
+
215
+ // Add patient name to the main review object
216
+ if (appointment.patientInfo) {
217
+ enhancedReview.patientName = appointment.patientInfo.fullName;
218
+ }
219
+
220
+ console.log('✅ ReviewService.getReview - Enhanced review:', {
221
+ reviewId,
222
+ hasEntityNames: !!(
223
+ enhancedReview.clinicReview?.clinicName ||
224
+ enhancedReview.practitionerReview?.practitionerName ||
225
+ enhancedReview.procedureReview?.procedureName ||
226
+ enhancedReview.patientName
227
+ ),
228
+ });
229
+
230
+ return enhancedReview;
231
+ }
232
+
233
+ console.log('⚠️ ReviewService.getReview - Appointment not found for review:', reviewId);
234
+ return review;
235
+ } catch (error) {
236
+ console.warn(`Failed to enhance review ${reviewId} with entity names:`, error);
237
+ return review;
238
+ }
161
239
  }
162
240
 
163
241
  /**
@@ -206,6 +284,11 @@ export class ReviewService extends BaseService {
206
284
  };
207
285
  }
208
286
 
287
+ // Add patient name to the main review object
288
+ if (appointment.patientInfo) {
289
+ enhancedReview.patientName = appointment.patientInfo.fullName;
290
+ }
291
+
209
292
  return enhancedReview;
210
293
  }
211
294
 
@@ -221,45 +304,281 @@ export class ReviewService extends BaseService {
221
304
  }
222
305
 
223
306
  /**
224
- * Gets all reviews for a specific clinic
307
+ * Gets all reviews for a specific clinic with enhanced entity names
225
308
  * @param clinicId The ID of the clinic
226
- * @returns Array of reviews containing clinic reviews
309
+ * @returns Array of reviews containing clinic reviews with clinic, practitioner, and procedure names
227
310
  */
228
311
  async getReviewsByClinic(clinicId: string): Promise<Review[]> {
312
+ console.log('🔍 ReviewService.getReviewsByClinic - Querying for clinic:', clinicId);
313
+
229
314
  const q = query(
230
315
  collection(this.db, REVIEWS_COLLECTION),
231
316
  where('clinicReview.clinicId', '==', clinicId),
232
317
  );
233
318
  const snapshot = await getDocs(q);
234
- return snapshot.docs.map(doc => doc.data() as Review);
319
+ const reviews = snapshot.docs.map(doc => {
320
+ const data = doc.data() as Review;
321
+ return { ...data, id: doc.id };
322
+ });
323
+
324
+ console.log('🔍 ReviewService.getReviewsByClinic - Found reviews before enhancement:', {
325
+ clinicId,
326
+ reviewCount: reviews.length,
327
+ reviewIds: reviews.map(r => r.id),
328
+ });
329
+
330
+ // Enhance reviews with entity names from appointments
331
+ const enhancedReviews = await Promise.all(
332
+ reviews.map(async review => {
333
+ try {
334
+ // Fetch the associated appointment
335
+ const appointmentDoc = await getDoc(
336
+ doc(this.db, APPOINTMENTS_COLLECTION, review.appointmentId),
337
+ );
338
+
339
+ if (appointmentDoc.exists()) {
340
+ const appointment = appointmentDoc.data() as Appointment;
341
+
342
+ // Create enhanced review with entity names
343
+ const enhancedReview = { ...review };
344
+
345
+ if (enhancedReview.clinicReview && appointment.clinicInfo) {
346
+ enhancedReview.clinicReview = {
347
+ ...enhancedReview.clinicReview,
348
+ clinicName: appointment.clinicInfo.name,
349
+ };
350
+ }
351
+
352
+ if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
353
+ enhancedReview.practitionerReview = {
354
+ ...enhancedReview.practitionerReview,
355
+ practitionerName: appointment.practitionerInfo.name,
356
+ };
357
+ }
358
+
359
+ if (enhancedReview.procedureReview && appointment.procedureInfo) {
360
+ enhancedReview.procedureReview = {
361
+ ...enhancedReview.procedureReview,
362
+ procedureName: appointment.procedureInfo.name,
363
+ };
364
+ }
365
+
366
+ // Add patient name to the main review object
367
+ if (appointment.patientInfo) {
368
+ enhancedReview.patientName = appointment.patientInfo.fullName;
369
+ }
370
+
371
+ return enhancedReview;
372
+ }
373
+
374
+ return review;
375
+ } catch (error) {
376
+ console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
377
+ return review;
378
+ }
379
+ }),
380
+ );
381
+
382
+ console.log('✅ ReviewService.getReviewsByClinic - Enhanced reviews:', {
383
+ clinicId,
384
+ reviewCount: enhancedReviews.length,
385
+ reviewIds: enhancedReviews.map(r => r.id),
386
+ hasEntityNames: enhancedReviews.some(
387
+ r =>
388
+ r.clinicReview?.clinicName ||
389
+ r.practitionerReview?.practitionerName ||
390
+ r.procedureReview?.procedureName ||
391
+ r.patientName,
392
+ ),
393
+ });
394
+
395
+ return enhancedReviews;
235
396
  }
236
397
 
237
398
  /**
238
- * Gets all reviews for a specific practitioner
399
+ * Gets all reviews for a specific practitioner with enhanced entity names
239
400
  * @param practitionerId The ID of the practitioner
240
- * @returns Array of reviews containing practitioner reviews
401
+ * @returns Array of reviews containing practitioner reviews with clinic, practitioner, and procedure names
241
402
  */
242
403
  async getReviewsByPractitioner(practitionerId: string): Promise<Review[]> {
404
+ console.log(
405
+ '🔍 ReviewService.getReviewsByPractitioner - Querying for practitioner:',
406
+ practitionerId,
407
+ );
408
+
243
409
  const q = query(
244
410
  collection(this.db, REVIEWS_COLLECTION),
245
411
  where('practitionerReview.practitionerId', '==', practitionerId),
246
412
  );
247
413
  const snapshot = await getDocs(q);
248
- return snapshot.docs.map(doc => doc.data() as Review);
414
+ const reviews = snapshot.docs.map(doc => {
415
+ const data = doc.data() as Review;
416
+ return { ...data, id: doc.id };
417
+ });
418
+
419
+ console.log('🔍 ReviewService.getReviewsByPractitioner - Found reviews before enhancement:', {
420
+ practitionerId,
421
+ reviewCount: reviews.length,
422
+ reviewIds: reviews.map(r => r.id),
423
+ });
424
+
425
+ // Enhance reviews with entity names from appointments
426
+ const enhancedReviews = await Promise.all(
427
+ reviews.map(async review => {
428
+ try {
429
+ // Fetch the associated appointment
430
+ const appointmentDoc = await getDoc(
431
+ doc(this.db, APPOINTMENTS_COLLECTION, review.appointmentId),
432
+ );
433
+
434
+ if (appointmentDoc.exists()) {
435
+ const appointment = appointmentDoc.data() as Appointment;
436
+
437
+ // Create enhanced review with entity names
438
+ const enhancedReview = { ...review };
439
+
440
+ if (enhancedReview.clinicReview && appointment.clinicInfo) {
441
+ enhancedReview.clinicReview = {
442
+ ...enhancedReview.clinicReview,
443
+ clinicName: appointment.clinicInfo.name,
444
+ };
445
+ }
446
+
447
+ if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
448
+ enhancedReview.practitionerReview = {
449
+ ...enhancedReview.practitionerReview,
450
+ practitionerName: appointment.practitionerInfo.name,
451
+ };
452
+ }
453
+
454
+ if (enhancedReview.procedureReview && appointment.procedureInfo) {
455
+ enhancedReview.procedureReview = {
456
+ ...enhancedReview.procedureReview,
457
+ procedureName: appointment.procedureInfo.name,
458
+ };
459
+ }
460
+
461
+ // Add patient name to the main review object
462
+ if (appointment.patientInfo) {
463
+ enhancedReview.patientName = appointment.patientInfo.fullName;
464
+ }
465
+
466
+ return enhancedReview;
467
+ }
468
+
469
+ return review;
470
+ } catch (error) {
471
+ console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
472
+ return review;
473
+ }
474
+ }),
475
+ );
476
+
477
+ console.log('✅ ReviewService.getReviewsByPractitioner - Enhanced reviews:', {
478
+ practitionerId,
479
+ reviewCount: enhancedReviews.length,
480
+ reviewIds: enhancedReviews.map(r => r.id),
481
+ hasEntityNames: enhancedReviews.some(
482
+ r =>
483
+ r.clinicReview?.clinicName ||
484
+ r.practitionerReview?.practitionerName ||
485
+ r.procedureReview?.procedureName,
486
+ ),
487
+ });
488
+
489
+ return enhancedReviews;
249
490
  }
250
491
 
251
492
  /**
252
- * Gets all reviews for a specific procedure
493
+ * Gets all reviews for a specific procedure with enhanced entity names
253
494
  * @param procedureId The ID of the procedure
254
- * @returns Array of reviews containing procedure reviews
495
+ * @returns Array of reviews containing procedure reviews with clinic, practitioner, and procedure names
255
496
  */
256
497
  async getReviewsByProcedure(procedureId: string): Promise<Review[]> {
498
+ console.log('🔍 ReviewService.getReviewsByProcedure - Querying for procedure:', procedureId);
499
+
257
500
  const q = query(
258
501
  collection(this.db, REVIEWS_COLLECTION),
259
502
  where('procedureReview.procedureId', '==', procedureId),
260
503
  );
261
504
  const snapshot = await getDocs(q);
262
- return snapshot.docs.map(doc => doc.data() as Review);
505
+ const reviews = snapshot.docs.map(doc => {
506
+ const data = doc.data() as Review;
507
+ return { ...data, id: doc.id };
508
+ });
509
+
510
+ console.log('🔍 ReviewService.getReviewsByProcedure - Found reviews before enhancement:', {
511
+ procedureId,
512
+ reviewCount: reviews.length,
513
+ reviewIds: reviews.map(r => r.id),
514
+ });
515
+
516
+ // Enhance reviews with entity names from appointments
517
+ const enhancedReviews = await Promise.all(
518
+ reviews.map(async review => {
519
+ try {
520
+ // Fetch the associated appointment
521
+ const appointmentDoc = await getDoc(
522
+ doc(this.db, APPOINTMENTS_COLLECTION, review.appointmentId),
523
+ );
524
+
525
+ if (appointmentDoc.exists()) {
526
+ const appointment = appointmentDoc.data() as Appointment;
527
+
528
+ // Create enhanced review with entity names
529
+ const enhancedReview = { ...review };
530
+
531
+ if (enhancedReview.clinicReview && appointment.clinicInfo) {
532
+ enhancedReview.clinicReview = {
533
+ ...enhancedReview.clinicReview,
534
+ clinicName: appointment.clinicInfo.name,
535
+ };
536
+ }
537
+
538
+ if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
539
+ enhancedReview.practitionerReview = {
540
+ ...enhancedReview.practitionerReview,
541
+ practitionerName: appointment.practitionerInfo.name,
542
+ };
543
+ }
544
+
545
+ if (enhancedReview.procedureReview && appointment.procedureInfo) {
546
+ enhancedReview.procedureReview = {
547
+ ...enhancedReview.procedureReview,
548
+ procedureName: appointment.procedureInfo.name,
549
+ };
550
+ }
551
+
552
+ // Add patient name to the main review object
553
+ if (appointment.patientInfo) {
554
+ enhancedReview.patientName = appointment.patientInfo.fullName;
555
+ }
556
+
557
+ return enhancedReview;
558
+ }
559
+
560
+ return review;
561
+ } catch (error) {
562
+ console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
563
+ return review;
564
+ }
565
+ }),
566
+ );
567
+
568
+ console.log('✅ ReviewService.getReviewsByProcedure - Enhanced reviews:', {
569
+ procedureId,
570
+ reviewCount: enhancedReviews.length,
571
+ reviewIds: enhancedReviews.map(r => r.id),
572
+ hasEntityNames: enhancedReviews.some(
573
+ r =>
574
+ r.clinicReview?.clinicName ||
575
+ r.practitionerReview?.practitionerName ||
576
+ r.procedureReview?.procedureName ||
577
+ r.patientName,
578
+ ),
579
+ });
580
+
581
+ return enhancedReviews;
263
582
  }
264
583
 
265
584
  /**
@@ -120,6 +120,17 @@ export interface BeforeAfterPerZone {
120
120
  beforeNote?: string | null;
121
121
  }
122
122
 
123
+ /**
124
+ * Interface for zone photo upload data
125
+ */
126
+ export interface ZonePhotoUploadData {
127
+ appointmentId: string;
128
+ zoneId: string;
129
+ photoType: 'before' | 'after';
130
+ file: File | Blob;
131
+ notes?: string;
132
+ }
133
+
123
134
  /**
124
135
  * Interface for billing information per zone
125
136
  */
@@ -117,6 +117,7 @@ export interface Review {
117
117
  id: string;
118
118
  appointmentId: string;
119
119
  patientId: string;
120
+ patientName?: string; // Enhanced field: patient name from appointment
120
121
  createdAt: Date;
121
122
  updatedAt: Date;
122
123
  clinicReview?: ClinicReview;
@@ -126,4 +127,4 @@ export interface Review {
126
127
  overallRating: number; // Average of all available ratings
127
128
  }
128
129
 
129
- export const REVIEWS_COLLECTION = "reviews";
130
+ export const REVIEWS_COLLECTION = 'reviews';
@@ -25,7 +25,7 @@ export const appointmentMediaItemSchema = z.object({
25
25
  uploadedAt: z
26
26
  .any()
27
27
  .refine(
28
- (val) =>
28
+ val =>
29
29
  val instanceof Date ||
30
30
  val?._seconds !== undefined ||
31
31
  val?.seconds !== undefined ||
@@ -69,7 +69,7 @@ export const linkedFormInfoSchema = z.object({
69
69
  submittedAt: z
70
70
  .any()
71
71
  .refine(
72
- (val) =>
72
+ val =>
73
73
  val === undefined ||
74
74
  val instanceof Date ||
75
75
  val?._seconds !== undefined ||
@@ -83,7 +83,7 @@ export const linkedFormInfoSchema = z.object({
83
83
  completedAt: z
84
84
  .any()
85
85
  .refine(
86
- (val) =>
86
+ val =>
87
87
  val === undefined ||
88
88
  val instanceof Date ||
89
89
  val?._seconds !== undefined ||
@@ -103,7 +103,7 @@ export const patientReviewInfoSchema = z.object({
103
103
  reviewedAt: z
104
104
  .any()
105
105
  .refine(
106
- (val) =>
106
+ val =>
107
107
  val instanceof Date ||
108
108
  val?._seconds !== undefined ||
109
109
  val?.seconds !== undefined ||
@@ -119,7 +119,7 @@ export const finalizedDetailsSchema = z.object({
119
119
  at: z
120
120
  .any()
121
121
  .refine(
122
- (val) =>
122
+ val =>
123
123
  val instanceof Date ||
124
124
  val?._seconds !== undefined ||
125
125
  val?.seconds !== undefined ||
@@ -194,7 +194,7 @@ export const createAppointmentSchema = z
194
194
  appointmentStartTime: z
195
195
  .any()
196
196
  .refine(
197
- (val) =>
197
+ val =>
198
198
  val instanceof Date ||
199
199
  val?._seconds !== undefined ||
200
200
  val?.seconds !== undefined ||
@@ -206,7 +206,7 @@ export const createAppointmentSchema = z
206
206
  appointmentEndTime: z
207
207
  .any()
208
208
  .refine(
209
- (val) =>
209
+ val =>
210
210
  val instanceof Date ||
211
211
  val?._seconds !== undefined ||
212
212
  val?.seconds !== undefined ||
@@ -222,7 +222,7 @@ export const createAppointmentSchema = z
222
222
  initialPaymentStatus: paymentStatusSchema.optional().default(PaymentStatus.UNPAID),
223
223
  clinic_tz: z.string().min(1, 'Timezone is required'),
224
224
  })
225
- .refine((data) => data.appointmentEndTime > data.appointmentStartTime, {
225
+ .refine(data => data.appointmentEndTime > data.appointmentStartTime, {
226
226
  message: 'Appointment end time must be after start time',
227
227
  path: ['appointmentEndTime'],
228
228
  });
@@ -263,7 +263,7 @@ export const updateAppointmentSchema = z
263
263
  appointmentStartTime: z
264
264
  .any()
265
265
  .refine(
266
- (val) =>
266
+ val =>
267
267
  val === undefined ||
268
268
  val instanceof Date ||
269
269
  val?._seconds !== undefined ||
@@ -277,7 +277,7 @@ export const updateAppointmentSchema = z
277
277
  appointmentEndTime: z
278
278
  .any()
279
279
  .refine(
280
- (val) =>
280
+ val =>
281
281
  val === undefined ||
282
282
  val instanceof Date ||
283
283
  val?._seconds !== undefined ||
@@ -302,7 +302,7 @@ export const updateAppointmentSchema = z
302
302
  metadata: appointmentMetadataSchema.optional(),
303
303
  })
304
304
  .refine(
305
- (data) => {
305
+ data => {
306
306
  if (
307
307
  data.status === AppointmentStatus.CANCELED_CLINIC ||
308
308
  data.status === AppointmentStatus.CANCELED_PATIENT ||
@@ -319,7 +319,7 @@ export const updateAppointmentSchema = z
319
319
  },
320
320
  )
321
321
  .refine(
322
- (data) => {
322
+ data => {
323
323
  if (data.appointmentStartTime && data.appointmentEndTime) {
324
324
  return data.appointmentEndTime > data.appointmentStartTime;
325
325
  }
@@ -342,7 +342,7 @@ export const searchAppointmentsSchema = z
342
342
  startDate: z
343
343
  .any()
344
344
  .refine(
345
- (val) =>
345
+ val =>
346
346
  val === undefined ||
347
347
  val instanceof Date ||
348
348
  val?._seconds !== undefined ||
@@ -356,7 +356,7 @@ export const searchAppointmentsSchema = z
356
356
  endDate: z
357
357
  .any()
358
358
  .refine(
359
- (val) =>
359
+ val =>
360
360
  val === undefined ||
361
361
  val instanceof Date ||
362
362
  val?._seconds !== undefined ||
@@ -374,7 +374,7 @@ export const searchAppointmentsSchema = z
374
374
  startAfter: z.any().optional(),
375
375
  })
376
376
  .refine(
377
- (data) => {
377
+ data => {
378
378
  if (!data.startDate && !data.endDate && !data.status) {
379
379
  return !!(data.patientId || data.practitionerId || data.clinicBranchId);
380
380
  }
@@ -387,7 +387,7 @@ export const searchAppointmentsSchema = z
387
387
  },
388
388
  )
389
389
  .refine(
390
- (data) => {
390
+ data => {
391
391
  if (data.startDate && data.endDate) {
392
392
  return data.endDate >= data.startDate;
393
393
  }
@@ -407,7 +407,7 @@ export const rescheduleAppointmentSchema = z.object({
407
407
  newStartTime: z
408
408
  .any()
409
409
  .refine(
410
- (val) =>
410
+ val =>
411
411
  val instanceof Date ||
412
412
  val?._seconds !== undefined ||
413
413
  val?.seconds !== undefined ||
@@ -419,7 +419,7 @@ export const rescheduleAppointmentSchema = z.object({
419
419
  newEndTime: z
420
420
  .any()
421
421
  .refine(
422
- (val) =>
422
+ val =>
423
423
  val instanceof Date ||
424
424
  val?._seconds !== undefined ||
425
425
  val?.seconds !== undefined ||
@@ -429,3 +429,23 @@ export const rescheduleAppointmentSchema = z.object({
429
429
  'New end time must be a valid timestamp, Date object, number, or string',
430
430
  ),
431
431
  });
432
+
433
+ /**
434
+ * Schema for validating zone photo upload data
435
+ */
436
+ export const zonePhotoUploadSchema = z.object({
437
+ appointmentId: z.string().min(MIN_STRING_LENGTH, 'Appointment ID is required'),
438
+ zoneId: z.string().min(MIN_STRING_LENGTH, 'Zone ID is required'),
439
+ photoType: z.enum(['before', 'after'], {
440
+ required_error: 'Photo type must be either "before" or "after"',
441
+ }),
442
+ file: z.any().refine(file => {
443
+ // Check if it's a File or Blob object
444
+ return (
445
+ file instanceof File ||
446
+ file instanceof Blob ||
447
+ (file && typeof file.size === 'number' && typeof file.type === 'string')
448
+ );
449
+ }, 'File must be a valid File or Blob object'),
450
+ notes: z.string().max(MAX_STRING_LENGTH_LONG, 'Notes too long').optional(),
451
+ });