@blackcode_sa/metaestetics-api 1.8.10 → 1.8.12
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 +2 -0
- package/dist/admin/index.d.ts +2 -0
- package/dist/index.d.mts +8 -9
- package/dist/index.d.ts +8 -9
- package/dist/index.js +79 -154
- package/dist/index.mjs +79 -154
- package/package.json +1 -1
- package/src/services/practitioner/practitioner.service.ts +1 -0
- package/src/services/procedure/procedure.service.ts +82 -248
- package/src/types/procedure/index.ts +6 -0
- package/src/validations/procedure.schema.ts +3 -0
package/dist/admin/index.d.mts
CHANGED
|
@@ -492,6 +492,8 @@ interface Procedure {
|
|
|
492
492
|
id: string;
|
|
493
493
|
/** Name of the procedure */
|
|
494
494
|
name: string;
|
|
495
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
496
|
+
nameLower: string;
|
|
495
497
|
/** Photos of the procedure */
|
|
496
498
|
photos?: MediaResource[];
|
|
497
499
|
/** Detailed description of the procedure */
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -492,6 +492,8 @@ interface Procedure {
|
|
|
492
492
|
id: string;
|
|
493
493
|
/** Name of the procedure */
|
|
494
494
|
name: string;
|
|
495
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
496
|
+
nameLower: string;
|
|
495
497
|
/** Photos of the procedure */
|
|
496
498
|
photos?: MediaResource[];
|
|
497
499
|
/** Detailed description of the procedure */
|
package/dist/index.d.mts
CHANGED
|
@@ -610,6 +610,8 @@ interface Procedure {
|
|
|
610
610
|
id: string;
|
|
611
611
|
/** Name of the procedure */
|
|
612
612
|
name: string;
|
|
613
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
614
|
+
nameLower: string;
|
|
613
615
|
/** Photos of the procedure */
|
|
614
616
|
photos?: MediaResource[];
|
|
615
617
|
/** Detailed description of the procedure */
|
|
@@ -668,6 +670,8 @@ interface Procedure {
|
|
|
668
670
|
*/
|
|
669
671
|
interface CreateProcedureData {
|
|
670
672
|
name: string;
|
|
673
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
674
|
+
nameLower: string;
|
|
671
675
|
description: string;
|
|
672
676
|
family: ProcedureFamily;
|
|
673
677
|
categoryId: string;
|
|
@@ -687,6 +691,8 @@ interface CreateProcedureData {
|
|
|
687
691
|
*/
|
|
688
692
|
interface UpdateProcedureData {
|
|
689
693
|
name?: string;
|
|
694
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
695
|
+
nameLower?: string;
|
|
690
696
|
description?: string;
|
|
691
697
|
price?: number;
|
|
692
698
|
currency?: Currency;
|
|
@@ -4201,6 +4207,8 @@ declare class ProcedureService extends BaseService {
|
|
|
4201
4207
|
/**
|
|
4202
4208
|
* Searches and filters procedures based on multiple criteria
|
|
4203
4209
|
*
|
|
4210
|
+
* @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
|
|
4211
|
+
*
|
|
4204
4212
|
* @param filters - Various filters to apply
|
|
4205
4213
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
4206
4214
|
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
@@ -4244,15 +4252,6 @@ declare class ProcedureService extends BaseService {
|
|
|
4244
4252
|
})[];
|
|
4245
4253
|
lastDoc: any;
|
|
4246
4254
|
}>;
|
|
4247
|
-
/**
|
|
4248
|
-
* Helper method to apply in-memory filters to procedures
|
|
4249
|
-
* Used by getProceduresByFilters to apply filters that can't be done in Firestore queries
|
|
4250
|
-
*
|
|
4251
|
-
* @param procedures - The procedures to filter
|
|
4252
|
-
* @param filters - The filters to apply
|
|
4253
|
-
* @returns Filtered procedures
|
|
4254
|
-
*/
|
|
4255
|
-
private applyInMemoryFilters;
|
|
4256
4255
|
/**
|
|
4257
4256
|
* Creates a consultation procedure without requiring a product
|
|
4258
4257
|
* This is a special method for consultation procedures that don't use products
|
package/dist/index.d.ts
CHANGED
|
@@ -610,6 +610,8 @@ interface Procedure {
|
|
|
610
610
|
id: string;
|
|
611
611
|
/** Name of the procedure */
|
|
612
612
|
name: string;
|
|
613
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
614
|
+
nameLower: string;
|
|
613
615
|
/** Photos of the procedure */
|
|
614
616
|
photos?: MediaResource[];
|
|
615
617
|
/** Detailed description of the procedure */
|
|
@@ -668,6 +670,8 @@ interface Procedure {
|
|
|
668
670
|
*/
|
|
669
671
|
interface CreateProcedureData {
|
|
670
672
|
name: string;
|
|
673
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
674
|
+
nameLower: string;
|
|
671
675
|
description: string;
|
|
672
676
|
family: ProcedureFamily;
|
|
673
677
|
categoryId: string;
|
|
@@ -687,6 +691,8 @@ interface CreateProcedureData {
|
|
|
687
691
|
*/
|
|
688
692
|
interface UpdateProcedureData {
|
|
689
693
|
name?: string;
|
|
694
|
+
/** Lowercase version of the name for case-insensitive search */
|
|
695
|
+
nameLower?: string;
|
|
690
696
|
description?: string;
|
|
691
697
|
price?: number;
|
|
692
698
|
currency?: Currency;
|
|
@@ -4201,6 +4207,8 @@ declare class ProcedureService extends BaseService {
|
|
|
4201
4207
|
/**
|
|
4202
4208
|
* Searches and filters procedures based on multiple criteria
|
|
4203
4209
|
*
|
|
4210
|
+
* @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
|
|
4211
|
+
*
|
|
4204
4212
|
* @param filters - Various filters to apply
|
|
4205
4213
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
4206
4214
|
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
@@ -4244,15 +4252,6 @@ declare class ProcedureService extends BaseService {
|
|
|
4244
4252
|
})[];
|
|
4245
4253
|
lastDoc: any;
|
|
4246
4254
|
}>;
|
|
4247
|
-
/**
|
|
4248
|
-
* Helper method to apply in-memory filters to procedures
|
|
4249
|
-
* Used by getProceduresByFilters to apply filters that can't be done in Firestore queries
|
|
4250
|
-
*
|
|
4251
|
-
* @param procedures - The procedures to filter
|
|
4252
|
-
* @param filters - The filters to apply
|
|
4253
|
-
* @returns Filtered procedures
|
|
4254
|
-
*/
|
|
4255
|
-
private applyInMemoryFilters;
|
|
4256
4255
|
/**
|
|
4257
4256
|
* Creates a consultation procedure without requiring a product
|
|
4258
4257
|
* This is a special method for consultation procedures that don't use products
|
package/dist/index.js
CHANGED
|
@@ -6993,6 +6993,7 @@ var PractitionerService = class extends BaseService {
|
|
|
6993
6993
|
}
|
|
6994
6994
|
const consultationData = {
|
|
6995
6995
|
name: "Free Consultation",
|
|
6996
|
+
nameLower: "free consultation",
|
|
6996
6997
|
description: "Free initial consultation to discuss treatment options and assess patient needs.",
|
|
6997
6998
|
family: "aesthetics" /* AESTHETICS */,
|
|
6998
6999
|
categoryId: "consultation",
|
|
@@ -14319,6 +14320,7 @@ var import_firestore45 = require("firebase/firestore");
|
|
|
14319
14320
|
var import_zod24 = require("zod");
|
|
14320
14321
|
var createProcedureSchema = import_zod24.z.object({
|
|
14321
14322
|
name: import_zod24.z.string().min(1).max(200),
|
|
14323
|
+
nameLower: import_zod24.z.string().min(1).max(200),
|
|
14322
14324
|
description: import_zod24.z.string().min(1).max(2e3),
|
|
14323
14325
|
family: import_zod24.z.nativeEnum(ProcedureFamily),
|
|
14324
14326
|
categoryId: import_zod24.z.string().min(1),
|
|
@@ -14336,6 +14338,7 @@ var createProcedureSchema = import_zod24.z.object({
|
|
|
14336
14338
|
});
|
|
14337
14339
|
var updateProcedureSchema = import_zod24.z.object({
|
|
14338
14340
|
name: import_zod24.z.string().min(3).max(100).optional(),
|
|
14341
|
+
nameLower: import_zod24.z.string().min(1).max(200).optional(),
|
|
14339
14342
|
description: import_zod24.z.string().min(3).max(1e3).optional(),
|
|
14340
14343
|
price: import_zod24.z.number().min(0).optional(),
|
|
14341
14344
|
currency: import_zod24.z.nativeEnum(Currency).optional(),
|
|
@@ -14352,6 +14355,7 @@ var updateProcedureSchema = import_zod24.z.object({
|
|
|
14352
14355
|
});
|
|
14353
14356
|
var procedureSchema = createProcedureSchema.extend({
|
|
14354
14357
|
id: import_zod24.z.string().min(1),
|
|
14358
|
+
nameLower: import_zod24.z.string().min(1).max(200),
|
|
14355
14359
|
category: import_zod24.z.any(),
|
|
14356
14360
|
// We'll validate the full category object separately
|
|
14357
14361
|
subcategory: import_zod24.z.any(),
|
|
@@ -14520,6 +14524,7 @@ var ProcedureService = class extends BaseService {
|
|
|
14520
14524
|
const newProcedure = {
|
|
14521
14525
|
id: procedureId,
|
|
14522
14526
|
...validatedData,
|
|
14527
|
+
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
14523
14528
|
photos: processedPhotos,
|
|
14524
14529
|
category,
|
|
14525
14530
|
// Embed full objects
|
|
@@ -14653,6 +14658,7 @@ var ProcedureService = class extends BaseService {
|
|
|
14653
14658
|
const newProcedure = {
|
|
14654
14659
|
id: procedureId,
|
|
14655
14660
|
...validatedData,
|
|
14661
|
+
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
14656
14662
|
practitionerId,
|
|
14657
14663
|
// Override practitionerId with the correct one
|
|
14658
14664
|
photos: processedPhotos,
|
|
@@ -14831,6 +14837,9 @@ var ProcedureService = class extends BaseService {
|
|
|
14831
14837
|
};
|
|
14832
14838
|
}
|
|
14833
14839
|
let finalCategoryId = existingProcedure.category.id;
|
|
14840
|
+
if (validatedData.name) {
|
|
14841
|
+
updatedProcedureData.nameLower = validatedData.nameLower || validatedData.name.toLowerCase();
|
|
14842
|
+
}
|
|
14834
14843
|
if (validatedData.categoryId) {
|
|
14835
14844
|
const category = await this.categoryService.getById(
|
|
14836
14845
|
validatedData.categoryId
|
|
@@ -14988,6 +14997,8 @@ var ProcedureService = class extends BaseService {
|
|
|
14988
14997
|
/**
|
|
14989
14998
|
* Searches and filters procedures based on multiple criteria
|
|
14990
14999
|
*
|
|
15000
|
+
* @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
|
|
15001
|
+
*
|
|
14991
15002
|
* @param filters - Various filters to apply
|
|
14992
15003
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
14993
15004
|
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
@@ -15008,10 +15019,6 @@ var ProcedureService = class extends BaseService {
|
|
|
15008
15019
|
*/
|
|
15009
15020
|
async getProceduresByFilters(filters) {
|
|
15010
15021
|
try {
|
|
15011
|
-
console.log(
|
|
15012
|
-
"[PROCEDURE_SERVICE] Starting procedure filtering with criteria:",
|
|
15013
|
-
filters
|
|
15014
|
-
);
|
|
15015
15022
|
const isGeoQuery = filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
15016
15023
|
const constraints = [];
|
|
15017
15024
|
if (filters.isActive !== void 0) {
|
|
@@ -15022,15 +15029,47 @@ var ProcedureService = class extends BaseService {
|
|
|
15022
15029
|
if (filters.procedureFamily) {
|
|
15023
15030
|
constraints.push((0, import_firestore45.where)("family", "==", filters.procedureFamily));
|
|
15024
15031
|
}
|
|
15025
|
-
|
|
15026
|
-
|
|
15032
|
+
if (filters.procedureCategory) {
|
|
15033
|
+
constraints.push((0, import_firestore45.where)("category.id", "==", filters.procedureCategory));
|
|
15034
|
+
}
|
|
15035
|
+
if (filters.procedureSubcategory) {
|
|
15036
|
+
constraints.push((0, import_firestore45.where)("subcategory.id", "==", filters.procedureSubcategory));
|
|
15037
|
+
}
|
|
15038
|
+
if (filters.procedureTechnology) {
|
|
15039
|
+
constraints.push((0, import_firestore45.where)("technology.id", "==", filters.procedureTechnology));
|
|
15040
|
+
}
|
|
15041
|
+
if (filters.minPrice !== void 0) {
|
|
15042
|
+
constraints.push((0, import_firestore45.where)("price", ">=", filters.minPrice));
|
|
15043
|
+
}
|
|
15044
|
+
if (filters.maxPrice !== void 0) {
|
|
15045
|
+
constraints.push((0, import_firestore45.where)("price", "<=", filters.maxPrice));
|
|
15046
|
+
}
|
|
15047
|
+
if (filters.minRating !== void 0) {
|
|
15048
|
+
constraints.push((0, import_firestore45.where)("reviewInfo.averageRating", ">=", filters.minRating));
|
|
15049
|
+
}
|
|
15050
|
+
if (filters.maxRating !== void 0) {
|
|
15051
|
+
constraints.push((0, import_firestore45.where)("reviewInfo.averageRating", "<=", filters.maxRating));
|
|
15052
|
+
}
|
|
15053
|
+
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
15054
|
+
constraints.push((0, import_firestore45.where)("treatmentBenefits", "array-contains-any", filters.treatmentBenefits));
|
|
15055
|
+
}
|
|
15056
|
+
let useNameLower = false;
|
|
15057
|
+
let searchTerm = "";
|
|
15058
|
+
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
15059
|
+
searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
15060
|
+
useNameLower = true;
|
|
15061
|
+
constraints.push((0, import_firestore45.where)("nameLower", ">=", searchTerm));
|
|
15062
|
+
constraints.push((0, import_firestore45.where)("nameLower", "<=", searchTerm + "\uF8FF"));
|
|
15063
|
+
constraints.push((0, import_firestore45.orderBy)("nameLower"));
|
|
15064
|
+
} else {
|
|
15065
|
+
constraints.push((0, import_firestore45.orderBy)("nameLower"));
|
|
15066
|
+
}
|
|
15067
|
+
if (filters.lastDoc) {
|
|
15027
15068
|
constraints.push((0, import_firestore45.startAfter)(filters.lastDoc));
|
|
15028
|
-
|
|
15029
|
-
|
|
15069
|
+
}
|
|
15070
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
15030
15071
|
constraints.push((0, import_firestore45.limit)(filters.pagination));
|
|
15031
15072
|
}
|
|
15032
|
-
let proceduresResult = [];
|
|
15033
|
-
let lastVisibleDoc = null;
|
|
15034
15073
|
if (isGeoQuery) {
|
|
15035
15074
|
const center = filters.location;
|
|
15036
15075
|
const radiusInKm = filters.radiusInKm;
|
|
@@ -15039,178 +15078,63 @@ var ProcedureService = class extends BaseService {
|
|
|
15039
15078
|
radiusInKm * 1e3
|
|
15040
15079
|
// Convert to meters
|
|
15041
15080
|
);
|
|
15042
|
-
|
|
15081
|
+
let allDocs = [];
|
|
15043
15082
|
for (const bound of bounds) {
|
|
15044
15083
|
const geoConstraints = [
|
|
15045
|
-
...constraints,
|
|
15084
|
+
...constraints.filter((c) => !c.fieldPath || c.fieldPath !== "name"),
|
|
15085
|
+
// Remove name orderBy for geo
|
|
15046
15086
|
(0, import_firestore45.where)("clinicInfo.location.geohash", ">=", bound[0]),
|
|
15047
|
-
(0, import_firestore45.where)("clinicInfo.location.geohash", "<=", bound[1])
|
|
15087
|
+
(0, import_firestore45.where)("clinicInfo.location.geohash", "<=", bound[1]),
|
|
15088
|
+
(0, import_firestore45.orderBy)("clinicInfo.location.geohash")
|
|
15048
15089
|
];
|
|
15049
|
-
const q = (0, import_firestore45.query)(
|
|
15050
|
-
(0, import_firestore45.collection)(this.db, PROCEDURES_COLLECTION),
|
|
15051
|
-
...geoConstraints
|
|
15052
|
-
);
|
|
15090
|
+
const q = (0, import_firestore45.query)((0, import_firestore45.collection)(this.db, PROCEDURES_COLLECTION), ...geoConstraints);
|
|
15053
15091
|
const querySnapshot = await (0, import_firestore45.getDocs)(q);
|
|
15054
|
-
console.log(
|
|
15055
|
-
`[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures in geo bound`
|
|
15056
|
-
);
|
|
15057
15092
|
for (const doc37 of querySnapshot.docs) {
|
|
15058
15093
|
const procedure = { ...doc37.data(), id: doc37.id };
|
|
15059
15094
|
const distance = (0, import_geofire_common8.distanceBetween)(
|
|
15060
15095
|
[center.latitude, center.longitude],
|
|
15061
|
-
[
|
|
15062
|
-
procedure.clinicInfo.location.latitude,
|
|
15063
|
-
procedure.clinicInfo.location.longitude
|
|
15064
|
-
]
|
|
15096
|
+
[procedure.clinicInfo.location.latitude, procedure.clinicInfo.location.longitude]
|
|
15065
15097
|
);
|
|
15066
15098
|
const distanceInKm = distance / 1e3;
|
|
15067
15099
|
if (distanceInKm <= radiusInKm) {
|
|
15068
|
-
|
|
15069
|
-
...procedure,
|
|
15070
|
-
distance: distanceInKm
|
|
15071
|
-
});
|
|
15100
|
+
allDocs.push({ ...procedure, distance: distanceInKm });
|
|
15072
15101
|
}
|
|
15073
15102
|
}
|
|
15074
15103
|
}
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
filteredProcedures,
|
|
15078
|
-
filters
|
|
15079
|
-
);
|
|
15080
|
-
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
15104
|
+
allDocs.sort((a, b) => a.distance - b.distance);
|
|
15105
|
+
let paginated = allDocs;
|
|
15081
15106
|
if (filters.pagination && filters.pagination > 0) {
|
|
15082
15107
|
let startIndex = 0;
|
|
15083
15108
|
if (filters.lastDoc) {
|
|
15084
|
-
const lastDocIndex =
|
|
15085
|
-
|
|
15086
|
-
);
|
|
15087
|
-
if (lastDocIndex !== -1) {
|
|
15088
|
-
startIndex = lastDocIndex + 1;
|
|
15089
|
-
}
|
|
15109
|
+
const lastDocIndex = allDocs.findIndex((p) => p.id === filters.lastDoc.id);
|
|
15110
|
+
if (lastDocIndex !== -1) startIndex = lastDocIndex + 1;
|
|
15090
15111
|
}
|
|
15091
|
-
|
|
15092
|
-
startIndex,
|
|
15093
|
-
startIndex + filters.pagination
|
|
15094
|
-
);
|
|
15095
|
-
lastVisibleDoc = paginatedProcedures.length > 0 ? paginatedProcedures[paginatedProcedures.length - 1] : null;
|
|
15096
|
-
proceduresResult = paginatedProcedures;
|
|
15097
|
-
} else {
|
|
15098
|
-
proceduresResult = filteredProcedures;
|
|
15112
|
+
paginated = allDocs.slice(startIndex, startIndex + filters.pagination);
|
|
15099
15113
|
}
|
|
15114
|
+
const lastVisibleDoc = paginated.length > 0 ? paginated[paginated.length - 1] : null;
|
|
15115
|
+
return { procedures: paginated, lastDoc: lastVisibleDoc };
|
|
15100
15116
|
} else {
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
if (filters.location) {
|
|
15113
|
-
const center = filters.location;
|
|
15114
|
-
const proceduresWithDistance = [];
|
|
15115
|
-
procedures.forEach((procedure) => {
|
|
15116
|
-
const distance = (0, import_geofire_common8.distanceBetween)(
|
|
15117
|
-
[center.latitude, center.longitude],
|
|
15118
|
-
[
|
|
15119
|
-
procedure.clinicInfo.location.latitude,
|
|
15120
|
-
procedure.clinicInfo.location.longitude
|
|
15121
|
-
]
|
|
15122
|
-
);
|
|
15123
|
-
proceduresWithDistance.push({
|
|
15124
|
-
...procedure,
|
|
15125
|
-
distance: distance / 1e3
|
|
15126
|
-
// Convert to kilometers
|
|
15127
|
-
});
|
|
15128
|
-
});
|
|
15129
|
-
let filteredProcedures = proceduresWithDistance;
|
|
15130
|
-
filteredProcedures = this.applyInMemoryFilters(
|
|
15131
|
-
filteredProcedures,
|
|
15132
|
-
filters
|
|
15133
|
-
);
|
|
15134
|
-
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
15135
|
-
proceduresResult = filteredProcedures;
|
|
15136
|
-
} else {
|
|
15137
|
-
let filteredProcedures = this.applyInMemoryFilters(
|
|
15138
|
-
procedures,
|
|
15139
|
-
filters
|
|
15140
|
-
);
|
|
15141
|
-
proceduresResult = filteredProcedures;
|
|
15117
|
+
let q = (0, import_firestore45.query)((0, import_firestore45.collection)(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
15118
|
+
let querySnapshot = await (0, import_firestore45.getDocs)(q);
|
|
15119
|
+
if (useNameLower && querySnapshot.empty && searchTerm) {
|
|
15120
|
+
constraints.pop();
|
|
15121
|
+
constraints.pop();
|
|
15122
|
+
constraints.pop();
|
|
15123
|
+
constraints.push((0, import_firestore45.where)("name", ">=", searchTerm));
|
|
15124
|
+
constraints.push((0, import_firestore45.where)("name", "<=", searchTerm + "\uF8FF"));
|
|
15125
|
+
constraints.push((0, import_firestore45.orderBy)("name"));
|
|
15126
|
+
q = (0, import_firestore45.query)((0, import_firestore45.collection)(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
15127
|
+
querySnapshot = await (0, import_firestore45.getDocs)(q);
|
|
15142
15128
|
}
|
|
15143
|
-
|
|
15129
|
+
const procedures = querySnapshot.docs.map((doc37) => ({ ...doc37.data(), id: doc37.id }));
|
|
15130
|
+
const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
15131
|
+
return { procedures, lastDoc: lastVisibleDoc };
|
|
15144
15132
|
}
|
|
15145
|
-
return {
|
|
15146
|
-
procedures: proceduresResult,
|
|
15147
|
-
lastDoc: lastVisibleDoc
|
|
15148
|
-
};
|
|
15149
15133
|
} catch (error) {
|
|
15150
15134
|
console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
|
|
15151
15135
|
throw error;
|
|
15152
15136
|
}
|
|
15153
15137
|
}
|
|
15154
|
-
/**
|
|
15155
|
-
* Helper method to apply in-memory filters to procedures
|
|
15156
|
-
* Used by getProceduresByFilters to apply filters that can't be done in Firestore queries
|
|
15157
|
-
*
|
|
15158
|
-
* @param procedures - The procedures to filter
|
|
15159
|
-
* @param filters - The filters to apply
|
|
15160
|
-
* @returns Filtered procedures
|
|
15161
|
-
*/
|
|
15162
|
-
applyInMemoryFilters(procedures, filters) {
|
|
15163
|
-
let filteredProcedures = procedures;
|
|
15164
|
-
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
15165
|
-
const searchTerm = filters.nameSearch.toLowerCase().trim();
|
|
15166
|
-
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
15167
|
-
return procedure.name.toLowerCase().includes(searchTerm);
|
|
15168
|
-
});
|
|
15169
|
-
}
|
|
15170
|
-
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
15171
|
-
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
15172
|
-
return filters.treatmentBenefits.every(
|
|
15173
|
-
(benefit) => procedure.treatmentBenefits.includes(benefit)
|
|
15174
|
-
);
|
|
15175
|
-
});
|
|
15176
|
-
}
|
|
15177
|
-
if (filters.procedureCategory) {
|
|
15178
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15179
|
-
(procedure) => procedure.category.id === filters.procedureCategory
|
|
15180
|
-
);
|
|
15181
|
-
}
|
|
15182
|
-
if (filters.procedureSubcategory) {
|
|
15183
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15184
|
-
(procedure) => procedure.subcategory.id === filters.procedureSubcategory
|
|
15185
|
-
);
|
|
15186
|
-
}
|
|
15187
|
-
if (filters.procedureTechnology) {
|
|
15188
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15189
|
-
(procedure) => procedure.technology.id === filters.procedureTechnology
|
|
15190
|
-
);
|
|
15191
|
-
}
|
|
15192
|
-
if (filters.minPrice !== void 0) {
|
|
15193
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15194
|
-
(procedure) => procedure.price >= filters.minPrice
|
|
15195
|
-
);
|
|
15196
|
-
}
|
|
15197
|
-
if (filters.maxPrice !== void 0) {
|
|
15198
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15199
|
-
(procedure) => procedure.price <= filters.maxPrice
|
|
15200
|
-
);
|
|
15201
|
-
}
|
|
15202
|
-
if (filters.minRating !== void 0) {
|
|
15203
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15204
|
-
(procedure) => procedure.reviewInfo.averageRating >= filters.minRating
|
|
15205
|
-
);
|
|
15206
|
-
}
|
|
15207
|
-
if (filters.maxRating !== void 0) {
|
|
15208
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15209
|
-
(procedure) => procedure.reviewInfo.averageRating <= filters.maxRating
|
|
15210
|
-
);
|
|
15211
|
-
}
|
|
15212
|
-
return filteredProcedures;
|
|
15213
|
-
}
|
|
15214
15138
|
/**
|
|
15215
15139
|
* Creates a consultation procedure without requiring a product
|
|
15216
15140
|
* This is a special method for consultation procedures that don't use products
|
|
@@ -15283,6 +15207,7 @@ var ProcedureService = class extends BaseService {
|
|
|
15283
15207
|
const newProcedure = {
|
|
15284
15208
|
id: procedureId,
|
|
15285
15209
|
...data,
|
|
15210
|
+
nameLower: data.nameLower || data.name.toLowerCase(),
|
|
15286
15211
|
photos: processedPhotos,
|
|
15287
15212
|
category,
|
|
15288
15213
|
subcategory,
|
package/dist/index.mjs
CHANGED
|
@@ -7039,6 +7039,7 @@ var PractitionerService = class extends BaseService {
|
|
|
7039
7039
|
}
|
|
7040
7040
|
const consultationData = {
|
|
7041
7041
|
name: "Free Consultation",
|
|
7042
|
+
nameLower: "free consultation",
|
|
7042
7043
|
description: "Free initial consultation to discuss treatment options and assess patient needs.",
|
|
7043
7044
|
family: "aesthetics" /* AESTHETICS */,
|
|
7044
7045
|
categoryId: "consultation",
|
|
@@ -14568,6 +14569,7 @@ import {
|
|
|
14568
14569
|
import { z as z24 } from "zod";
|
|
14569
14570
|
var createProcedureSchema = z24.object({
|
|
14570
14571
|
name: z24.string().min(1).max(200),
|
|
14572
|
+
nameLower: z24.string().min(1).max(200),
|
|
14571
14573
|
description: z24.string().min(1).max(2e3),
|
|
14572
14574
|
family: z24.nativeEnum(ProcedureFamily),
|
|
14573
14575
|
categoryId: z24.string().min(1),
|
|
@@ -14585,6 +14587,7 @@ var createProcedureSchema = z24.object({
|
|
|
14585
14587
|
});
|
|
14586
14588
|
var updateProcedureSchema = z24.object({
|
|
14587
14589
|
name: z24.string().min(3).max(100).optional(),
|
|
14590
|
+
nameLower: z24.string().min(1).max(200).optional(),
|
|
14588
14591
|
description: z24.string().min(3).max(1e3).optional(),
|
|
14589
14592
|
price: z24.number().min(0).optional(),
|
|
14590
14593
|
currency: z24.nativeEnum(Currency).optional(),
|
|
@@ -14601,6 +14604,7 @@ var updateProcedureSchema = z24.object({
|
|
|
14601
14604
|
});
|
|
14602
14605
|
var procedureSchema = createProcedureSchema.extend({
|
|
14603
14606
|
id: z24.string().min(1),
|
|
14607
|
+
nameLower: z24.string().min(1).max(200),
|
|
14604
14608
|
category: z24.any(),
|
|
14605
14609
|
// We'll validate the full category object separately
|
|
14606
14610
|
subcategory: z24.any(),
|
|
@@ -14769,6 +14773,7 @@ var ProcedureService = class extends BaseService {
|
|
|
14769
14773
|
const newProcedure = {
|
|
14770
14774
|
id: procedureId,
|
|
14771
14775
|
...validatedData,
|
|
14776
|
+
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
14772
14777
|
photos: processedPhotos,
|
|
14773
14778
|
category,
|
|
14774
14779
|
// Embed full objects
|
|
@@ -14902,6 +14907,7 @@ var ProcedureService = class extends BaseService {
|
|
|
14902
14907
|
const newProcedure = {
|
|
14903
14908
|
id: procedureId,
|
|
14904
14909
|
...validatedData,
|
|
14910
|
+
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
14905
14911
|
practitionerId,
|
|
14906
14912
|
// Override practitionerId with the correct one
|
|
14907
14913
|
photos: processedPhotos,
|
|
@@ -15080,6 +15086,9 @@ var ProcedureService = class extends BaseService {
|
|
|
15080
15086
|
};
|
|
15081
15087
|
}
|
|
15082
15088
|
let finalCategoryId = existingProcedure.category.id;
|
|
15089
|
+
if (validatedData.name) {
|
|
15090
|
+
updatedProcedureData.nameLower = validatedData.nameLower || validatedData.name.toLowerCase();
|
|
15091
|
+
}
|
|
15083
15092
|
if (validatedData.categoryId) {
|
|
15084
15093
|
const category = await this.categoryService.getById(
|
|
15085
15094
|
validatedData.categoryId
|
|
@@ -15237,6 +15246,8 @@ var ProcedureService = class extends BaseService {
|
|
|
15237
15246
|
/**
|
|
15238
15247
|
* Searches and filters procedures based on multiple criteria
|
|
15239
15248
|
*
|
|
15249
|
+
* @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
|
|
15250
|
+
*
|
|
15240
15251
|
* @param filters - Various filters to apply
|
|
15241
15252
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
15242
15253
|
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
@@ -15257,10 +15268,6 @@ var ProcedureService = class extends BaseService {
|
|
|
15257
15268
|
*/
|
|
15258
15269
|
async getProceduresByFilters(filters) {
|
|
15259
15270
|
try {
|
|
15260
|
-
console.log(
|
|
15261
|
-
"[PROCEDURE_SERVICE] Starting procedure filtering with criteria:",
|
|
15262
|
-
filters
|
|
15263
|
-
);
|
|
15264
15271
|
const isGeoQuery = filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
15265
15272
|
const constraints = [];
|
|
15266
15273
|
if (filters.isActive !== void 0) {
|
|
@@ -15271,15 +15278,47 @@ var ProcedureService = class extends BaseService {
|
|
|
15271
15278
|
if (filters.procedureFamily) {
|
|
15272
15279
|
constraints.push(where29("family", "==", filters.procedureFamily));
|
|
15273
15280
|
}
|
|
15274
|
-
|
|
15275
|
-
|
|
15281
|
+
if (filters.procedureCategory) {
|
|
15282
|
+
constraints.push(where29("category.id", "==", filters.procedureCategory));
|
|
15283
|
+
}
|
|
15284
|
+
if (filters.procedureSubcategory) {
|
|
15285
|
+
constraints.push(where29("subcategory.id", "==", filters.procedureSubcategory));
|
|
15286
|
+
}
|
|
15287
|
+
if (filters.procedureTechnology) {
|
|
15288
|
+
constraints.push(where29("technology.id", "==", filters.procedureTechnology));
|
|
15289
|
+
}
|
|
15290
|
+
if (filters.minPrice !== void 0) {
|
|
15291
|
+
constraints.push(where29("price", ">=", filters.minPrice));
|
|
15292
|
+
}
|
|
15293
|
+
if (filters.maxPrice !== void 0) {
|
|
15294
|
+
constraints.push(where29("price", "<=", filters.maxPrice));
|
|
15295
|
+
}
|
|
15296
|
+
if (filters.minRating !== void 0) {
|
|
15297
|
+
constraints.push(where29("reviewInfo.averageRating", ">=", filters.minRating));
|
|
15298
|
+
}
|
|
15299
|
+
if (filters.maxRating !== void 0) {
|
|
15300
|
+
constraints.push(where29("reviewInfo.averageRating", "<=", filters.maxRating));
|
|
15301
|
+
}
|
|
15302
|
+
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
15303
|
+
constraints.push(where29("treatmentBenefits", "array-contains-any", filters.treatmentBenefits));
|
|
15304
|
+
}
|
|
15305
|
+
let useNameLower = false;
|
|
15306
|
+
let searchTerm = "";
|
|
15307
|
+
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
15308
|
+
searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
15309
|
+
useNameLower = true;
|
|
15310
|
+
constraints.push(where29("nameLower", ">=", searchTerm));
|
|
15311
|
+
constraints.push(where29("nameLower", "<=", searchTerm + "\uF8FF"));
|
|
15312
|
+
constraints.push(orderBy17("nameLower"));
|
|
15313
|
+
} else {
|
|
15314
|
+
constraints.push(orderBy17("nameLower"));
|
|
15315
|
+
}
|
|
15316
|
+
if (filters.lastDoc) {
|
|
15276
15317
|
constraints.push(startAfter13(filters.lastDoc));
|
|
15277
|
-
|
|
15278
|
-
|
|
15318
|
+
}
|
|
15319
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
15279
15320
|
constraints.push(limit15(filters.pagination));
|
|
15280
15321
|
}
|
|
15281
|
-
let proceduresResult = [];
|
|
15282
|
-
let lastVisibleDoc = null;
|
|
15283
15322
|
if (isGeoQuery) {
|
|
15284
15323
|
const center = filters.location;
|
|
15285
15324
|
const radiusInKm = filters.radiusInKm;
|
|
@@ -15288,178 +15327,63 @@ var ProcedureService = class extends BaseService {
|
|
|
15288
15327
|
radiusInKm * 1e3
|
|
15289
15328
|
// Convert to meters
|
|
15290
15329
|
);
|
|
15291
|
-
|
|
15330
|
+
let allDocs = [];
|
|
15292
15331
|
for (const bound of bounds) {
|
|
15293
15332
|
const geoConstraints = [
|
|
15294
|
-
...constraints,
|
|
15333
|
+
...constraints.filter((c) => !c.fieldPath || c.fieldPath !== "name"),
|
|
15334
|
+
// Remove name orderBy for geo
|
|
15295
15335
|
where29("clinicInfo.location.geohash", ">=", bound[0]),
|
|
15296
|
-
where29("clinicInfo.location.geohash", "<=", bound[1])
|
|
15336
|
+
where29("clinicInfo.location.geohash", "<=", bound[1]),
|
|
15337
|
+
orderBy17("clinicInfo.location.geohash")
|
|
15297
15338
|
];
|
|
15298
|
-
const q = query29(
|
|
15299
|
-
collection29(this.db, PROCEDURES_COLLECTION),
|
|
15300
|
-
...geoConstraints
|
|
15301
|
-
);
|
|
15339
|
+
const q = query29(collection29(this.db, PROCEDURES_COLLECTION), ...geoConstraints);
|
|
15302
15340
|
const querySnapshot = await getDocs29(q);
|
|
15303
|
-
console.log(
|
|
15304
|
-
`[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures in geo bound`
|
|
15305
|
-
);
|
|
15306
15341
|
for (const doc37 of querySnapshot.docs) {
|
|
15307
15342
|
const procedure = { ...doc37.data(), id: doc37.id };
|
|
15308
15343
|
const distance = distanceBetween6(
|
|
15309
15344
|
[center.latitude, center.longitude],
|
|
15310
|
-
[
|
|
15311
|
-
procedure.clinicInfo.location.latitude,
|
|
15312
|
-
procedure.clinicInfo.location.longitude
|
|
15313
|
-
]
|
|
15345
|
+
[procedure.clinicInfo.location.latitude, procedure.clinicInfo.location.longitude]
|
|
15314
15346
|
);
|
|
15315
15347
|
const distanceInKm = distance / 1e3;
|
|
15316
15348
|
if (distanceInKm <= radiusInKm) {
|
|
15317
|
-
|
|
15318
|
-
...procedure,
|
|
15319
|
-
distance: distanceInKm
|
|
15320
|
-
});
|
|
15349
|
+
allDocs.push({ ...procedure, distance: distanceInKm });
|
|
15321
15350
|
}
|
|
15322
15351
|
}
|
|
15323
15352
|
}
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
filteredProcedures,
|
|
15327
|
-
filters
|
|
15328
|
-
);
|
|
15329
|
-
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
15353
|
+
allDocs.sort((a, b) => a.distance - b.distance);
|
|
15354
|
+
let paginated = allDocs;
|
|
15330
15355
|
if (filters.pagination && filters.pagination > 0) {
|
|
15331
15356
|
let startIndex = 0;
|
|
15332
15357
|
if (filters.lastDoc) {
|
|
15333
|
-
const lastDocIndex =
|
|
15334
|
-
|
|
15335
|
-
);
|
|
15336
|
-
if (lastDocIndex !== -1) {
|
|
15337
|
-
startIndex = lastDocIndex + 1;
|
|
15338
|
-
}
|
|
15358
|
+
const lastDocIndex = allDocs.findIndex((p) => p.id === filters.lastDoc.id);
|
|
15359
|
+
if (lastDocIndex !== -1) startIndex = lastDocIndex + 1;
|
|
15339
15360
|
}
|
|
15340
|
-
|
|
15341
|
-
startIndex,
|
|
15342
|
-
startIndex + filters.pagination
|
|
15343
|
-
);
|
|
15344
|
-
lastVisibleDoc = paginatedProcedures.length > 0 ? paginatedProcedures[paginatedProcedures.length - 1] : null;
|
|
15345
|
-
proceduresResult = paginatedProcedures;
|
|
15346
|
-
} else {
|
|
15347
|
-
proceduresResult = filteredProcedures;
|
|
15361
|
+
paginated = allDocs.slice(startIndex, startIndex + filters.pagination);
|
|
15348
15362
|
}
|
|
15363
|
+
const lastVisibleDoc = paginated.length > 0 ? paginated[paginated.length - 1] : null;
|
|
15364
|
+
return { procedures: paginated, lastDoc: lastVisibleDoc };
|
|
15349
15365
|
} else {
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
if (filters.location) {
|
|
15362
|
-
const center = filters.location;
|
|
15363
|
-
const proceduresWithDistance = [];
|
|
15364
|
-
procedures.forEach((procedure) => {
|
|
15365
|
-
const distance = distanceBetween6(
|
|
15366
|
-
[center.latitude, center.longitude],
|
|
15367
|
-
[
|
|
15368
|
-
procedure.clinicInfo.location.latitude,
|
|
15369
|
-
procedure.clinicInfo.location.longitude
|
|
15370
|
-
]
|
|
15371
|
-
);
|
|
15372
|
-
proceduresWithDistance.push({
|
|
15373
|
-
...procedure,
|
|
15374
|
-
distance: distance / 1e3
|
|
15375
|
-
// Convert to kilometers
|
|
15376
|
-
});
|
|
15377
|
-
});
|
|
15378
|
-
let filteredProcedures = proceduresWithDistance;
|
|
15379
|
-
filteredProcedures = this.applyInMemoryFilters(
|
|
15380
|
-
filteredProcedures,
|
|
15381
|
-
filters
|
|
15382
|
-
);
|
|
15383
|
-
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
15384
|
-
proceduresResult = filteredProcedures;
|
|
15385
|
-
} else {
|
|
15386
|
-
let filteredProcedures = this.applyInMemoryFilters(
|
|
15387
|
-
procedures,
|
|
15388
|
-
filters
|
|
15389
|
-
);
|
|
15390
|
-
proceduresResult = filteredProcedures;
|
|
15366
|
+
let q = query29(collection29(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
15367
|
+
let querySnapshot = await getDocs29(q);
|
|
15368
|
+
if (useNameLower && querySnapshot.empty && searchTerm) {
|
|
15369
|
+
constraints.pop();
|
|
15370
|
+
constraints.pop();
|
|
15371
|
+
constraints.pop();
|
|
15372
|
+
constraints.push(where29("name", ">=", searchTerm));
|
|
15373
|
+
constraints.push(where29("name", "<=", searchTerm + "\uF8FF"));
|
|
15374
|
+
constraints.push(orderBy17("name"));
|
|
15375
|
+
q = query29(collection29(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
15376
|
+
querySnapshot = await getDocs29(q);
|
|
15391
15377
|
}
|
|
15392
|
-
|
|
15378
|
+
const procedures = querySnapshot.docs.map((doc37) => ({ ...doc37.data(), id: doc37.id }));
|
|
15379
|
+
const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
15380
|
+
return { procedures, lastDoc: lastVisibleDoc };
|
|
15393
15381
|
}
|
|
15394
|
-
return {
|
|
15395
|
-
procedures: proceduresResult,
|
|
15396
|
-
lastDoc: lastVisibleDoc
|
|
15397
|
-
};
|
|
15398
15382
|
} catch (error) {
|
|
15399
15383
|
console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
|
|
15400
15384
|
throw error;
|
|
15401
15385
|
}
|
|
15402
15386
|
}
|
|
15403
|
-
/**
|
|
15404
|
-
* Helper method to apply in-memory filters to procedures
|
|
15405
|
-
* Used by getProceduresByFilters to apply filters that can't be done in Firestore queries
|
|
15406
|
-
*
|
|
15407
|
-
* @param procedures - The procedures to filter
|
|
15408
|
-
* @param filters - The filters to apply
|
|
15409
|
-
* @returns Filtered procedures
|
|
15410
|
-
*/
|
|
15411
|
-
applyInMemoryFilters(procedures, filters) {
|
|
15412
|
-
let filteredProcedures = procedures;
|
|
15413
|
-
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
15414
|
-
const searchTerm = filters.nameSearch.toLowerCase().trim();
|
|
15415
|
-
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
15416
|
-
return procedure.name.toLowerCase().includes(searchTerm);
|
|
15417
|
-
});
|
|
15418
|
-
}
|
|
15419
|
-
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
15420
|
-
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
15421
|
-
return filters.treatmentBenefits.every(
|
|
15422
|
-
(benefit) => procedure.treatmentBenefits.includes(benefit)
|
|
15423
|
-
);
|
|
15424
|
-
});
|
|
15425
|
-
}
|
|
15426
|
-
if (filters.procedureCategory) {
|
|
15427
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15428
|
-
(procedure) => procedure.category.id === filters.procedureCategory
|
|
15429
|
-
);
|
|
15430
|
-
}
|
|
15431
|
-
if (filters.procedureSubcategory) {
|
|
15432
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15433
|
-
(procedure) => procedure.subcategory.id === filters.procedureSubcategory
|
|
15434
|
-
);
|
|
15435
|
-
}
|
|
15436
|
-
if (filters.procedureTechnology) {
|
|
15437
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15438
|
-
(procedure) => procedure.technology.id === filters.procedureTechnology
|
|
15439
|
-
);
|
|
15440
|
-
}
|
|
15441
|
-
if (filters.minPrice !== void 0) {
|
|
15442
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15443
|
-
(procedure) => procedure.price >= filters.minPrice
|
|
15444
|
-
);
|
|
15445
|
-
}
|
|
15446
|
-
if (filters.maxPrice !== void 0) {
|
|
15447
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15448
|
-
(procedure) => procedure.price <= filters.maxPrice
|
|
15449
|
-
);
|
|
15450
|
-
}
|
|
15451
|
-
if (filters.minRating !== void 0) {
|
|
15452
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15453
|
-
(procedure) => procedure.reviewInfo.averageRating >= filters.minRating
|
|
15454
|
-
);
|
|
15455
|
-
}
|
|
15456
|
-
if (filters.maxRating !== void 0) {
|
|
15457
|
-
filteredProcedures = filteredProcedures.filter(
|
|
15458
|
-
(procedure) => procedure.reviewInfo.averageRating <= filters.maxRating
|
|
15459
|
-
);
|
|
15460
|
-
}
|
|
15461
|
-
return filteredProcedures;
|
|
15462
|
-
}
|
|
15463
15387
|
/**
|
|
15464
15388
|
* Creates a consultation procedure without requiring a product
|
|
15465
15389
|
* This is a special method for consultation procedures that don't use products
|
|
@@ -15532,6 +15456,7 @@ var ProcedureService = class extends BaseService {
|
|
|
15532
15456
|
const newProcedure = {
|
|
15533
15457
|
id: procedureId,
|
|
15534
15458
|
...data,
|
|
15459
|
+
nameLower: data.nameLower || data.name.toLowerCase(),
|
|
15535
15460
|
photos: processedPhotos,
|
|
15536
15461
|
category,
|
|
15537
15462
|
subcategory,
|
package/package.json
CHANGED
|
@@ -1286,6 +1286,7 @@ export class PractitionerService extends BaseService {
|
|
|
1286
1286
|
// Create procedure data for free consultation (without productId)
|
|
1287
1287
|
const consultationData: Omit<CreateProcedureData, "productId"> = {
|
|
1288
1288
|
name: "Free Consultation",
|
|
1289
|
+
nameLower: "free consultation",
|
|
1289
1290
|
description:
|
|
1290
1291
|
"Free initial consultation to discuss treatment options and assess patient needs.",
|
|
1291
1292
|
family: ProcedureFamily.AESTHETICS,
|
|
@@ -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.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,122 @@ 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
|
-
|
|
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
|
+
// lastDoc treba da bude ceo snapshot, ne samo id!
|
|
924
959
|
constraints.push(startAfter(filters.lastDoc));
|
|
925
|
-
|
|
926
|
-
|
|
960
|
+
}
|
|
961
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
927
962
|
constraints.push(limit(filters.pagination));
|
|
928
963
|
}
|
|
929
964
|
|
|
930
|
-
|
|
931
|
-
let lastVisibleDoc = null;
|
|
932
|
-
|
|
933
|
-
// For geo queries, we need a different approach
|
|
965
|
+
// Geo-query: special handling
|
|
934
966
|
if (isGeoQuery) {
|
|
967
|
+
// For geo queries, use geohash bounds and add all other constraints
|
|
935
968
|
const center = filters.location!;
|
|
936
969
|
const radiusInKm = filters.radiusInKm!;
|
|
937
|
-
|
|
938
|
-
// Get the geohash query bounds
|
|
939
970
|
const bounds = geohashQueryBounds(
|
|
940
971
|
[center.latitude, center.longitude],
|
|
941
972
|
radiusInKm * 1000 // Convert to meters
|
|
942
973
|
);
|
|
943
|
-
|
|
944
|
-
// Collect matching procedures from all bounds
|
|
945
|
-
const matchingProcedures: (Procedure & { distance: number })[] = [];
|
|
946
|
-
|
|
947
|
-
// Execute queries for each bound
|
|
974
|
+
let allDocs: (Procedure & { distance: number })[] = [];
|
|
948
975
|
for (const bound of bounds) {
|
|
949
|
-
// Create a geo query for this bound
|
|
950
976
|
const geoConstraints = [
|
|
951
|
-
...constraints,
|
|
977
|
+
...constraints.filter(c => !(c as any).fieldPath || (c as any).fieldPath !== "name"), // Remove name orderBy for geo
|
|
952
978
|
where("clinicInfo.location.geohash", ">=", bound[0]),
|
|
953
979
|
where("clinicInfo.location.geohash", "<=", bound[1]),
|
|
980
|
+
orderBy("clinicInfo.location.geohash"),
|
|
954
981
|
];
|
|
955
|
-
|
|
956
|
-
const q = query(
|
|
957
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
958
|
-
...geoConstraints
|
|
959
|
-
);
|
|
982
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...geoConstraints);
|
|
960
983
|
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
984
|
for (const doc of querySnapshot.docs) {
|
|
968
985
|
const procedure = { ...doc.data(), id: doc.id } as Procedure;
|
|
969
|
-
|
|
970
|
-
// Calculate actual distance
|
|
971
986
|
const distance = distanceBetween(
|
|
972
987
|
[center.latitude, center.longitude],
|
|
973
|
-
[
|
|
974
|
-
procedure.clinicInfo.location.latitude,
|
|
975
|
-
procedure.clinicInfo.location.longitude,
|
|
976
|
-
]
|
|
988
|
+
[procedure.clinicInfo.location.latitude, procedure.clinicInfo.location.longitude]
|
|
977
989
|
);
|
|
978
|
-
|
|
979
|
-
// Convert to kilometers
|
|
980
990
|
const distanceInKm = distance / 1000;
|
|
981
|
-
|
|
982
|
-
// Check if within radius
|
|
983
991
|
if (distanceInKm <= radiusInKm) {
|
|
984
|
-
|
|
985
|
-
matchingProcedures.push({
|
|
986
|
-
...procedure,
|
|
987
|
-
distance: distanceInKm,
|
|
988
|
-
});
|
|
992
|
+
allDocs.push({ ...procedure, distance: distanceInKm });
|
|
989
993
|
}
|
|
990
994
|
}
|
|
991
995
|
}
|
|
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
996
|
// Sort by distance
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
997
|
+
allDocs.sort((a, b) => a.distance - b.distance);
|
|
998
|
+
// Paginate
|
|
999
|
+
let paginated = allDocs;
|
|
1006
1000
|
if (filters.pagination && filters.pagination > 0) {
|
|
1007
|
-
// If we have a lastDoc, find its index
|
|
1008
1001
|
let startIndex = 0;
|
|
1009
1002
|
if (filters.lastDoc) {
|
|
1010
|
-
const lastDocIndex =
|
|
1011
|
-
|
|
1012
|
-
);
|
|
1013
|
-
if (lastDocIndex !== -1) {
|
|
1014
|
-
startIndex = lastDocIndex + 1;
|
|
1015
|
-
}
|
|
1003
|
+
const lastDocIndex = allDocs.findIndex(p => p.id === filters.lastDoc.id);
|
|
1004
|
+
if (lastDocIndex !== -1) startIndex = lastDocIndex + 1;
|
|
1016
1005
|
}
|
|
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;
|
|
1006
|
+
paginated = allDocs.slice(startIndex, startIndex + filters.pagination);
|
|
1033
1007
|
}
|
|
1008
|
+
const lastVisibleDoc = paginated.length > 0 ? paginated[paginated.length - 1] : null;
|
|
1009
|
+
return { procedures: paginated, lastDoc: lastVisibleDoc };
|
|
1034
1010
|
} 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;
|
|
1011
|
+
// Regular query
|
|
1012
|
+
let q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1013
|
+
let querySnapshot = await getDocs(q);
|
|
1014
|
+
// Fallback na name ako nema rezultata i koristi se nameLower
|
|
1015
|
+
if (useNameLower && querySnapshot.empty && searchTerm) {
|
|
1016
|
+
// Ukloni poslednja 3 constraints (nameLower >=, nameLower <=, orderBy nameLower)
|
|
1017
|
+
constraints.pop();
|
|
1018
|
+
constraints.pop();
|
|
1019
|
+
constraints.pop();
|
|
1020
|
+
constraints.push(where("name", ">=", searchTerm));
|
|
1021
|
+
constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
|
|
1022
|
+
constraints.push(orderBy("name"));
|
|
1023
|
+
q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1024
|
+
querySnapshot = await getDocs(q);
|
|
1092
1025
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
lastVisibleDoc
|
|
1096
|
-
querySnapshot.docs.length > 0
|
|
1097
|
-
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1098
|
-
: null;
|
|
1026
|
+
const procedures = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Procedure));
|
|
1027
|
+
const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1028
|
+
return { procedures, lastDoc: lastVisibleDoc };
|
|
1099
1029
|
}
|
|
1100
|
-
|
|
1101
|
-
return {
|
|
1102
|
-
procedures: proceduresResult,
|
|
1103
|
-
lastDoc: lastVisibleDoc,
|
|
1104
|
-
};
|
|
1105
1030
|
} catch (error) {
|
|
1106
1031
|
console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
|
|
1107
1032
|
throw error;
|
|
1108
1033
|
}
|
|
1109
1034
|
}
|
|
1110
1035
|
|
|
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
1036
|
/**
|
|
1204
1037
|
* Creates a consultation procedure without requiring a product
|
|
1205
1038
|
* This is a special method for consultation procedures that don't use products
|
|
@@ -1301,6 +1134,7 @@ export class ProcedureService extends BaseService {
|
|
|
1301
1134
|
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
1302
1135
|
id: procedureId,
|
|
1303
1136
|
...data,
|
|
1137
|
+
nameLower: (data as any).nameLower || data.name.toLowerCase(),
|
|
1304
1138
|
photos: processedPhotos,
|
|
1305
1139
|
category,
|
|
1306
1140
|
subcategory,
|
|
@@ -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
|