@blackcode_sa/metaestetics-api 1.12.20 → 1.12.21
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 +13 -3
- package/dist/admin/index.d.ts +13 -3
- package/dist/admin/index.js +194 -180
- package/dist/admin/index.mjs +194 -180
- package/dist/index.d.mts +51 -7
- package/dist/index.d.ts +51 -7
- package/dist/index.js +2087 -1888
- package/dist/index.mjs +2158 -1959
- 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/reviews.service.ts +30 -71
- package/src/types/appointment/index.ts +11 -0
- package/src/types/reviews/index.ts +0 -3
- 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
|
}
|
|
@@ -8,21 +8,23 @@ import {
|
|
|
8
8
|
setDoc,
|
|
9
9
|
deleteDoc,
|
|
10
10
|
serverTimestamp,
|
|
11
|
-
} from
|
|
12
|
-
import { BaseService } from
|
|
11
|
+
} from "firebase/firestore";
|
|
12
|
+
import { BaseService } from "../base.service";
|
|
13
13
|
import {
|
|
14
14
|
Review,
|
|
15
15
|
ClinicReview,
|
|
16
16
|
PractitionerReview,
|
|
17
17
|
ProcedureReview,
|
|
18
18
|
REVIEWS_COLLECTION,
|
|
19
|
-
} from
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
19
|
+
} from "../../types/reviews";
|
|
20
|
+
import {
|
|
21
|
+
createReviewSchema,
|
|
22
|
+
reviewSchema,
|
|
23
|
+
} from "../../validations/reviews.schema";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { Auth } from "firebase/auth";
|
|
26
|
+
import { Firestore } from "firebase/firestore";
|
|
27
|
+
import { FirebaseApp } from "firebase/app";
|
|
26
28
|
|
|
27
29
|
export class ReviewService extends BaseService {
|
|
28
30
|
constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
|
|
@@ -36,8 +38,11 @@ export class ReviewService extends BaseService {
|
|
|
36
38
|
* @returns The created review
|
|
37
39
|
*/
|
|
38
40
|
async createReview(
|
|
39
|
-
data: Omit<
|
|
40
|
-
|
|
41
|
+
data: Omit<
|
|
42
|
+
Review,
|
|
43
|
+
"id" | "createdAt" | "updatedAt" | "appointmentId" | "overallRating"
|
|
44
|
+
>,
|
|
45
|
+
appointmentId: string
|
|
41
46
|
): Promise<Review> {
|
|
42
47
|
try {
|
|
43
48
|
// Validate input data
|
|
@@ -161,63 +166,17 @@ export class ReviewService extends BaseService {
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
/**
|
|
164
|
-
* Gets all reviews for a specific patient
|
|
169
|
+
* Gets all reviews for a specific patient
|
|
165
170
|
* @param patientId The ID of the patient
|
|
166
|
-
* @returns Array of reviews for the patient
|
|
171
|
+
* @returns Array of reviews for the patient
|
|
167
172
|
*/
|
|
168
173
|
async getReviewsByPatient(patientId: string): Promise<Review[]> {
|
|
169
|
-
const q = query(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// Enhance reviews with entity names from appointments
|
|
174
|
-
const enhancedReviews = await Promise.all(
|
|
175
|
-
reviews.map(async review => {
|
|
176
|
-
try {
|
|
177
|
-
// Fetch the associated appointment
|
|
178
|
-
const appointmentDoc = await getDoc(
|
|
179
|
-
doc(this.db, APPOINTMENTS_COLLECTION, review.appointmentId),
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
if (appointmentDoc.exists()) {
|
|
183
|
-
const appointment = appointmentDoc.data() as Appointment;
|
|
184
|
-
|
|
185
|
-
// Create enhanced review with entity names
|
|
186
|
-
const enhancedReview = { ...review };
|
|
187
|
-
|
|
188
|
-
if (enhancedReview.clinicReview && appointment.clinicInfo) {
|
|
189
|
-
enhancedReview.clinicReview = {
|
|
190
|
-
...enhancedReview.clinicReview,
|
|
191
|
-
clinicName: appointment.clinicInfo.name,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
|
|
196
|
-
enhancedReview.practitionerReview = {
|
|
197
|
-
...enhancedReview.practitionerReview,
|
|
198
|
-
practitionerName: appointment.practitionerInfo.name,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (enhancedReview.procedureReview && appointment.procedureInfo) {
|
|
203
|
-
enhancedReview.procedureReview = {
|
|
204
|
-
...enhancedReview.procedureReview,
|
|
205
|
-
procedureName: appointment.procedureInfo.name,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return enhancedReview;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return review;
|
|
213
|
-
} catch (error) {
|
|
214
|
-
console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
|
|
215
|
-
return review;
|
|
216
|
-
}
|
|
217
|
-
}),
|
|
174
|
+
const q = query(
|
|
175
|
+
collection(this.db, REVIEWS_COLLECTION),
|
|
176
|
+
where("patientId", "==", patientId)
|
|
218
177
|
);
|
|
219
|
-
|
|
220
|
-
return
|
|
178
|
+
const snapshot = await getDocs(q);
|
|
179
|
+
return snapshot.docs.map((doc) => doc.data() as Review);
|
|
221
180
|
}
|
|
222
181
|
|
|
223
182
|
/**
|
|
@@ -228,10 +187,10 @@ export class ReviewService extends BaseService {
|
|
|
228
187
|
async getReviewsByClinic(clinicId: string): Promise<Review[]> {
|
|
229
188
|
const q = query(
|
|
230
189
|
collection(this.db, REVIEWS_COLLECTION),
|
|
231
|
-
where(
|
|
190
|
+
where("clinicReview.clinicId", "==", clinicId)
|
|
232
191
|
);
|
|
233
192
|
const snapshot = await getDocs(q);
|
|
234
|
-
return snapshot.docs.map(doc => doc.data() as Review);
|
|
193
|
+
return snapshot.docs.map((doc) => doc.data() as Review);
|
|
235
194
|
}
|
|
236
195
|
|
|
237
196
|
/**
|
|
@@ -242,10 +201,10 @@ export class ReviewService extends BaseService {
|
|
|
242
201
|
async getReviewsByPractitioner(practitionerId: string): Promise<Review[]> {
|
|
243
202
|
const q = query(
|
|
244
203
|
collection(this.db, REVIEWS_COLLECTION),
|
|
245
|
-
where(
|
|
204
|
+
where("practitionerReview.practitionerId", "==", practitionerId)
|
|
246
205
|
);
|
|
247
206
|
const snapshot = await getDocs(q);
|
|
248
|
-
return snapshot.docs.map(doc => doc.data() as Review);
|
|
207
|
+
return snapshot.docs.map((doc) => doc.data() as Review);
|
|
249
208
|
}
|
|
250
209
|
|
|
251
210
|
/**
|
|
@@ -256,10 +215,10 @@ export class ReviewService extends BaseService {
|
|
|
256
215
|
async getReviewsByProcedure(procedureId: string): Promise<Review[]> {
|
|
257
216
|
const q = query(
|
|
258
217
|
collection(this.db, REVIEWS_COLLECTION),
|
|
259
|
-
where(
|
|
218
|
+
where("procedureReview.procedureId", "==", procedureId)
|
|
260
219
|
);
|
|
261
220
|
const snapshot = await getDocs(q);
|
|
262
|
-
return snapshot.docs.map(doc => doc.data() as Review);
|
|
221
|
+
return snapshot.docs.map((doc) => doc.data() as Review);
|
|
263
222
|
}
|
|
264
223
|
|
|
265
224
|
/**
|
|
@@ -270,7 +229,7 @@ export class ReviewService extends BaseService {
|
|
|
270
229
|
async getReviewByAppointment(appointmentId: string): Promise<Review | null> {
|
|
271
230
|
const q = query(
|
|
272
231
|
collection(this.db, REVIEWS_COLLECTION),
|
|
273
|
-
where(
|
|
232
|
+
where("appointmentId", "==", appointmentId)
|
|
274
233
|
);
|
|
275
234
|
const snapshot = await getDocs(q);
|
|
276
235
|
|
|
@@ -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
|
*/
|
|
@@ -22,7 +22,6 @@ interface BaseReview {
|
|
|
22
22
|
*/
|
|
23
23
|
export interface ClinicReview extends BaseReview {
|
|
24
24
|
clinicId: string;
|
|
25
|
-
clinicName?: string; // Enhanced field: clinic name from appointment
|
|
26
25
|
cleanliness: number; // 1-5 stars
|
|
27
26
|
facilities: number; // 1-5 stars
|
|
28
27
|
staffFriendliness: number; // 1-5 stars
|
|
@@ -38,7 +37,6 @@ export interface ClinicReview extends BaseReview {
|
|
|
38
37
|
*/
|
|
39
38
|
export interface PractitionerReview extends BaseReview {
|
|
40
39
|
practitionerId: string;
|
|
41
|
-
practitionerName?: string; // Enhanced field: practitioner name from appointment
|
|
42
40
|
knowledgeAndExpertise: number; // 1-5 stars
|
|
43
41
|
communicationSkills: number; // 1-5 stars
|
|
44
42
|
bedSideManner: number; // 1-5 stars
|
|
@@ -54,7 +52,6 @@ export interface PractitionerReview extends BaseReview {
|
|
|
54
52
|
*/
|
|
55
53
|
export interface ProcedureReview extends BaseReview {
|
|
56
54
|
procedureId: string;
|
|
57
|
-
procedureName?: string; // Enhanced field: procedure name from appointment
|
|
58
55
|
effectivenessOfTreatment: number; // 1-5 stars
|
|
59
56
|
outcomeExplanation: number; // 1-5 stars
|
|
60
57
|
painManagement: number; // 1-5 stars
|