@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.35",
4
+ "version": "1.12.37",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -1549,15 +1549,25 @@ export class AppointmentAggregationService {
1549
1549
  return true;
1550
1550
  }
1551
1551
 
1552
- // Compare before and after photos and notes
1553
- if (
1554
- beforeZonePhotos.before !== afterZonePhotos.before ||
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 (!beforeZonePhotos) {
1596
+ if (beforeZonePhotos.length === 0 && afterZonePhotos.length > 0) {
1587
1597
  // New zone with photos
1588
1598
  updatedZones.push(zoneId);
1589
- if (afterZonePhotos.before) {
1590
- newPhotoTypes.push({ zoneId, photoType: 'before' });
1591
- }
1592
- if (afterZonePhotos.after) {
1593
- newPhotoTypes.push({ zoneId, photoType: 'after' });
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 new or updated photos
1597
- if (beforeZonePhotos.before !== afterZonePhotos.before && afterZonePhotos.before) {
1598
- updatedZones.push(zoneId);
1599
- newPhotoTypes.push({ zoneId, photoType: 'before' });
1600
- }
1601
- if (beforeZonePhotos.after !== afterZonePhotos.after && afterZonePhotos.after) {
1602
- updatedZones.push(zoneId);
1603
- newPhotoTypes.push({ zoneId, photoType: 'after' });
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 && (zonePhotos.before || zonePhotos.after);
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;
@@ -893,6 +893,7 @@ export class BookingAdmin {
893
893
  zonesData: null,
894
894
  appointmentProducts: appointmentProducts,
895
895
  extendedProcedures: [],
896
+ recommendedProcedures: [],
896
897
  finalbilling: null,
897
898
  finalizationNotes: null,
898
899
  },
@@ -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 = currentMetadata.zonePhotos || {};
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 entry if it doesn't exist
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
- // Update the specific photo type
1296
- if (photoType === 'before') {
1297
- currentZonePhotos[zoneId].before = mediaMetadata.url;
1298
- if (notes) {
1299
- currentZonePhotos[zoneId].beforeNote = notes;
1300
- }
1301
- } else {
1302
- currentZonePhotos[zoneId].after = mediaMetadata.url;
1303
- if (notes) {
1304
- currentZonePhotos[zoneId].afterNote = notes;
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
- zoneBilling: currentMetadata.zoneBilling,
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 photoType Type of photo to delete ('before' or 'after')
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
- photoType: 'before' | 'after',
1406
+ photoIndex: number,
1388
1407
  ): Promise<Appointment> {
1389
1408
  try {
1390
1409
  console.log(
1391
- `[APPOINTMENT_SERVICE] Deleting ${photoType} photo for zone ${zoneId} in appointment ${appointmentId}`,
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
- if (!zonePhotos || !zonePhotos[zoneId]) {
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 photoUrl =
1406
- photoType === 'before' ? zonePhotos[zoneId].before : zonePhotos[zoneId].after;
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 ${photoType} photo found for zone ${zoneId}`);
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 reference
1430
- const updatedZonePhotos = { ...zonePhotos };
1431
- if (photoType === 'before') {
1432
- updatedZonePhotos[zoneId].before = null;
1433
- updatedZonePhotos[zoneId].beforeNote = null;
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
- zoneBilling: appointment.metadata?.zoneBilling || null,
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 ${photoType} photo for zone ${zoneId}`,
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(this.db, appointmentId, zoneId, itemIndex, newPrice);
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(`[APPOINTMENT_SERVICE] Getting extended procedures for appointment ${appointmentId}`);
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(`[APPOINTMENT_SERVICE] Getting appointment products for appointment ${appointmentId}`);
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(`[APPOINTMENT_SERVICE] Recalculating final billing for appointment ${appointmentId}`);
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
  }