@blackcode_sa/metaestetics-api 1.12.35 → 1.12.37
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 -1
- package/dist/admin/index.d.ts +13 -1
- package/dist/admin/index.js +45 -18
- package/dist/admin/index.mjs +45 -18
- package/dist/index.d.mts +106 -6
- package/dist/index.d.ts +106 -6
- package/dist/index.js +2016 -1620
- package/dist/index.mjs +1212 -816
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +56 -25
- package/src/admin/booking/booking.admin.ts +1 -0
- package/src/services/appointment/appointment.service.ts +309 -53
- package/src/services/appointment/utils/recommended-procedure.utils.ts +193 -0
- package/src/services/appointment/utils/zone-management.utils.ts +27 -23
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -0
- package/src/services/procedure/procedure.service.ts +42 -2
- package/src/types/appointment/index.ts +16 -2
- package/src/validations/appointment.schema.ts +24 -4
package/package.json
CHANGED
|
@@ -1549,15 +1549,25 @@ export class AppointmentAggregationService {
|
|
|
1549
1549
|
return true;
|
|
1550
1550
|
}
|
|
1551
1551
|
|
|
1552
|
-
// Compare before and after photos
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
beforeZonePhotos.after !== afterZonePhotos.after ||
|
|
1556
|
-
beforeZonePhotos.beforeNote !== afterZonePhotos.beforeNote ||
|
|
1557
|
-
beforeZonePhotos.afterNote !== afterZonePhotos.afterNote
|
|
1558
|
-
) {
|
|
1552
|
+
// Compare before and after photos arrays
|
|
1553
|
+
// If array lengths differ or any entry differs, consider it changed
|
|
1554
|
+
if (beforeZonePhotos.length !== afterZonePhotos.length) {
|
|
1559
1555
|
return true;
|
|
1560
1556
|
}
|
|
1557
|
+
|
|
1558
|
+
// Compare each entry in the arrays
|
|
1559
|
+
for (let i = 0; i < beforeZonePhotos.length; i++) {
|
|
1560
|
+
const beforeEntry = beforeZonePhotos[i];
|
|
1561
|
+
const afterEntry = afterZonePhotos[i];
|
|
1562
|
+
if (
|
|
1563
|
+
beforeEntry.before !== afterEntry.before ||
|
|
1564
|
+
beforeEntry.after !== afterEntry.after ||
|
|
1565
|
+
beforeEntry.beforeNote !== afterEntry.beforeNote ||
|
|
1566
|
+
beforeEntry.afterNote !== afterEntry.afterNote
|
|
1567
|
+
) {
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1561
1571
|
}
|
|
1562
1572
|
|
|
1563
1573
|
return false;
|
|
@@ -1580,27 +1590,48 @@ export class AppointmentAggregationService {
|
|
|
1580
1590
|
const newPhotoTypes: { zoneId: string; photoType: 'before' | 'after' }[] = [];
|
|
1581
1591
|
|
|
1582
1592
|
for (const zoneId of Object.keys(afterPhotos)) {
|
|
1583
|
-
const beforeZonePhotos = beforePhotos[zoneId];
|
|
1584
|
-
const afterZonePhotos = afterPhotos[zoneId];
|
|
1593
|
+
const beforeZonePhotos = beforePhotos[zoneId] || [];
|
|
1594
|
+
const afterZonePhotos = afterPhotos[zoneId] || [];
|
|
1585
1595
|
|
|
1586
|
-
if (
|
|
1596
|
+
if (beforeZonePhotos.length === 0 && afterZonePhotos.length > 0) {
|
|
1587
1597
|
// New zone with photos
|
|
1588
1598
|
updatedZones.push(zoneId);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1599
|
+
afterZonePhotos.forEach(entry => {
|
|
1600
|
+
if (entry.before) {
|
|
1601
|
+
newPhotoTypes.push({ zoneId, photoType: 'before' });
|
|
1602
|
+
}
|
|
1603
|
+
if (entry.after) {
|
|
1604
|
+
newPhotoTypes.push({ zoneId, photoType: 'after' });
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
} else if (afterZonePhotos.length > beforeZonePhotos.length) {
|
|
1608
|
+
// New photos added to existing zone
|
|
1609
|
+
updatedZones.push(zoneId);
|
|
1610
|
+
const newEntries = afterZonePhotos.slice(beforeZonePhotos.length);
|
|
1611
|
+
newEntries.forEach(entry => {
|
|
1612
|
+
if (entry.before) {
|
|
1613
|
+
newPhotoTypes.push({ zoneId, photoType: 'before' });
|
|
1614
|
+
}
|
|
1615
|
+
if (entry.after) {
|
|
1616
|
+
newPhotoTypes.push({ zoneId, photoType: 'after' });
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1595
1619
|
} else {
|
|
1596
|
-
// Check for
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1620
|
+
// Check for updated photos in existing entries
|
|
1621
|
+
for (let i = 0; i < afterZonePhotos.length; i++) {
|
|
1622
|
+
const beforeEntry = beforeZonePhotos[i];
|
|
1623
|
+
const afterEntry = afterZonePhotos[i];
|
|
1624
|
+
|
|
1625
|
+
if (beforeEntry && afterEntry) {
|
|
1626
|
+
if (beforeEntry.before !== afterEntry.before && afterEntry.before) {
|
|
1627
|
+
updatedZones.push(zoneId);
|
|
1628
|
+
newPhotoTypes.push({ zoneId, photoType: 'before' });
|
|
1629
|
+
}
|
|
1630
|
+
if (beforeEntry.after !== afterEntry.after && afterEntry.after) {
|
|
1631
|
+
updatedZones.push(zoneId);
|
|
1632
|
+
newPhotoTypes.push({ zoneId, photoType: 'after' });
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1604
1635
|
}
|
|
1605
1636
|
}
|
|
1606
1637
|
}
|
|
@@ -1629,7 +1660,7 @@ export class AppointmentAggregationService {
|
|
|
1629
1660
|
if (selectedZones.length > 0) {
|
|
1630
1661
|
const completedZones = selectedZones.filter(zoneId => {
|
|
1631
1662
|
const zonePhotos = afterPhotos[zoneId];
|
|
1632
|
-
return zonePhotos &&
|
|
1663
|
+
return zonePhotos && zonePhotos.length > 0 && zonePhotos.some(entry => entry.before || entry.after);
|
|
1633
1664
|
});
|
|
1634
1665
|
|
|
1635
1666
|
const completionPercentage = (completedZones.length / selectedZones.length) * 100;
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
ZoneItemData,
|
|
34
34
|
ExtendedProcedureInfo,
|
|
35
35
|
AppointmentProductMetadata,
|
|
36
|
+
RecommendedProcedure,
|
|
36
37
|
APPOINTMENTS_COLLECTION,
|
|
37
38
|
} from '../../types/appointment';
|
|
38
39
|
import {
|
|
@@ -48,7 +49,7 @@ import { PatientService } from '../patient/patient.service';
|
|
|
48
49
|
import { PractitionerService } from '../practitioner/practitioner.service';
|
|
49
50
|
import { ClinicService } from '../clinic/clinic.service';
|
|
50
51
|
import { FilledDocumentService } from '../documentation-templates/filled-document.service';
|
|
51
|
-
import { MediaService, MediaAccessLevel, MediaMetadata } from '../media/media.service';
|
|
52
|
+
import { MediaService, MediaAccessLevel, MediaMetadata, MediaResource } from '../media/media.service';
|
|
52
53
|
|
|
53
54
|
// Import utility functions
|
|
54
55
|
import {
|
|
@@ -70,6 +71,18 @@ import {
|
|
|
70
71
|
getExtendedProceduresUtil,
|
|
71
72
|
getAppointmentProductsUtil,
|
|
72
73
|
} from './utils/extended-procedure.utils';
|
|
74
|
+
import {
|
|
75
|
+
addRecommendedProcedureUtil,
|
|
76
|
+
removeRecommendedProcedureUtil,
|
|
77
|
+
updateRecommendedProcedureUtil,
|
|
78
|
+
getRecommendedProceduresUtil,
|
|
79
|
+
} from './utils/recommended-procedure.utils';
|
|
80
|
+
import {
|
|
81
|
+
updateZonePhotoEntryUtil,
|
|
82
|
+
addAfterPhotoToEntryUtil,
|
|
83
|
+
updateZonePhotoNotesUtil,
|
|
84
|
+
getZonePhotoEntryUtil,
|
|
85
|
+
} from './utils/zone-photo.utils';
|
|
73
86
|
|
|
74
87
|
/**
|
|
75
88
|
* Interface for available booking slot
|
|
@@ -1274,35 +1287,34 @@ export class AppointmentService extends BaseService {
|
|
|
1274
1287
|
zonesData: null,
|
|
1275
1288
|
appointmentProducts: [],
|
|
1276
1289
|
extendedProcedures: [],
|
|
1290
|
+
recommendedProcedures: [],
|
|
1277
1291
|
zoneBilling: null,
|
|
1278
1292
|
finalbilling: null,
|
|
1279
1293
|
finalizationNotes: null,
|
|
1280
1294
|
};
|
|
1281
1295
|
|
|
1282
|
-
// Initialize zonePhotos if it doesn't exist
|
|
1283
|
-
const currentZonePhotos
|
|
1296
|
+
// Initialize zonePhotos if it doesn't exist (array model per zone)
|
|
1297
|
+
const currentZonePhotos: Record<string, BeforeAfterPerZone[]> =
|
|
1298
|
+
(currentMetadata.zonePhotos as Record<string, BeforeAfterPerZone[]>) || {};
|
|
1284
1299
|
|
|
1285
|
-
// Initialize the zone
|
|
1300
|
+
// Initialize the zone array if it doesn't exist
|
|
1286
1301
|
if (!currentZonePhotos[zoneId]) {
|
|
1287
|
-
currentZonePhotos[zoneId] =
|
|
1288
|
-
before: null,
|
|
1289
|
-
after: null,
|
|
1290
|
-
beforeNote: null,
|
|
1291
|
-
afterNote: null,
|
|
1292
|
-
};
|
|
1302
|
+
currentZonePhotos[zoneId] = [];
|
|
1293
1303
|
}
|
|
1294
1304
|
|
|
1295
|
-
//
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1305
|
+
// Create a new entry for this uploaded photo with per-photo notes
|
|
1306
|
+
const newEntry: BeforeAfterPerZone = {
|
|
1307
|
+
before: photoType === 'before' ? mediaMetadata.url : null,
|
|
1308
|
+
after: photoType === 'after' ? mediaMetadata.url : null,
|
|
1309
|
+
beforeNote: photoType === 'before' ? (notes || null) : null,
|
|
1310
|
+
afterNote: photoType === 'after' ? (notes || null) : null,
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// Append to the zone's photo list
|
|
1314
|
+
currentZonePhotos[zoneId] = [...currentZonePhotos[zoneId], newEntry];
|
|
1315
|
+
// Enforce max 10 photos per zone by keeping the most recent 10
|
|
1316
|
+
if (currentZonePhotos[zoneId].length > 10) {
|
|
1317
|
+
currentZonePhotos[zoneId] = currentZonePhotos[zoneId].slice(-10);
|
|
1306
1318
|
}
|
|
1307
1319
|
|
|
1308
1320
|
// Update the appointment with new metadata
|
|
@@ -1313,7 +1325,11 @@ export class AppointmentService extends BaseService {
|
|
|
1313
1325
|
zonesData: currentMetadata.zonesData || null,
|
|
1314
1326
|
appointmentProducts: currentMetadata.appointmentProducts || [],
|
|
1315
1327
|
extendedProcedures: currentMetadata.extendedProcedures || [],
|
|
1316
|
-
|
|
1328
|
+
recommendedProcedures: currentMetadata.recommendedProcedures || [],
|
|
1329
|
+
// Only include zoneBilling if it exists (avoid undefined values in Firestore)
|
|
1330
|
+
...(currentMetadata.zoneBilling !== undefined && {
|
|
1331
|
+
zoneBilling: currentMetadata.zoneBilling,
|
|
1332
|
+
}),
|
|
1317
1333
|
finalbilling: currentMetadata.finalbilling,
|
|
1318
1334
|
finalizationNotes: currentMetadata.finalizationNotes,
|
|
1319
1335
|
},
|
|
@@ -1346,7 +1362,7 @@ export class AppointmentService extends BaseService {
|
|
|
1346
1362
|
async getZonePhotos(
|
|
1347
1363
|
appointmentId: string,
|
|
1348
1364
|
zoneId?: string,
|
|
1349
|
-
): Promise<Record<string, BeforeAfterPerZone> | BeforeAfterPerZone | null> {
|
|
1365
|
+
): Promise<Record<string, BeforeAfterPerZone[]> | BeforeAfterPerZone[] | null> {
|
|
1350
1366
|
try {
|
|
1351
1367
|
console.log(`[APPOINTMENT_SERVICE] Getting zone photos for appointment ${appointmentId}`);
|
|
1352
1368
|
|
|
@@ -1355,7 +1371,10 @@ export class AppointmentService extends BaseService {
|
|
|
1355
1371
|
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
1356
1372
|
}
|
|
1357
1373
|
|
|
1358
|
-
const zonePhotos = appointment.metadata?.zonePhotos
|
|
1374
|
+
const zonePhotos = appointment.metadata?.zonePhotos as
|
|
1375
|
+
| Record<string, BeforeAfterPerZone[]>
|
|
1376
|
+
| undefined
|
|
1377
|
+
| null;
|
|
1359
1378
|
if (!zonePhotos) {
|
|
1360
1379
|
return null;
|
|
1361
1380
|
}
|
|
@@ -1374,21 +1393,21 @@ export class AppointmentService extends BaseService {
|
|
|
1374
1393
|
}
|
|
1375
1394
|
|
|
1376
1395
|
/**
|
|
1377
|
-
* Deletes a zone photo and updates appointment metadata
|
|
1396
|
+
* Deletes a zone photo entry (by index) and updates appointment metadata
|
|
1378
1397
|
*
|
|
1379
1398
|
* @param appointmentId ID of the appointment
|
|
1380
1399
|
* @param zoneId ID of the zone
|
|
1381
|
-
* @param
|
|
1400
|
+
* @param photoIndex Index of the photo entry to delete in the zone array
|
|
1382
1401
|
* @returns The updated appointment
|
|
1383
1402
|
*/
|
|
1384
1403
|
async deleteZonePhoto(
|
|
1385
1404
|
appointmentId: string,
|
|
1386
1405
|
zoneId: string,
|
|
1387
|
-
|
|
1406
|
+
photoIndex: number,
|
|
1388
1407
|
): Promise<Appointment> {
|
|
1389
1408
|
try {
|
|
1390
1409
|
console.log(
|
|
1391
|
-
`[APPOINTMENT_SERVICE] Deleting ${
|
|
1410
|
+
`[APPOINTMENT_SERVICE] Deleting zone photo index ${photoIndex} for zone ${zoneId} in appointment ${appointmentId}`,
|
|
1392
1411
|
);
|
|
1393
1412
|
|
|
1394
1413
|
// Get current appointment
|
|
@@ -1397,15 +1416,23 @@ export class AppointmentService extends BaseService {
|
|
|
1397
1416
|
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
1398
1417
|
}
|
|
1399
1418
|
|
|
1400
|
-
const zonePhotos = appointment.metadata?.zonePhotos
|
|
1401
|
-
|
|
1419
|
+
const zonePhotos = appointment.metadata?.zonePhotos as
|
|
1420
|
+
| Record<string, BeforeAfterPerZone[]>
|
|
1421
|
+
| undefined
|
|
1422
|
+
| null;
|
|
1423
|
+
if (!zonePhotos || !zonePhotos[zoneId] || !Array.isArray(zonePhotos[zoneId])) {
|
|
1402
1424
|
throw new Error(`No photos found for zone ${zoneId} in appointment ${appointmentId}`);
|
|
1403
1425
|
}
|
|
1404
1426
|
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1427
|
+
const zoneArray = [...zonePhotos[zoneId]];
|
|
1428
|
+
if (photoIndex < 0 || photoIndex >= zoneArray.length) {
|
|
1429
|
+
throw new Error(`Invalid photo index ${photoIndex} for zone ${zoneId}`);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const entry = zoneArray[photoIndex];
|
|
1433
|
+
const photoUrl = (entry.before || entry.after) as MediaResource | null;
|
|
1407
1434
|
if (!photoUrl) {
|
|
1408
|
-
throw new Error(`No
|
|
1435
|
+
throw new Error(`No photo URL found for index ${photoIndex} in zone ${zoneId}`);
|
|
1409
1436
|
}
|
|
1410
1437
|
|
|
1411
1438
|
// Try to find and delete the media from storage
|
|
@@ -1426,19 +1453,14 @@ export class AppointmentService extends BaseService {
|
|
|
1426
1453
|
// Continue with metadata update even if media deletion fails
|
|
1427
1454
|
}
|
|
1428
1455
|
|
|
1429
|
-
// Update appointment metadata to remove the photo
|
|
1430
|
-
const updatedZonePhotos = { ...zonePhotos };
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
} else {
|
|
1435
|
-
updatedZonePhotos[zoneId].after = null;
|
|
1436
|
-
updatedZonePhotos[zoneId].afterNote = null;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// If both photos are null, we could optionally remove the zone entry entirely
|
|
1440
|
-
if (!updatedZonePhotos[zoneId].before && !updatedZonePhotos[zoneId].after) {
|
|
1456
|
+
// Update appointment metadata to remove the photo entry at the specified index
|
|
1457
|
+
const updatedZonePhotos: Record<string, BeforeAfterPerZone[]> = { ...zonePhotos } as any;
|
|
1458
|
+
const updatedZoneArray = [...zoneArray];
|
|
1459
|
+
updatedZoneArray.splice(photoIndex, 1);
|
|
1460
|
+
if (updatedZoneArray.length === 0) {
|
|
1441
1461
|
delete updatedZonePhotos[zoneId];
|
|
1462
|
+
} else {
|
|
1463
|
+
updatedZonePhotos[zoneId] = updatedZoneArray;
|
|
1442
1464
|
}
|
|
1443
1465
|
|
|
1444
1466
|
const updateData: UpdateAppointmentData = {
|
|
@@ -1448,7 +1470,11 @@ export class AppointmentService extends BaseService {
|
|
|
1448
1470
|
zonesData: appointment.metadata?.zonesData || null,
|
|
1449
1471
|
appointmentProducts: appointment.metadata?.appointmentProducts || [],
|
|
1450
1472
|
extendedProcedures: appointment.metadata?.extendedProcedures || [],
|
|
1451
|
-
|
|
1473
|
+
recommendedProcedures: appointment.metadata?.recommendedProcedures || [],
|
|
1474
|
+
// Only include zoneBilling if it exists (avoid undefined values in Firestore)
|
|
1475
|
+
...(appointment.metadata?.zoneBilling !== undefined && {
|
|
1476
|
+
zoneBilling: appointment.metadata.zoneBilling,
|
|
1477
|
+
}),
|
|
1452
1478
|
finalbilling: appointment.metadata?.finalbilling || null,
|
|
1453
1479
|
finalizationNotes: appointment.metadata?.finalizationNotes || null,
|
|
1454
1480
|
},
|
|
@@ -1458,7 +1484,7 @@ export class AppointmentService extends BaseService {
|
|
|
1458
1484
|
const updatedAppointment = await this.updateAppointment(appointmentId, updateData);
|
|
1459
1485
|
|
|
1460
1486
|
console.log(
|
|
1461
|
-
`[APPOINTMENT_SERVICE] Successfully deleted ${
|
|
1487
|
+
`[APPOINTMENT_SERVICE] Successfully deleted photo index ${photoIndex} for zone ${zoneId}`,
|
|
1462
1488
|
);
|
|
1463
1489
|
|
|
1464
1490
|
return updatedAppointment;
|
|
@@ -1479,7 +1505,7 @@ export class AppointmentService extends BaseService {
|
|
|
1479
1505
|
async addItemToZone(
|
|
1480
1506
|
appointmentId: string,
|
|
1481
1507
|
zoneId: string,
|
|
1482
|
-
item: Omit<ZoneItemData, 'subtotal' | 'parentZone'
|
|
1508
|
+
item: Omit<ZoneItemData, 'subtotal' | 'parentZone'>,
|
|
1483
1509
|
): Promise<Appointment> {
|
|
1484
1510
|
try {
|
|
1485
1511
|
console.log(
|
|
@@ -1561,7 +1587,13 @@ export class AppointmentService extends BaseService {
|
|
|
1561
1587
|
console.log(
|
|
1562
1588
|
`[APPOINTMENT_SERVICE] Overriding price for item ${itemIndex} in zone ${zoneId} to ${newPrice}`,
|
|
1563
1589
|
);
|
|
1564
|
-
return await overridePriceForZoneItemUtil(
|
|
1590
|
+
return await overridePriceForZoneItemUtil(
|
|
1591
|
+
this.db,
|
|
1592
|
+
appointmentId,
|
|
1593
|
+
zoneId,
|
|
1594
|
+
itemIndex,
|
|
1595
|
+
newPrice,
|
|
1596
|
+
);
|
|
1565
1597
|
} catch (error) {
|
|
1566
1598
|
console.error(`[APPOINTMENT_SERVICE] Error overriding price:`, error);
|
|
1567
1599
|
throw error;
|
|
@@ -1642,7 +1674,9 @@ export class AppointmentService extends BaseService {
|
|
|
1642
1674
|
*/
|
|
1643
1675
|
async getExtendedProcedures(appointmentId: string): Promise<ExtendedProcedureInfo[]> {
|
|
1644
1676
|
try {
|
|
1645
|
-
console.log(
|
|
1677
|
+
console.log(
|
|
1678
|
+
`[APPOINTMENT_SERVICE] Getting extended procedures for appointment ${appointmentId}`,
|
|
1679
|
+
);
|
|
1646
1680
|
return await getExtendedProceduresUtil(this.db, appointmentId);
|
|
1647
1681
|
} catch (error) {
|
|
1648
1682
|
console.error(`[APPOINTMENT_SERVICE] Error getting extended procedures:`, error);
|
|
@@ -1659,7 +1693,9 @@ export class AppointmentService extends BaseService {
|
|
|
1659
1693
|
*/
|
|
1660
1694
|
async getAppointmentProducts(appointmentId: string): Promise<AppointmentProductMetadata[]> {
|
|
1661
1695
|
try {
|
|
1662
|
-
console.log(
|
|
1696
|
+
console.log(
|
|
1697
|
+
`[APPOINTMENT_SERVICE] Getting appointment products for appointment ${appointmentId}`,
|
|
1698
|
+
);
|
|
1663
1699
|
return await getAppointmentProductsUtil(this.db, appointmentId);
|
|
1664
1700
|
} catch (error) {
|
|
1665
1701
|
console.error(`[APPOINTMENT_SERVICE] Error getting appointment products:`, error);
|
|
@@ -1676,8 +1712,10 @@ export class AppointmentService extends BaseService {
|
|
|
1676
1712
|
*/
|
|
1677
1713
|
async recalculateFinalBilling(appointmentId: string, taxRate?: number): Promise<Appointment> {
|
|
1678
1714
|
try {
|
|
1679
|
-
console.log(
|
|
1680
|
-
|
|
1715
|
+
console.log(
|
|
1716
|
+
`[APPOINTMENT_SERVICE] Recalculating final billing for appointment ${appointmentId}`,
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1681
1719
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
1682
1720
|
if (!appointment) {
|
|
1683
1721
|
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
@@ -1696,6 +1734,7 @@ export class AppointmentService extends BaseService {
|
|
|
1696
1734
|
zonesData: null,
|
|
1697
1735
|
appointmentProducts: [],
|
|
1698
1736
|
extendedProcedures: [],
|
|
1737
|
+
recommendedProcedures: [],
|
|
1699
1738
|
finalbilling: null,
|
|
1700
1739
|
finalizationNotes: null,
|
|
1701
1740
|
};
|
|
@@ -1707,6 +1746,11 @@ export class AppointmentService extends BaseService {
|
|
|
1707
1746
|
zonesData: currentMetadata.zonesData,
|
|
1708
1747
|
appointmentProducts: currentMetadata.appointmentProducts || [],
|
|
1709
1748
|
extendedProcedures: currentMetadata.extendedProcedures || [],
|
|
1749
|
+
recommendedProcedures: currentMetadata.recommendedProcedures || [],
|
|
1750
|
+
// Only include zoneBilling if it exists (avoid undefined values in Firestore)
|
|
1751
|
+
...(currentMetadata.zoneBilling !== undefined && {
|
|
1752
|
+
zoneBilling: currentMetadata.zoneBilling,
|
|
1753
|
+
}),
|
|
1710
1754
|
finalbilling,
|
|
1711
1755
|
finalizationNotes: currentMetadata.finalizationNotes,
|
|
1712
1756
|
},
|
|
@@ -1719,4 +1763,216 @@ export class AppointmentService extends BaseService {
|
|
|
1719
1763
|
throw error;
|
|
1720
1764
|
}
|
|
1721
1765
|
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Adds a recommended procedure to an appointment
|
|
1769
|
+
* Multiple recommendations of the same procedure are allowed (e.g., touch-up in 2 weeks, full treatment in 3 months)
|
|
1770
|
+
*
|
|
1771
|
+
* @param appointmentId ID of the appointment
|
|
1772
|
+
* @param procedureId ID of the procedure to recommend
|
|
1773
|
+
* @param note Note explaining the recommendation
|
|
1774
|
+
* @param timeframe Suggested timeframe for the procedure
|
|
1775
|
+
* @returns The updated appointment
|
|
1776
|
+
*/
|
|
1777
|
+
async addRecommendedProcedure(
|
|
1778
|
+
appointmentId: string,
|
|
1779
|
+
procedureId: string,
|
|
1780
|
+
note: string,
|
|
1781
|
+
timeframe: { value: number; unit: 'day' | 'week' | 'month' | 'year' }
|
|
1782
|
+
): Promise<Appointment> {
|
|
1783
|
+
try {
|
|
1784
|
+
console.log(
|
|
1785
|
+
`[APPOINTMENT_SERVICE] Adding recommended procedure ${procedureId} to appointment ${appointmentId}`,
|
|
1786
|
+
);
|
|
1787
|
+
return await addRecommendedProcedureUtil(this.db, appointmentId, procedureId, note, timeframe);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
console.error(`[APPOINTMENT_SERVICE] Error adding recommended procedure:`, error);
|
|
1790
|
+
throw error;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Removes a recommended procedure from an appointment by index
|
|
1796
|
+
*
|
|
1797
|
+
* @param appointmentId ID of the appointment
|
|
1798
|
+
* @param recommendationIndex Index of the recommendation to remove
|
|
1799
|
+
* @returns The updated appointment
|
|
1800
|
+
*/
|
|
1801
|
+
async removeRecommendedProcedure(appointmentId: string, recommendationIndex: number): Promise<Appointment> {
|
|
1802
|
+
try {
|
|
1803
|
+
console.log(
|
|
1804
|
+
`[APPOINTMENT_SERVICE] Removing recommended procedure at index ${recommendationIndex} from appointment ${appointmentId}`,
|
|
1805
|
+
);
|
|
1806
|
+
return await removeRecommendedProcedureUtil(this.db, appointmentId, recommendationIndex);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
console.error(`[APPOINTMENT_SERVICE] Error removing recommended procedure:`, error);
|
|
1809
|
+
throw error;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Updates a recommended procedure in an appointment by index
|
|
1815
|
+
*
|
|
1816
|
+
* @param appointmentId ID of the appointment
|
|
1817
|
+
* @param recommendationIndex Index of the recommendation to update
|
|
1818
|
+
* @param updates Partial updates (note and/or timeframe)
|
|
1819
|
+
* @returns The updated appointment
|
|
1820
|
+
*/
|
|
1821
|
+
async updateRecommendedProcedure(
|
|
1822
|
+
appointmentId: string,
|
|
1823
|
+
recommendationIndex: number,
|
|
1824
|
+
updates: {
|
|
1825
|
+
note?: string;
|
|
1826
|
+
timeframe?: { value: number; unit: 'day' | 'week' | 'month' | 'year' };
|
|
1827
|
+
}
|
|
1828
|
+
): Promise<Appointment> {
|
|
1829
|
+
try {
|
|
1830
|
+
console.log(
|
|
1831
|
+
`[APPOINTMENT_SERVICE] Updating recommended procedure at index ${recommendationIndex} in appointment ${appointmentId}`,
|
|
1832
|
+
);
|
|
1833
|
+
return await updateRecommendedProcedureUtil(this.db, appointmentId, recommendationIndex, updates);
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
console.error(`[APPOINTMENT_SERVICE] Error updating recommended procedure:`, error);
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Gets all recommended procedures for an appointment
|
|
1842
|
+
*
|
|
1843
|
+
* @param appointmentId ID of the appointment
|
|
1844
|
+
* @returns Array of recommended procedures
|
|
1845
|
+
*/
|
|
1846
|
+
async getRecommendedProcedures(appointmentId: string): Promise<RecommendedProcedure[]> {
|
|
1847
|
+
try {
|
|
1848
|
+
console.log(
|
|
1849
|
+
`[APPOINTMENT_SERVICE] Getting recommended procedures for appointment ${appointmentId}`,
|
|
1850
|
+
);
|
|
1851
|
+
return await getRecommendedProceduresUtil(this.db, appointmentId);
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
console.error(`[APPOINTMENT_SERVICE] Error getting recommended procedures:`, error);
|
|
1854
|
+
throw error;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Updates a specific photo entry in a zone by index
|
|
1860
|
+
* Can update before/after photos and their notes
|
|
1861
|
+
*
|
|
1862
|
+
* @param appointmentId ID of the appointment
|
|
1863
|
+
* @param zoneId Zone ID
|
|
1864
|
+
* @param photoIndex Index of the photo entry to update
|
|
1865
|
+
* @param updates Partial updates to apply (before, after, beforeNote, afterNote)
|
|
1866
|
+
* @returns The updated appointment
|
|
1867
|
+
*/
|
|
1868
|
+
async updateZonePhotoEntry(
|
|
1869
|
+
appointmentId: string,
|
|
1870
|
+
zoneId: string,
|
|
1871
|
+
photoIndex: number,
|
|
1872
|
+
updates: Partial<BeforeAfterPerZone>
|
|
1873
|
+
): Promise<Appointment> {
|
|
1874
|
+
try {
|
|
1875
|
+
console.log(
|
|
1876
|
+
`[APPOINTMENT_SERVICE] Updating photo entry at index ${photoIndex} for zone ${zoneId} in appointment ${appointmentId}`,
|
|
1877
|
+
);
|
|
1878
|
+
return await updateZonePhotoEntryUtil(this.db, appointmentId, zoneId, photoIndex, updates);
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
console.error(`[APPOINTMENT_SERVICE] Error updating zone photo entry:`, error);
|
|
1881
|
+
throw error;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Adds an after photo to an existing before photo entry
|
|
1887
|
+
*
|
|
1888
|
+
* @param appointmentId ID of the appointment
|
|
1889
|
+
* @param zoneId Zone ID
|
|
1890
|
+
* @param photoIndex Index of the entry to add after photo to
|
|
1891
|
+
* @param afterPhotoUrl URL of the after photo
|
|
1892
|
+
* @param afterNote Optional note for the after photo
|
|
1893
|
+
* @returns The updated appointment
|
|
1894
|
+
*/
|
|
1895
|
+
async addAfterPhotoToEntry(
|
|
1896
|
+
appointmentId: string,
|
|
1897
|
+
zoneId: string,
|
|
1898
|
+
photoIndex: number,
|
|
1899
|
+
afterPhotoUrl: MediaResource,
|
|
1900
|
+
afterNote?: string
|
|
1901
|
+
): Promise<Appointment> {
|
|
1902
|
+
try {
|
|
1903
|
+
console.log(
|
|
1904
|
+
`[APPOINTMENT_SERVICE] Adding after photo to entry at index ${photoIndex} for zone ${zoneId}`,
|
|
1905
|
+
);
|
|
1906
|
+
return await addAfterPhotoToEntryUtil(
|
|
1907
|
+
this.db,
|
|
1908
|
+
appointmentId,
|
|
1909
|
+
zoneId,
|
|
1910
|
+
photoIndex,
|
|
1911
|
+
afterPhotoUrl,
|
|
1912
|
+
afterNote
|
|
1913
|
+
);
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
console.error(`[APPOINTMENT_SERVICE] Error adding after photo to entry:`, error);
|
|
1916
|
+
throw error;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Updates notes for a photo entry
|
|
1922
|
+
*
|
|
1923
|
+
* @param appointmentId ID of the appointment
|
|
1924
|
+
* @param zoneId Zone ID
|
|
1925
|
+
* @param photoIndex Index of the entry
|
|
1926
|
+
* @param beforeNote Optional note for before photo
|
|
1927
|
+
* @param afterNote Optional note for after photo
|
|
1928
|
+
* @returns The updated appointment
|
|
1929
|
+
*/
|
|
1930
|
+
async updateZonePhotoNotes(
|
|
1931
|
+
appointmentId: string,
|
|
1932
|
+
zoneId: string,
|
|
1933
|
+
photoIndex: number,
|
|
1934
|
+
beforeNote?: string,
|
|
1935
|
+
afterNote?: string
|
|
1936
|
+
): Promise<Appointment> {
|
|
1937
|
+
try {
|
|
1938
|
+
console.log(
|
|
1939
|
+
`[APPOINTMENT_SERVICE] Updating notes for photo entry at index ${photoIndex} for zone ${zoneId}`,
|
|
1940
|
+
);
|
|
1941
|
+
return await updateZonePhotoNotesUtil(
|
|
1942
|
+
this.db,
|
|
1943
|
+
appointmentId,
|
|
1944
|
+
zoneId,
|
|
1945
|
+
photoIndex,
|
|
1946
|
+
beforeNote,
|
|
1947
|
+
afterNote
|
|
1948
|
+
);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
console.error(`[APPOINTMENT_SERVICE] Error updating zone photo notes:`, error);
|
|
1951
|
+
throw error;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Gets a specific photo entry from a zone
|
|
1957
|
+
*
|
|
1958
|
+
* @param appointmentId ID of the appointment
|
|
1959
|
+
* @param zoneId Zone ID
|
|
1960
|
+
* @param photoIndex Index of the entry
|
|
1961
|
+
* @returns Photo entry
|
|
1962
|
+
*/
|
|
1963
|
+
async getZonePhotoEntry(
|
|
1964
|
+
appointmentId: string,
|
|
1965
|
+
zoneId: string,
|
|
1966
|
+
photoIndex: number
|
|
1967
|
+
): Promise<BeforeAfterPerZone> {
|
|
1968
|
+
try {
|
|
1969
|
+
console.log(
|
|
1970
|
+
`[APPOINTMENT_SERVICE] Getting photo entry at index ${photoIndex} for zone ${zoneId}`,
|
|
1971
|
+
);
|
|
1972
|
+
return await getZonePhotoEntryUtil(this.db, appointmentId, zoneId, photoIndex);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
console.error(`[APPOINTMENT_SERVICE] Error getting zone photo entry:`, error);
|
|
1975
|
+
throw error;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1722
1978
|
}
|