@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.
- package/dist/admin/index.d.mts +14 -0
- package/dist/admin/index.d.ts +14 -0
- package/dist/admin/index.js +194 -180
- package/dist/admin/index.mjs +194 -180
- package/dist/index.d.mts +57 -9
- package/dist/index.d.ts +57 -9
- package/dist/index.js +2340 -1862
- package/dist/index.mjs +2412 -1934
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +459 -457
- package/src/services/appointment/appointment.service.ts +297 -0
- package/src/services/reviews/README.md +129 -0
- package/src/services/reviews/reviews.service.ts +331 -12
- package/src/types/appointment/index.ts +11 -0
- package/src/types/reviews/index.ts +2 -1
- package/src/validations/appointment.schema.ts +38 -18
|
@@ -28,12 +28,15 @@ import {
|
|
|
28
28
|
AppointmentMediaItem,
|
|
29
29
|
PatientReviewInfo,
|
|
30
30
|
type CreateAppointmentHttpData,
|
|
31
|
+
type ZonePhotoUploadData,
|
|
32
|
+
BeforeAfterPerZone,
|
|
31
33
|
APPOINTMENTS_COLLECTION,
|
|
32
34
|
} from '../../types/appointment';
|
|
33
35
|
import {
|
|
34
36
|
updateAppointmentSchema,
|
|
35
37
|
searchAppointmentsSchema,
|
|
36
38
|
rescheduleAppointmentSchema,
|
|
39
|
+
zonePhotoUploadSchema,
|
|
37
40
|
} from '../../validations/appointment.schema';
|
|
38
41
|
|
|
39
42
|
// Import other services needed (dependency injection pattern)
|
|
@@ -42,6 +45,7 @@ import { PatientService } from '../patient/patient.service';
|
|
|
42
45
|
import { PractitionerService } from '../practitioner/practitioner.service';
|
|
43
46
|
import { ClinicService } from '../clinic/clinic.service';
|
|
44
47
|
import { FilledDocumentService } from '../documentation-templates/filled-document.service';
|
|
48
|
+
import { MediaService, MediaAccessLevel, MediaMetadata } from '../media/media.service';
|
|
45
49
|
|
|
46
50
|
// Import utility functions
|
|
47
51
|
import {
|
|
@@ -68,6 +72,7 @@ export class AppointmentService extends BaseService {
|
|
|
68
72
|
private practitionerService: PractitionerService;
|
|
69
73
|
private clinicService: ClinicService;
|
|
70
74
|
private filledDocumentService: FilledDocumentService;
|
|
75
|
+
private mediaService: MediaService;
|
|
71
76
|
private functions: Functions;
|
|
72
77
|
|
|
73
78
|
/**
|
|
@@ -80,6 +85,7 @@ export class AppointmentService extends BaseService {
|
|
|
80
85
|
* @param patientService Patient service instance
|
|
81
86
|
* @param practitionerService Practitioner service instance
|
|
82
87
|
* @param clinicService Clinic service instance
|
|
88
|
+
* @param filledDocumentService Filled document service instance
|
|
83
89
|
*/
|
|
84
90
|
constructor(
|
|
85
91
|
db: Firestore,
|
|
@@ -97,6 +103,7 @@ export class AppointmentService extends BaseService {
|
|
|
97
103
|
this.practitionerService = practitionerService;
|
|
98
104
|
this.clinicService = clinicService;
|
|
99
105
|
this.filledDocumentService = filledDocumentService;
|
|
106
|
+
this.mediaService = new MediaService(db, auth, app);
|
|
100
107
|
this.functions = getFunctions(app, 'europe-west6'); // Initialize Firebase Functions with the correct region
|
|
101
108
|
}
|
|
102
109
|
|
|
@@ -1144,4 +1151,294 @@ export class AppointmentService extends BaseService {
|
|
|
1144
1151
|
throw error;
|
|
1145
1152
|
}
|
|
1146
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Uploads a zone photo and updates appointment metadata
|
|
1157
|
+
*
|
|
1158
|
+
* @param uploadData Zone photo upload data containing appointment ID, zone ID, photo type, file, and optional notes
|
|
1159
|
+
* @returns The uploaded media metadata
|
|
1160
|
+
*/
|
|
1161
|
+
async uploadZonePhoto(uploadData: ZonePhotoUploadData): Promise<MediaMetadata> {
|
|
1162
|
+
try {
|
|
1163
|
+
console.log(
|
|
1164
|
+
`[APPOINTMENT_SERVICE] Uploading ${uploadData.photoType} photo for zone ${uploadData.zoneId} in appointment ${uploadData.appointmentId}`,
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
// Validate input data
|
|
1168
|
+
const validatedData = await zonePhotoUploadSchema.parseAsync(uploadData);
|
|
1169
|
+
|
|
1170
|
+
// Check if user is authenticated
|
|
1171
|
+
const currentUser = this.auth.currentUser;
|
|
1172
|
+
if (!currentUser) {
|
|
1173
|
+
throw new Error('User must be authenticated to upload zone photos');
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Get the appointment to verify it exists and user has access
|
|
1177
|
+
const appointment = await this.getAppointmentById(validatedData.appointmentId);
|
|
1178
|
+
if (!appointment) {
|
|
1179
|
+
throw new Error(`Appointment with ID ${validatedData.appointmentId} not found`);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Generate collection name for the media
|
|
1183
|
+
const collectionName = `appointment_${validatedData.appointmentId}_zone_photos`;
|
|
1184
|
+
|
|
1185
|
+
// Generate filename with zone and photo type info
|
|
1186
|
+
const timestamp = Date.now();
|
|
1187
|
+
const fileExtension = validatedData.file.type?.split('/')[1] || 'jpg';
|
|
1188
|
+
const fileName = `${validatedData.photoType}_${validatedData.zoneId}_${timestamp}.${fileExtension}`;
|
|
1189
|
+
|
|
1190
|
+
console.log(
|
|
1191
|
+
`[APPOINTMENT_SERVICE] Uploading file: ${fileName} to collection: ${collectionName}`,
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
// Upload the media file using MediaService
|
|
1195
|
+
const uploadedMedia = await this.mediaService.uploadMedia(
|
|
1196
|
+
validatedData.file,
|
|
1197
|
+
validatedData.appointmentId, // ownerId is the appointment ID
|
|
1198
|
+
MediaAccessLevel.PRIVATE, // Zone photos are private
|
|
1199
|
+
collectionName,
|
|
1200
|
+
fileName,
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
console.log(`[APPOINTMENT_SERVICE] Media uploaded successfully with ID: ${uploadedMedia.id}`);
|
|
1204
|
+
|
|
1205
|
+
// Update appointment metadata with the new photo
|
|
1206
|
+
await this.updateAppointmentZonePhoto(
|
|
1207
|
+
validatedData.appointmentId,
|
|
1208
|
+
validatedData.zoneId,
|
|
1209
|
+
validatedData.photoType,
|
|
1210
|
+
uploadedMedia,
|
|
1211
|
+
validatedData.notes,
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
console.log(
|
|
1215
|
+
`[APPOINTMENT_SERVICE] Successfully uploaded and linked ${validatedData.photoType} photo for zone ${validatedData.zoneId}`,
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
return uploadedMedia;
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
console.error('[APPOINTMENT_SERVICE] Error uploading zone photo:', error);
|
|
1221
|
+
throw error;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Updates appointment metadata with zone photo information
|
|
1227
|
+
*
|
|
1228
|
+
* @param appointmentId ID of the appointment
|
|
1229
|
+
* @param zoneId ID of the zone
|
|
1230
|
+
* @param photoType Type of photo ('before' or 'after')
|
|
1231
|
+
* @param mediaMetadata Uploaded media metadata
|
|
1232
|
+
* @param notes Optional notes for the photo
|
|
1233
|
+
* @returns The updated appointment
|
|
1234
|
+
*/
|
|
1235
|
+
private async updateAppointmentZonePhoto(
|
|
1236
|
+
appointmentId: string,
|
|
1237
|
+
zoneId: string,
|
|
1238
|
+
photoType: 'before' | 'after',
|
|
1239
|
+
mediaMetadata: MediaMetadata,
|
|
1240
|
+
notes?: string,
|
|
1241
|
+
): Promise<Appointment> {
|
|
1242
|
+
try {
|
|
1243
|
+
console.log(
|
|
1244
|
+
`[APPOINTMENT_SERVICE] Updating appointment metadata for ${photoType} photo in zone ${zoneId}`,
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
// Get current appointment
|
|
1248
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
1249
|
+
if (!appointment) {
|
|
1250
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Initialize metadata if it doesn't exist
|
|
1254
|
+
const currentMetadata = appointment.metadata || {
|
|
1255
|
+
selectedZones: null,
|
|
1256
|
+
zonePhotos: null,
|
|
1257
|
+
zoneBilling: null,
|
|
1258
|
+
finalbilling: null,
|
|
1259
|
+
finalizationNotes: null,
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// Initialize zonePhotos if it doesn't exist
|
|
1263
|
+
const currentZonePhotos = currentMetadata.zonePhotos || {};
|
|
1264
|
+
|
|
1265
|
+
// Initialize the zone entry if it doesn't exist
|
|
1266
|
+
if (!currentZonePhotos[zoneId]) {
|
|
1267
|
+
currentZonePhotos[zoneId] = {
|
|
1268
|
+
before: null,
|
|
1269
|
+
after: null,
|
|
1270
|
+
beforeNote: null,
|
|
1271
|
+
afterNote: null,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Update the specific photo type
|
|
1276
|
+
if (photoType === 'before') {
|
|
1277
|
+
currentZonePhotos[zoneId].before = mediaMetadata.url;
|
|
1278
|
+
if (notes) {
|
|
1279
|
+
currentZonePhotos[zoneId].beforeNote = notes;
|
|
1280
|
+
}
|
|
1281
|
+
} else {
|
|
1282
|
+
currentZonePhotos[zoneId].after = mediaMetadata.url;
|
|
1283
|
+
if (notes) {
|
|
1284
|
+
currentZonePhotos[zoneId].afterNote = notes;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Update the appointment with new metadata
|
|
1289
|
+
const updateData: UpdateAppointmentData = {
|
|
1290
|
+
metadata: {
|
|
1291
|
+
selectedZones: currentMetadata.selectedZones,
|
|
1292
|
+
zonePhotos: currentZonePhotos,
|
|
1293
|
+
zoneBilling: currentMetadata.zoneBilling,
|
|
1294
|
+
finalbilling: currentMetadata.finalbilling,
|
|
1295
|
+
finalizationNotes: currentMetadata.finalizationNotes,
|
|
1296
|
+
},
|
|
1297
|
+
updatedAt: serverTimestamp(),
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
const updatedAppointment = await this.updateAppointment(appointmentId, updateData);
|
|
1301
|
+
|
|
1302
|
+
console.log(
|
|
1303
|
+
`[APPOINTMENT_SERVICE] Successfully updated appointment metadata for ${photoType} photo in zone ${zoneId}`,
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
return updatedAppointment;
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
console.error(
|
|
1309
|
+
`[APPOINTMENT_SERVICE] Error updating appointment metadata for zone photo:`,
|
|
1310
|
+
error,
|
|
1311
|
+
);
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Gets zone photos for a specific appointment and zone
|
|
1318
|
+
*
|
|
1319
|
+
* @param appointmentId ID of the appointment
|
|
1320
|
+
* @param zoneId ID of the zone (optional - if not provided, returns all zones)
|
|
1321
|
+
* @returns Zone photos data
|
|
1322
|
+
*/
|
|
1323
|
+
async getZonePhotos(
|
|
1324
|
+
appointmentId: string,
|
|
1325
|
+
zoneId?: string,
|
|
1326
|
+
): Promise<Record<string, BeforeAfterPerZone> | BeforeAfterPerZone | null> {
|
|
1327
|
+
try {
|
|
1328
|
+
console.log(`[APPOINTMENT_SERVICE] Getting zone photos for appointment ${appointmentId}`);
|
|
1329
|
+
|
|
1330
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
1331
|
+
if (!appointment) {
|
|
1332
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const zonePhotos = appointment.metadata?.zonePhotos;
|
|
1336
|
+
if (!zonePhotos) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// If specific zone requested, return only that zone's photos
|
|
1341
|
+
if (zoneId) {
|
|
1342
|
+
return zonePhotos[zoneId] || null;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Return all zone photos
|
|
1346
|
+
return zonePhotos;
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
console.error(`[APPOINTMENT_SERVICE] Error getting zone photos:`, error);
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Deletes a zone photo and updates appointment metadata
|
|
1355
|
+
*
|
|
1356
|
+
* @param appointmentId ID of the appointment
|
|
1357
|
+
* @param zoneId ID of the zone
|
|
1358
|
+
* @param photoType Type of photo to delete ('before' or 'after')
|
|
1359
|
+
* @returns The updated appointment
|
|
1360
|
+
*/
|
|
1361
|
+
async deleteZonePhoto(
|
|
1362
|
+
appointmentId: string,
|
|
1363
|
+
zoneId: string,
|
|
1364
|
+
photoType: 'before' | 'after',
|
|
1365
|
+
): Promise<Appointment> {
|
|
1366
|
+
try {
|
|
1367
|
+
console.log(
|
|
1368
|
+
`[APPOINTMENT_SERVICE] Deleting ${photoType} photo for zone ${zoneId} in appointment ${appointmentId}`,
|
|
1369
|
+
);
|
|
1370
|
+
|
|
1371
|
+
// Get current appointment
|
|
1372
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
1373
|
+
if (!appointment) {
|
|
1374
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const zonePhotos = appointment.metadata?.zonePhotos;
|
|
1378
|
+
if (!zonePhotos || !zonePhotos[zoneId]) {
|
|
1379
|
+
throw new Error(`No photos found for zone ${zoneId} in appointment ${appointmentId}`);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const photoUrl =
|
|
1383
|
+
photoType === 'before' ? zonePhotos[zoneId].before : zonePhotos[zoneId].after;
|
|
1384
|
+
if (!photoUrl) {
|
|
1385
|
+
throw new Error(`No ${photoType} photo found for zone ${zoneId}`);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Try to find and delete the media from storage
|
|
1389
|
+
try {
|
|
1390
|
+
// Only try to delete if photoUrl is a string (URL)
|
|
1391
|
+
if (typeof photoUrl === 'string') {
|
|
1392
|
+
const mediaMetadata = await this.mediaService.getMediaMetadataByUrl(photoUrl);
|
|
1393
|
+
if (mediaMetadata) {
|
|
1394
|
+
await this.mediaService.deleteMedia(mediaMetadata.id);
|
|
1395
|
+
console.log(`[APPOINTMENT_SERVICE] Deleted media file with ID: ${mediaMetadata.id}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
} catch (mediaError) {
|
|
1399
|
+
console.warn(
|
|
1400
|
+
`[APPOINTMENT_SERVICE] Could not delete media file for URL ${photoUrl}:`,
|
|
1401
|
+
mediaError,
|
|
1402
|
+
);
|
|
1403
|
+
// Continue with metadata update even if media deletion fails
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Update appointment metadata to remove the photo reference
|
|
1407
|
+
const updatedZonePhotos = { ...zonePhotos };
|
|
1408
|
+
if (photoType === 'before') {
|
|
1409
|
+
updatedZonePhotos[zoneId].before = null;
|
|
1410
|
+
updatedZonePhotos[zoneId].beforeNote = null;
|
|
1411
|
+
} else {
|
|
1412
|
+
updatedZonePhotos[zoneId].after = null;
|
|
1413
|
+
updatedZonePhotos[zoneId].afterNote = null;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// If both photos are null, we could optionally remove the zone entry entirely
|
|
1417
|
+
if (!updatedZonePhotos[zoneId].before && !updatedZonePhotos[zoneId].after) {
|
|
1418
|
+
delete updatedZonePhotos[zoneId];
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const updateData: UpdateAppointmentData = {
|
|
1422
|
+
metadata: {
|
|
1423
|
+
selectedZones: appointment.metadata?.selectedZones || null,
|
|
1424
|
+
zonePhotos: updatedZonePhotos,
|
|
1425
|
+
zoneBilling: appointment.metadata?.zoneBilling || null,
|
|
1426
|
+
finalbilling: appointment.metadata?.finalbilling || null,
|
|
1427
|
+
finalizationNotes: appointment.metadata?.finalizationNotes || null,
|
|
1428
|
+
},
|
|
1429
|
+
updatedAt: serverTimestamp(),
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const updatedAppointment = await this.updateAppointment(appointmentId, updateData);
|
|
1433
|
+
|
|
1434
|
+
console.log(
|
|
1435
|
+
`[APPOINTMENT_SERVICE] Successfully deleted ${photoType} photo for zone ${zoneId}`,
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
return updatedAppointment;
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
console.error(`[APPOINTMENT_SERVICE] Error deleting zone photo:`, error);
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1147
1444
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Reviews Service - Enhanced with Entity Names
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Reviews Service has been enhanced to automatically include entity names (clinic, practitioner, procedure, and patient names) in all review responses. This eliminates the need for frontend applications to make additional API calls to fetch entity details.
|
|
6
|
+
|
|
7
|
+
## Enhanced Methods
|
|
8
|
+
|
|
9
|
+
All review retrieval methods now include entity names from associated appointments:
|
|
10
|
+
|
|
11
|
+
### 1. `getReview(reviewId: string)`
|
|
12
|
+
- **Enhanced**: ✅ Returns single review with entity names
|
|
13
|
+
- **Entity Names**: Clinic name, practitioner name, procedure name, patient name
|
|
14
|
+
- **Source**: Fetched from associated appointment data
|
|
15
|
+
|
|
16
|
+
### 2. `getReviewsByPatient(patientId: string)`
|
|
17
|
+
- **Enhanced**: ✅ Returns patient reviews with entity names
|
|
18
|
+
- **Entity Names**: Clinic name, practitioner name, procedure name, patient name
|
|
19
|
+
- **Use Case**: Patient app - "My Reviews" screen
|
|
20
|
+
|
|
21
|
+
### 3. `getReviewsByPractitioner(practitionerId: string)`
|
|
22
|
+
- **Enhanced**: ✅ Returns practitioner reviews with entity names
|
|
23
|
+
- **Entity Names**: Clinic name, practitioner name, procedure name, patient name
|
|
24
|
+
- **Use Case**: Doctor app - "Reviews" screen
|
|
25
|
+
|
|
26
|
+
### 4. `getReviewsByClinic(clinicId: string)`
|
|
27
|
+
- **Enhanced**: ✅ Returns clinic reviews with entity names
|
|
28
|
+
- **Entity Names**: Clinic name, practitioner name, procedure name, patient name
|
|
29
|
+
- **Use Case**: Clinic dashboard - "Reviews" section
|
|
30
|
+
|
|
31
|
+
### 5. `getReviewsByProcedure(procedureId: string)`
|
|
32
|
+
- **Enhanced**: ✅ Returns procedure reviews with entity names
|
|
33
|
+
- **Entity Names**: Clinic name, practitioner name, procedure name, patient name
|
|
34
|
+
- **Use Case**: Procedure analytics and reporting
|
|
35
|
+
|
|
36
|
+
## Enhanced Review Structure
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface Review {
|
|
40
|
+
id: string;
|
|
41
|
+
appointmentId: string;
|
|
42
|
+
patientId: string;
|
|
43
|
+
patientName?: string; // 🆕 Enhanced field
|
|
44
|
+
createdAt: Date;
|
|
45
|
+
updatedAt: Date;
|
|
46
|
+
clinicReview?: {
|
|
47
|
+
// ... existing fields
|
|
48
|
+
clinicName?: string; // 🆕 Enhanced field
|
|
49
|
+
};
|
|
50
|
+
practitionerReview?: {
|
|
51
|
+
// ... existing fields
|
|
52
|
+
practitionerName?: string; // 🆕 Enhanced field
|
|
53
|
+
};
|
|
54
|
+
procedureReview?: {
|
|
55
|
+
// ... existing fields
|
|
56
|
+
procedureName?: string; // 🆕 Enhanced field
|
|
57
|
+
};
|
|
58
|
+
overallComment: string;
|
|
59
|
+
overallRating: number;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Data Sources
|
|
64
|
+
|
|
65
|
+
Entity names are fetched from the associated appointment's aggregated information:
|
|
66
|
+
|
|
67
|
+
- **Clinic Name**: `appointment.clinicInfo.name`
|
|
68
|
+
- **Practitioner Name**: `appointment.practitionerInfo.name`
|
|
69
|
+
- **Procedure Name**: `appointment.procedureInfo.name`
|
|
70
|
+
- **Patient Name**: `appointment.patientInfo.fullName`
|
|
71
|
+
|
|
72
|
+
## Error Handling
|
|
73
|
+
|
|
74
|
+
- If appointment is not found, review is returned without enhancement
|
|
75
|
+
- Individual enhancement failures are logged but don't break the entire operation
|
|
76
|
+
- Graceful fallback ensures API reliability
|
|
77
|
+
|
|
78
|
+
## Logging
|
|
79
|
+
|
|
80
|
+
Enhanced logging provides detailed information about:
|
|
81
|
+
- Query parameters
|
|
82
|
+
- Number of reviews found
|
|
83
|
+
- Enhancement success/failure
|
|
84
|
+
- Entity names availability
|
|
85
|
+
|
|
86
|
+
## Performance Considerations
|
|
87
|
+
|
|
88
|
+
- Each review requires one additional Firestore read for appointment data
|
|
89
|
+
- Enhancement is performed in parallel for multiple reviews
|
|
90
|
+
- Consider caching strategies for high-volume applications
|
|
91
|
+
|
|
92
|
+
## Migration Notes
|
|
93
|
+
|
|
94
|
+
- **Backward Compatible**: Existing code continues to work
|
|
95
|
+
- **Optional Fields**: All enhanced fields are optional
|
|
96
|
+
- **No Breaking Changes**: API signature remains the same
|
|
97
|
+
|
|
98
|
+
## Usage Examples
|
|
99
|
+
|
|
100
|
+
### Doctor App - Reviews Screen
|
|
101
|
+
```typescript
|
|
102
|
+
const reviews = await reviewService.getReviewsByPractitioner(practitionerId);
|
|
103
|
+
// Now includes practitioner names, clinic names, procedure names, and patient names
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Patient App - My Reviews
|
|
107
|
+
```typescript
|
|
108
|
+
const reviews = await reviewService.getReviewsByPatient(patientId);
|
|
109
|
+
// Now includes all entity names for better UX
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Clinic Dashboard
|
|
113
|
+
```typescript
|
|
114
|
+
const reviews = await reviewService.getReviewsByClinic(clinicId);
|
|
115
|
+
// Now includes practitioner and procedure names for each review
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Testing
|
|
119
|
+
|
|
120
|
+
All enhanced methods include comprehensive logging for debugging:
|
|
121
|
+
- Input parameters
|
|
122
|
+
- Query results
|
|
123
|
+
- Enhancement status
|
|
124
|
+
- Entity name availability
|
|
125
|
+
|
|
126
|
+
Monitor logs with:
|
|
127
|
+
```bash
|
|
128
|
+
firebase functions:log --only reviewService
|
|
129
|
+
```
|