@blackcode_sa/metaestetics-api 1.12.32 → 1.12.33
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 +68 -21
- package/dist/admin/index.d.ts +68 -21
- package/dist/admin/index.js +35 -5
- package/dist/admin/index.mjs +35 -5
- package/dist/index.d.mts +169 -22
- package/dist/index.d.ts +169 -22
- package/dist/index.js +2602 -1771
- package/dist/index.mjs +1823 -992
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +40 -4
- package/src/services/appointment/appointment.service.ts +278 -0
- package/src/services/appointment/utils/extended-procedure.utils.ts +232 -0
- package/src/services/appointment/utils/zone-management.utils.ts +335 -0
- package/src/services/patient/patient.service.ts +46 -0
- package/src/services/procedure/procedure.service.ts +54 -0
- package/src/types/appointment/index.ts +75 -26
- package/src/validations/appointment.schema.ts +100 -4
package/package.json
CHANGED
|
@@ -840,6 +840,9 @@ export class BookingAdmin {
|
|
|
840
840
|
allLinkedFormTemplateIds = formInitResult.allLinkedTemplateIds;
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
// --- Generate appointment products from procedure ---
|
|
844
|
+
const appointmentProducts = this._generateAppointmentProductsFromProcedure(procedure);
|
|
845
|
+
|
|
843
846
|
// --- Construct Appointment Object ---
|
|
844
847
|
const newAppointmentData: Appointment = {
|
|
845
848
|
id: newAppointmentId,
|
|
@@ -884,6 +887,15 @@ export class BookingAdmin {
|
|
|
884
887
|
linkedForms: initializedFormsInfo,
|
|
885
888
|
media: [],
|
|
886
889
|
reviewInfo: null,
|
|
890
|
+
metadata: {
|
|
891
|
+
selectedZones: null,
|
|
892
|
+
zonePhotos: null,
|
|
893
|
+
zonesData: null,
|
|
894
|
+
appointmentProducts: appointmentProducts,
|
|
895
|
+
extendedProcedures: [],
|
|
896
|
+
finalbilling: null,
|
|
897
|
+
finalizationNotes: null,
|
|
898
|
+
},
|
|
887
899
|
finalizedDetails: {
|
|
888
900
|
by: "",
|
|
889
901
|
at: adminTsNow as any,
|
|
@@ -974,6 +986,8 @@ export class BookingAdmin {
|
|
|
974
986
|
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
975
987
|
const procedureTechnology = procedure.technology as Technology;
|
|
976
988
|
const procedureProduct = procedure.product as Product;
|
|
989
|
+
const productsMetadata = procedure.productsMetadata || [];
|
|
990
|
+
|
|
977
991
|
return {
|
|
978
992
|
id: procedure.id,
|
|
979
993
|
name: procedure.name,
|
|
@@ -987,10 +1001,32 @@ export class BookingAdmin {
|
|
|
987
1001
|
procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
988
1002
|
procedureTechnologyId: procedureTechnology?.id || "",
|
|
989
1003
|
procedureTechnologyName: procedureTechnology?.name || "",
|
|
990
|
-
procedureProductBrandId:
|
|
991
|
-
procedureProductBrandName:
|
|
992
|
-
|
|
993
|
-
|
|
1004
|
+
procedureProductBrandId: procedureProduct?.brandId || "",
|
|
1005
|
+
procedureProductBrandName: procedureProduct?.brandName || "",
|
|
1006
|
+
procedureProducts: productsMetadata.map((pp: any) => ({
|
|
1007
|
+
productId: pp.product.id,
|
|
1008
|
+
productName: pp.product.name,
|
|
1009
|
+
brandId: pp.product.brandId,
|
|
1010
|
+
brandName: pp.product.brandName,
|
|
1011
|
+
})),
|
|
994
1012
|
};
|
|
995
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
private _generateAppointmentProductsFromProcedure(procedure: Procedure): any[] {
|
|
1016
|
+
const productsMetadata = procedure.productsMetadata || [];
|
|
1017
|
+
|
|
1018
|
+
return productsMetadata.map((pp: any) => {
|
|
1019
|
+
const product = pp.product;
|
|
1020
|
+
return {
|
|
1021
|
+
productId: product.id,
|
|
1022
|
+
productName: product.name,
|
|
1023
|
+
brandId: product.brandId,
|
|
1024
|
+
brandName: product.brandName,
|
|
1025
|
+
procedureId: procedure.id,
|
|
1026
|
+
price: pp.price,
|
|
1027
|
+
currency: pp.currency,
|
|
1028
|
+
unitOfMeasurement: pp.pricingMeasure,
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
996
1032
|
}
|
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
type CreateAppointmentHttpData,
|
|
31
31
|
type ZonePhotoUploadData,
|
|
32
32
|
BeforeAfterPerZone,
|
|
33
|
+
ZoneItemData,
|
|
34
|
+
ExtendedProcedureInfo,
|
|
35
|
+
AppointmentProductMetadata,
|
|
33
36
|
APPOINTMENTS_COLLECTION,
|
|
34
37
|
} from '../../types/appointment';
|
|
35
38
|
import {
|
|
@@ -53,6 +56,20 @@ import {
|
|
|
53
56
|
getAppointmentByIdUtil,
|
|
54
57
|
searchAppointmentsUtil,
|
|
55
58
|
} from './utils/appointment.utils';
|
|
59
|
+
import {
|
|
60
|
+
addItemToZoneUtil,
|
|
61
|
+
removeItemFromZoneUtil,
|
|
62
|
+
updateZoneItemUtil,
|
|
63
|
+
overridePriceForZoneItemUtil,
|
|
64
|
+
updateSubzonesUtil,
|
|
65
|
+
calculateFinalBilling,
|
|
66
|
+
} from './utils/zone-management.utils';
|
|
67
|
+
import {
|
|
68
|
+
addExtendedProcedureUtil,
|
|
69
|
+
removeExtendedProcedureUtil,
|
|
70
|
+
getExtendedProceduresUtil,
|
|
71
|
+
getAppointmentProductsUtil,
|
|
72
|
+
} from './utils/extended-procedure.utils';
|
|
56
73
|
|
|
57
74
|
/**
|
|
58
75
|
* Interface for available booking slot
|
|
@@ -1254,6 +1271,9 @@ export class AppointmentService extends BaseService {
|
|
|
1254
1271
|
const currentMetadata = appointment.metadata || {
|
|
1255
1272
|
selectedZones: null,
|
|
1256
1273
|
zonePhotos: null,
|
|
1274
|
+
zonesData: null,
|
|
1275
|
+
appointmentProducts: [],
|
|
1276
|
+
extendedProcedures: [],
|
|
1257
1277
|
zoneBilling: null,
|
|
1258
1278
|
finalbilling: null,
|
|
1259
1279
|
finalizationNotes: null,
|
|
@@ -1290,6 +1310,9 @@ export class AppointmentService extends BaseService {
|
|
|
1290
1310
|
metadata: {
|
|
1291
1311
|
selectedZones: currentMetadata.selectedZones,
|
|
1292
1312
|
zonePhotos: currentZonePhotos,
|
|
1313
|
+
zonesData: currentMetadata.zonesData || null,
|
|
1314
|
+
appointmentProducts: currentMetadata.appointmentProducts || [],
|
|
1315
|
+
extendedProcedures: currentMetadata.extendedProcedures || [],
|
|
1293
1316
|
zoneBilling: currentMetadata.zoneBilling,
|
|
1294
1317
|
finalbilling: currentMetadata.finalbilling,
|
|
1295
1318
|
finalizationNotes: currentMetadata.finalizationNotes,
|
|
@@ -1422,6 +1445,9 @@ export class AppointmentService extends BaseService {
|
|
|
1422
1445
|
metadata: {
|
|
1423
1446
|
selectedZones: appointment.metadata?.selectedZones || null,
|
|
1424
1447
|
zonePhotos: updatedZonePhotos,
|
|
1448
|
+
zonesData: appointment.metadata?.zonesData || null,
|
|
1449
|
+
appointmentProducts: appointment.metadata?.appointmentProducts || [],
|
|
1450
|
+
extendedProcedures: appointment.metadata?.extendedProcedures || [],
|
|
1425
1451
|
zoneBilling: appointment.metadata?.zoneBilling || null,
|
|
1426
1452
|
finalbilling: appointment.metadata?.finalbilling || null,
|
|
1427
1453
|
finalizationNotes: appointment.metadata?.finalizationNotes || null,
|
|
@@ -1441,4 +1467,256 @@ export class AppointmentService extends BaseService {
|
|
|
1441
1467
|
throw error;
|
|
1442
1468
|
}
|
|
1443
1469
|
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Adds an item (product or note) to a specific zone
|
|
1473
|
+
*
|
|
1474
|
+
* @param appointmentId ID of the appointment
|
|
1475
|
+
* @param zoneId Zone ID (must be category.zone format, e.g., "face.forehead")
|
|
1476
|
+
* @param item Zone item data to add (without parentZone - it's inferred from zoneId)
|
|
1477
|
+
* @returns The updated appointment
|
|
1478
|
+
*/
|
|
1479
|
+
async addItemToZone(
|
|
1480
|
+
appointmentId: string,
|
|
1481
|
+
zoneId: string,
|
|
1482
|
+
item: Omit<ZoneItemData, 'subtotal' | 'parentZone'>
|
|
1483
|
+
): Promise<Appointment> {
|
|
1484
|
+
try {
|
|
1485
|
+
console.log(
|
|
1486
|
+
`[APPOINTMENT_SERVICE] Adding item to zone ${zoneId} in appointment ${appointmentId}`,
|
|
1487
|
+
);
|
|
1488
|
+
return await addItemToZoneUtil(this.db, appointmentId, zoneId, item);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
console.error(`[APPOINTMENT_SERVICE] Error adding item to zone:`, error);
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Removes an item from a specific zone
|
|
1497
|
+
*
|
|
1498
|
+
* @param appointmentId ID of the appointment
|
|
1499
|
+
* @param zoneId Zone ID
|
|
1500
|
+
* @param itemIndex Index of the item to remove in the zone's items array
|
|
1501
|
+
* @returns The updated appointment
|
|
1502
|
+
*/
|
|
1503
|
+
async removeItemFromZone(
|
|
1504
|
+
appointmentId: string,
|
|
1505
|
+
zoneId: string,
|
|
1506
|
+
itemIndex: number,
|
|
1507
|
+
): Promise<Appointment> {
|
|
1508
|
+
try {
|
|
1509
|
+
console.log(
|
|
1510
|
+
`[APPOINTMENT_SERVICE] Removing item ${itemIndex} from zone ${zoneId} in appointment ${appointmentId}`,
|
|
1511
|
+
);
|
|
1512
|
+
return await removeItemFromZoneUtil(this.db, appointmentId, zoneId, itemIndex);
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
console.error(`[APPOINTMENT_SERVICE] Error removing item from zone:`, error);
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Updates a specific item in a zone
|
|
1521
|
+
*
|
|
1522
|
+
* @param appointmentId ID of the appointment
|
|
1523
|
+
* @param zoneId Zone ID
|
|
1524
|
+
* @param itemIndex Index of the item to update
|
|
1525
|
+
* @param updates Partial updates to apply to the item
|
|
1526
|
+
* @returns The updated appointment
|
|
1527
|
+
*/
|
|
1528
|
+
async updateZoneItem(
|
|
1529
|
+
appointmentId: string,
|
|
1530
|
+
zoneId: string,
|
|
1531
|
+
itemIndex: number,
|
|
1532
|
+
updates: Partial<ZoneItemData>,
|
|
1533
|
+
): Promise<Appointment> {
|
|
1534
|
+
try {
|
|
1535
|
+
console.log(
|
|
1536
|
+
`[APPOINTMENT_SERVICE] Updating item ${itemIndex} in zone ${zoneId} in appointment ${appointmentId}`,
|
|
1537
|
+
);
|
|
1538
|
+
return await updateZoneItemUtil(this.db, appointmentId, zoneId, itemIndex, updates);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
console.error(`[APPOINTMENT_SERVICE] Error updating zone item:`, error);
|
|
1541
|
+
throw error;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Overrides the price for a specific zone item
|
|
1547
|
+
*
|
|
1548
|
+
* @param appointmentId ID of the appointment
|
|
1549
|
+
* @param zoneId Zone ID
|
|
1550
|
+
* @param itemIndex Index of the item
|
|
1551
|
+
* @param newPrice New price amount to set
|
|
1552
|
+
* @returns The updated appointment
|
|
1553
|
+
*/
|
|
1554
|
+
async overridePriceForZoneItem(
|
|
1555
|
+
appointmentId: string,
|
|
1556
|
+
zoneId: string,
|
|
1557
|
+
itemIndex: number,
|
|
1558
|
+
newPrice: number,
|
|
1559
|
+
): Promise<Appointment> {
|
|
1560
|
+
try {
|
|
1561
|
+
console.log(
|
|
1562
|
+
`[APPOINTMENT_SERVICE] Overriding price for item ${itemIndex} in zone ${zoneId} to ${newPrice}`,
|
|
1563
|
+
);
|
|
1564
|
+
return await overridePriceForZoneItemUtil(this.db, appointmentId, zoneId, itemIndex, newPrice);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
console.error(`[APPOINTMENT_SERVICE] Error overriding price:`, error);
|
|
1567
|
+
throw error;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Updates subzones for a specific zone item
|
|
1573
|
+
*
|
|
1574
|
+
* @param appointmentId ID of the appointment
|
|
1575
|
+
* @param zoneId Zone ID
|
|
1576
|
+
* @param itemIndex Index of the item
|
|
1577
|
+
* @param subzones Array of subzone keys (category.zone.subzone format)
|
|
1578
|
+
* @returns The updated appointment
|
|
1579
|
+
*/
|
|
1580
|
+
async updateSubzones(
|
|
1581
|
+
appointmentId: string,
|
|
1582
|
+
zoneId: string,
|
|
1583
|
+
itemIndex: number,
|
|
1584
|
+
subzones: string[],
|
|
1585
|
+
): Promise<Appointment> {
|
|
1586
|
+
try {
|
|
1587
|
+
console.log(
|
|
1588
|
+
`[APPOINTMENT_SERVICE] Updating subzones for item ${itemIndex} in zone ${zoneId}`,
|
|
1589
|
+
);
|
|
1590
|
+
return await updateSubzonesUtil(this.db, appointmentId, zoneId, itemIndex, subzones);
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
console.error(`[APPOINTMENT_SERVICE] Error updating subzones:`, error);
|
|
1593
|
+
throw error;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Adds an extended procedure to an appointment
|
|
1599
|
+
* Automatically aggregates products into appointmentProducts
|
|
1600
|
+
*
|
|
1601
|
+
* @param appointmentId ID of the appointment
|
|
1602
|
+
* @param procedureId ID of the procedure to add
|
|
1603
|
+
* @returns The updated appointment
|
|
1604
|
+
*/
|
|
1605
|
+
async addExtendedProcedure(appointmentId: string, procedureId: string): Promise<Appointment> {
|
|
1606
|
+
try {
|
|
1607
|
+
console.log(
|
|
1608
|
+
`[APPOINTMENT_SERVICE] Adding extended procedure ${procedureId} to appointment ${appointmentId}`,
|
|
1609
|
+
);
|
|
1610
|
+
return await addExtendedProcedureUtil(this.db, appointmentId, procedureId);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
console.error(`[APPOINTMENT_SERVICE] Error adding extended procedure:`, error);
|
|
1613
|
+
throw error;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Removes an extended procedure from an appointment
|
|
1619
|
+
* Also removes associated products from appointmentProducts
|
|
1620
|
+
*
|
|
1621
|
+
* @param appointmentId ID of the appointment
|
|
1622
|
+
* @param procedureId ID of the procedure to remove
|
|
1623
|
+
* @returns The updated appointment
|
|
1624
|
+
*/
|
|
1625
|
+
async removeExtendedProcedure(appointmentId: string, procedureId: string): Promise<Appointment> {
|
|
1626
|
+
try {
|
|
1627
|
+
console.log(
|
|
1628
|
+
`[APPOINTMENT_SERVICE] Removing extended procedure ${procedureId} from appointment ${appointmentId}`,
|
|
1629
|
+
);
|
|
1630
|
+
return await removeExtendedProcedureUtil(this.db, appointmentId, procedureId);
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
console.error(`[APPOINTMENT_SERVICE] Error removing extended procedure:`, error);
|
|
1633
|
+
throw error;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Gets all extended procedures for an appointment
|
|
1639
|
+
*
|
|
1640
|
+
* @param appointmentId ID of the appointment
|
|
1641
|
+
* @returns Array of extended procedures
|
|
1642
|
+
*/
|
|
1643
|
+
async getExtendedProcedures(appointmentId: string): Promise<ExtendedProcedureInfo[]> {
|
|
1644
|
+
try {
|
|
1645
|
+
console.log(`[APPOINTMENT_SERVICE] Getting extended procedures for appointment ${appointmentId}`);
|
|
1646
|
+
return await getExtendedProceduresUtil(this.db, appointmentId);
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
console.error(`[APPOINTMENT_SERVICE] Error getting extended procedures:`, error);
|
|
1649
|
+
throw error;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Gets all aggregated products for an appointment
|
|
1655
|
+
* Includes products from main procedure and extended procedures
|
|
1656
|
+
*
|
|
1657
|
+
* @param appointmentId ID of the appointment
|
|
1658
|
+
* @returns Array of appointment products
|
|
1659
|
+
*/
|
|
1660
|
+
async getAppointmentProducts(appointmentId: string): Promise<AppointmentProductMetadata[]> {
|
|
1661
|
+
try {
|
|
1662
|
+
console.log(`[APPOINTMENT_SERVICE] Getting appointment products for appointment ${appointmentId}`);
|
|
1663
|
+
return await getAppointmentProductsUtil(this.db, appointmentId);
|
|
1664
|
+
} catch (error) {
|
|
1665
|
+
console.error(`[APPOINTMENT_SERVICE] Error getting appointment products:`, error);
|
|
1666
|
+
throw error;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Recalculates final billing for an appointment based on zone items
|
|
1672
|
+
*
|
|
1673
|
+
* @param appointmentId ID of the appointment
|
|
1674
|
+
* @param taxRate Tax rate (e.g., 0.20 for 20%)
|
|
1675
|
+
* @returns The updated appointment with recalculated billing
|
|
1676
|
+
*/
|
|
1677
|
+
async recalculateFinalBilling(appointmentId: string, taxRate?: number): Promise<Appointment> {
|
|
1678
|
+
try {
|
|
1679
|
+
console.log(`[APPOINTMENT_SERVICE] Recalculating final billing for appointment ${appointmentId}`);
|
|
1680
|
+
|
|
1681
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
1682
|
+
if (!appointment) {
|
|
1683
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const zonesData = appointment.metadata?.zonesData;
|
|
1687
|
+
if (!zonesData || Object.keys(zonesData).length === 0) {
|
|
1688
|
+
throw new Error('No zone data available for billing calculation');
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const finalbilling = calculateFinalBilling(zonesData, taxRate);
|
|
1692
|
+
|
|
1693
|
+
const currentMetadata = appointment.metadata || {
|
|
1694
|
+
selectedZones: null,
|
|
1695
|
+
zonePhotos: null,
|
|
1696
|
+
zonesData: null,
|
|
1697
|
+
appointmentProducts: [],
|
|
1698
|
+
extendedProcedures: [],
|
|
1699
|
+
finalbilling: null,
|
|
1700
|
+
finalizationNotes: null,
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
const updateData: UpdateAppointmentData = {
|
|
1704
|
+
metadata: {
|
|
1705
|
+
selectedZones: currentMetadata.selectedZones,
|
|
1706
|
+
zonePhotos: currentMetadata.zonePhotos,
|
|
1707
|
+
zonesData: currentMetadata.zonesData,
|
|
1708
|
+
appointmentProducts: currentMetadata.appointmentProducts || [],
|
|
1709
|
+
extendedProcedures: currentMetadata.extendedProcedures || [],
|
|
1710
|
+
finalbilling,
|
|
1711
|
+
finalizationNotes: currentMetadata.finalizationNotes,
|
|
1712
|
+
},
|
|
1713
|
+
updatedAt: serverTimestamp(),
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
return await this.updateAppointment(appointmentId, updateData);
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
console.error(`[APPOINTMENT_SERVICE] Error recalculating final billing:`, error);
|
|
1719
|
+
throw error;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1444
1722
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Firestore, updateDoc, serverTimestamp, doc, getDoc } from 'firebase/firestore';
|
|
2
|
+
import {
|
|
3
|
+
Appointment,
|
|
4
|
+
ExtendedProcedureInfo,
|
|
5
|
+
AppointmentProductMetadata,
|
|
6
|
+
APPOINTMENTS_COLLECTION,
|
|
7
|
+
} from '../../../types/appointment';
|
|
8
|
+
import { getAppointmentOrThrow, initializeMetadata } from './zone-management.utils';
|
|
9
|
+
import { PROCEDURES_COLLECTION } from '../../../types/procedure';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Aggregates products from a procedure into appointmentProducts
|
|
13
|
+
* @param db Firestore instance
|
|
14
|
+
* @param procedureId Procedure ID to fetch
|
|
15
|
+
* @param existingProducts Current appointment products
|
|
16
|
+
* @returns Updated appointment products array
|
|
17
|
+
*/
|
|
18
|
+
async function aggregateProductsFromProcedure(
|
|
19
|
+
db: Firestore,
|
|
20
|
+
procedureId: string,
|
|
21
|
+
existingProducts: AppointmentProductMetadata[]
|
|
22
|
+
): Promise<AppointmentProductMetadata[]> {
|
|
23
|
+
const procedureRef = doc(db, PROCEDURES_COLLECTION, procedureId);
|
|
24
|
+
const procedureSnap = await getDoc(procedureRef);
|
|
25
|
+
|
|
26
|
+
if (!procedureSnap.exists()) {
|
|
27
|
+
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const procedureData = procedureSnap.data();
|
|
31
|
+
|
|
32
|
+
// Get procedure products from productsMetadata array
|
|
33
|
+
const productsMetadata = procedureData.productsMetadata || [];
|
|
34
|
+
|
|
35
|
+
// Map procedure products to AppointmentProductMetadata
|
|
36
|
+
const newProducts: AppointmentProductMetadata[] = productsMetadata.map((pp: any) => {
|
|
37
|
+
// Each item in productsMetadata is a ProcedureProduct with embedded Product
|
|
38
|
+
const product = pp.product;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
productId: product.id,
|
|
42
|
+
productName: product.name,
|
|
43
|
+
brandId: product.brandId,
|
|
44
|
+
brandName: product.brandName,
|
|
45
|
+
procedureId: procedureId,
|
|
46
|
+
price: pp.price, // Price from ProcedureProduct
|
|
47
|
+
currency: pp.currency, // Currency from ProcedureProduct
|
|
48
|
+
unitOfMeasurement: pp.pricingMeasure, // PricingMeasure from ProcedureProduct
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Merge with existing products, avoiding duplicates
|
|
53
|
+
const productMap = new Map<string, AppointmentProductMetadata>();
|
|
54
|
+
|
|
55
|
+
// Add existing products
|
|
56
|
+
existingProducts.forEach(p => {
|
|
57
|
+
const key = `${p.productId}-${p.procedureId}`;
|
|
58
|
+
productMap.set(key, p);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Add new products
|
|
62
|
+
newProducts.forEach(p => {
|
|
63
|
+
const key = `${p.productId}-${p.procedureId}`;
|
|
64
|
+
if (!productMap.has(key)) {
|
|
65
|
+
productMap.set(key, p);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return Array.from(productMap.values());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates ExtendedProcedureInfo from procedure document
|
|
74
|
+
* @param db Firestore instance
|
|
75
|
+
* @param procedureId Procedure ID
|
|
76
|
+
* @returns Extended procedure info
|
|
77
|
+
*/
|
|
78
|
+
async function createExtendedProcedureInfo(
|
|
79
|
+
db: Firestore,
|
|
80
|
+
procedureId: string
|
|
81
|
+
): Promise<ExtendedProcedureInfo> {
|
|
82
|
+
const procedureRef = doc(db, PROCEDURES_COLLECTION, procedureId);
|
|
83
|
+
const procedureSnap = await getDoc(procedureRef);
|
|
84
|
+
|
|
85
|
+
if (!procedureSnap.exists()) {
|
|
86
|
+
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data = procedureSnap.data();
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
procedureId: procedureId,
|
|
93
|
+
procedureName: data.name,
|
|
94
|
+
procedureFamily: data.family, // Use embedded family object
|
|
95
|
+
procedureCategoryId: data.category.id, // Access embedded category
|
|
96
|
+
procedureCategoryName: data.category.name, // Access embedded category
|
|
97
|
+
procedureSubCategoryId: data.subcategory.id, // Access embedded subcategory
|
|
98
|
+
procedureSubCategoryName: data.subcategory.name, // Access embedded subcategory
|
|
99
|
+
procedureTechnologyId: data.technology.id, // Access embedded technology
|
|
100
|
+
procedureTechnologyName: data.technology.name, // Access embedded technology
|
|
101
|
+
procedureProducts: (data.productsMetadata || []).map((pp: any) => ({
|
|
102
|
+
productId: pp.product.id, // Access embedded product
|
|
103
|
+
productName: pp.product.name, // Access embedded product
|
|
104
|
+
brandId: pp.product.brandId, // Access embedded product
|
|
105
|
+
brandName: pp.product.brandName, // Access embedded product
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Adds an extended procedure to an appointment
|
|
112
|
+
* Automatically aggregates products into appointmentProducts
|
|
113
|
+
* @param db Firestore instance
|
|
114
|
+
* @param appointmentId Appointment ID
|
|
115
|
+
* @param procedureId Procedure ID to add
|
|
116
|
+
* @returns Updated appointment
|
|
117
|
+
*/
|
|
118
|
+
export async function addExtendedProcedureUtil(
|
|
119
|
+
db: Firestore,
|
|
120
|
+
appointmentId: string,
|
|
121
|
+
procedureId: string
|
|
122
|
+
): Promise<Appointment> {
|
|
123
|
+
const appointment = await getAppointmentOrThrow(db, appointmentId);
|
|
124
|
+
const metadata = initializeMetadata(appointment);
|
|
125
|
+
|
|
126
|
+
// Check if procedure is already added
|
|
127
|
+
const existingProcedure = metadata.extendedProcedures?.find(
|
|
128
|
+
p => p.procedureId === procedureId
|
|
129
|
+
);
|
|
130
|
+
if (existingProcedure) {
|
|
131
|
+
throw new Error(`Procedure ${procedureId} is already added to this appointment`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create extended procedure info
|
|
135
|
+
const extendedProcedureInfo = await createExtendedProcedureInfo(db, procedureId);
|
|
136
|
+
|
|
137
|
+
// Aggregate products
|
|
138
|
+
const updatedProducts = await aggregateProductsFromProcedure(
|
|
139
|
+
db,
|
|
140
|
+
procedureId,
|
|
141
|
+
metadata.appointmentProducts || []
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Add extended procedure
|
|
145
|
+
const extendedProcedures = [...(metadata.extendedProcedures || []), extendedProcedureInfo];
|
|
146
|
+
|
|
147
|
+
// Update appointment
|
|
148
|
+
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
149
|
+
await updateDoc(appointmentRef, {
|
|
150
|
+
'metadata.extendedProcedures': extendedProcedures,
|
|
151
|
+
'metadata.appointmentProducts': updatedProducts,
|
|
152
|
+
updatedAt: serverTimestamp(),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return getAppointmentOrThrow(db, appointmentId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Removes an extended procedure from an appointment
|
|
160
|
+
* Also removes associated products from appointmentProducts
|
|
161
|
+
* @param db Firestore instance
|
|
162
|
+
* @param appointmentId Appointment ID
|
|
163
|
+
* @param procedureId Procedure ID to remove
|
|
164
|
+
* @returns Updated appointment
|
|
165
|
+
*/
|
|
166
|
+
export async function removeExtendedProcedureUtil(
|
|
167
|
+
db: Firestore,
|
|
168
|
+
appointmentId: string,
|
|
169
|
+
procedureId: string
|
|
170
|
+
): Promise<Appointment> {
|
|
171
|
+
const appointment = await getAppointmentOrThrow(db, appointmentId);
|
|
172
|
+
const metadata = initializeMetadata(appointment);
|
|
173
|
+
|
|
174
|
+
if (!metadata.extendedProcedures || metadata.extendedProcedures.length === 0) {
|
|
175
|
+
throw new Error('No extended procedures found for this appointment');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Find and remove the procedure
|
|
179
|
+
const procedureIndex = metadata.extendedProcedures.findIndex(
|
|
180
|
+
p => p.procedureId === procedureId
|
|
181
|
+
);
|
|
182
|
+
if (procedureIndex === -1) {
|
|
183
|
+
throw new Error(`Extended procedure ${procedureId} not found in this appointment`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove procedure
|
|
187
|
+
metadata.extendedProcedures.splice(procedureIndex, 1);
|
|
188
|
+
|
|
189
|
+
// Remove products associated with this procedure
|
|
190
|
+
const updatedProducts = (metadata.appointmentProducts || []).filter(
|
|
191
|
+
p => p.procedureId !== procedureId
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Update appointment
|
|
195
|
+
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
196
|
+
await updateDoc(appointmentRef, {
|
|
197
|
+
'metadata.extendedProcedures': metadata.extendedProcedures,
|
|
198
|
+
'metadata.appointmentProducts': updatedProducts,
|
|
199
|
+
updatedAt: serverTimestamp(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return getAppointmentOrThrow(db, appointmentId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Gets all extended procedures for an appointment
|
|
207
|
+
* @param db Firestore instance
|
|
208
|
+
* @param appointmentId Appointment ID
|
|
209
|
+
* @returns Array of extended procedures
|
|
210
|
+
*/
|
|
211
|
+
export async function getExtendedProceduresUtil(
|
|
212
|
+
db: Firestore,
|
|
213
|
+
appointmentId: string
|
|
214
|
+
): Promise<ExtendedProcedureInfo[]> {
|
|
215
|
+
const appointment = await getAppointmentOrThrow(db, appointmentId);
|
|
216
|
+
return appointment.metadata?.extendedProcedures || [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Gets all aggregated products for an appointment
|
|
221
|
+
* @param db Firestore instance
|
|
222
|
+
* @param appointmentId Appointment ID
|
|
223
|
+
* @returns Array of appointment products
|
|
224
|
+
*/
|
|
225
|
+
export async function getAppointmentProductsUtil(
|
|
226
|
+
db: Firestore,
|
|
227
|
+
appointmentId: string
|
|
228
|
+
): Promise<AppointmentProductMetadata[]> {
|
|
229
|
+
const appointment = await getAppointmentOrThrow(db, appointmentId);
|
|
230
|
+
return appointment.metadata?.appointmentProducts || [];
|
|
231
|
+
}
|
|
232
|
+
|