@blackcode_sa/metaestetics-api 1.13.1 → 1.13.3
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/index.d.mts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +287 -0
- package/dist/index.mjs +287 -0
- package/package.json +1 -1
- package/src/services/procedure/procedure.service.ts +366 -3
package/dist/index.d.mts
CHANGED
|
@@ -6736,6 +6736,40 @@ declare class ProcedureService extends BaseService {
|
|
|
6736
6736
|
* @returns The created procedure
|
|
6737
6737
|
*/
|
|
6738
6738
|
createProcedure(data: CreateProcedureData): Promise<Procedure>;
|
|
6739
|
+
/**
|
|
6740
|
+
* Validates if a practitioner can perform a procedure based on certification requirements.
|
|
6741
|
+
*
|
|
6742
|
+
* @param procedure - The procedure to check
|
|
6743
|
+
* @param practitioner - The practitioner to validate
|
|
6744
|
+
* @returns true if practitioner can perform the procedure, false otherwise
|
|
6745
|
+
*/
|
|
6746
|
+
canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean;
|
|
6747
|
+
/**
|
|
6748
|
+
* Clones an existing procedure for a target practitioner.
|
|
6749
|
+
* This creates a new procedure document with the same data as the source procedure,
|
|
6750
|
+
* but linked to the target practitioner.
|
|
6751
|
+
*
|
|
6752
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
6753
|
+
* @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
|
|
6754
|
+
* @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
|
|
6755
|
+
* @returns The newly created procedure
|
|
6756
|
+
*/
|
|
6757
|
+
cloneProcedureForPractitioner(sourceProcedureId: string, targetPractitionerId: string, overrides?: Partial<CreateProcedureData> & {
|
|
6758
|
+
isActive?: boolean;
|
|
6759
|
+
}): Promise<Procedure>;
|
|
6760
|
+
/**
|
|
6761
|
+
* Clones an existing procedure for multiple target practitioners.
|
|
6762
|
+
* This creates new procedure documents with the same data as the source procedure,
|
|
6763
|
+
* but linked to each target practitioner.
|
|
6764
|
+
*
|
|
6765
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
6766
|
+
* @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
|
|
6767
|
+
* @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
|
|
6768
|
+
* @returns Array of newly created procedures
|
|
6769
|
+
*/
|
|
6770
|
+
bulkCloneProcedureForPractitioners(sourceProcedureId: string, targetPractitionerIds: string[], overrides?: Partial<CreateProcedureData> & {
|
|
6771
|
+
isActive?: boolean;
|
|
6772
|
+
}): Promise<Procedure[]>;
|
|
6739
6773
|
/**
|
|
6740
6774
|
* Creates multiple procedures for a list of practitioners based on common data.
|
|
6741
6775
|
* This method is optimized for bulk creation to reduce database reads and writes.
|
package/dist/index.d.ts
CHANGED
|
@@ -6736,6 +6736,40 @@ declare class ProcedureService extends BaseService {
|
|
|
6736
6736
|
* @returns The created procedure
|
|
6737
6737
|
*/
|
|
6738
6738
|
createProcedure(data: CreateProcedureData): Promise<Procedure>;
|
|
6739
|
+
/**
|
|
6740
|
+
* Validates if a practitioner can perform a procedure based on certification requirements.
|
|
6741
|
+
*
|
|
6742
|
+
* @param procedure - The procedure to check
|
|
6743
|
+
* @param practitioner - The practitioner to validate
|
|
6744
|
+
* @returns true if practitioner can perform the procedure, false otherwise
|
|
6745
|
+
*/
|
|
6746
|
+
canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean;
|
|
6747
|
+
/**
|
|
6748
|
+
* Clones an existing procedure for a target practitioner.
|
|
6749
|
+
* This creates a new procedure document with the same data as the source procedure,
|
|
6750
|
+
* but linked to the target practitioner.
|
|
6751
|
+
*
|
|
6752
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
6753
|
+
* @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
|
|
6754
|
+
* @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
|
|
6755
|
+
* @returns The newly created procedure
|
|
6756
|
+
*/
|
|
6757
|
+
cloneProcedureForPractitioner(sourceProcedureId: string, targetPractitionerId: string, overrides?: Partial<CreateProcedureData> & {
|
|
6758
|
+
isActive?: boolean;
|
|
6759
|
+
}): Promise<Procedure>;
|
|
6760
|
+
/**
|
|
6761
|
+
* Clones an existing procedure for multiple target practitioners.
|
|
6762
|
+
* This creates new procedure documents with the same data as the source procedure,
|
|
6763
|
+
* but linked to each target practitioner.
|
|
6764
|
+
*
|
|
6765
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
6766
|
+
* @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
|
|
6767
|
+
* @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
|
|
6768
|
+
* @returns Array of newly created procedures
|
|
6769
|
+
*/
|
|
6770
|
+
bulkCloneProcedureForPractitioners(sourceProcedureId: string, targetPractitionerIds: string[], overrides?: Partial<CreateProcedureData> & {
|
|
6771
|
+
isActive?: boolean;
|
|
6772
|
+
}): Promise<Procedure[]>;
|
|
6739
6773
|
/**
|
|
6740
6774
|
* Creates multiple procedures for a list of practitioners based on common data.
|
|
6741
6775
|
* This method is optimized for bulk creation to reduce database reads and writes.
|
package/dist/index.js
CHANGED
|
@@ -20035,6 +20035,25 @@ var ProcedureService = class extends BaseService {
|
|
|
20035
20035
|
throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
|
|
20036
20036
|
}
|
|
20037
20037
|
const practitioner = practitionerSnapshot.data();
|
|
20038
|
+
const existingProceduresQuery = (0, import_firestore58.query)(
|
|
20039
|
+
(0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
|
|
20040
|
+
(0, import_firestore58.where)("practitionerId", "==", validatedData.practitionerId),
|
|
20041
|
+
(0, import_firestore58.where)("clinicBranchId", "==", validatedData.clinicBranchId),
|
|
20042
|
+
(0, import_firestore58.where)("isActive", "==", true)
|
|
20043
|
+
);
|
|
20044
|
+
const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
|
|
20045
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20046
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20047
|
+
(proc) => {
|
|
20048
|
+
var _a2;
|
|
20049
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
|
|
20050
|
+
}
|
|
20051
|
+
);
|
|
20052
|
+
if (hasSameTechnology) {
|
|
20053
|
+
throw new Error(
|
|
20054
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch`
|
|
20055
|
+
);
|
|
20056
|
+
}
|
|
20038
20057
|
let processedPhotos = [];
|
|
20039
20058
|
if (validatedData.photos && validatedData.photos.length > 0) {
|
|
20040
20059
|
processedPhotos = await this.processMediaArray(
|
|
@@ -20137,6 +20156,240 @@ var ProcedureService = class extends BaseService {
|
|
|
20137
20156
|
const savedDoc = await (0, import_firestore58.getDoc)(procedureRef);
|
|
20138
20157
|
return savedDoc.data();
|
|
20139
20158
|
}
|
|
20159
|
+
/**
|
|
20160
|
+
* Validates if a practitioner can perform a procedure based on certification requirements.
|
|
20161
|
+
*
|
|
20162
|
+
* @param procedure - The procedure to check
|
|
20163
|
+
* @param practitioner - The practitioner to validate
|
|
20164
|
+
* @returns true if practitioner can perform the procedure, false otherwise
|
|
20165
|
+
*/
|
|
20166
|
+
canPractitionerPerformProcedure(procedure, practitioner) {
|
|
20167
|
+
if (!practitioner.certification) {
|
|
20168
|
+
return false;
|
|
20169
|
+
}
|
|
20170
|
+
const requiredCert = procedure.certificationRequirement;
|
|
20171
|
+
const practitionerCert = practitioner.certification;
|
|
20172
|
+
const levelOrder = [
|
|
20173
|
+
"aesthetician",
|
|
20174
|
+
"nurse_assistant",
|
|
20175
|
+
"nurse",
|
|
20176
|
+
"nurse_practitioner",
|
|
20177
|
+
"physician_assistant",
|
|
20178
|
+
"doctor",
|
|
20179
|
+
"specialist",
|
|
20180
|
+
"plastic_surgeon"
|
|
20181
|
+
];
|
|
20182
|
+
const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
|
|
20183
|
+
const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
|
|
20184
|
+
if (practitionerLevelIndex < requiredLevelIndex) {
|
|
20185
|
+
return false;
|
|
20186
|
+
}
|
|
20187
|
+
const requiredSpecialties = requiredCert.requiredSpecialties || [];
|
|
20188
|
+
if (requiredSpecialties.length > 0) {
|
|
20189
|
+
const practitionerSpecialties = practitionerCert.specialties || [];
|
|
20190
|
+
const hasAllRequired = requiredSpecialties.every(
|
|
20191
|
+
(specialty) => practitionerSpecialties.includes(specialty)
|
|
20192
|
+
);
|
|
20193
|
+
if (!hasAllRequired) {
|
|
20194
|
+
return false;
|
|
20195
|
+
}
|
|
20196
|
+
}
|
|
20197
|
+
return true;
|
|
20198
|
+
}
|
|
20199
|
+
/**
|
|
20200
|
+
* Clones an existing procedure for a target practitioner.
|
|
20201
|
+
* This creates a new procedure document with the same data as the source procedure,
|
|
20202
|
+
* but linked to the target practitioner.
|
|
20203
|
+
*
|
|
20204
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
20205
|
+
* @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
|
|
20206
|
+
* @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
|
|
20207
|
+
* @returns The newly created procedure
|
|
20208
|
+
*/
|
|
20209
|
+
async cloneProcedureForPractitioner(sourceProcedureId, targetPractitionerId, overrides) {
|
|
20210
|
+
var _a, _b, _c;
|
|
20211
|
+
const sourceProcedure = await this.getProcedure(sourceProcedureId);
|
|
20212
|
+
if (!sourceProcedure) {
|
|
20213
|
+
throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
|
|
20214
|
+
}
|
|
20215
|
+
const practitionerRef = (0, import_firestore58.doc)(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
|
|
20216
|
+
const practitionerSnapshot = await (0, import_firestore58.getDoc)(practitionerRef);
|
|
20217
|
+
if (!practitionerSnapshot.exists()) {
|
|
20218
|
+
throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
|
|
20219
|
+
}
|
|
20220
|
+
const practitioner = practitionerSnapshot.data();
|
|
20221
|
+
if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
|
|
20222
|
+
throw new Error(
|
|
20223
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
|
|
20224
|
+
);
|
|
20225
|
+
}
|
|
20226
|
+
const existingProceduresQuery = (0, import_firestore58.query)(
|
|
20227
|
+
(0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
|
|
20228
|
+
(0, import_firestore58.where)("practitionerId", "==", targetPractitionerId),
|
|
20229
|
+
(0, import_firestore58.where)("clinicBranchId", "==", sourceProcedure.clinicBranchId),
|
|
20230
|
+
(0, import_firestore58.where)("isActive", "==", true)
|
|
20231
|
+
);
|
|
20232
|
+
const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
|
|
20233
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20234
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20235
|
+
(proc) => {
|
|
20236
|
+
var _a2, _b2;
|
|
20237
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === ((_b2 = sourceProcedure.technology) == null ? void 0 : _b2.id);
|
|
20238
|
+
}
|
|
20239
|
+
);
|
|
20240
|
+
if (hasSameTechnology) {
|
|
20241
|
+
throw new Error(
|
|
20242
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_a = sourceProcedure.technology) == null ? void 0 : _a.name) || ((_b = sourceProcedure.technology) == null ? void 0 : _b.id)}" in this clinic branch`
|
|
20243
|
+
);
|
|
20244
|
+
}
|
|
20245
|
+
const newProcedureId = this.generateId();
|
|
20246
|
+
const doctorInfo = {
|
|
20247
|
+
id: practitioner.id,
|
|
20248
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
20249
|
+
description: practitioner.basicInfo.bio || "",
|
|
20250
|
+
photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
|
|
20251
|
+
rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
|
|
20252
|
+
services: practitioner.procedures || []
|
|
20253
|
+
};
|
|
20254
|
+
const newProcedure = {
|
|
20255
|
+
...sourceProcedure,
|
|
20256
|
+
id: newProcedureId,
|
|
20257
|
+
practitionerId: targetPractitionerId,
|
|
20258
|
+
doctorInfo,
|
|
20259
|
+
// Link to new doctor
|
|
20260
|
+
// Reset review info for the new procedure
|
|
20261
|
+
reviewInfo: {
|
|
20262
|
+
totalReviews: 0,
|
|
20263
|
+
averageRating: 0,
|
|
20264
|
+
effectivenessOfTreatment: 0,
|
|
20265
|
+
outcomeExplanation: 0,
|
|
20266
|
+
painManagement: 0,
|
|
20267
|
+
followUpCare: 0,
|
|
20268
|
+
valueForMoney: 0,
|
|
20269
|
+
recommendationPercentage: 0
|
|
20270
|
+
},
|
|
20271
|
+
// Apply any overrides if provided
|
|
20272
|
+
...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
|
|
20273
|
+
...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
|
|
20274
|
+
...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
|
|
20275
|
+
// Ensure it's active by default unless specified otherwise
|
|
20276
|
+
isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
|
|
20277
|
+
};
|
|
20278
|
+
const procedureRef = (0, import_firestore58.doc)(this.db, PROCEDURES_COLLECTION, newProcedureId);
|
|
20279
|
+
await (0, import_firestore58.setDoc)(procedureRef, {
|
|
20280
|
+
...newProcedure,
|
|
20281
|
+
createdAt: (0, import_firestore58.serverTimestamp)(),
|
|
20282
|
+
updatedAt: (0, import_firestore58.serverTimestamp)()
|
|
20283
|
+
});
|
|
20284
|
+
const savedDoc = await (0, import_firestore58.getDoc)(procedureRef);
|
|
20285
|
+
return savedDoc.data();
|
|
20286
|
+
}
|
|
20287
|
+
/**
|
|
20288
|
+
* Clones an existing procedure for multiple target practitioners.
|
|
20289
|
+
* This creates new procedure documents with the same data as the source procedure,
|
|
20290
|
+
* but linked to each target practitioner.
|
|
20291
|
+
*
|
|
20292
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
20293
|
+
* @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
|
|
20294
|
+
* @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
|
|
20295
|
+
* @returns Array of newly created procedures
|
|
20296
|
+
*/
|
|
20297
|
+
async bulkCloneProcedureForPractitioners(sourceProcedureId, targetPractitionerIds, overrides) {
|
|
20298
|
+
var _a, _b, _c;
|
|
20299
|
+
if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
|
|
20300
|
+
throw new Error("At least one target practitioner ID is required");
|
|
20301
|
+
}
|
|
20302
|
+
const sourceProcedure = await this.getProcedure(sourceProcedureId);
|
|
20303
|
+
if (!sourceProcedure) {
|
|
20304
|
+
throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
|
|
20305
|
+
}
|
|
20306
|
+
const practitionerPromises = targetPractitionerIds.map(
|
|
20307
|
+
(id) => (0, import_firestore58.getDoc)((0, import_firestore58.doc)(this.db, PRACTITIONERS_COLLECTION, id))
|
|
20308
|
+
);
|
|
20309
|
+
const practitionerSnapshots = await Promise.all(practitionerPromises);
|
|
20310
|
+
const practitioners = [];
|
|
20311
|
+
const sourceTechnologyId = (_a = sourceProcedure.technology) == null ? void 0 : _a.id;
|
|
20312
|
+
for (let i = 0; i < practitionerSnapshots.length; i++) {
|
|
20313
|
+
const snapshot = practitionerSnapshots[i];
|
|
20314
|
+
if (!snapshot.exists()) {
|
|
20315
|
+
throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
|
|
20316
|
+
}
|
|
20317
|
+
const practitioner = snapshot.data();
|
|
20318
|
+
if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
|
|
20319
|
+
throw new Error(
|
|
20320
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
|
|
20321
|
+
);
|
|
20322
|
+
}
|
|
20323
|
+
const existingProceduresQuery = (0, import_firestore58.query)(
|
|
20324
|
+
(0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
|
|
20325
|
+
(0, import_firestore58.where)("practitionerId", "==", practitioner.id),
|
|
20326
|
+
(0, import_firestore58.where)("clinicBranchId", "==", sourceProcedure.clinicBranchId),
|
|
20327
|
+
(0, import_firestore58.where)("isActive", "==", true)
|
|
20328
|
+
);
|
|
20329
|
+
const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
|
|
20330
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20331
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20332
|
+
(proc) => {
|
|
20333
|
+
var _a2;
|
|
20334
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === sourceTechnologyId;
|
|
20335
|
+
}
|
|
20336
|
+
);
|
|
20337
|
+
if (hasSameTechnology) {
|
|
20338
|
+
throw new Error(
|
|
20339
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_b = sourceProcedure.technology) == null ? void 0 : _b.name) || sourceTechnologyId}" in this clinic branch`
|
|
20340
|
+
);
|
|
20341
|
+
}
|
|
20342
|
+
practitioners.push(practitioner);
|
|
20343
|
+
}
|
|
20344
|
+
const batch = (0, import_firestore58.writeBatch)(this.db);
|
|
20345
|
+
const newProcedures = [];
|
|
20346
|
+
for (const practitioner of practitioners) {
|
|
20347
|
+
const newProcedureId = this.generateId();
|
|
20348
|
+
const doctorInfo = {
|
|
20349
|
+
id: practitioner.id,
|
|
20350
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
20351
|
+
description: practitioner.basicInfo.bio || "",
|
|
20352
|
+
photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
|
|
20353
|
+
rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
|
|
20354
|
+
services: practitioner.procedures || []
|
|
20355
|
+
};
|
|
20356
|
+
const newProcedure = {
|
|
20357
|
+
...sourceProcedure,
|
|
20358
|
+
id: newProcedureId,
|
|
20359
|
+
practitionerId: practitioner.id,
|
|
20360
|
+
doctorInfo,
|
|
20361
|
+
// Reset review info for the new procedure
|
|
20362
|
+
reviewInfo: {
|
|
20363
|
+
totalReviews: 0,
|
|
20364
|
+
averageRating: 0,
|
|
20365
|
+
effectivenessOfTreatment: 0,
|
|
20366
|
+
outcomeExplanation: 0,
|
|
20367
|
+
painManagement: 0,
|
|
20368
|
+
followUpCare: 0,
|
|
20369
|
+
valueForMoney: 0,
|
|
20370
|
+
recommendationPercentage: 0
|
|
20371
|
+
},
|
|
20372
|
+
// Apply any overrides if provided
|
|
20373
|
+
...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
|
|
20374
|
+
...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
|
|
20375
|
+
...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
|
|
20376
|
+
// Ensure it's active by default unless specified otherwise
|
|
20377
|
+
isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
|
|
20378
|
+
};
|
|
20379
|
+
newProcedures.push(newProcedure);
|
|
20380
|
+
const procedureRef = (0, import_firestore58.doc)(this.db, PROCEDURES_COLLECTION, newProcedureId);
|
|
20381
|
+
batch.set(procedureRef, {
|
|
20382
|
+
...newProcedure,
|
|
20383
|
+
createdAt: (0, import_firestore58.serverTimestamp)(),
|
|
20384
|
+
updatedAt: (0, import_firestore58.serverTimestamp)()
|
|
20385
|
+
});
|
|
20386
|
+
}
|
|
20387
|
+
await batch.commit();
|
|
20388
|
+
const createdProcedures = await Promise.all(
|
|
20389
|
+
newProcedures.map((p) => this.getProcedure(p.id))
|
|
20390
|
+
);
|
|
20391
|
+
return createdProcedures.filter((p) => p !== null);
|
|
20392
|
+
}
|
|
20140
20393
|
/**
|
|
20141
20394
|
* Creates multiple procedures for a list of practitioners based on common data.
|
|
20142
20395
|
* This method is optimized for bulk creation to reduce database reads and writes.
|
|
@@ -20221,6 +20474,40 @@ var ProcedureService = class extends BaseService {
|
|
|
20221
20474
|
const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
|
|
20222
20475
|
throw new Error(`The following practitioners were not found: ${notFoundIds.join(", ")}`);
|
|
20223
20476
|
}
|
|
20477
|
+
const duplicatePractitioners = [];
|
|
20478
|
+
const duplicateChecks = await Promise.all(
|
|
20479
|
+
practitionerIds.map(async (practitionerId) => {
|
|
20480
|
+
const existingProceduresQuery = (0, import_firestore58.query)(
|
|
20481
|
+
(0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION),
|
|
20482
|
+
(0, import_firestore58.where)("practitionerId", "==", practitionerId),
|
|
20483
|
+
(0, import_firestore58.where)("clinicBranchId", "==", validatedData.clinicBranchId),
|
|
20484
|
+
(0, import_firestore58.where)("isActive", "==", true)
|
|
20485
|
+
);
|
|
20486
|
+
const existingProceduresSnapshot = await (0, import_firestore58.getDocs)(existingProceduresQuery);
|
|
20487
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20488
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20489
|
+
(proc) => {
|
|
20490
|
+
var _a2;
|
|
20491
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
|
|
20492
|
+
}
|
|
20493
|
+
);
|
|
20494
|
+
return { practitionerId, hasSameTechnology };
|
|
20495
|
+
})
|
|
20496
|
+
);
|
|
20497
|
+
duplicateChecks.forEach(({ practitionerId, hasSameTechnology }) => {
|
|
20498
|
+
if (hasSameTechnology) {
|
|
20499
|
+
duplicatePractitioners.push(practitionerId);
|
|
20500
|
+
}
|
|
20501
|
+
});
|
|
20502
|
+
if (duplicatePractitioners.length > 0) {
|
|
20503
|
+
const duplicateNames = duplicatePractitioners.map((id) => {
|
|
20504
|
+
const practitioner = practitionersMap.get(id);
|
|
20505
|
+
return `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`;
|
|
20506
|
+
}).join(", ");
|
|
20507
|
+
throw new Error(
|
|
20508
|
+
`The following practitioner(s) already have a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch: ${duplicateNames}. Please remove them from the selection and try again.`
|
|
20509
|
+
);
|
|
20510
|
+
}
|
|
20224
20511
|
const batch = (0, import_firestore58.writeBatch)(this.db);
|
|
20225
20512
|
const createdProcedureIds = [];
|
|
20226
20513
|
const clinicInfo = {
|
package/dist/index.mjs
CHANGED
|
@@ -20271,6 +20271,25 @@ var ProcedureService = class extends BaseService {
|
|
|
20271
20271
|
throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
|
|
20272
20272
|
}
|
|
20273
20273
|
const practitioner = practitionerSnapshot.data();
|
|
20274
|
+
const existingProceduresQuery = query33(
|
|
20275
|
+
collection33(this.db, PROCEDURES_COLLECTION),
|
|
20276
|
+
where33("practitionerId", "==", validatedData.practitionerId),
|
|
20277
|
+
where33("clinicBranchId", "==", validatedData.clinicBranchId),
|
|
20278
|
+
where33("isActive", "==", true)
|
|
20279
|
+
);
|
|
20280
|
+
const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
|
|
20281
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20282
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20283
|
+
(proc) => {
|
|
20284
|
+
var _a2;
|
|
20285
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
|
|
20286
|
+
}
|
|
20287
|
+
);
|
|
20288
|
+
if (hasSameTechnology) {
|
|
20289
|
+
throw new Error(
|
|
20290
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch`
|
|
20291
|
+
);
|
|
20292
|
+
}
|
|
20274
20293
|
let processedPhotos = [];
|
|
20275
20294
|
if (validatedData.photos && validatedData.photos.length > 0) {
|
|
20276
20295
|
processedPhotos = await this.processMediaArray(
|
|
@@ -20373,6 +20392,240 @@ var ProcedureService = class extends BaseService {
|
|
|
20373
20392
|
const savedDoc = await getDoc40(procedureRef);
|
|
20374
20393
|
return savedDoc.data();
|
|
20375
20394
|
}
|
|
20395
|
+
/**
|
|
20396
|
+
* Validates if a practitioner can perform a procedure based on certification requirements.
|
|
20397
|
+
*
|
|
20398
|
+
* @param procedure - The procedure to check
|
|
20399
|
+
* @param practitioner - The practitioner to validate
|
|
20400
|
+
* @returns true if practitioner can perform the procedure, false otherwise
|
|
20401
|
+
*/
|
|
20402
|
+
canPractitionerPerformProcedure(procedure, practitioner) {
|
|
20403
|
+
if (!practitioner.certification) {
|
|
20404
|
+
return false;
|
|
20405
|
+
}
|
|
20406
|
+
const requiredCert = procedure.certificationRequirement;
|
|
20407
|
+
const practitionerCert = practitioner.certification;
|
|
20408
|
+
const levelOrder = [
|
|
20409
|
+
"aesthetician",
|
|
20410
|
+
"nurse_assistant",
|
|
20411
|
+
"nurse",
|
|
20412
|
+
"nurse_practitioner",
|
|
20413
|
+
"physician_assistant",
|
|
20414
|
+
"doctor",
|
|
20415
|
+
"specialist",
|
|
20416
|
+
"plastic_surgeon"
|
|
20417
|
+
];
|
|
20418
|
+
const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
|
|
20419
|
+
const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
|
|
20420
|
+
if (practitionerLevelIndex < requiredLevelIndex) {
|
|
20421
|
+
return false;
|
|
20422
|
+
}
|
|
20423
|
+
const requiredSpecialties = requiredCert.requiredSpecialties || [];
|
|
20424
|
+
if (requiredSpecialties.length > 0) {
|
|
20425
|
+
const practitionerSpecialties = practitionerCert.specialties || [];
|
|
20426
|
+
const hasAllRequired = requiredSpecialties.every(
|
|
20427
|
+
(specialty) => practitionerSpecialties.includes(specialty)
|
|
20428
|
+
);
|
|
20429
|
+
if (!hasAllRequired) {
|
|
20430
|
+
return false;
|
|
20431
|
+
}
|
|
20432
|
+
}
|
|
20433
|
+
return true;
|
|
20434
|
+
}
|
|
20435
|
+
/**
|
|
20436
|
+
* Clones an existing procedure for a target practitioner.
|
|
20437
|
+
* This creates a new procedure document with the same data as the source procedure,
|
|
20438
|
+
* but linked to the target practitioner.
|
|
20439
|
+
*
|
|
20440
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
20441
|
+
* @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
|
|
20442
|
+
* @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
|
|
20443
|
+
* @returns The newly created procedure
|
|
20444
|
+
*/
|
|
20445
|
+
async cloneProcedureForPractitioner(sourceProcedureId, targetPractitionerId, overrides) {
|
|
20446
|
+
var _a, _b, _c;
|
|
20447
|
+
const sourceProcedure = await this.getProcedure(sourceProcedureId);
|
|
20448
|
+
if (!sourceProcedure) {
|
|
20449
|
+
throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
|
|
20450
|
+
}
|
|
20451
|
+
const practitionerRef = doc39(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
|
|
20452
|
+
const practitionerSnapshot = await getDoc40(practitionerRef);
|
|
20453
|
+
if (!practitionerSnapshot.exists()) {
|
|
20454
|
+
throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
|
|
20455
|
+
}
|
|
20456
|
+
const practitioner = practitionerSnapshot.data();
|
|
20457
|
+
if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
|
|
20458
|
+
throw new Error(
|
|
20459
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
|
|
20460
|
+
);
|
|
20461
|
+
}
|
|
20462
|
+
const existingProceduresQuery = query33(
|
|
20463
|
+
collection33(this.db, PROCEDURES_COLLECTION),
|
|
20464
|
+
where33("practitionerId", "==", targetPractitionerId),
|
|
20465
|
+
where33("clinicBranchId", "==", sourceProcedure.clinicBranchId),
|
|
20466
|
+
where33("isActive", "==", true)
|
|
20467
|
+
);
|
|
20468
|
+
const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
|
|
20469
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20470
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20471
|
+
(proc) => {
|
|
20472
|
+
var _a2, _b2;
|
|
20473
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === ((_b2 = sourceProcedure.technology) == null ? void 0 : _b2.id);
|
|
20474
|
+
}
|
|
20475
|
+
);
|
|
20476
|
+
if (hasSameTechnology) {
|
|
20477
|
+
throw new Error(
|
|
20478
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_a = sourceProcedure.technology) == null ? void 0 : _a.name) || ((_b = sourceProcedure.technology) == null ? void 0 : _b.id)}" in this clinic branch`
|
|
20479
|
+
);
|
|
20480
|
+
}
|
|
20481
|
+
const newProcedureId = this.generateId();
|
|
20482
|
+
const doctorInfo = {
|
|
20483
|
+
id: practitioner.id,
|
|
20484
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
20485
|
+
description: practitioner.basicInfo.bio || "",
|
|
20486
|
+
photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
|
|
20487
|
+
rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
|
|
20488
|
+
services: practitioner.procedures || []
|
|
20489
|
+
};
|
|
20490
|
+
const newProcedure = {
|
|
20491
|
+
...sourceProcedure,
|
|
20492
|
+
id: newProcedureId,
|
|
20493
|
+
practitionerId: targetPractitionerId,
|
|
20494
|
+
doctorInfo,
|
|
20495
|
+
// Link to new doctor
|
|
20496
|
+
// Reset review info for the new procedure
|
|
20497
|
+
reviewInfo: {
|
|
20498
|
+
totalReviews: 0,
|
|
20499
|
+
averageRating: 0,
|
|
20500
|
+
effectivenessOfTreatment: 0,
|
|
20501
|
+
outcomeExplanation: 0,
|
|
20502
|
+
painManagement: 0,
|
|
20503
|
+
followUpCare: 0,
|
|
20504
|
+
valueForMoney: 0,
|
|
20505
|
+
recommendationPercentage: 0
|
|
20506
|
+
},
|
|
20507
|
+
// Apply any overrides if provided
|
|
20508
|
+
...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
|
|
20509
|
+
...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
|
|
20510
|
+
...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
|
|
20511
|
+
// Ensure it's active by default unless specified otherwise
|
|
20512
|
+
isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
|
|
20513
|
+
};
|
|
20514
|
+
const procedureRef = doc39(this.db, PROCEDURES_COLLECTION, newProcedureId);
|
|
20515
|
+
await setDoc27(procedureRef, {
|
|
20516
|
+
...newProcedure,
|
|
20517
|
+
createdAt: serverTimestamp31(),
|
|
20518
|
+
updatedAt: serverTimestamp31()
|
|
20519
|
+
});
|
|
20520
|
+
const savedDoc = await getDoc40(procedureRef);
|
|
20521
|
+
return savedDoc.data();
|
|
20522
|
+
}
|
|
20523
|
+
/**
|
|
20524
|
+
* Clones an existing procedure for multiple target practitioners.
|
|
20525
|
+
* This creates new procedure documents with the same data as the source procedure,
|
|
20526
|
+
* but linked to each target practitioner.
|
|
20527
|
+
*
|
|
20528
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
20529
|
+
* @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
|
|
20530
|
+
* @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
|
|
20531
|
+
* @returns Array of newly created procedures
|
|
20532
|
+
*/
|
|
20533
|
+
async bulkCloneProcedureForPractitioners(sourceProcedureId, targetPractitionerIds, overrides) {
|
|
20534
|
+
var _a, _b, _c;
|
|
20535
|
+
if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
|
|
20536
|
+
throw new Error("At least one target practitioner ID is required");
|
|
20537
|
+
}
|
|
20538
|
+
const sourceProcedure = await this.getProcedure(sourceProcedureId);
|
|
20539
|
+
if (!sourceProcedure) {
|
|
20540
|
+
throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
|
|
20541
|
+
}
|
|
20542
|
+
const practitionerPromises = targetPractitionerIds.map(
|
|
20543
|
+
(id) => getDoc40(doc39(this.db, PRACTITIONERS_COLLECTION, id))
|
|
20544
|
+
);
|
|
20545
|
+
const practitionerSnapshots = await Promise.all(practitionerPromises);
|
|
20546
|
+
const practitioners = [];
|
|
20547
|
+
const sourceTechnologyId = (_a = sourceProcedure.technology) == null ? void 0 : _a.id;
|
|
20548
|
+
for (let i = 0; i < practitionerSnapshots.length; i++) {
|
|
20549
|
+
const snapshot = practitionerSnapshots[i];
|
|
20550
|
+
if (!snapshot.exists()) {
|
|
20551
|
+
throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
|
|
20552
|
+
}
|
|
20553
|
+
const practitioner = snapshot.data();
|
|
20554
|
+
if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
|
|
20555
|
+
throw new Error(
|
|
20556
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
|
|
20557
|
+
);
|
|
20558
|
+
}
|
|
20559
|
+
const existingProceduresQuery = query33(
|
|
20560
|
+
collection33(this.db, PROCEDURES_COLLECTION),
|
|
20561
|
+
where33("practitionerId", "==", practitioner.id),
|
|
20562
|
+
where33("clinicBranchId", "==", sourceProcedure.clinicBranchId),
|
|
20563
|
+
where33("isActive", "==", true)
|
|
20564
|
+
);
|
|
20565
|
+
const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
|
|
20566
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20567
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20568
|
+
(proc) => {
|
|
20569
|
+
var _a2;
|
|
20570
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === sourceTechnologyId;
|
|
20571
|
+
}
|
|
20572
|
+
);
|
|
20573
|
+
if (hasSameTechnology) {
|
|
20574
|
+
throw new Error(
|
|
20575
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${((_b = sourceProcedure.technology) == null ? void 0 : _b.name) || sourceTechnologyId}" in this clinic branch`
|
|
20576
|
+
);
|
|
20577
|
+
}
|
|
20578
|
+
practitioners.push(practitioner);
|
|
20579
|
+
}
|
|
20580
|
+
const batch = writeBatch6(this.db);
|
|
20581
|
+
const newProcedures = [];
|
|
20582
|
+
for (const practitioner of practitioners) {
|
|
20583
|
+
const newProcedureId = this.generateId();
|
|
20584
|
+
const doctorInfo = {
|
|
20585
|
+
id: practitioner.id,
|
|
20586
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
20587
|
+
description: practitioner.basicInfo.bio || "",
|
|
20588
|
+
photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
|
|
20589
|
+
rating: ((_c = practitioner.reviewInfo) == null ? void 0 : _c.averageRating) || 0,
|
|
20590
|
+
services: practitioner.procedures || []
|
|
20591
|
+
};
|
|
20592
|
+
const newProcedure = {
|
|
20593
|
+
...sourceProcedure,
|
|
20594
|
+
id: newProcedureId,
|
|
20595
|
+
practitionerId: practitioner.id,
|
|
20596
|
+
doctorInfo,
|
|
20597
|
+
// Reset review info for the new procedure
|
|
20598
|
+
reviewInfo: {
|
|
20599
|
+
totalReviews: 0,
|
|
20600
|
+
averageRating: 0,
|
|
20601
|
+
effectivenessOfTreatment: 0,
|
|
20602
|
+
outcomeExplanation: 0,
|
|
20603
|
+
painManagement: 0,
|
|
20604
|
+
followUpCare: 0,
|
|
20605
|
+
valueForMoney: 0,
|
|
20606
|
+
recommendationPercentage: 0
|
|
20607
|
+
},
|
|
20608
|
+
// Apply any overrides if provided
|
|
20609
|
+
...(overrides == null ? void 0 : overrides.price) !== void 0 && { price: overrides.price },
|
|
20610
|
+
...(overrides == null ? void 0 : overrides.duration) !== void 0 && { duration: overrides.duration },
|
|
20611
|
+
...(overrides == null ? void 0 : overrides.description) !== void 0 && { description: overrides.description },
|
|
20612
|
+
// Ensure it's active by default unless specified otherwise
|
|
20613
|
+
isActive: (overrides == null ? void 0 : overrides.isActive) !== void 0 ? overrides.isActive : true
|
|
20614
|
+
};
|
|
20615
|
+
newProcedures.push(newProcedure);
|
|
20616
|
+
const procedureRef = doc39(this.db, PROCEDURES_COLLECTION, newProcedureId);
|
|
20617
|
+
batch.set(procedureRef, {
|
|
20618
|
+
...newProcedure,
|
|
20619
|
+
createdAt: serverTimestamp31(),
|
|
20620
|
+
updatedAt: serverTimestamp31()
|
|
20621
|
+
});
|
|
20622
|
+
}
|
|
20623
|
+
await batch.commit();
|
|
20624
|
+
const createdProcedures = await Promise.all(
|
|
20625
|
+
newProcedures.map((p) => this.getProcedure(p.id))
|
|
20626
|
+
);
|
|
20627
|
+
return createdProcedures.filter((p) => p !== null);
|
|
20628
|
+
}
|
|
20376
20629
|
/**
|
|
20377
20630
|
* Creates multiple procedures for a list of practitioners based on common data.
|
|
20378
20631
|
* This method is optimized for bulk creation to reduce database reads and writes.
|
|
@@ -20457,6 +20710,40 @@ var ProcedureService = class extends BaseService {
|
|
|
20457
20710
|
const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
|
|
20458
20711
|
throw new Error(`The following practitioners were not found: ${notFoundIds.join(", ")}`);
|
|
20459
20712
|
}
|
|
20713
|
+
const duplicatePractitioners = [];
|
|
20714
|
+
const duplicateChecks = await Promise.all(
|
|
20715
|
+
practitionerIds.map(async (practitionerId) => {
|
|
20716
|
+
const existingProceduresQuery = query33(
|
|
20717
|
+
collection33(this.db, PROCEDURES_COLLECTION),
|
|
20718
|
+
where33("practitionerId", "==", practitionerId),
|
|
20719
|
+
where33("clinicBranchId", "==", validatedData.clinicBranchId),
|
|
20720
|
+
where33("isActive", "==", true)
|
|
20721
|
+
);
|
|
20722
|
+
const existingProceduresSnapshot = await getDocs33(existingProceduresQuery);
|
|
20723
|
+
const existingProcedures = existingProceduresSnapshot.docs.map((doc47) => doc47.data());
|
|
20724
|
+
const hasSameTechnology = existingProcedures.some(
|
|
20725
|
+
(proc) => {
|
|
20726
|
+
var _a2;
|
|
20727
|
+
return ((_a2 = proc.technology) == null ? void 0 : _a2.id) === validatedData.technologyId;
|
|
20728
|
+
}
|
|
20729
|
+
);
|
|
20730
|
+
return { practitionerId, hasSameTechnology };
|
|
20731
|
+
})
|
|
20732
|
+
);
|
|
20733
|
+
duplicateChecks.forEach(({ practitionerId, hasSameTechnology }) => {
|
|
20734
|
+
if (hasSameTechnology) {
|
|
20735
|
+
duplicatePractitioners.push(practitionerId);
|
|
20736
|
+
}
|
|
20737
|
+
});
|
|
20738
|
+
if (duplicatePractitioners.length > 0) {
|
|
20739
|
+
const duplicateNames = duplicatePractitioners.map((id) => {
|
|
20740
|
+
const practitioner = practitionersMap.get(id);
|
|
20741
|
+
return `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`;
|
|
20742
|
+
}).join(", ");
|
|
20743
|
+
throw new Error(
|
|
20744
|
+
`The following practitioner(s) already have a procedure with technology "${(technology == null ? void 0 : technology.name) || validatedData.technologyId}" in this clinic branch: ${duplicateNames}. Please remove them from the selection and try again.`
|
|
20745
|
+
);
|
|
20746
|
+
}
|
|
20460
20747
|
const batch = writeBatch6(this.db);
|
|
20461
20748
|
const createdProcedureIds = [];
|
|
20462
20749
|
const clinicInfo = {
|
package/package.json
CHANGED
|
@@ -244,6 +244,25 @@ export class ProcedureService extends BaseService {
|
|
|
244
244
|
}
|
|
245
245
|
const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
|
|
246
246
|
|
|
247
|
+
// Check if practitioner already has a procedure with the same technology ID in this clinic branch
|
|
248
|
+
const existingProceduresQuery = query(
|
|
249
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
250
|
+
where('practitionerId', '==', validatedData.practitionerId),
|
|
251
|
+
where('clinicBranchId', '==', validatedData.clinicBranchId),
|
|
252
|
+
where('isActive', '==', true)
|
|
253
|
+
);
|
|
254
|
+
const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
|
|
255
|
+
const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
|
|
256
|
+
|
|
257
|
+
const hasSameTechnology = existingProcedures.some(
|
|
258
|
+
proc => proc.technology?.id === validatedData.technologyId
|
|
259
|
+
);
|
|
260
|
+
if (hasSameTechnology) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${technology?.name || validatedData.technologyId}" in this clinic branch`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
// Process photos if provided
|
|
248
267
|
let processedPhotos: string[] = [];
|
|
249
268
|
if (validatedData.photos && validatedData.photos.length > 0) {
|
|
@@ -370,6 +389,308 @@ export class ProcedureService extends BaseService {
|
|
|
370
389
|
return savedDoc.data() as Procedure;
|
|
371
390
|
}
|
|
372
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Validates if a practitioner can perform a procedure based on certification requirements.
|
|
394
|
+
*
|
|
395
|
+
* @param procedure - The procedure to check
|
|
396
|
+
* @param practitioner - The practitioner to validate
|
|
397
|
+
* @returns true if practitioner can perform the procedure, false otherwise
|
|
398
|
+
*/
|
|
399
|
+
canPractitionerPerformProcedure(procedure: Procedure, practitioner: Practitioner): boolean {
|
|
400
|
+
if (!practitioner.certification) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const requiredCert = procedure.certificationRequirement;
|
|
405
|
+
const practitionerCert = practitioner.certification;
|
|
406
|
+
|
|
407
|
+
// Check certification level
|
|
408
|
+
const levelOrder = [
|
|
409
|
+
'aesthetician',
|
|
410
|
+
'nurse_assistant',
|
|
411
|
+
'nurse',
|
|
412
|
+
'nurse_practitioner',
|
|
413
|
+
'physician_assistant',
|
|
414
|
+
'doctor',
|
|
415
|
+
'specialist',
|
|
416
|
+
'plastic_surgeon',
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
const practitionerLevelIndex = levelOrder.indexOf(practitionerCert.level);
|
|
420
|
+
const requiredLevelIndex = levelOrder.indexOf(requiredCert.minimumLevel);
|
|
421
|
+
|
|
422
|
+
if (practitionerLevelIndex < requiredLevelIndex) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check required specialties
|
|
427
|
+
const requiredSpecialties = requiredCert.requiredSpecialties || [];
|
|
428
|
+
if (requiredSpecialties.length > 0) {
|
|
429
|
+
const practitionerSpecialties = practitionerCert.specialties || [];
|
|
430
|
+
const hasAllRequired = requiredSpecialties.every(specialty =>
|
|
431
|
+
practitionerSpecialties.includes(specialty)
|
|
432
|
+
);
|
|
433
|
+
if (!hasAllRequired) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Clones an existing procedure for a target practitioner.
|
|
443
|
+
* This creates a new procedure document with the same data as the source procedure,
|
|
444
|
+
* but linked to the target practitioner.
|
|
445
|
+
*
|
|
446
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
447
|
+
* @param targetPractitionerId - The ID of the practitioner to assign the cloned procedure to
|
|
448
|
+
* @param overrides - Optional overrides for the new procedure (e.g. price, duration, isActive)
|
|
449
|
+
* @returns The newly created procedure
|
|
450
|
+
*/
|
|
451
|
+
async cloneProcedureForPractitioner(
|
|
452
|
+
sourceProcedureId: string,
|
|
453
|
+
targetPractitionerId: string,
|
|
454
|
+
overrides?: Partial<CreateProcedureData> & { isActive?: boolean }
|
|
455
|
+
): Promise<Procedure> {
|
|
456
|
+
// 1. Fetch source procedure
|
|
457
|
+
const sourceProcedure = await this.getProcedure(sourceProcedureId);
|
|
458
|
+
if (!sourceProcedure) {
|
|
459
|
+
throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 2. Fetch target practitioner
|
|
463
|
+
const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, targetPractitionerId);
|
|
464
|
+
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
465
|
+
if (!practitionerSnapshot.exists()) {
|
|
466
|
+
throw new Error(`Target practitioner with ID ${targetPractitionerId} not found`);
|
|
467
|
+
}
|
|
468
|
+
const practitioner = practitionerSnapshot.data() as Practitioner;
|
|
469
|
+
|
|
470
|
+
// 3. Validate certification
|
|
471
|
+
if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 4. Check if practitioner already has a procedure with the same technology ID in this clinic branch
|
|
478
|
+
const existingProceduresQuery = query(
|
|
479
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
480
|
+
where('practitionerId', '==', targetPractitionerId),
|
|
481
|
+
where('clinicBranchId', '==', sourceProcedure.clinicBranchId),
|
|
482
|
+
where('isActive', '==', true)
|
|
483
|
+
);
|
|
484
|
+
const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
|
|
485
|
+
const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
|
|
486
|
+
|
|
487
|
+
const hasSameTechnology = existingProcedures.some(
|
|
488
|
+
proc => proc.technology?.id === sourceProcedure.technology?.id
|
|
489
|
+
);
|
|
490
|
+
if (hasSameTechnology) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${sourceProcedure.technology?.name || sourceProcedure.technology?.id}" in this clinic branch`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 5. Prepare data for new procedure
|
|
497
|
+
const newProcedureId = this.generateId();
|
|
498
|
+
|
|
499
|
+
// Create aggregated doctor info for the new procedure
|
|
500
|
+
const doctorInfo = {
|
|
501
|
+
id: practitioner.id,
|
|
502
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
503
|
+
description: practitioner.basicInfo.bio || '',
|
|
504
|
+
photo:
|
|
505
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
506
|
+
? practitioner.basicInfo.profileImageUrl
|
|
507
|
+
: '',
|
|
508
|
+
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
509
|
+
services: practitioner.procedures || [],
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Construct the new procedure object
|
|
513
|
+
// We copy everything from source, but override specific fields
|
|
514
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
515
|
+
...sourceProcedure,
|
|
516
|
+
id: newProcedureId,
|
|
517
|
+
practitionerId: targetPractitionerId,
|
|
518
|
+
doctorInfo, // Link to new doctor
|
|
519
|
+
|
|
520
|
+
// Reset review info for the new procedure
|
|
521
|
+
reviewInfo: {
|
|
522
|
+
totalReviews: 0,
|
|
523
|
+
averageRating: 0,
|
|
524
|
+
effectivenessOfTreatment: 0,
|
|
525
|
+
outcomeExplanation: 0,
|
|
526
|
+
painManagement: 0,
|
|
527
|
+
followUpCare: 0,
|
|
528
|
+
valueForMoney: 0,
|
|
529
|
+
recommendationPercentage: 0,
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
// Apply any overrides if provided
|
|
533
|
+
...(overrides?.price !== undefined && { price: overrides.price }),
|
|
534
|
+
...(overrides?.duration !== undefined && { duration: overrides.duration }),
|
|
535
|
+
...(overrides?.description !== undefined && { description: overrides.description }),
|
|
536
|
+
|
|
537
|
+
// Ensure it's active by default unless specified otherwise
|
|
538
|
+
isActive: overrides?.isActive !== undefined ? overrides.isActive : true,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// 6. Save to Firestore
|
|
542
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, newProcedureId);
|
|
543
|
+
await setDoc(procedureRef, {
|
|
544
|
+
...newProcedure,
|
|
545
|
+
createdAt: serverTimestamp(),
|
|
546
|
+
updatedAt: serverTimestamp(),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// 7. Return the new procedure
|
|
550
|
+
const savedDoc = await getDoc(procedureRef);
|
|
551
|
+
return savedDoc.data() as Procedure;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Clones an existing procedure for multiple target practitioners.
|
|
556
|
+
* This creates new procedure documents with the same data as the source procedure,
|
|
557
|
+
* but linked to each target practitioner.
|
|
558
|
+
*
|
|
559
|
+
* @param sourceProcedureId - The ID of the procedure to clone
|
|
560
|
+
* @param targetPractitionerIds - Array of practitioner IDs to assign the cloned procedure to
|
|
561
|
+
* @param overrides - Optional overrides for the new procedures (e.g. price, duration, isActive)
|
|
562
|
+
* @returns Array of newly created procedures
|
|
563
|
+
*/
|
|
564
|
+
async bulkCloneProcedureForPractitioners(
|
|
565
|
+
sourceProcedureId: string,
|
|
566
|
+
targetPractitionerIds: string[],
|
|
567
|
+
overrides?: Partial<CreateProcedureData> & { isActive?: boolean }
|
|
568
|
+
): Promise<Procedure[]> {
|
|
569
|
+
if (!targetPractitionerIds || targetPractitionerIds.length === 0) {
|
|
570
|
+
throw new Error('At least one target practitioner ID is required');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 1. Fetch source procedure
|
|
574
|
+
const sourceProcedure = await this.getProcedure(sourceProcedureId);
|
|
575
|
+
if (!sourceProcedure) {
|
|
576
|
+
throw new Error(`Source procedure with ID ${sourceProcedureId} not found`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 2. Fetch all target practitioners
|
|
580
|
+
const practitionerPromises = targetPractitionerIds.map(id =>
|
|
581
|
+
getDoc(doc(this.db, PRACTITIONERS_COLLECTION, id))
|
|
582
|
+
);
|
|
583
|
+
const practitionerSnapshots = await Promise.all(practitionerPromises);
|
|
584
|
+
|
|
585
|
+
// 3. Validate all practitioners exist, can perform the procedure, and don't already have the same technology
|
|
586
|
+
const practitioners: Practitioner[] = [];
|
|
587
|
+
const sourceTechnologyId = sourceProcedure.technology?.id;
|
|
588
|
+
|
|
589
|
+
for (let i = 0; i < practitionerSnapshots.length; i++) {
|
|
590
|
+
const snapshot = practitionerSnapshots[i];
|
|
591
|
+
if (!snapshot.exists()) {
|
|
592
|
+
throw new Error(`Target practitioner with ID ${targetPractitionerIds[i]} not found`);
|
|
593
|
+
}
|
|
594
|
+
const practitioner = snapshot.data() as Practitioner;
|
|
595
|
+
|
|
596
|
+
if (!this.canPractitionerPerformProcedure(sourceProcedure, practitioner)) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} does not meet the certification requirements for this procedure`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check if practitioner already has a procedure with the same technology ID in this clinic branch
|
|
603
|
+
const existingProceduresQuery = query(
|
|
604
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
605
|
+
where('practitionerId', '==', practitioner.id),
|
|
606
|
+
where('clinicBranchId', '==', sourceProcedure.clinicBranchId),
|
|
607
|
+
where('isActive', '==', true)
|
|
608
|
+
);
|
|
609
|
+
const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
|
|
610
|
+
const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
|
|
611
|
+
|
|
612
|
+
const hasSameTechnology = existingProcedures.some(
|
|
613
|
+
proc => proc.technology?.id === sourceTechnologyId
|
|
614
|
+
);
|
|
615
|
+
if (hasSameTechnology) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`Practitioner ${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName} already has a procedure with technology "${sourceProcedure.technology?.name || sourceTechnologyId}" in this clinic branch`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
practitioners.push(practitioner);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 4. Create procedures in batch
|
|
625
|
+
const batch = writeBatch(this.db);
|
|
626
|
+
const newProcedures: Omit<Procedure, 'createdAt' | 'updatedAt'>[] = [];
|
|
627
|
+
|
|
628
|
+
for (const practitioner of practitioners) {
|
|
629
|
+
const newProcedureId = this.generateId();
|
|
630
|
+
|
|
631
|
+
// Create aggregated doctor info for the new procedure
|
|
632
|
+
const doctorInfo = {
|
|
633
|
+
id: practitioner.id,
|
|
634
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
635
|
+
description: practitioner.basicInfo.bio || '',
|
|
636
|
+
photo:
|
|
637
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
638
|
+
? practitioner.basicInfo.profileImageUrl
|
|
639
|
+
: '',
|
|
640
|
+
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
641
|
+
services: practitioner.procedures || [],
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// Construct the new procedure object
|
|
645
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
646
|
+
...sourceProcedure,
|
|
647
|
+
id: newProcedureId,
|
|
648
|
+
practitionerId: practitioner.id,
|
|
649
|
+
doctorInfo,
|
|
650
|
+
|
|
651
|
+
// Reset review info for the new procedure
|
|
652
|
+
reviewInfo: {
|
|
653
|
+
totalReviews: 0,
|
|
654
|
+
averageRating: 0,
|
|
655
|
+
effectivenessOfTreatment: 0,
|
|
656
|
+
outcomeExplanation: 0,
|
|
657
|
+
painManagement: 0,
|
|
658
|
+
followUpCare: 0,
|
|
659
|
+
valueForMoney: 0,
|
|
660
|
+
recommendationPercentage: 0,
|
|
661
|
+
},
|
|
662
|
+
|
|
663
|
+
// Apply any overrides if provided
|
|
664
|
+
...(overrides?.price !== undefined && { price: overrides.price }),
|
|
665
|
+
...(overrides?.duration !== undefined && { duration: overrides.duration }),
|
|
666
|
+
...(overrides?.description !== undefined && { description: overrides.description }),
|
|
667
|
+
|
|
668
|
+
// Ensure it's active by default unless specified otherwise
|
|
669
|
+
isActive: overrides?.isActive !== undefined ? overrides.isActive : true,
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
newProcedures.push(newProcedure);
|
|
673
|
+
|
|
674
|
+
// Add to batch
|
|
675
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, newProcedureId);
|
|
676
|
+
batch.set(procedureRef, {
|
|
677
|
+
...newProcedure,
|
|
678
|
+
createdAt: serverTimestamp(),
|
|
679
|
+
updatedAt: serverTimestamp(),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 5. Commit batch
|
|
684
|
+
await batch.commit();
|
|
685
|
+
|
|
686
|
+
// 6. Fetch and return the created procedures
|
|
687
|
+
const createdProcedures = await Promise.all(
|
|
688
|
+
newProcedures.map(p => this.getProcedure(p.id))
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
return createdProcedures.filter((p): p is Procedure => p !== null);
|
|
692
|
+
}
|
|
693
|
+
|
|
373
694
|
/**
|
|
374
695
|
* Creates multiple procedures for a list of practitioners based on common data.
|
|
375
696
|
* This method is optimized for bulk creation to reduce database reads and writes.
|
|
@@ -485,7 +806,49 @@ export class ProcedureService extends BaseService {
|
|
|
485
806
|
throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
|
|
486
807
|
}
|
|
487
808
|
|
|
488
|
-
// 5.
|
|
809
|
+
// 5. Check for duplicates across all practitioners before creating any procedures
|
|
810
|
+
const duplicatePractitioners: string[] = [];
|
|
811
|
+
const duplicateChecks = await Promise.all(
|
|
812
|
+
practitionerIds.map(async (practitionerId) => {
|
|
813
|
+
const existingProceduresQuery = query(
|
|
814
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
815
|
+
where('practitionerId', '==', practitionerId),
|
|
816
|
+
where('clinicBranchId', '==', validatedData.clinicBranchId),
|
|
817
|
+
where('isActive', '==', true)
|
|
818
|
+
);
|
|
819
|
+
const existingProceduresSnapshot = await getDocs(existingProceduresQuery);
|
|
820
|
+
const existingProcedures = existingProceduresSnapshot.docs.map(doc => doc.data() as Procedure);
|
|
821
|
+
|
|
822
|
+
const hasSameTechnology = existingProcedures.some(
|
|
823
|
+
proc => proc.technology?.id === validatedData.technologyId
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
return { practitionerId, hasSameTechnology };
|
|
827
|
+
})
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
// Collect all practitioners with duplicates
|
|
831
|
+
duplicateChecks.forEach(({ practitionerId, hasSameTechnology }) => {
|
|
832
|
+
if (hasSameTechnology) {
|
|
833
|
+
duplicatePractitioners.push(practitionerId);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// If any duplicates found, throw error listing all of them
|
|
838
|
+
if (duplicatePractitioners.length > 0) {
|
|
839
|
+
const duplicateNames = duplicatePractitioners
|
|
840
|
+
.map(id => {
|
|
841
|
+
const practitioner = practitionersMap.get(id)!;
|
|
842
|
+
return `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`;
|
|
843
|
+
})
|
|
844
|
+
.join(', ');
|
|
845
|
+
|
|
846
|
+
throw new Error(
|
|
847
|
+
`The following practitioner(s) already have a procedure with technology "${technology?.name || validatedData.technologyId}" in this clinic branch: ${duplicateNames}. Please remove them from the selection and try again.`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// 6. Use a Firestore batch for atomic creation
|
|
489
852
|
const batch = writeBatch(this.db);
|
|
490
853
|
const createdProcedureIds: string[] = [];
|
|
491
854
|
const clinicInfo = {
|
|
@@ -585,10 +948,10 @@ export class ProcedureService extends BaseService {
|
|
|
585
948
|
});
|
|
586
949
|
}
|
|
587
950
|
|
|
588
|
-
//
|
|
951
|
+
// 7. Commit the atomic batch write
|
|
589
952
|
await batch.commit();
|
|
590
953
|
|
|
591
|
-
//
|
|
954
|
+
// 8. Fetch and return the newly created procedures
|
|
592
955
|
const fetchedProcedures: Procedure[] = [];
|
|
593
956
|
for (let i = 0; i < createdProcedureIds.length; i += 30) {
|
|
594
957
|
const chunk = createdProcedureIds.slice(i, i + 30);
|