@blackcode_sa/metaestetics-api 1.5.25 → 1.5.28
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 +87 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +461 -0
- package/dist/index.mjs +470 -1
- package/package.json +1 -1
- package/src/services/calendar/calendar-refactored.service.ts +2 -0
- package/src/services/clinic/clinic.service.ts +54 -0
- package/src/services/clinic/utils/clinic.utils.ts +230 -0
- package/src/services/procedure/procedure.service.ts +380 -1
- package/src/types/procedure/index.ts +7 -0
- package/src/validations/procedure.schema.ts +3 -0
|
@@ -768,3 +768,233 @@ export async function getActiveClinicsByAdmin(
|
|
|
768
768
|
clinicGroupService
|
|
769
769
|
);
|
|
770
770
|
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Retrieves a clinic by its ID
|
|
774
|
+
*
|
|
775
|
+
* @param db - Firestore database instance
|
|
776
|
+
* @param clinicId - ID of the clinic to retrieve
|
|
777
|
+
* @returns The clinic if found, null otherwise
|
|
778
|
+
*/
|
|
779
|
+
export async function getClinicById(
|
|
780
|
+
db: Firestore,
|
|
781
|
+
clinicId: string
|
|
782
|
+
): Promise<Clinic | null> {
|
|
783
|
+
try {
|
|
784
|
+
const clinicRef = doc(db, CLINICS_COLLECTION, clinicId);
|
|
785
|
+
const clinicSnapshot = await getDoc(clinicRef);
|
|
786
|
+
|
|
787
|
+
if (!clinicSnapshot.exists()) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const clinicData = clinicSnapshot.data() as Clinic;
|
|
792
|
+
return {
|
|
793
|
+
...clinicData,
|
|
794
|
+
id: clinicSnapshot.id,
|
|
795
|
+
};
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Retrieves all clinics with optional pagination
|
|
804
|
+
*
|
|
805
|
+
* @param db - Firestore database instance
|
|
806
|
+
* @param pagination - Optional number of clinics per page (0 or undefined returns all)
|
|
807
|
+
* @param lastDoc - Optional last document for pagination (if continuing from a previous page)
|
|
808
|
+
* @returns Array of clinics and the last document for pagination
|
|
809
|
+
*/
|
|
810
|
+
export async function getAllClinics(
|
|
811
|
+
db: Firestore,
|
|
812
|
+
pagination?: number,
|
|
813
|
+
lastDoc?: any
|
|
814
|
+
): Promise<{ clinics: Clinic[]; lastDoc: any }> {
|
|
815
|
+
try {
|
|
816
|
+
const clinicsCollection = collection(db, CLINICS_COLLECTION);
|
|
817
|
+
let clinicsQuery = query(clinicsCollection);
|
|
818
|
+
|
|
819
|
+
// If pagination is specified and greater than 0, limit the query
|
|
820
|
+
if (pagination && pagination > 0) {
|
|
821
|
+
const { limit, startAfter } = require("firebase/firestore");
|
|
822
|
+
|
|
823
|
+
if (lastDoc) {
|
|
824
|
+
clinicsQuery = query(
|
|
825
|
+
clinicsCollection,
|
|
826
|
+
startAfter(lastDoc),
|
|
827
|
+
limit(pagination)
|
|
828
|
+
);
|
|
829
|
+
} else {
|
|
830
|
+
clinicsQuery = query(clinicsCollection, limit(pagination));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const clinicsSnapshot = await getDocs(clinicsQuery);
|
|
835
|
+
const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
|
|
836
|
+
|
|
837
|
+
const clinics = clinicsSnapshot.docs.map((doc) => {
|
|
838
|
+
const data = doc.data() as Clinic;
|
|
839
|
+
return {
|
|
840
|
+
...data,
|
|
841
|
+
id: doc.id,
|
|
842
|
+
};
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
clinics,
|
|
847
|
+
lastDoc: lastVisible,
|
|
848
|
+
};
|
|
849
|
+
} catch (error) {
|
|
850
|
+
console.error("[CLINIC_UTILS] Error getting all clinics:", error);
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Retrieves all clinics within a specified range from a location with optional pagination
|
|
857
|
+
*
|
|
858
|
+
* @param db - Firestore database instance
|
|
859
|
+
* @param center - The center location coordinates {latitude, longitude}
|
|
860
|
+
* @param rangeInKm - The range in kilometers to search within
|
|
861
|
+
* @param pagination - Optional number of clinics per page (0 or undefined returns all)
|
|
862
|
+
* @param lastDoc - Optional last document for pagination (if continuing from a previous page)
|
|
863
|
+
* @param filters - Optional filters to apply to the search (isActive, tags, etc.)
|
|
864
|
+
* @returns Array of clinics within range and the last document for pagination
|
|
865
|
+
*/
|
|
866
|
+
export async function getAllClinicsInRange(
|
|
867
|
+
db: Firestore,
|
|
868
|
+
center: { latitude: number; longitude: number },
|
|
869
|
+
rangeInKm: number,
|
|
870
|
+
pagination?: number,
|
|
871
|
+
lastDoc?: any,
|
|
872
|
+
filters?: {
|
|
873
|
+
isActive?: boolean;
|
|
874
|
+
tags?: ClinicTag[];
|
|
875
|
+
}
|
|
876
|
+
): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
|
|
877
|
+
try {
|
|
878
|
+
const { distanceBetween } = require("geofire-common");
|
|
879
|
+
const centerLat = center.latitude;
|
|
880
|
+
const centerLng = center.longitude;
|
|
881
|
+
|
|
882
|
+
// We'll need to get all clinics and filter them by distance
|
|
883
|
+
const clinicsCollection = collection(db, CLINICS_COLLECTION);
|
|
884
|
+
let clinicsQuery = query(clinicsCollection);
|
|
885
|
+
|
|
886
|
+
// Add active filter if specified
|
|
887
|
+
if (filters?.isActive !== undefined) {
|
|
888
|
+
clinicsQuery = query(
|
|
889
|
+
clinicsCollection,
|
|
890
|
+
where("isActive", "==", filters.isActive)
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const querySnapshot = await getDocs(clinicsQuery);
|
|
895
|
+
console.log(
|
|
896
|
+
`[CLINIC_UTILS] Found ${querySnapshot.docs.length} total clinics to filter by distance`
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
// Filter the results by distance
|
|
900
|
+
const filteredDocs = [];
|
|
901
|
+
for (const doc of querySnapshot.docs) {
|
|
902
|
+
const clinic = doc.data() as Clinic;
|
|
903
|
+
|
|
904
|
+
// Skip clinics without proper location data
|
|
905
|
+
if (
|
|
906
|
+
!clinic.location ||
|
|
907
|
+
!clinic.location.latitude ||
|
|
908
|
+
!clinic.location.longitude
|
|
909
|
+
) {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Calculate distance
|
|
914
|
+
const distanceInM = distanceBetween(
|
|
915
|
+
[centerLat, centerLng],
|
|
916
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
// Convert to km and check if within range
|
|
920
|
+
const distanceInKm = distanceInM / 1000;
|
|
921
|
+
if (distanceInKm <= rangeInKm) {
|
|
922
|
+
// If tags filter exists, apply it
|
|
923
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
924
|
+
const hasAllTags = filters.tags.every((filterTag) =>
|
|
925
|
+
clinic.tags.some(
|
|
926
|
+
(clinicTag) =>
|
|
927
|
+
(filterTag as any).id === (clinicTag as any).id ||
|
|
928
|
+
(filterTag as any).name === (clinicTag as any).name
|
|
929
|
+
)
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
if (hasAllTags) {
|
|
933
|
+
// Add distance to clinic object for reference
|
|
934
|
+
filteredDocs.push({
|
|
935
|
+
doc,
|
|
936
|
+
distance: distanceInKm,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
// Add distance to clinic object for reference
|
|
941
|
+
filteredDocs.push({
|
|
942
|
+
doc,
|
|
943
|
+
distance: distanceInKm,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
console.log(
|
|
950
|
+
`[CLINIC_UTILS] Filtered to ${filteredDocs.length} clinics within ${rangeInKm}km`
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
// Sort results by distance
|
|
954
|
+
filteredDocs.sort((a, b) => a.distance - b.distance);
|
|
955
|
+
|
|
956
|
+
// Apply pagination if needed
|
|
957
|
+
let paginatedDocs = filteredDocs;
|
|
958
|
+
let lastVisible = null;
|
|
959
|
+
|
|
960
|
+
if (pagination && pagination > 0) {
|
|
961
|
+
// If we have a lastDoc, find its index in our sorted results
|
|
962
|
+
let startIndex = 0;
|
|
963
|
+
if (lastDoc) {
|
|
964
|
+
const lastDocIndex = filteredDocs.findIndex(
|
|
965
|
+
(item) => item.doc.id === lastDoc.id
|
|
966
|
+
);
|
|
967
|
+
if (lastDocIndex !== -1) {
|
|
968
|
+
startIndex = lastDocIndex + 1;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Get the paginated subset
|
|
973
|
+
paginatedDocs = filteredDocs.slice(startIndex, startIndex + pagination);
|
|
974
|
+
|
|
975
|
+
// Set the last document for the next pagination
|
|
976
|
+
lastVisible =
|
|
977
|
+
paginatedDocs.length > 0
|
|
978
|
+
? paginatedDocs[paginatedDocs.length - 1].doc
|
|
979
|
+
: null;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Map to Clinic objects with distance information
|
|
983
|
+
const clinics = paginatedDocs.map((item) => {
|
|
984
|
+
const data = item.doc.data() as Clinic;
|
|
985
|
+
return {
|
|
986
|
+
...data,
|
|
987
|
+
id: item.doc.id,
|
|
988
|
+
distance: item.distance, // Include distance in response
|
|
989
|
+
} as Clinic & { distance: number };
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
clinics,
|
|
994
|
+
lastDoc: lastVisible,
|
|
995
|
+
};
|
|
996
|
+
} catch (error) {
|
|
997
|
+
console.error("[CLINIC_UTILS] Error getting clinics in range:", error);
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Timestamp,
|
|
12
12
|
serverTimestamp,
|
|
13
13
|
DocumentData,
|
|
14
|
+
writeBatch,
|
|
14
15
|
} from "firebase/firestore";
|
|
15
16
|
import { BaseService } from "../base.service";
|
|
16
17
|
import {
|
|
@@ -47,12 +48,16 @@ import { CategoryService } from "../../backoffice/services/category.service";
|
|
|
47
48
|
import { SubcategoryService } from "../../backoffice/services/subcategory.service";
|
|
48
49
|
import { TechnologyService } from "../../backoffice/services/technology.service";
|
|
49
50
|
import { ProductService } from "../../backoffice/services/product.service";
|
|
50
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
Practitioner,
|
|
53
|
+
PRACTITIONERS_COLLECTION,
|
|
54
|
+
} from "../../types/practitioner";
|
|
51
55
|
import {
|
|
52
56
|
CertificationLevel,
|
|
53
57
|
CertificationSpecialty,
|
|
54
58
|
ProcedureFamily,
|
|
55
59
|
} from "../../backoffice/types";
|
|
60
|
+
import { CLINICS_COLLECTION } from "../../types/clinic";
|
|
56
61
|
|
|
57
62
|
export class ProcedureService extends BaseService {
|
|
58
63
|
private categoryService: CategoryService;
|
|
@@ -103,6 +108,68 @@ export class ProcedureService extends BaseService {
|
|
|
103
108
|
throw new Error("One or more required entities not found");
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
// Get clinic and practitioner information
|
|
112
|
+
const clinicRef = doc(
|
|
113
|
+
this.db,
|
|
114
|
+
CLINICS_COLLECTION,
|
|
115
|
+
validatedData.clinicBranchId
|
|
116
|
+
);
|
|
117
|
+
const clinicSnapshot = await getDoc(clinicRef);
|
|
118
|
+
|
|
119
|
+
if (!clinicSnapshot.exists()) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const clinic = clinicSnapshot.data();
|
|
126
|
+
|
|
127
|
+
// Create clinic info
|
|
128
|
+
const clinicInfo = {
|
|
129
|
+
id: clinicSnapshot.id,
|
|
130
|
+
name: clinic.name,
|
|
131
|
+
description: clinic.description || "",
|
|
132
|
+
featuredPhoto:
|
|
133
|
+
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
134
|
+
? clinic.featuredPhotos[0]
|
|
135
|
+
: clinic.coverPhoto || "",
|
|
136
|
+
location: clinic.location,
|
|
137
|
+
contactInfo: clinic.contactInfo,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Get practitioner information
|
|
141
|
+
const practitionerRef = doc(
|
|
142
|
+
this.db,
|
|
143
|
+
PRACTITIONERS_COLLECTION,
|
|
144
|
+
validatedData.practitionerId
|
|
145
|
+
);
|
|
146
|
+
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
147
|
+
|
|
148
|
+
if (!practitionerSnapshot.exists()) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Practitioner with ID ${validatedData.practitionerId} not found`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const practitioner = practitionerSnapshot.data();
|
|
155
|
+
|
|
156
|
+
// Find doctor info in clinic's doctorsInfo array
|
|
157
|
+
let doctorInfo = clinic.doctorsInfo?.find(
|
|
158
|
+
(doctor: any) => doctor.id === validatedData.practitionerId
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// If not found, create basic doctor info
|
|
162
|
+
if (!doctorInfo) {
|
|
163
|
+
doctorInfo = {
|
|
164
|
+
id: practitionerSnapshot.id,
|
|
165
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
166
|
+
description: practitioner.basicInfo.bio || "",
|
|
167
|
+
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
168
|
+
rating: 0,
|
|
169
|
+
services: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
106
173
|
// Create the procedure object
|
|
107
174
|
const procedure: Omit<Procedure, "id"> = {
|
|
108
175
|
...validatedData,
|
|
@@ -116,6 +183,8 @@ export class ProcedureService extends BaseService {
|
|
|
116
183
|
postRequirements: technology.requirements.post,
|
|
117
184
|
certificationRequirement: technology.certificationRequirement,
|
|
118
185
|
documentationTemplates: technology.documentationTemplates || [],
|
|
186
|
+
clinicInfo,
|
|
187
|
+
doctorInfo,
|
|
119
188
|
isActive: true,
|
|
120
189
|
createdAt: new Date(),
|
|
121
190
|
updatedAt: new Date(),
|
|
@@ -210,6 +279,9 @@ export class ProcedureService extends BaseService {
|
|
|
210
279
|
updatedAt: serverTimestamp(),
|
|
211
280
|
});
|
|
212
281
|
|
|
282
|
+
// Return the updated procedure with combined data
|
|
283
|
+
// Note: Since we're not changing the clinicInfo or doctorInfo in this update,
|
|
284
|
+
// we just keep the existing values from the procedure
|
|
213
285
|
return {
|
|
214
286
|
...existingProcedure,
|
|
215
287
|
...validatedData,
|
|
@@ -267,4 +339,311 @@ export class ProcedureService extends BaseService {
|
|
|
267
339
|
subcategories,
|
|
268
340
|
};
|
|
269
341
|
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Gets all procedures with optional pagination
|
|
345
|
+
*
|
|
346
|
+
* @param pagination - Optional number of procedures per page (0 or undefined returns all)
|
|
347
|
+
* @param lastDoc - Optional last document for pagination (if continuing from a previous page)
|
|
348
|
+
* @returns Object containing procedures array and the last document for pagination
|
|
349
|
+
*/
|
|
350
|
+
async getAllProcedures(
|
|
351
|
+
pagination?: number,
|
|
352
|
+
lastDoc?: any
|
|
353
|
+
): Promise<{ procedures: Procedure[]; lastDoc: any }> {
|
|
354
|
+
try {
|
|
355
|
+
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
356
|
+
let proceduresQuery = query(proceduresCollection);
|
|
357
|
+
|
|
358
|
+
// If pagination is specified and greater than 0, limit the query
|
|
359
|
+
if (pagination && pagination > 0) {
|
|
360
|
+
const { limit, startAfter } = require("firebase/firestore");
|
|
361
|
+
|
|
362
|
+
if (lastDoc) {
|
|
363
|
+
proceduresQuery = query(
|
|
364
|
+
proceduresCollection,
|
|
365
|
+
startAfter(lastDoc),
|
|
366
|
+
limit(pagination)
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
proceduresQuery = query(proceduresCollection, limit(pagination));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
374
|
+
const lastVisible =
|
|
375
|
+
proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
376
|
+
|
|
377
|
+
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
378
|
+
const data = doc.data() as Procedure;
|
|
379
|
+
|
|
380
|
+
// Ensure clinicInfo and doctorInfo are present
|
|
381
|
+
if (!data.clinicInfo || !data.doctorInfo) {
|
|
382
|
+
console.warn(
|
|
383
|
+
`Procedure ${data.id} is missing clinicInfo or doctorInfo fields. These should be updated.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
...data,
|
|
389
|
+
id: doc.id,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
procedures,
|
|
395
|
+
lastDoc: lastVisible,
|
|
396
|
+
};
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Updates the clinicInfo and doctorInfo fields in procedures when the source data changes
|
|
405
|
+
*
|
|
406
|
+
* @param entityType - The type of entity that changed ("clinic" or "doctor")
|
|
407
|
+
* @param entityId - The ID of the entity that changed
|
|
408
|
+
* @param updatedData - The updated data for the entity
|
|
409
|
+
* @returns Number of procedures updated
|
|
410
|
+
*/
|
|
411
|
+
async updateProcedureAggregateData(
|
|
412
|
+
entityType: "clinic" | "doctor",
|
|
413
|
+
entityId: string,
|
|
414
|
+
updatedData: any
|
|
415
|
+
): Promise<number> {
|
|
416
|
+
let proceduresQuery;
|
|
417
|
+
const updatedField = entityType === "clinic" ? "clinicInfo" : "doctorInfo";
|
|
418
|
+
|
|
419
|
+
// Find all procedures related to this entity
|
|
420
|
+
if (entityType === "clinic") {
|
|
421
|
+
proceduresQuery = query(
|
|
422
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
423
|
+
where("clinicBranchId", "==", entityId)
|
|
424
|
+
);
|
|
425
|
+
} else {
|
|
426
|
+
proceduresQuery = query(
|
|
427
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
428
|
+
where("practitionerId", "==", entityId)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const snapshot = await getDocs(proceduresQuery);
|
|
433
|
+
|
|
434
|
+
if (snapshot.empty) {
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Create the updated data object
|
|
439
|
+
let updatedFieldData;
|
|
440
|
+
|
|
441
|
+
if (entityType === "clinic") {
|
|
442
|
+
// Clinic info format
|
|
443
|
+
updatedFieldData = {
|
|
444
|
+
id: entityId,
|
|
445
|
+
name: updatedData.name,
|
|
446
|
+
description: updatedData.description || "",
|
|
447
|
+
featuredPhoto:
|
|
448
|
+
updatedData.featuredPhotos && updatedData.featuredPhotos.length > 0
|
|
449
|
+
? updatedData.featuredPhotos[0]
|
|
450
|
+
: updatedData.coverPhoto || "",
|
|
451
|
+
location: updatedData.location,
|
|
452
|
+
contactInfo: updatedData.contactInfo,
|
|
453
|
+
};
|
|
454
|
+
} else {
|
|
455
|
+
// Doctor info format
|
|
456
|
+
if (updatedData.basicInfo) {
|
|
457
|
+
// If it's a practitioner object
|
|
458
|
+
updatedFieldData = {
|
|
459
|
+
id: entityId,
|
|
460
|
+
name: `${updatedData.basicInfo.firstName} ${updatedData.basicInfo.lastName}`,
|
|
461
|
+
description: updatedData.basicInfo.bio || "",
|
|
462
|
+
photo: updatedData.basicInfo.profileImageUrl || "",
|
|
463
|
+
rating: 0, // This would need to be calculated or passed in
|
|
464
|
+
services: [], // This would need to be determined or passed in
|
|
465
|
+
};
|
|
466
|
+
} else {
|
|
467
|
+
// If it's already a doctorInfo object
|
|
468
|
+
updatedFieldData = {
|
|
469
|
+
id: entityId,
|
|
470
|
+
name: updatedData.name,
|
|
471
|
+
description: updatedData.description || "",
|
|
472
|
+
photo: updatedData.photo || "",
|
|
473
|
+
rating: updatedData.rating || 0,
|
|
474
|
+
services: updatedData.services || [],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Update all found procedures
|
|
480
|
+
const batch = writeBatch(this.db);
|
|
481
|
+
|
|
482
|
+
snapshot.docs.forEach((doc) => {
|
|
483
|
+
batch.update(doc.ref, {
|
|
484
|
+
[updatedField]: updatedFieldData,
|
|
485
|
+
updatedAt: serverTimestamp(),
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await batch.commit();
|
|
490
|
+
|
|
491
|
+
return snapshot.size;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Updates the clinicInfo for all procedures associated with a specific clinic
|
|
496
|
+
*
|
|
497
|
+
* @param clinicId - The ID of the clinic that was updated
|
|
498
|
+
* @param clinicData - The updated clinic data
|
|
499
|
+
* @returns Number of procedures updated
|
|
500
|
+
*/
|
|
501
|
+
async updateClinicInfoInProcedures(
|
|
502
|
+
clinicId: string,
|
|
503
|
+
clinicData: any
|
|
504
|
+
): Promise<number> {
|
|
505
|
+
return this.updateProcedureAggregateData("clinic", clinicId, clinicData);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Updates the doctorInfo for all procedures associated with a specific practitioner
|
|
510
|
+
*
|
|
511
|
+
* @param practitionerId - The ID of the practitioner that was updated
|
|
512
|
+
* @param practitionerData - The updated practitioner data
|
|
513
|
+
* @returns Number of procedures updated
|
|
514
|
+
*/
|
|
515
|
+
async updateDoctorInfoInProcedures(
|
|
516
|
+
practitionerId: string,
|
|
517
|
+
practitionerData: any
|
|
518
|
+
): Promise<number> {
|
|
519
|
+
return this.updateProcedureAggregateData(
|
|
520
|
+
"doctor",
|
|
521
|
+
practitionerId,
|
|
522
|
+
practitionerData
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Updates all existing procedures to include clinicInfo and doctorInfo
|
|
528
|
+
* This is a migration helper method that can be used to update existing procedures
|
|
529
|
+
*
|
|
530
|
+
* @returns Number of procedures updated
|
|
531
|
+
*/
|
|
532
|
+
async migrateAllProceduresWithAggregateData(): Promise<number> {
|
|
533
|
+
// Get all procedures
|
|
534
|
+
const proceduresQuery = query(collection(this.db, PROCEDURES_COLLECTION));
|
|
535
|
+
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
536
|
+
|
|
537
|
+
if (proceduresSnapshot.empty) {
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let updatedCount = 0;
|
|
542
|
+
const batch = writeBatch(this.db);
|
|
543
|
+
const batchLimit = 500; // Firestore batch limit
|
|
544
|
+
let batchCount = 0;
|
|
545
|
+
|
|
546
|
+
// Process each procedure
|
|
547
|
+
for (const procedureDoc of proceduresSnapshot.docs) {
|
|
548
|
+
const procedure = procedureDoc.data() as Procedure;
|
|
549
|
+
|
|
550
|
+
// Skip if already has both required fields
|
|
551
|
+
if (procedure.clinicInfo && procedure.doctorInfo) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
// Get clinic data
|
|
557
|
+
const clinicRef = doc(
|
|
558
|
+
this.db,
|
|
559
|
+
CLINICS_COLLECTION,
|
|
560
|
+
procedure.clinicBranchId
|
|
561
|
+
);
|
|
562
|
+
const clinicSnapshot = await getDoc(clinicRef);
|
|
563
|
+
|
|
564
|
+
if (!clinicSnapshot.exists()) {
|
|
565
|
+
console.warn(
|
|
566
|
+
`Clinic ${procedure.clinicBranchId} not found for procedure ${procedure.id}`
|
|
567
|
+
);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const clinic = clinicSnapshot.data();
|
|
572
|
+
|
|
573
|
+
// Create clinic info
|
|
574
|
+
const clinicInfo = {
|
|
575
|
+
id: clinicSnapshot.id,
|
|
576
|
+
name: clinic.name,
|
|
577
|
+
description: clinic.description || "",
|
|
578
|
+
featuredPhoto:
|
|
579
|
+
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
580
|
+
? clinic.featuredPhotos[0]
|
|
581
|
+
: clinic.coverPhoto || "",
|
|
582
|
+
location: clinic.location,
|
|
583
|
+
contactInfo: clinic.contactInfo,
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Get practitioner data
|
|
587
|
+
const practitionerRef = doc(
|
|
588
|
+
this.db,
|
|
589
|
+
PRACTITIONERS_COLLECTION,
|
|
590
|
+
procedure.practitionerId
|
|
591
|
+
);
|
|
592
|
+
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
593
|
+
|
|
594
|
+
if (!practitionerSnapshot.exists()) {
|
|
595
|
+
console.warn(
|
|
596
|
+
`Practitioner ${procedure.practitionerId} not found for procedure ${procedure.id}`
|
|
597
|
+
);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const practitioner = practitionerSnapshot.data();
|
|
602
|
+
|
|
603
|
+
// Find doctor info in clinic's doctorsInfo array
|
|
604
|
+
let doctorInfo = clinic.doctorsInfo?.find(
|
|
605
|
+
(doctor: any) => doctor.id === procedure.practitionerId
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// If not found, create basic doctor info
|
|
609
|
+
if (!doctorInfo) {
|
|
610
|
+
doctorInfo = {
|
|
611
|
+
id: practitionerSnapshot.id,
|
|
612
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
613
|
+
description: practitioner.basicInfo.bio || "",
|
|
614
|
+
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
615
|
+
rating: 0,
|
|
616
|
+
services: [],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Add to batch
|
|
621
|
+
batch.update(procedureDoc.ref, {
|
|
622
|
+
clinicInfo,
|
|
623
|
+
doctorInfo,
|
|
624
|
+
updatedAt: serverTimestamp(),
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
batchCount++;
|
|
628
|
+
updatedCount++;
|
|
629
|
+
|
|
630
|
+
// Commit batch if we've reached the limit
|
|
631
|
+
if (batchCount >= batchLimit) {
|
|
632
|
+
await batch.commit();
|
|
633
|
+
console.log(`Committed batch of ${batchCount} procedure updates`);
|
|
634
|
+
batchCount = 0;
|
|
635
|
+
}
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error(`Error updating procedure ${procedure.id}:`, error);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Commit any remaining updates
|
|
642
|
+
if (batchCount > 0) {
|
|
643
|
+
await batch.commit();
|
|
644
|
+
console.log(`Committed final batch of ${batchCount} procedure updates`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return updatedCount;
|
|
648
|
+
}
|
|
270
649
|
}
|
|
@@ -12,6 +12,9 @@ import { BlockingCondition } from "../../backoffice/types/static/blocking-condit
|
|
|
12
12
|
import { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
|
|
13
13
|
import { CertificationRequirement } from "../../backoffice/types/static/certification.types";
|
|
14
14
|
import { DocumentTemplate } from "../documentation-templates";
|
|
15
|
+
import { ClinicInfo } from "../profile";
|
|
16
|
+
import { DoctorInfo } from "../clinic";
|
|
17
|
+
import { PRACTITIONERS_COLLECTION } from "../practitioner";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Procedure represents a specific medical procedure that can be performed by a practitioner in a clinic
|
|
@@ -58,6 +61,10 @@ export interface Procedure {
|
|
|
58
61
|
practitionerId: string;
|
|
59
62
|
/** ID of the clinic branch where this procedure is performed */
|
|
60
63
|
clinicBranchId: string;
|
|
64
|
+
/** Aggregated clinic information */
|
|
65
|
+
clinicInfo: ClinicInfo;
|
|
66
|
+
/** Aggregated doctor information */
|
|
67
|
+
doctorInfo: DoctorInfo;
|
|
61
68
|
/** Whether this procedure is active */
|
|
62
69
|
isActive: boolean;
|
|
63
70
|
/** When this procedure was created */
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Currency,
|
|
5
5
|
PricingMeasure,
|
|
6
6
|
} from "../backoffice/types/static/pricing.types";
|
|
7
|
+
import { clinicInfoSchema, doctorInfoSchema } from "./clinic.schema";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Schema for creating a new procedure
|
|
@@ -52,6 +53,8 @@ export const procedureSchema = createProcedureSchema.extend({
|
|
|
52
53
|
postRequirements: z.array(z.any()), // We'll validate requirements separately
|
|
53
54
|
certificationRequirement: z.any(), // We'll validate certification requirement separately
|
|
54
55
|
documentationTemplates: z.array(z.any()), // We'll validate documentation templates separately
|
|
56
|
+
clinicInfo: clinicInfoSchema, // Clinic info validation
|
|
57
|
+
doctorInfo: doctorInfoSchema, // Doctor info validation
|
|
55
58
|
isActive: z.boolean(),
|
|
56
59
|
createdAt: z.date(),
|
|
57
60
|
updatedAt: z.date(),
|