@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.
@@ -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 */
@@ -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
- constraints.push((0, import_firestore45.orderBy)("clinicInfo.location.geohash"));
15026
- if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
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
- constraints.push((0, import_firestore45.limit)(filters.pagination));
15029
- } else if (filters.pagination && filters.pagination > 0) {
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
- const matchingProcedures = [];
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
- matchingProcedures.push({
15069
- ...procedure,
15070
- distance: distanceInKm
15071
- });
15100
+ allDocs.push({ ...procedure, distance: distanceInKm });
15072
15101
  }
15073
15102
  }
15074
15103
  }
15075
- let filteredProcedures = matchingProcedures;
15076
- filteredProcedures = this.applyInMemoryFilters(
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 = filteredProcedures.findIndex(
15085
- (procedure) => procedure.id === filters.lastDoc.id
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
- const paginatedProcedures = filteredProcedures.slice(
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
- const q = (0, import_firestore45.query)(
15102
- (0, import_firestore45.collection)(this.db, PROCEDURES_COLLECTION),
15103
- ...constraints
15104
- );
15105
- const querySnapshot = await (0, import_firestore45.getDocs)(q);
15106
- console.log(
15107
- `[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures with regular query`
15108
- );
15109
- const procedures = querySnapshot.docs.map((doc37) => {
15110
- return { ...doc37.data(), id: doc37.id };
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
- lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
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
- constraints.push(orderBy17("clinicInfo.location.geohash"));
15275
- if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
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
- constraints.push(limit15(filters.pagination));
15278
- } else if (filters.pagination && filters.pagination > 0) {
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
- const matchingProcedures = [];
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
- matchingProcedures.push({
15318
- ...procedure,
15319
- distance: distanceInKm
15320
- });
15349
+ allDocs.push({ ...procedure, distance: distanceInKm });
15321
15350
  }
15322
15351
  }
15323
15352
  }
15324
- let filteredProcedures = matchingProcedures;
15325
- filteredProcedures = this.applyInMemoryFilters(
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 = filteredProcedures.findIndex(
15334
- (procedure) => procedure.id === filters.lastDoc.id
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
- const paginatedProcedures = filteredProcedures.slice(
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
- const q = query29(
15351
- collection29(this.db, PROCEDURES_COLLECTION),
15352
- ...constraints
15353
- );
15354
- const querySnapshot = await getDocs29(q);
15355
- console.log(
15356
- `[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures with regular query`
15357
- );
15358
- const procedures = querySnapshot.docs.map((doc37) => {
15359
- return { ...doc37.data(), id: doc37.id };
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
- lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.8.10",
4
+ "version": "1.8.12",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -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
- // Add ordering to make pagination consistent
920
- constraints.push(orderBy("clinicInfo.location.geohash"));
921
-
922
- // Add pagination if specified
923
- if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
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
- constraints.push(limit(filters.pagination));
926
- } else if (filters.pagination && filters.pagination > 0) {
960
+ }
961
+ if (filters.pagination && filters.pagination > 0) {
927
962
  constraints.push(limit(filters.pagination));
928
963
  }
929
964
 
930
- let proceduresResult: (Procedure & { distance?: number })[] = [];
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
- // Add distance to procedure object
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
- filteredProcedures.sort((a, b) => a.distance - b.distance);
1004
-
1005
- // Apply pagination after all filters have been applied
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 = filteredProcedures.findIndex(
1011
- (procedure) => procedure.id === filters.lastDoc.id
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
- // For non-geo queries, execute a single query with all constraints
1036
- const q = query(
1037
- collection(this.db, PROCEDURES_COLLECTION),
1038
- ...constraints
1039
- );
1040
- const querySnapshot = await getDocs(q);
1041
-
1042
- console.log(
1043
- `[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures with regular query`
1044
- );
1045
-
1046
- // Convert docs to procedures
1047
- const procedures = querySnapshot.docs.map((doc) => {
1048
- return { ...doc.data(), id: doc.id } as Procedure;
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
- // Set last document for pagination
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