@blackcode_sa/metaestetics-api 1.8.11 → 1.8.13
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 +4 -0
- package/dist/admin/index.d.ts +4 -0
- package/dist/backoffice/index.d.mts +1 -0
- package/dist/backoffice/index.d.ts +1 -0
- package/dist/index.d.mts +36 -9
- package/dist/index.d.ts +36 -9
- package/dist/index.js +218 -375
- package/dist/index.mjs +221 -376
- package/package.json +1 -1
- package/src/admin/scripts/migrateProcedures.js +23 -0
- package/src/admin/scripts/serviceAccountKey.json +13 -0
- package/src/services/clinic/clinic.service.ts +33 -0
- package/src/services/clinic/utils/filter.utils.ts +54 -225
- package/src/services/practitioner/practitioner.service.ts +48 -115
- package/src/services/procedure/procedure.service.ts +123 -249
- package/src/types/clinic/index.ts +1 -0
- package/src/types/practitioner/index.ts +1 -0
- package/src/types/procedure/index.ts +6 -0
- package/src/validations/procedure.schema.ts +3 -0
|
@@ -263,6 +263,7 @@ export class ProcedureService extends BaseService {
|
|
|
263
263
|
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
264
264
|
id: procedureId,
|
|
265
265
|
...validatedData,
|
|
266
|
+
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
266
267
|
photos: processedPhotos,
|
|
267
268
|
category, // Embed full objects
|
|
268
269
|
subcategory,
|
|
@@ -430,6 +431,7 @@ export class ProcedureService extends BaseService {
|
|
|
430
431
|
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
431
432
|
id: procedureId,
|
|
432
433
|
...validatedData,
|
|
434
|
+
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
433
435
|
practitionerId: practitionerId, // Override practitionerId with the correct one
|
|
434
436
|
photos: processedPhotos,
|
|
435
437
|
category,
|
|
@@ -657,6 +659,9 @@ export class ProcedureService extends BaseService {
|
|
|
657
659
|
|
|
658
660
|
// Handle Category/Subcategory/Technology/Product Changes
|
|
659
661
|
let finalCategoryId = existingProcedure.category.id;
|
|
662
|
+
if (validatedData.name) {
|
|
663
|
+
updatedProcedureData.nameLower = validatedData.name.toLowerCase();
|
|
664
|
+
}
|
|
660
665
|
if (validatedData.categoryId) {
|
|
661
666
|
const category = await this.categoryService.getById(
|
|
662
667
|
validatedData.categoryId
|
|
@@ -853,6 +858,8 @@ export class ProcedureService extends BaseService {
|
|
|
853
858
|
/**
|
|
854
859
|
* Searches and filters procedures based on multiple criteria
|
|
855
860
|
*
|
|
861
|
+
* @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
|
|
862
|
+
*
|
|
856
863
|
* @param filters - Various filters to apply
|
|
857
864
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
858
865
|
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
@@ -892,11 +899,6 @@ export class ProcedureService extends BaseService {
|
|
|
892
899
|
lastDoc: any;
|
|
893
900
|
}> {
|
|
894
901
|
try {
|
|
895
|
-
console.log(
|
|
896
|
-
"[PROCEDURE_SERVICE] Starting procedure filtering with criteria:",
|
|
897
|
-
filters
|
|
898
|
-
);
|
|
899
|
-
|
|
900
902
|
// Determine if we're doing a geo query or a regular query
|
|
901
903
|
const isGeoQuery =
|
|
902
904
|
filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
@@ -915,291 +917,131 @@ export class ProcedureService extends BaseService {
|
|
|
915
917
|
if (filters.procedureFamily) {
|
|
916
918
|
constraints.push(where("family", "==", filters.procedureFamily));
|
|
917
919
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
constraints.push(
|
|
926
|
-
}
|
|
920
|
+
if (filters.procedureCategory) {
|
|
921
|
+
constraints.push(where("category.id", "==", filters.procedureCategory));
|
|
922
|
+
}
|
|
923
|
+
if (filters.procedureSubcategory) {
|
|
924
|
+
constraints.push(where("subcategory.id", "==", filters.procedureSubcategory));
|
|
925
|
+
}
|
|
926
|
+
if (filters.procedureTechnology) {
|
|
927
|
+
constraints.push(where("technology.id", "==", filters.procedureTechnology));
|
|
928
|
+
}
|
|
929
|
+
if (filters.minPrice !== undefined) {
|
|
930
|
+
constraints.push(where("price", ">=", filters.minPrice));
|
|
931
|
+
}
|
|
932
|
+
if (filters.maxPrice !== undefined) {
|
|
933
|
+
constraints.push(where("price", "<=", filters.maxPrice));
|
|
934
|
+
}
|
|
935
|
+
if (filters.minRating !== undefined) {
|
|
936
|
+
constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
|
|
937
|
+
}
|
|
938
|
+
if (filters.maxRating !== undefined) {
|
|
939
|
+
constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
|
|
940
|
+
}
|
|
941
|
+
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
942
|
+
// Firestore ne podržava array-contains-all, koristi array-contains-any
|
|
943
|
+
constraints.push(where("treatmentBenefits", "array-contains-any", filters.treatmentBenefits));
|
|
944
|
+
}
|
|
945
|
+
// Text search by name (case-sensitive, Firestore limitation)
|
|
946
|
+
let useNameLower = false;
|
|
947
|
+
let searchTerm = "";
|
|
948
|
+
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
949
|
+
searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
950
|
+
useNameLower = true;
|
|
951
|
+
constraints.push(where("nameLower", ">=", searchTerm));
|
|
952
|
+
constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
|
|
953
|
+
constraints.push(orderBy("nameLower"));
|
|
954
|
+
} else {
|
|
955
|
+
constraints.push(orderBy("nameLower"));
|
|
956
|
+
}
|
|
957
|
+
if (filters.lastDoc) {
|
|
958
|
+
// Firestore pagination: support both QueryDocumentSnapshot and value arrays
|
|
959
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
960
|
+
// QueryDocumentSnapshot
|
|
961
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
962
|
+
} else if (Array.isArray(filters.lastDoc)) {
|
|
963
|
+
// Array of values for multiple orderBy fields
|
|
964
|
+
constraints.push(startAfter(...filters.lastDoc));
|
|
965
|
+
} else {
|
|
966
|
+
// Single value
|
|
967
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
927
971
|
constraints.push(limit(filters.pagination));
|
|
928
972
|
}
|
|
929
973
|
|
|
930
|
-
|
|
931
|
-
let lastVisibleDoc = null;
|
|
932
|
-
|
|
933
|
-
// For geo queries, we need a different approach
|
|
974
|
+
// Geo-query: special handling
|
|
934
975
|
if (isGeoQuery) {
|
|
976
|
+
// For geo queries, use geohash bounds and add all other constraints
|
|
935
977
|
const center = filters.location!;
|
|
936
978
|
const radiusInKm = filters.radiusInKm!;
|
|
937
|
-
|
|
938
|
-
// Get the geohash query bounds
|
|
939
979
|
const bounds = geohashQueryBounds(
|
|
940
980
|
[center.latitude, center.longitude],
|
|
941
981
|
radiusInKm * 1000 // Convert to meters
|
|
942
982
|
);
|
|
943
|
-
|
|
944
|
-
// Collect matching procedures from all bounds
|
|
945
|
-
const matchingProcedures: (Procedure & { distance: number })[] = [];
|
|
946
|
-
|
|
947
|
-
// Execute queries for each bound
|
|
983
|
+
let allDocs: (Procedure & { distance: number })[] = [];
|
|
948
984
|
for (const bound of bounds) {
|
|
949
|
-
// Create a geo query for this bound
|
|
950
985
|
const geoConstraints = [
|
|
951
|
-
...constraints,
|
|
986
|
+
...constraints.filter(c => !(c as any).fieldPath || (c as any).fieldPath !== "name"), // Remove name orderBy for geo
|
|
952
987
|
where("clinicInfo.location.geohash", ">=", bound[0]),
|
|
953
988
|
where("clinicInfo.location.geohash", "<=", bound[1]),
|
|
989
|
+
orderBy("clinicInfo.location.geohash"),
|
|
954
990
|
];
|
|
955
|
-
|
|
956
|
-
const q = query(
|
|
957
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
958
|
-
...geoConstraints
|
|
959
|
-
);
|
|
991
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...geoConstraints);
|
|
960
992
|
const querySnapshot = await getDocs(q);
|
|
961
|
-
|
|
962
|
-
console.log(
|
|
963
|
-
`[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures in geo bound`
|
|
964
|
-
);
|
|
965
|
-
|
|
966
|
-
// Process results and filter by actual distance
|
|
967
993
|
for (const doc of querySnapshot.docs) {
|
|
968
994
|
const procedure = { ...doc.data(), id: doc.id } as Procedure;
|
|
969
|
-
|
|
970
|
-
// Calculate actual distance
|
|
971
995
|
const distance = distanceBetween(
|
|
972
996
|
[center.latitude, center.longitude],
|
|
973
|
-
[
|
|
974
|
-
procedure.clinicInfo.location.latitude,
|
|
975
|
-
procedure.clinicInfo.location.longitude,
|
|
976
|
-
]
|
|
997
|
+
[procedure.clinicInfo.location.latitude, procedure.clinicInfo.location.longitude]
|
|
977
998
|
);
|
|
978
|
-
|
|
979
|
-
// Convert to kilometers
|
|
980
999
|
const distanceInKm = distance / 1000;
|
|
981
|
-
|
|
982
|
-
// Check if within radius
|
|
983
1000
|
if (distanceInKm <= radiusInKm) {
|
|
984
|
-
|
|
985
|
-
matchingProcedures.push({
|
|
986
|
-
...procedure,
|
|
987
|
-
distance: distanceInKm,
|
|
988
|
-
});
|
|
1001
|
+
allDocs.push({ ...procedure, distance: distanceInKm });
|
|
989
1002
|
}
|
|
990
1003
|
}
|
|
991
1004
|
}
|
|
992
|
-
|
|
993
|
-
// Apply additional filters that couldn't be applied in the query
|
|
994
|
-
let filteredProcedures = matchingProcedures;
|
|
995
|
-
|
|
996
|
-
// Apply remaining filters in memory
|
|
997
|
-
filteredProcedures = this.applyInMemoryFilters(
|
|
998
|
-
filteredProcedures,
|
|
999
|
-
filters
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1002
1005
|
// Sort by distance
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
+
allDocs.sort((a, b) => a.distance - b.distance);
|
|
1007
|
+
// Paginate
|
|
1008
|
+
let paginated = allDocs;
|
|
1006
1009
|
if (filters.pagination && filters.pagination > 0) {
|
|
1007
|
-
// If we have a lastDoc, find its index
|
|
1008
1010
|
let startIndex = 0;
|
|
1009
1011
|
if (filters.lastDoc) {
|
|
1010
|
-
const lastDocIndex =
|
|
1011
|
-
|
|
1012
|
-
);
|
|
1013
|
-
if (lastDocIndex !== -1) {
|
|
1014
|
-
startIndex = lastDocIndex + 1;
|
|
1015
|
-
}
|
|
1012
|
+
const lastDocIndex = allDocs.findIndex(p => p.id === filters.lastDoc.id);
|
|
1013
|
+
if (lastDocIndex !== -1) startIndex = lastDocIndex + 1;
|
|
1016
1014
|
}
|
|
1017
|
-
|
|
1018
|
-
// Get paginated subset
|
|
1019
|
-
const paginatedProcedures = filteredProcedures.slice(
|
|
1020
|
-
startIndex,
|
|
1021
|
-
startIndex + filters.pagination
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
// Set last document for next pagination
|
|
1025
|
-
lastVisibleDoc =
|
|
1026
|
-
paginatedProcedures.length > 0
|
|
1027
|
-
? paginatedProcedures[paginatedProcedures.length - 1]
|
|
1028
|
-
: null;
|
|
1029
|
-
|
|
1030
|
-
proceduresResult = paginatedProcedures;
|
|
1031
|
-
} else {
|
|
1032
|
-
proceduresResult = filteredProcedures;
|
|
1015
|
+
paginated = allDocs.slice(startIndex, startIndex + filters.pagination);
|
|
1033
1016
|
}
|
|
1017
|
+
const lastVisibleDoc = paginated.length > 0 ? paginated[paginated.length - 1] : null;
|
|
1018
|
+
return { procedures: paginated, lastDoc: lastVisibleDoc };
|
|
1034
1019
|
} else {
|
|
1035
|
-
//
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
)
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
// Calculate distance for each procedure if location is provided
|
|
1052
|
-
if (filters.location) {
|
|
1053
|
-
const center = filters.location;
|
|
1054
|
-
const proceduresWithDistance: (Procedure & { distance: number })[] =
|
|
1055
|
-
[];
|
|
1056
|
-
|
|
1057
|
-
procedures.forEach((procedure) => {
|
|
1058
|
-
const distance = distanceBetween(
|
|
1059
|
-
[center.latitude, center.longitude],
|
|
1060
|
-
[
|
|
1061
|
-
procedure.clinicInfo.location.latitude,
|
|
1062
|
-
procedure.clinicInfo.location.longitude,
|
|
1063
|
-
]
|
|
1064
|
-
);
|
|
1065
|
-
|
|
1066
|
-
proceduresWithDistance.push({
|
|
1067
|
-
...procedure,
|
|
1068
|
-
distance: distance / 1000, // Convert to kilometers
|
|
1069
|
-
});
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
// Replace procedures with version that includes distances
|
|
1073
|
-
let filteredProcedures = proceduresWithDistance;
|
|
1074
|
-
|
|
1075
|
-
// Apply in-memory filters
|
|
1076
|
-
filteredProcedures = this.applyInMemoryFilters(
|
|
1077
|
-
filteredProcedures,
|
|
1078
|
-
filters
|
|
1079
|
-
);
|
|
1080
|
-
|
|
1081
|
-
// Sort by distance
|
|
1082
|
-
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
1083
|
-
|
|
1084
|
-
proceduresResult = filteredProcedures;
|
|
1085
|
-
} else {
|
|
1086
|
-
// Apply filters that couldn't be applied in the query
|
|
1087
|
-
let filteredProcedures = this.applyInMemoryFilters(
|
|
1088
|
-
procedures,
|
|
1089
|
-
filters
|
|
1090
|
-
);
|
|
1091
|
-
proceduresResult = filteredProcedures;
|
|
1020
|
+
// Regular query
|
|
1021
|
+
let q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1022
|
+
let querySnapshot = await getDocs(q);
|
|
1023
|
+
// Fallback na name ako nema rezultata i koristi se nameLower
|
|
1024
|
+
if (useNameLower && querySnapshot.empty && searchTerm) {
|
|
1025
|
+
// Ukloni poslednja 3 constraints (nameLower >=, nameLower <=, orderBy nameLower)
|
|
1026
|
+
constraints.pop();
|
|
1027
|
+
constraints.pop();
|
|
1028
|
+
constraints.pop();
|
|
1029
|
+
constraints.push(where("name", ">=", searchTerm));
|
|
1030
|
+
constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
|
|
1031
|
+
constraints.push(orderBy("name"));
|
|
1032
|
+
q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1033
|
+
querySnapshot = await getDocs(q);
|
|
1092
1034
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
lastVisibleDoc
|
|
1096
|
-
querySnapshot.docs.length > 0
|
|
1097
|
-
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1098
|
-
: null;
|
|
1035
|
+
const procedures = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Procedure));
|
|
1036
|
+
const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1037
|
+
return { procedures, lastDoc: lastVisibleDoc };
|
|
1099
1038
|
}
|
|
1100
|
-
|
|
1101
|
-
return {
|
|
1102
|
-
procedures: proceduresResult,
|
|
1103
|
-
lastDoc: lastVisibleDoc,
|
|
1104
|
-
};
|
|
1105
1039
|
} catch (error) {
|
|
1106
1040
|
console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
|
|
1107
1041
|
throw error;
|
|
1108
1042
|
}
|
|
1109
1043
|
}
|
|
1110
1044
|
|
|
1111
|
-
/**
|
|
1112
|
-
* Helper method to apply in-memory filters to procedures
|
|
1113
|
-
* Used by getProceduresByFilters to apply filters that can't be done in Firestore queries
|
|
1114
|
-
*
|
|
1115
|
-
* @param procedures - The procedures to filter
|
|
1116
|
-
* @param filters - The filters to apply
|
|
1117
|
-
* @returns Filtered procedures
|
|
1118
|
-
*/
|
|
1119
|
-
private applyInMemoryFilters<T extends Procedure & { distance?: number }>(
|
|
1120
|
-
procedures: T[],
|
|
1121
|
-
filters: {
|
|
1122
|
-
nameSearch?: string;
|
|
1123
|
-
treatmentBenefits?: TreatmentBenefit[];
|
|
1124
|
-
procedureCategory?: string;
|
|
1125
|
-
procedureSubcategory?: string;
|
|
1126
|
-
procedureTechnology?: string;
|
|
1127
|
-
minPrice?: number;
|
|
1128
|
-
maxPrice?: number;
|
|
1129
|
-
minRating?: number;
|
|
1130
|
-
maxRating?: number;
|
|
1131
|
-
}
|
|
1132
|
-
): T[] {
|
|
1133
|
-
let filteredProcedures = procedures;
|
|
1134
|
-
|
|
1135
|
-
// Filter by name search if specified
|
|
1136
|
-
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
1137
|
-
const searchTerm = filters.nameSearch.toLowerCase().trim();
|
|
1138
|
-
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1139
|
-
return procedure.name.toLowerCase().includes(searchTerm);
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Filter by treatment benefits if specified
|
|
1144
|
-
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
1145
|
-
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1146
|
-
// Check if procedure has all specified treatment benefits
|
|
1147
|
-
return filters.treatmentBenefits!.every((benefit) =>
|
|
1148
|
-
procedure.treatmentBenefits.includes(benefit)
|
|
1149
|
-
);
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
// Filter by procedure category if specified
|
|
1154
|
-
if (filters.procedureCategory) {
|
|
1155
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1156
|
-
(procedure) => procedure.category.id === filters.procedureCategory
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Filter by procedure subcategory if specified
|
|
1161
|
-
if (filters.procedureSubcategory) {
|
|
1162
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1163
|
-
(procedure) => procedure.subcategory.id === filters.procedureSubcategory
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// Filter by procedure technology if specified
|
|
1168
|
-
if (filters.procedureTechnology) {
|
|
1169
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1170
|
-
(procedure) => procedure.technology.id === filters.procedureTechnology
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Filter by price range if specified
|
|
1175
|
-
if (filters.minPrice !== undefined) {
|
|
1176
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1177
|
-
(procedure) => procedure.price >= filters.minPrice!
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
if (filters.maxPrice !== undefined) {
|
|
1182
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1183
|
-
(procedure) => procedure.price <= filters.maxPrice!
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// Filter by rating if specified
|
|
1188
|
-
if (filters.minRating !== undefined) {
|
|
1189
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1190
|
-
(procedure) => procedure.reviewInfo.averageRating >= filters.minRating!
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
if (filters.maxRating !== undefined) {
|
|
1195
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1196
|
-
(procedure) => procedure.reviewInfo.averageRating <= filters.maxRating!
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return filteredProcedures;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
1045
|
/**
|
|
1204
1046
|
* Creates a consultation procedure without requiring a product
|
|
1205
1047
|
* This is a special method for consultation procedures that don't use products
|
|
@@ -1301,6 +1143,7 @@ export class ProcedureService extends BaseService {
|
|
|
1301
1143
|
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
1302
1144
|
id: procedureId,
|
|
1303
1145
|
...data,
|
|
1146
|
+
nameLower: (data as any).nameLower || data.name.toLowerCase(),
|
|
1304
1147
|
photos: processedPhotos,
|
|
1305
1148
|
category,
|
|
1306
1149
|
subcategory,
|
|
@@ -1340,4 +1183,35 @@ export class ProcedureService extends BaseService {
|
|
|
1340
1183
|
const savedDoc = await getDoc(procedureRef);
|
|
1341
1184
|
return savedDoc.data() as Procedure;
|
|
1342
1185
|
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Gets all procedures with minimal info for map display (id, name, clinicId, clinicName, address, latitude, longitude)
|
|
1189
|
+
* This is optimized for mobile map usage to reduce payload size.
|
|
1190
|
+
* @returns Array of minimal procedure info for map
|
|
1191
|
+
*/
|
|
1192
|
+
async getProceduresForMap(): Promise<{
|
|
1193
|
+
id: string;
|
|
1194
|
+
name: string;
|
|
1195
|
+
clinicId: string | undefined;
|
|
1196
|
+
clinicName: string | undefined;
|
|
1197
|
+
address: string;
|
|
1198
|
+
latitude: number | undefined;
|
|
1199
|
+
longitude: number | undefined;
|
|
1200
|
+
}[]> {
|
|
1201
|
+
const proceduresRef = collection(this.db, PROCEDURES_COLLECTION);
|
|
1202
|
+
const snapshot = await getDocs(proceduresRef);
|
|
1203
|
+
const proceduresForMap = snapshot.docs.map(doc => {
|
|
1204
|
+
const data = doc.data();
|
|
1205
|
+
return {
|
|
1206
|
+
id: doc.id,
|
|
1207
|
+
name: data.name,
|
|
1208
|
+
clinicId: data.clinicInfo?.id,
|
|
1209
|
+
clinicName: data.clinicInfo?.name,
|
|
1210
|
+
address: data.clinicInfo?.location?.address || '',
|
|
1211
|
+
latitude: data.clinicInfo?.location?.latitude,
|
|
1212
|
+
longitude: data.clinicInfo?.location?.longitude,
|
|
1213
|
+
};
|
|
1214
|
+
});
|
|
1215
|
+
return proceduresForMap;
|
|
1216
|
+
}
|
|
1343
1217
|
}
|
|
@@ -89,6 +89,7 @@ export interface Practitioner {
|
|
|
89
89
|
id: string;
|
|
90
90
|
userRef: string;
|
|
91
91
|
basicInfo: PractitionerBasicInfo;
|
|
92
|
+
fullNameLower: string; // Add this line
|
|
92
93
|
certification: PractitionerCertification;
|
|
93
94
|
clinics: string[]; // Reference na klinike gde radi
|
|
94
95
|
clinicWorkingHours: PractitionerClinicWorkingHours[]; // Radno vreme za svaku kliniku
|
|
@@ -31,6 +31,8 @@ export interface Procedure {
|
|
|
31
31
|
id: string;
|
|
32
32
|
/** Name of the procedure */
|
|
33
33
|
name: string;
|
|
34
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
35
|
+
nameLower: string;
|
|
34
36
|
/** Photos of the procedure */
|
|
35
37
|
photos?: MediaResource[];
|
|
36
38
|
/** Detailed description of the procedure */
|
|
@@ -90,6 +92,8 @@ export interface Procedure {
|
|
|
90
92
|
*/
|
|
91
93
|
export interface CreateProcedureData {
|
|
92
94
|
name: string;
|
|
95
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
96
|
+
nameLower: string;
|
|
93
97
|
description: string;
|
|
94
98
|
family: ProcedureFamily;
|
|
95
99
|
categoryId: string;
|
|
@@ -110,6 +114,8 @@ export interface CreateProcedureData {
|
|
|
110
114
|
*/
|
|
111
115
|
export interface UpdateProcedureData {
|
|
112
116
|
name?: string;
|
|
117
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
118
|
+
nameLower?: string;
|
|
113
119
|
description?: string;
|
|
114
120
|
price?: number;
|
|
115
121
|
currency?: Currency;
|
|
@@ -12,6 +12,7 @@ import { mediaResourceSchema } from "./media.schema";
|
|
|
12
12
|
*/
|
|
13
13
|
export const createProcedureSchema = z.object({
|
|
14
14
|
name: z.string().min(1).max(200),
|
|
15
|
+
nameLower: z.string().min(1).max(200),
|
|
15
16
|
description: z.string().min(1).max(2000),
|
|
16
17
|
family: z.nativeEnum(ProcedureFamily),
|
|
17
18
|
categoryId: z.string().min(1),
|
|
@@ -32,6 +33,7 @@ export const createProcedureSchema = z.object({
|
|
|
32
33
|
*/
|
|
33
34
|
export const updateProcedureSchema = z.object({
|
|
34
35
|
name: z.string().min(3).max(100).optional(),
|
|
36
|
+
nameLower: z.string().min(1).max(200).optional(),
|
|
35
37
|
description: z.string().min(3).max(1000).optional(),
|
|
36
38
|
price: z.number().min(0).optional(),
|
|
37
39
|
currency: z.nativeEnum(Currency).optional(),
|
|
@@ -52,6 +54,7 @@ export const updateProcedureSchema = z.object({
|
|
|
52
54
|
*/
|
|
53
55
|
export const procedureSchema = createProcedureSchema.extend({
|
|
54
56
|
id: z.string().min(1),
|
|
57
|
+
nameLower: z.string().min(1).max(200),
|
|
55
58
|
category: z.any(), // We'll validate the full category object separately
|
|
56
59
|
subcategory: z.any(), // We'll validate the full subcategory object separately
|
|
57
60
|
technology: z.any(), // We'll validate the full technology object separately
|