@blackcode_sa/metaestetics-api 1.11.3 → 1.12.1
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 +378 -334
- package/dist/admin/index.d.ts +378 -334
- package/dist/backoffice/index.d.mts +1198 -430
- package/dist/backoffice/index.d.ts +1198 -430
- package/dist/backoffice/index.js +1128 -245
- package/dist/backoffice/index.mjs +1119 -209
- package/dist/index.d.mts +4478 -4031
- package/dist/index.d.ts +4478 -4031
- package/dist/index.js +1974 -757
- package/dist/index.mjs +1735 -490
- package/package.json +1 -1
- package/src/backoffice/expo-safe/index.ts +4 -0
- package/src/backoffice/services/README.md +40 -0
- package/src/backoffice/services/brand.service.ts +85 -6
- package/src/backoffice/services/category.service.ts +92 -10
- package/src/backoffice/services/constants.service.ts +308 -0
- package/src/backoffice/services/documentation-template.service.ts +56 -2
- package/src/backoffice/services/index.ts +1 -0
- package/src/backoffice/services/product.service.ts +126 -5
- package/src/backoffice/services/requirement.service.ts +13 -0
- package/src/backoffice/services/subcategory.service.ts +184 -13
- package/src/backoffice/services/technology.service.ts +344 -129
- package/src/backoffice/types/admin-constants.types.ts +69 -0
- package/src/backoffice/types/brand.types.ts +1 -0
- package/src/backoffice/types/index.ts +2 -0
- package/src/backoffice/types/procedure-product.types.ts +38 -0
- package/src/backoffice/types/product.types.ts +31 -4
- package/src/backoffice/types/static/contraindication.types.ts +1 -0
- package/src/backoffice/types/static/treatment-benefit.types.ts +1 -0
- package/src/backoffice/types/technology.types.ts +113 -4
- package/src/backoffice/validations/schemas.ts +35 -9
- package/src/services/appointment/appointment.service.ts +0 -5
- package/src/services/appointment/utils/appointment.utils.ts +124 -113
- package/src/services/base.service.ts +10 -3
- package/src/services/documentation-templates/documentation-template.service.ts +116 -0
- package/src/services/media/media.service.ts +2 -2
- package/src/services/practitioner/practitioner.service.ts +201 -83
- package/src/services/procedure/README.md +76 -1
- package/src/services/procedure/procedure.service.ts +538 -235
- package/src/types/appointment/index.ts +2 -3
- package/src/types/clinic/index.ts +1 -6
- package/src/types/patient/medical-info.types.ts +3 -3
- package/src/types/procedure/index.ts +39 -20
- package/src/validations/clinic.schema.ts +1 -6
- package/src/validations/patient/medical-info.schema.ts +7 -2
- package/src/validations/procedure-product.schema.ts +41 -0
- package/src/validations/procedure.schema.ts +59 -8
- package/src/backoffice/services/__tests__/brand.service.test.ts +0 -196
- package/src/backoffice/services/__tests__/category.service.test.ts +0 -201
- package/src/backoffice/services/__tests__/product.service.test.ts +0 -358
- package/src/backoffice/services/__tests__/requirement.service.test.ts +0 -226
- package/src/backoffice/services/__tests__/subcategory.service.test.ts +0 -181
- package/src/backoffice/services/__tests__/technology.service.test.ts +0 -1097
|
@@ -20,39 +20,62 @@ import {
|
|
|
20
20
|
startAfter,
|
|
21
21
|
QueryConstraint,
|
|
22
22
|
documentId,
|
|
23
|
-
} from
|
|
24
|
-
import { BaseService } from
|
|
23
|
+
} from "firebase/firestore";
|
|
24
|
+
import { BaseService } from "../base.service";
|
|
25
25
|
import {
|
|
26
26
|
Procedure,
|
|
27
27
|
CreateProcedureData,
|
|
28
28
|
UpdateProcedureData,
|
|
29
29
|
PROCEDURES_COLLECTION,
|
|
30
30
|
ProcedureSummaryInfo,
|
|
31
|
-
} from
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import {
|
|
45
|
-
|
|
31
|
+
} from "../../types/procedure";
|
|
32
|
+
import {
|
|
33
|
+
createProcedureSchema,
|
|
34
|
+
updateProcedureSchema,
|
|
35
|
+
} from "../../validations/procedure.schema";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
import { Auth } from "firebase/auth";
|
|
38
|
+
import { Firestore } from "firebase/firestore";
|
|
39
|
+
import { FirebaseApp } from "firebase/app";
|
|
40
|
+
import {
|
|
41
|
+
Category,
|
|
42
|
+
CATEGORIES_COLLECTION,
|
|
43
|
+
} from "../../backoffice/types/category.types";
|
|
44
|
+
import {
|
|
45
|
+
Subcategory,
|
|
46
|
+
SUBCATEGORIES_COLLECTION,
|
|
47
|
+
} from "../../backoffice/types/subcategory.types";
|
|
48
|
+
import {
|
|
49
|
+
Technology,
|
|
50
|
+
TECHNOLOGIES_COLLECTION,
|
|
51
|
+
} from "../../backoffice/types/technology.types";
|
|
52
|
+
import {
|
|
53
|
+
Product,
|
|
54
|
+
PRODUCTS_COLLECTION,
|
|
55
|
+
} from "../../backoffice/types/product.types";
|
|
56
|
+
import { CategoryService } from "../../backoffice/services/category.service";
|
|
57
|
+
import { SubcategoryService } from "../../backoffice/services/subcategory.service";
|
|
58
|
+
import { TechnologyService } from "../../backoffice/services/technology.service";
|
|
59
|
+
import { ProductService } from "../../backoffice/services/product.service";
|
|
60
|
+
import {
|
|
61
|
+
Practitioner,
|
|
62
|
+
PRACTITIONERS_COLLECTION,
|
|
63
|
+
} from "../../types/practitioner";
|
|
46
64
|
import {
|
|
47
65
|
CertificationLevel,
|
|
48
66
|
CertificationSpecialty,
|
|
49
67
|
ProcedureFamily,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
type TreatmentBenefitDynamic,
|
|
69
|
+
} from "../../backoffice/types";
|
|
70
|
+
import {
|
|
71
|
+
Currency,
|
|
72
|
+
PricingMeasure,
|
|
73
|
+
} from "../../backoffice/types/static/pricing.types";
|
|
74
|
+
import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
|
|
75
|
+
import { ProcedureReviewInfo } from "../../types/reviews";
|
|
76
|
+
import { distanceBetween, geohashQueryBounds } from "geofire-common";
|
|
77
|
+
import { MediaService, MediaAccessLevel } from "../media/media.service";
|
|
78
|
+
import type { ProcedureProduct } from "../../backoffice/types/procedure-product.types";
|
|
56
79
|
|
|
57
80
|
export class ProcedureService extends BaseService {
|
|
58
81
|
private categoryService: CategoryService;
|
|
@@ -69,7 +92,7 @@ export class ProcedureService extends BaseService {
|
|
|
69
92
|
subcategoryService: SubcategoryService,
|
|
70
93
|
technologyService: TechnologyService,
|
|
71
94
|
productService: ProductService,
|
|
72
|
-
mediaService: MediaService
|
|
95
|
+
mediaService: MediaService
|
|
73
96
|
) {
|
|
74
97
|
super(db, auth, app);
|
|
75
98
|
this.categoryService = categoryService;
|
|
@@ -89,23 +112,25 @@ export class ProcedureService extends BaseService {
|
|
|
89
112
|
private async processMedia(
|
|
90
113
|
media: string | File | Blob | null | undefined,
|
|
91
114
|
ownerId: string,
|
|
92
|
-
collectionName: string
|
|
115
|
+
collectionName: string
|
|
93
116
|
): Promise<string | null> {
|
|
94
117
|
if (!media) return null;
|
|
95
118
|
|
|
96
119
|
// If already a string URL, return it directly
|
|
97
|
-
if (typeof media ===
|
|
120
|
+
if (typeof media === "string") {
|
|
98
121
|
return media;
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
// If it's a File, upload it using MediaService
|
|
102
125
|
if (media instanceof File || media instanceof Blob) {
|
|
103
|
-
console.log(
|
|
126
|
+
console.log(
|
|
127
|
+
`[ProcedureService] Uploading ${collectionName} media for ${ownerId}`
|
|
128
|
+
);
|
|
104
129
|
const metadata = await this.mediaService.uploadMedia(
|
|
105
130
|
media,
|
|
106
131
|
ownerId,
|
|
107
132
|
MediaAccessLevel.PUBLIC,
|
|
108
|
-
collectionName
|
|
133
|
+
collectionName
|
|
109
134
|
);
|
|
110
135
|
return metadata.url;
|
|
111
136
|
}
|
|
@@ -123,14 +148,18 @@ export class ProcedureService extends BaseService {
|
|
|
123
148
|
private async processMediaArray(
|
|
124
149
|
mediaArray: (string | File | Blob)[] | undefined,
|
|
125
150
|
ownerId: string,
|
|
126
|
-
collectionName: string
|
|
151
|
+
collectionName: string
|
|
127
152
|
): Promise<string[]> {
|
|
128
153
|
if (!mediaArray || mediaArray.length === 0) return [];
|
|
129
154
|
|
|
130
155
|
const result: string[] = [];
|
|
131
156
|
|
|
132
157
|
for (const media of mediaArray) {
|
|
133
|
-
const processedUrl = await this.processMedia(
|
|
158
|
+
const processedUrl = await this.processMedia(
|
|
159
|
+
media,
|
|
160
|
+
ownerId,
|
|
161
|
+
collectionName
|
|
162
|
+
);
|
|
134
163
|
if (processedUrl) {
|
|
135
164
|
result.push(processedUrl);
|
|
136
165
|
}
|
|
@@ -139,6 +168,49 @@ export class ProcedureService extends BaseService {
|
|
|
139
168
|
return result;
|
|
140
169
|
}
|
|
141
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Transforms validated procedure product data (with productId) to ProcedureProduct objects (with full product)
|
|
173
|
+
* @param productsMetadata Array of validated procedure product data
|
|
174
|
+
* @param technologyId Technology ID to fetch products from
|
|
175
|
+
* @returns Array of ProcedureProduct objects with full product information
|
|
176
|
+
*/
|
|
177
|
+
private async transformProductsMetadata(
|
|
178
|
+
productsMetadata: {
|
|
179
|
+
productId: string;
|
|
180
|
+
price: number;
|
|
181
|
+
currency: Currency;
|
|
182
|
+
pricingMeasure: PricingMeasure;
|
|
183
|
+
isDefault?: boolean;
|
|
184
|
+
}[],
|
|
185
|
+
technologyId: string
|
|
186
|
+
): Promise<ProcedureProduct[]> {
|
|
187
|
+
const transformedProducts: ProcedureProduct[] = [];
|
|
188
|
+
|
|
189
|
+
for (const productData of productsMetadata) {
|
|
190
|
+
// Fetch the full product object
|
|
191
|
+
const product = await this.productService.getById(
|
|
192
|
+
technologyId,
|
|
193
|
+
productData.productId
|
|
194
|
+
);
|
|
195
|
+
if (!product) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Product with ID ${productData.productId} not found for technology ${technologyId}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Transform to ProcedureProduct
|
|
202
|
+
transformedProducts.push({
|
|
203
|
+
product,
|
|
204
|
+
price: productData.price,
|
|
205
|
+
currency: productData.currency,
|
|
206
|
+
pricingMeasure: productData.pricingMeasure,
|
|
207
|
+
isDefault: productData.isDefault,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return transformedProducts;
|
|
212
|
+
}
|
|
213
|
+
|
|
142
214
|
/**
|
|
143
215
|
* Creates a new procedure
|
|
144
216
|
* @param data - The data for creating a new procedure
|
|
@@ -153,27 +225,45 @@ export class ProcedureService extends BaseService {
|
|
|
153
225
|
// Get references to related entities (Category, Subcategory, Technology, Product)
|
|
154
226
|
const [category, subcategory, technology, product] = await Promise.all([
|
|
155
227
|
this.categoryService.getById(validatedData.categoryId),
|
|
156
|
-
this.subcategoryService.getById(
|
|
228
|
+
this.subcategoryService.getById(
|
|
229
|
+
validatedData.categoryId,
|
|
230
|
+
validatedData.subcategoryId
|
|
231
|
+
),
|
|
157
232
|
this.technologyService.getById(validatedData.technologyId),
|
|
158
|
-
this.productService.getById(
|
|
233
|
+
this.productService.getById(
|
|
234
|
+
validatedData.technologyId,
|
|
235
|
+
validatedData.productId
|
|
236
|
+
),
|
|
159
237
|
]);
|
|
160
238
|
|
|
161
239
|
if (!category || !subcategory || !technology || !product) {
|
|
162
|
-
throw new Error(
|
|
240
|
+
throw new Error("One or more required base entities not found");
|
|
163
241
|
}
|
|
164
242
|
|
|
165
243
|
// Get clinic and practitioner information for aggregation
|
|
166
|
-
const clinicRef = doc(
|
|
244
|
+
const clinicRef = doc(
|
|
245
|
+
this.db,
|
|
246
|
+
CLINICS_COLLECTION,
|
|
247
|
+
validatedData.clinicBranchId
|
|
248
|
+
);
|
|
167
249
|
const clinicSnapshot = await getDoc(clinicRef);
|
|
168
250
|
if (!clinicSnapshot.exists()) {
|
|
169
|
-
throw new Error(
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
253
|
+
);
|
|
170
254
|
}
|
|
171
255
|
const clinic = clinicSnapshot.data() as Clinic; // Assert type
|
|
172
256
|
|
|
173
|
-
const practitionerRef = doc(
|
|
257
|
+
const practitionerRef = doc(
|
|
258
|
+
this.db,
|
|
259
|
+
PRACTITIONERS_COLLECTION,
|
|
260
|
+
validatedData.practitionerId
|
|
261
|
+
);
|
|
174
262
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
175
263
|
if (!practitionerSnapshot.exists()) {
|
|
176
|
-
throw new Error(
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Practitioner with ID ${validatedData.practitionerId} not found`
|
|
266
|
+
);
|
|
177
267
|
}
|
|
178
268
|
const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
|
|
179
269
|
|
|
@@ -183,23 +273,29 @@ export class ProcedureService extends BaseService {
|
|
|
183
273
|
processedPhotos = await this.processMediaArray(
|
|
184
274
|
validatedData.photos,
|
|
185
275
|
procedureId,
|
|
186
|
-
|
|
276
|
+
"procedure-photos"
|
|
187
277
|
);
|
|
188
278
|
}
|
|
189
279
|
|
|
280
|
+
// Transform productsMetadata from validation format to ProcedureProduct format
|
|
281
|
+
const transformedProductsMetadata = await this.transformProductsMetadata(
|
|
282
|
+
validatedData.productsMetadata,
|
|
283
|
+
validatedData.technologyId
|
|
284
|
+
);
|
|
285
|
+
|
|
190
286
|
// Create aggregated clinic info for the procedure document
|
|
191
287
|
const clinicInfo = {
|
|
192
288
|
id: clinicSnapshot.id,
|
|
193
289
|
name: clinic.name,
|
|
194
|
-
description: clinic.description ||
|
|
290
|
+
description: clinic.description || "",
|
|
195
291
|
featuredPhoto:
|
|
196
292
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
197
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
293
|
+
? typeof clinic.featuredPhotos[0] === "string"
|
|
198
294
|
? clinic.featuredPhotos[0]
|
|
199
|
-
:
|
|
200
|
-
: typeof clinic.coverPhoto ===
|
|
295
|
+
: ""
|
|
296
|
+
: typeof clinic.coverPhoto === "string"
|
|
201
297
|
? clinic.coverPhoto
|
|
202
|
-
:
|
|
298
|
+
: "",
|
|
203
299
|
location: clinic.location,
|
|
204
300
|
contactInfo: clinic.contactInfo,
|
|
205
301
|
};
|
|
@@ -208,29 +304,33 @@ export class ProcedureService extends BaseService {
|
|
|
208
304
|
const doctorInfo = {
|
|
209
305
|
id: practitionerSnapshot.id,
|
|
210
306
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
211
|
-
description: practitioner.basicInfo.bio ||
|
|
307
|
+
description: practitioner.basicInfo.bio || "",
|
|
212
308
|
photo:
|
|
213
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
309
|
+
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
214
310
|
? practitioner.basicInfo.profileImageUrl
|
|
215
|
-
:
|
|
311
|
+
: "", // Default to empty string if not a processed URL
|
|
216
312
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
217
313
|
services: practitioner.procedures || [],
|
|
218
314
|
};
|
|
219
315
|
|
|
220
316
|
// Create the procedure object
|
|
221
|
-
const newProcedure: Omit<Procedure,
|
|
317
|
+
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
222
318
|
id: procedureId,
|
|
223
319
|
...validatedData,
|
|
224
320
|
// Ensure nameLower is always set even if omitted by client
|
|
225
|
-
nameLower:
|
|
321
|
+
nameLower:
|
|
322
|
+
(validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
226
323
|
photos: processedPhotos,
|
|
227
324
|
category, // Embed full objects
|
|
228
325
|
subcategory,
|
|
229
326
|
technology,
|
|
230
327
|
product,
|
|
328
|
+
productsMetadata: transformedProductsMetadata,
|
|
231
329
|
blockingConditions: technology.blockingConditions,
|
|
232
330
|
contraindications: technology.contraindications || [],
|
|
331
|
+
contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
|
|
233
332
|
treatmentBenefits: technology.benefits,
|
|
333
|
+
treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
|
|
234
334
|
preRequirements: technology.requirements.pre,
|
|
235
335
|
postRequirements: technology.requirements.post,
|
|
236
336
|
certificationRequirement: technology.certificationRequirement,
|
|
@@ -273,12 +373,12 @@ export class ProcedureService extends BaseService {
|
|
|
273
373
|
* @returns A promise that resolves to an array of the newly created procedures.
|
|
274
374
|
*/
|
|
275
375
|
async bulkCreateProcedures(
|
|
276
|
-
baseData: Omit<CreateProcedureData,
|
|
277
|
-
practitionerIds: string[]
|
|
376
|
+
baseData: Omit<CreateProcedureData, "practitionerId">,
|
|
377
|
+
practitionerIds: string[]
|
|
278
378
|
): Promise<Procedure[]> {
|
|
279
379
|
// 1. Validation
|
|
280
380
|
if (!practitionerIds || practitionerIds.length === 0) {
|
|
281
|
-
throw new Error(
|
|
381
|
+
throw new Error("Practitioner IDs array cannot be empty.");
|
|
282
382
|
}
|
|
283
383
|
|
|
284
384
|
// Add a dummy practitionerId for the validation schema to pass
|
|
@@ -286,19 +386,28 @@ export class ProcedureService extends BaseService {
|
|
|
286
386
|
const validatedData = createProcedureSchema.parse(validationData);
|
|
287
387
|
|
|
288
388
|
// 2. Fetch common data once to avoid redundant reads
|
|
289
|
-
const [category, subcategory, technology, product, clinicSnapshot] =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
389
|
+
const [category, subcategory, technology, product, clinicSnapshot] =
|
|
390
|
+
await Promise.all([
|
|
391
|
+
this.categoryService.getById(validatedData.categoryId),
|
|
392
|
+
this.subcategoryService.getById(
|
|
393
|
+
validatedData.categoryId,
|
|
394
|
+
validatedData.subcategoryId
|
|
395
|
+
),
|
|
396
|
+
this.technologyService.getById(validatedData.technologyId),
|
|
397
|
+
this.productService.getById(
|
|
398
|
+
validatedData.technologyId,
|
|
399
|
+
validatedData.productId
|
|
400
|
+
),
|
|
401
|
+
getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
|
|
402
|
+
]);
|
|
296
403
|
|
|
297
404
|
if (!category || !subcategory || !technology || !product) {
|
|
298
|
-
throw new Error(
|
|
405
|
+
throw new Error("One or more required base entities not found");
|
|
299
406
|
}
|
|
300
407
|
if (!clinicSnapshot.exists()) {
|
|
301
|
-
throw new Error(
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
410
|
+
);
|
|
302
411
|
}
|
|
303
412
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
304
413
|
|
|
@@ -309,10 +418,16 @@ export class ProcedureService extends BaseService {
|
|
|
309
418
|
processedPhotos = await this.processMediaArray(
|
|
310
419
|
validatedData.photos,
|
|
311
420
|
batchId,
|
|
312
|
-
|
|
421
|
+
"procedure-photos-batch"
|
|
313
422
|
);
|
|
314
423
|
}
|
|
315
424
|
|
|
425
|
+
// Transform productsMetadata from validation format to ProcedureProduct format
|
|
426
|
+
const transformedProductsMetadata = await this.transformProductsMetadata(
|
|
427
|
+
validatedData.productsMetadata,
|
|
428
|
+
validatedData.technologyId
|
|
429
|
+
);
|
|
430
|
+
|
|
316
431
|
// 4. Fetch all practitioner data efficiently
|
|
317
432
|
const practitionersMap = new Map<string, Practitioner>();
|
|
318
433
|
// Use 'in' query in chunks of 30, as this is the Firestore limit
|
|
@@ -320,7 +435,7 @@ export class ProcedureService extends BaseService {
|
|
|
320
435
|
const chunk = practitionerIds.slice(i, i + 30);
|
|
321
436
|
const practitionersQuery = query(
|
|
322
437
|
collection(this.db, PRACTITIONERS_COLLECTION),
|
|
323
|
-
where(documentId(),
|
|
438
|
+
where(documentId(), "in", chunk)
|
|
324
439
|
);
|
|
325
440
|
const practitionersSnapshot = await getDocs(practitionersQuery);
|
|
326
441
|
practitionersSnapshot.docs.forEach((doc) => {
|
|
@@ -331,8 +446,12 @@ export class ProcedureService extends BaseService {
|
|
|
331
446
|
// Verify all practitioners were found
|
|
332
447
|
if (practitionersMap.size !== practitionerIds.length) {
|
|
333
448
|
const foundIds = Array.from(practitionersMap.keys());
|
|
334
|
-
const notFoundIds = practitionerIds.filter(
|
|
335
|
-
|
|
449
|
+
const notFoundIds = practitionerIds.filter(
|
|
450
|
+
(id) => !foundIds.includes(id)
|
|
451
|
+
);
|
|
452
|
+
throw new Error(
|
|
453
|
+
`The following practitioners were not found: ${notFoundIds.join(", ")}`
|
|
454
|
+
);
|
|
336
455
|
}
|
|
337
456
|
|
|
338
457
|
// 5. Use a Firestore batch for atomic creation
|
|
@@ -341,15 +460,15 @@ export class ProcedureService extends BaseService {
|
|
|
341
460
|
const clinicInfo = {
|
|
342
461
|
id: clinicSnapshot.id,
|
|
343
462
|
name: clinic.name,
|
|
344
|
-
description: clinic.description ||
|
|
463
|
+
description: clinic.description || "",
|
|
345
464
|
featuredPhoto:
|
|
346
465
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
347
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
466
|
+
? typeof clinic.featuredPhotos[0] === "string"
|
|
348
467
|
? clinic.featuredPhotos[0]
|
|
349
|
-
:
|
|
350
|
-
: typeof clinic.coverPhoto ===
|
|
468
|
+
: ""
|
|
469
|
+
: typeof clinic.coverPhoto === "string"
|
|
351
470
|
? clinic.coverPhoto
|
|
352
|
-
:
|
|
471
|
+
: "",
|
|
353
472
|
location: clinic.location,
|
|
354
473
|
contactInfo: clinic.contactInfo,
|
|
355
474
|
};
|
|
@@ -360,11 +479,11 @@ export class ProcedureService extends BaseService {
|
|
|
360
479
|
const doctorInfo = {
|
|
361
480
|
id: practitioner.id,
|
|
362
481
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
363
|
-
description: practitioner.basicInfo.bio ||
|
|
482
|
+
description: practitioner.basicInfo.bio || "",
|
|
364
483
|
photo:
|
|
365
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
484
|
+
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
366
485
|
? practitioner.basicInfo.profileImageUrl
|
|
367
|
-
:
|
|
486
|
+
: "",
|
|
368
487
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
369
488
|
services: practitioner.procedures || [],
|
|
370
489
|
};
|
|
@@ -374,19 +493,24 @@ export class ProcedureService extends BaseService {
|
|
|
374
493
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
|
|
375
494
|
|
|
376
495
|
// Construct the new procedure, reusing common data
|
|
377
|
-
const newProcedure: Omit<Procedure,
|
|
496
|
+
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
378
497
|
id: procedureId,
|
|
379
498
|
...validatedData,
|
|
380
|
-
nameLower:
|
|
499
|
+
nameLower:
|
|
500
|
+
(validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
381
501
|
practitionerId: practitionerId, // Override practitionerId with the correct one
|
|
382
502
|
photos: processedPhotos,
|
|
383
503
|
category,
|
|
384
504
|
subcategory,
|
|
385
505
|
technology,
|
|
386
506
|
product,
|
|
507
|
+
productsMetadata: transformedProductsMetadata,
|
|
387
508
|
blockingConditions: technology.blockingConditions,
|
|
388
509
|
contraindications: technology.contraindications || [],
|
|
510
|
+
contraindicationIds:
|
|
511
|
+
technology.contraindications?.map((c) => c.id) || [],
|
|
389
512
|
treatmentBenefits: technology.benefits,
|
|
513
|
+
treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
|
|
390
514
|
preRequirements: technology.requirements.pre,
|
|
391
515
|
postRequirements: technology.requirements.post,
|
|
392
516
|
certificationRequirement: technology.certificationRequirement,
|
|
@@ -420,7 +544,10 @@ export class ProcedureService extends BaseService {
|
|
|
420
544
|
const fetchedProcedures: Procedure[] = [];
|
|
421
545
|
for (let i = 0; i < createdProcedureIds.length; i += 30) {
|
|
422
546
|
const chunk = createdProcedureIds.slice(i, i + 30);
|
|
423
|
-
const q = query(
|
|
547
|
+
const q = query(
|
|
548
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
549
|
+
where(documentId(), "in", chunk)
|
|
550
|
+
);
|
|
424
551
|
const snapshot = await getDocs(q);
|
|
425
552
|
snapshot.forEach((doc) => {
|
|
426
553
|
fetchedProcedures.push(doc.data() as Procedure);
|
|
@@ -451,11 +578,13 @@ export class ProcedureService extends BaseService {
|
|
|
451
578
|
* @param clinicBranchId - The ID of the clinic branch
|
|
452
579
|
* @returns List of procedures
|
|
453
580
|
*/
|
|
454
|
-
async getProceduresByClinicBranch(
|
|
581
|
+
async getProceduresByClinicBranch(
|
|
582
|
+
clinicBranchId: string
|
|
583
|
+
): Promise<Procedure[]> {
|
|
455
584
|
const q = query(
|
|
456
585
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
457
|
-
where(
|
|
458
|
-
where(
|
|
586
|
+
where("clinicBranchId", "==", clinicBranchId),
|
|
587
|
+
where("isActive", "==", true)
|
|
459
588
|
);
|
|
460
589
|
const snapshot = await getDocs(q);
|
|
461
590
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -466,11 +595,13 @@ export class ProcedureService extends BaseService {
|
|
|
466
595
|
* @param practitionerId - The ID of the practitioner
|
|
467
596
|
* @returns List of procedures
|
|
468
597
|
*/
|
|
469
|
-
async getProceduresByPractitioner(
|
|
598
|
+
async getProceduresByPractitioner(
|
|
599
|
+
practitionerId: string
|
|
600
|
+
): Promise<Procedure[]> {
|
|
470
601
|
const q = query(
|
|
471
602
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
472
|
-
where(
|
|
473
|
-
where(
|
|
603
|
+
where("practitionerId", "==", practitionerId),
|
|
604
|
+
where("isActive", "==", true)
|
|
474
605
|
);
|
|
475
606
|
const snapshot = await getDocs(q);
|
|
476
607
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -481,11 +612,13 @@ export class ProcedureService extends BaseService {
|
|
|
481
612
|
* @param practitionerId - The ID of the practitioner
|
|
482
613
|
* @returns List of inactive procedures
|
|
483
614
|
*/
|
|
484
|
-
async getInactiveProceduresByPractitioner(
|
|
615
|
+
async getInactiveProceduresByPractitioner(
|
|
616
|
+
practitionerId: string
|
|
617
|
+
): Promise<Procedure[]> {
|
|
485
618
|
const q = query(
|
|
486
619
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
487
|
-
where(
|
|
488
|
-
where(
|
|
620
|
+
where("practitionerId", "==", practitionerId),
|
|
621
|
+
where("isActive", "==", false)
|
|
489
622
|
);
|
|
490
623
|
const snapshot = await getDocs(q);
|
|
491
624
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -497,7 +630,10 @@ export class ProcedureService extends BaseService {
|
|
|
497
630
|
* @param data - The data to update the procedure with
|
|
498
631
|
* @returns The updated procedure
|
|
499
632
|
*/
|
|
500
|
-
async updateProcedure(
|
|
633
|
+
async updateProcedure(
|
|
634
|
+
id: string,
|
|
635
|
+
data: UpdateProcedureData
|
|
636
|
+
): Promise<Procedure> {
|
|
501
637
|
const validatedData = updateProcedureSchema.parse(data);
|
|
502
638
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
503
639
|
const procedureSnapshot = await getDoc(procedureRef);
|
|
@@ -507,7 +643,23 @@ export class ProcedureService extends BaseService {
|
|
|
507
643
|
}
|
|
508
644
|
|
|
509
645
|
const existingProcedure = procedureSnapshot.data() as Procedure;
|
|
510
|
-
let updatedProcedureData: Partial<Procedure> = {
|
|
646
|
+
let updatedProcedureData: Partial<Procedure> = {};
|
|
647
|
+
|
|
648
|
+
// Copy validated simple fields
|
|
649
|
+
if (validatedData.name !== undefined)
|
|
650
|
+
updatedProcedureData.name = validatedData.name;
|
|
651
|
+
if (validatedData.description !== undefined)
|
|
652
|
+
updatedProcedureData.description = validatedData.description;
|
|
653
|
+
if (validatedData.price !== undefined)
|
|
654
|
+
updatedProcedureData.price = validatedData.price;
|
|
655
|
+
if (validatedData.currency !== undefined)
|
|
656
|
+
updatedProcedureData.currency = validatedData.currency;
|
|
657
|
+
if (validatedData.pricingMeasure !== undefined)
|
|
658
|
+
updatedProcedureData.pricingMeasure = validatedData.pricingMeasure;
|
|
659
|
+
if (validatedData.duration !== undefined)
|
|
660
|
+
updatedProcedureData.duration = validatedData.duration;
|
|
661
|
+
if (validatedData.isActive !== undefined)
|
|
662
|
+
updatedProcedureData.isActive = validatedData.isActive;
|
|
511
663
|
|
|
512
664
|
let practitionerChanged = false;
|
|
513
665
|
let clinicChanged = false;
|
|
@@ -521,42 +673,70 @@ export class ProcedureService extends BaseService {
|
|
|
521
673
|
updatedProcedureData.photos = await this.processMediaArray(
|
|
522
674
|
validatedData.photos,
|
|
523
675
|
id,
|
|
524
|
-
|
|
676
|
+
"procedure-photos"
|
|
525
677
|
);
|
|
526
678
|
}
|
|
527
679
|
|
|
680
|
+
// Transform productsMetadata if provided
|
|
681
|
+
if (validatedData.productsMetadata !== undefined) {
|
|
682
|
+
const technologyId =
|
|
683
|
+
validatedData.technologyId ?? existingProcedure.technology.id;
|
|
684
|
+
if (!technologyId) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
"Technology ID is required for updating products metadata"
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
updatedProcedureData.productsMetadata =
|
|
690
|
+
await this.transformProductsMetadata(
|
|
691
|
+
validatedData.productsMetadata,
|
|
692
|
+
technologyId
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
528
696
|
// --- Prepare updates and fetch new related data if IDs change ---
|
|
529
697
|
|
|
530
698
|
// Handle Practitioner Change
|
|
531
|
-
if (
|
|
699
|
+
if (
|
|
700
|
+
validatedData.practitionerId &&
|
|
701
|
+
validatedData.practitionerId !== oldPractitionerId
|
|
702
|
+
) {
|
|
532
703
|
practitionerChanged = true;
|
|
533
704
|
const newPractitionerRef = doc(
|
|
534
705
|
this.db,
|
|
535
706
|
PRACTITIONERS_COLLECTION,
|
|
536
|
-
validatedData.practitionerId
|
|
707
|
+
validatedData.practitionerId
|
|
537
708
|
);
|
|
538
709
|
const newPractitionerSnap = await getDoc(newPractitionerRef);
|
|
539
710
|
if (!newPractitionerSnap.exists())
|
|
540
|
-
throw new Error(
|
|
711
|
+
throw new Error(
|
|
712
|
+
`New Practitioner ${validatedData.practitionerId} not found`
|
|
713
|
+
);
|
|
541
714
|
newPractitioner = newPractitionerSnap.data() as Practitioner;
|
|
542
715
|
// Update doctorInfo within the procedure document
|
|
543
716
|
updatedProcedureData.doctorInfo = {
|
|
544
717
|
id: newPractitioner.id,
|
|
545
718
|
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
546
|
-
description: newPractitioner.basicInfo.bio ||
|
|
719
|
+
description: newPractitioner.basicInfo.bio || "",
|
|
547
720
|
photo:
|
|
548
|
-
typeof newPractitioner.basicInfo.profileImageUrl ===
|
|
721
|
+
typeof newPractitioner.basicInfo.profileImageUrl === "string"
|
|
549
722
|
? newPractitioner.basicInfo.profileImageUrl
|
|
550
|
-
:
|
|
723
|
+
: "", // Default to empty string if not a processed URL
|
|
551
724
|
rating: newPractitioner.reviewInfo?.averageRating || 0,
|
|
552
725
|
services: newPractitioner.procedures || [],
|
|
553
726
|
};
|
|
554
727
|
}
|
|
555
728
|
|
|
556
729
|
// Handle Clinic Change
|
|
557
|
-
if (
|
|
730
|
+
if (
|
|
731
|
+
validatedData.clinicBranchId &&
|
|
732
|
+
validatedData.clinicBranchId !== oldClinicId
|
|
733
|
+
) {
|
|
558
734
|
clinicChanged = true;
|
|
559
|
-
const newClinicRef = doc(
|
|
735
|
+
const newClinicRef = doc(
|
|
736
|
+
this.db,
|
|
737
|
+
CLINICS_COLLECTION,
|
|
738
|
+
validatedData.clinicBranchId
|
|
739
|
+
);
|
|
560
740
|
const newClinicSnap = await getDoc(newClinicRef);
|
|
561
741
|
if (!newClinicSnap.exists())
|
|
562
742
|
throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
|
|
@@ -565,15 +745,15 @@ export class ProcedureService extends BaseService {
|
|
|
565
745
|
updatedProcedureData.clinicInfo = {
|
|
566
746
|
id: newClinic.id,
|
|
567
747
|
name: newClinic.name,
|
|
568
|
-
description: newClinic.description ||
|
|
748
|
+
description: newClinic.description || "",
|
|
569
749
|
featuredPhoto:
|
|
570
750
|
newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
|
|
571
|
-
? typeof newClinic.featuredPhotos[0] ===
|
|
751
|
+
? typeof newClinic.featuredPhotos[0] === "string"
|
|
572
752
|
? newClinic.featuredPhotos[0]
|
|
573
|
-
:
|
|
574
|
-
: typeof newClinic.coverPhoto ===
|
|
753
|
+
: ""
|
|
754
|
+
: typeof newClinic.coverPhoto === "string"
|
|
575
755
|
? newClinic.coverPhoto
|
|
576
|
-
:
|
|
756
|
+
: "",
|
|
577
757
|
location: newClinic.location,
|
|
578
758
|
contactInfo: newClinic.contactInfo,
|
|
579
759
|
};
|
|
@@ -585,8 +765,11 @@ export class ProcedureService extends BaseService {
|
|
|
585
765
|
updatedProcedureData.nameLower = validatedData.name.toLowerCase();
|
|
586
766
|
}
|
|
587
767
|
if (validatedData.categoryId) {
|
|
588
|
-
const category = await this.categoryService.getById(
|
|
589
|
-
|
|
768
|
+
const category = await this.categoryService.getById(
|
|
769
|
+
validatedData.categoryId
|
|
770
|
+
);
|
|
771
|
+
if (!category)
|
|
772
|
+
throw new Error(`Category ${validatedData.categoryId} not found`);
|
|
590
773
|
updatedProcedureData.category = category;
|
|
591
774
|
finalCategoryId = category.id; // Update finalCategoryId if category changed
|
|
592
775
|
}
|
|
@@ -595,42 +778,58 @@ export class ProcedureService extends BaseService {
|
|
|
595
778
|
if (validatedData.subcategoryId && finalCategoryId) {
|
|
596
779
|
const subcategory = await this.subcategoryService.getById(
|
|
597
780
|
finalCategoryId,
|
|
598
|
-
validatedData.subcategoryId
|
|
781
|
+
validatedData.subcategoryId
|
|
599
782
|
);
|
|
600
783
|
if (!subcategory)
|
|
601
784
|
throw new Error(
|
|
602
|
-
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}
|
|
785
|
+
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
|
|
603
786
|
);
|
|
604
787
|
updatedProcedureData.subcategory = subcategory;
|
|
605
788
|
} else if (validatedData.subcategoryId) {
|
|
606
|
-
console.warn(
|
|
789
|
+
console.warn(
|
|
790
|
+
"Attempted to update subcategory without a valid categoryId"
|
|
791
|
+
);
|
|
607
792
|
}
|
|
608
793
|
|
|
609
794
|
let finalTechnologyId = existingProcedure.technology.id;
|
|
610
795
|
if (validatedData.technologyId) {
|
|
611
|
-
const technology = await this.technologyService.getById(
|
|
612
|
-
|
|
796
|
+
const technology = await this.technologyService.getById(
|
|
797
|
+
validatedData.technologyId
|
|
798
|
+
);
|
|
799
|
+
if (!technology)
|
|
800
|
+
throw new Error(`Technology ${validatedData.technologyId} not found`);
|
|
613
801
|
updatedProcedureData.technology = technology;
|
|
614
802
|
finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
|
|
615
803
|
// Update related fields derived from technology
|
|
616
804
|
updatedProcedureData.blockingConditions = technology.blockingConditions;
|
|
805
|
+
updatedProcedureData.contraindications =
|
|
806
|
+
technology.contraindications || [];
|
|
807
|
+
updatedProcedureData.contraindicationIds =
|
|
808
|
+
technology.contraindications?.map((c) => c.id) || [];
|
|
617
809
|
updatedProcedureData.treatmentBenefits = technology.benefits;
|
|
810
|
+
updatedProcedureData.treatmentBenefitIds =
|
|
811
|
+
technology.benefits?.map((b) => b.id) || [];
|
|
618
812
|
updatedProcedureData.preRequirements = technology.requirements.pre;
|
|
619
813
|
updatedProcedureData.postRequirements = technology.requirements.post;
|
|
620
|
-
updatedProcedureData.certificationRequirement =
|
|
621
|
-
|
|
814
|
+
updatedProcedureData.certificationRequirement =
|
|
815
|
+
technology.certificationRequirement;
|
|
816
|
+
updatedProcedureData.documentationTemplates =
|
|
817
|
+
technology.documentationTemplates || [];
|
|
622
818
|
}
|
|
623
819
|
|
|
624
820
|
// Only fetch product if its ID is provided AND we have a valid finalTechnologyId
|
|
625
821
|
if (validatedData.productId && finalTechnologyId) {
|
|
626
|
-
const product = await this.productService.getById(
|
|
822
|
+
const product = await this.productService.getById(
|
|
823
|
+
finalTechnologyId,
|
|
824
|
+
validatedData.productId
|
|
825
|
+
);
|
|
627
826
|
if (!product)
|
|
628
827
|
throw new Error(
|
|
629
|
-
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}
|
|
828
|
+
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
|
|
630
829
|
);
|
|
631
830
|
updatedProcedureData.product = product;
|
|
632
831
|
} else if (validatedData.productId) {
|
|
633
|
-
console.warn(
|
|
832
|
+
console.warn("Attempted to update product without a valid technologyId");
|
|
634
833
|
}
|
|
635
834
|
|
|
636
835
|
// Update the procedure document
|
|
@@ -714,7 +913,7 @@ export class ProcedureService extends BaseService {
|
|
|
714
913
|
*/
|
|
715
914
|
async getAllProcedures(
|
|
716
915
|
pagination?: number,
|
|
717
|
-
lastDoc?: any
|
|
916
|
+
lastDoc?: any
|
|
718
917
|
): Promise<{ procedures: Procedure[]; lastDoc: any }> {
|
|
719
918
|
try {
|
|
720
919
|
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
@@ -722,24 +921,29 @@ export class ProcedureService extends BaseService {
|
|
|
722
921
|
|
|
723
922
|
// Apply pagination if specified
|
|
724
923
|
if (pagination && pagination > 0) {
|
|
725
|
-
const { limit, startAfter } = await import(
|
|
924
|
+
const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
|
|
726
925
|
|
|
727
926
|
if (lastDoc) {
|
|
728
927
|
proceduresQuery = query(
|
|
729
928
|
proceduresCollection,
|
|
730
|
-
orderBy(
|
|
929
|
+
orderBy("name"), // Use imported orderBy
|
|
731
930
|
startAfter(lastDoc),
|
|
732
|
-
limit(pagination)
|
|
931
|
+
limit(pagination)
|
|
733
932
|
);
|
|
734
933
|
} else {
|
|
735
|
-
proceduresQuery = query(
|
|
934
|
+
proceduresQuery = query(
|
|
935
|
+
proceduresCollection,
|
|
936
|
+
orderBy("name"),
|
|
937
|
+
limit(pagination)
|
|
938
|
+
); // Use imported orderBy
|
|
736
939
|
}
|
|
737
940
|
} else {
|
|
738
|
-
proceduresQuery = query(proceduresCollection, orderBy(
|
|
941
|
+
proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
|
|
739
942
|
}
|
|
740
943
|
|
|
741
944
|
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
742
|
-
const lastVisible =
|
|
945
|
+
const lastVisible =
|
|
946
|
+
proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
743
947
|
|
|
744
948
|
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
745
949
|
const data = doc.data() as Procedure;
|
|
@@ -754,7 +958,7 @@ export class ProcedureService extends BaseService {
|
|
|
754
958
|
lastDoc: lastVisible,
|
|
755
959
|
};
|
|
756
960
|
} catch (error) {
|
|
757
|
-
console.error(
|
|
961
|
+
console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
|
|
758
962
|
throw error;
|
|
759
963
|
}
|
|
760
964
|
}
|
|
@@ -766,7 +970,7 @@ export class ProcedureService extends BaseService {
|
|
|
766
970
|
*
|
|
767
971
|
* @param filters - Various filters to apply
|
|
768
972
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
769
|
-
* @param filters.
|
|
973
|
+
* @param filters.treatmentBenefitIds - Optional array of treatment benefits to filter by
|
|
770
974
|
* @param filters.procedureFamily - Optional procedure family to filter by
|
|
771
975
|
* @param filters.procedureCategory - Optional procedure category to filter by
|
|
772
976
|
* @param filters.procedureSubcategory - Optional procedure subcategory to filter by
|
|
@@ -784,7 +988,7 @@ export class ProcedureService extends BaseService {
|
|
|
784
988
|
*/
|
|
785
989
|
async getProceduresByFilters(filters: {
|
|
786
990
|
nameSearch?: string;
|
|
787
|
-
treatmentBenefits?:
|
|
991
|
+
treatmentBenefits?: string[];
|
|
788
992
|
procedureFamily?: ProcedureFamily;
|
|
789
993
|
procedureCategory?: string;
|
|
790
994
|
procedureSubcategory?: string;
|
|
@@ -803,26 +1007,32 @@ export class ProcedureService extends BaseService {
|
|
|
803
1007
|
lastDoc: any;
|
|
804
1008
|
}> {
|
|
805
1009
|
try {
|
|
806
|
-
console.log(
|
|
1010
|
+
console.log(
|
|
1011
|
+
"[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies"
|
|
1012
|
+
);
|
|
807
1013
|
|
|
808
1014
|
// Geo query debug i validacija
|
|
809
1015
|
if (filters.location && filters.radiusInKm) {
|
|
810
|
-
console.log(
|
|
1016
|
+
console.log("[PROCEDURE_SERVICE] Executing geo query:", {
|
|
811
1017
|
location: filters.location,
|
|
812
1018
|
radius: filters.radiusInKm,
|
|
813
|
-
serviceName:
|
|
1019
|
+
serviceName: "ProcedureService",
|
|
814
1020
|
});
|
|
815
1021
|
|
|
816
1022
|
// Validacija location podataka
|
|
817
1023
|
if (!filters.location.latitude || !filters.location.longitude) {
|
|
818
|
-
console.warn(
|
|
1024
|
+
console.warn(
|
|
1025
|
+
"[PROCEDURE_SERVICE] Invalid location data:",
|
|
1026
|
+
filters.location
|
|
1027
|
+
);
|
|
819
1028
|
filters.location = undefined;
|
|
820
1029
|
filters.radiusInKm = undefined;
|
|
821
1030
|
}
|
|
822
1031
|
}
|
|
823
1032
|
|
|
824
1033
|
// Handle geo queries separately (they work differently)
|
|
825
|
-
const isGeoQuery =
|
|
1034
|
+
const isGeoQuery =
|
|
1035
|
+
filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
826
1036
|
if (isGeoQuery) {
|
|
827
1037
|
return this.handleGeoQuery(filters);
|
|
828
1038
|
}
|
|
@@ -833,39 +1043,55 @@ export class ProcedureService extends BaseService {
|
|
|
833
1043
|
|
|
834
1044
|
// Active status filter
|
|
835
1045
|
if (filters.isActive !== undefined) {
|
|
836
|
-
constraints.push(where(
|
|
1046
|
+
constraints.push(where("isActive", "==", filters.isActive));
|
|
837
1047
|
} else {
|
|
838
|
-
constraints.push(where(
|
|
1048
|
+
constraints.push(where("isActive", "==", true));
|
|
839
1049
|
}
|
|
840
1050
|
|
|
841
1051
|
// Filter constraints
|
|
842
1052
|
if (filters.procedureFamily) {
|
|
843
|
-
constraints.push(where(
|
|
1053
|
+
constraints.push(where("family", "==", filters.procedureFamily));
|
|
844
1054
|
}
|
|
845
1055
|
if (filters.procedureCategory) {
|
|
846
|
-
constraints.push(
|
|
1056
|
+
constraints.push(
|
|
1057
|
+
where("category.id", "==", filters.procedureCategory)
|
|
1058
|
+
);
|
|
847
1059
|
}
|
|
848
1060
|
if (filters.procedureSubcategory) {
|
|
849
|
-
constraints.push(
|
|
1061
|
+
constraints.push(
|
|
1062
|
+
where("subcategory.id", "==", filters.procedureSubcategory)
|
|
1063
|
+
);
|
|
850
1064
|
}
|
|
851
1065
|
if (filters.procedureTechnology) {
|
|
852
|
-
constraints.push(
|
|
1066
|
+
constraints.push(
|
|
1067
|
+
where("technology.id", "==", filters.procedureTechnology)
|
|
1068
|
+
);
|
|
853
1069
|
}
|
|
854
1070
|
if (filters.minPrice !== undefined) {
|
|
855
|
-
constraints.push(where(
|
|
1071
|
+
constraints.push(where("price", ">=", filters.minPrice));
|
|
856
1072
|
}
|
|
857
1073
|
if (filters.maxPrice !== undefined) {
|
|
858
|
-
constraints.push(where(
|
|
1074
|
+
constraints.push(where("price", "<=", filters.maxPrice));
|
|
859
1075
|
}
|
|
860
1076
|
if (filters.minRating !== undefined) {
|
|
861
|
-
constraints.push(
|
|
1077
|
+
constraints.push(
|
|
1078
|
+
where("reviewInfo.averageRating", ">=", filters.minRating)
|
|
1079
|
+
);
|
|
862
1080
|
}
|
|
863
1081
|
if (filters.maxRating !== undefined) {
|
|
864
|
-
constraints.push(
|
|
1082
|
+
constraints.push(
|
|
1083
|
+
where("reviewInfo.averageRating", "<=", filters.maxRating)
|
|
1084
|
+
);
|
|
865
1085
|
}
|
|
866
1086
|
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
867
|
-
const
|
|
868
|
-
constraints.push(
|
|
1087
|
+
const benefitIdsToMatch = filters.treatmentBenefits;
|
|
1088
|
+
constraints.push(
|
|
1089
|
+
where(
|
|
1090
|
+
"treatmentBenefitIds",
|
|
1091
|
+
"array-contains-any",
|
|
1092
|
+
benefitIdsToMatch
|
|
1093
|
+
)
|
|
1094
|
+
);
|
|
869
1095
|
}
|
|
870
1096
|
|
|
871
1097
|
return constraints;
|
|
@@ -874,15 +1100,17 @@ export class ProcedureService extends BaseService {
|
|
|
874
1100
|
// Strategy 1: Try nameLower search if nameSearch exists
|
|
875
1101
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
876
1102
|
try {
|
|
877
|
-
console.log(
|
|
1103
|
+
console.log(
|
|
1104
|
+
"[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search"
|
|
1105
|
+
);
|
|
878
1106
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
879
1107
|
const constraints = getBaseConstraints();
|
|
880
|
-
constraints.push(where(
|
|
881
|
-
constraints.push(where(
|
|
882
|
-
constraints.push(orderBy(
|
|
1108
|
+
constraints.push(where("nameLower", ">=", searchTerm));
|
|
1109
|
+
constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
|
|
1110
|
+
constraints.push(orderBy("nameLower"));
|
|
883
1111
|
|
|
884
1112
|
if (filters.lastDoc) {
|
|
885
|
-
if (typeof filters.lastDoc.data ===
|
|
1113
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
886
1114
|
constraints.push(startAfter(filters.lastDoc));
|
|
887
1115
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
888
1116
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -892,17 +1120,22 @@ export class ProcedureService extends BaseService {
|
|
|
892
1120
|
}
|
|
893
1121
|
constraints.push(limit(filters.pagination || 10));
|
|
894
1122
|
|
|
895
|
-
const q = query(
|
|
1123
|
+
const q = query(
|
|
1124
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
1125
|
+
...constraints
|
|
1126
|
+
);
|
|
896
1127
|
const querySnapshot = await getDocs(q);
|
|
897
1128
|
const procedures = querySnapshot.docs.map(
|
|
898
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
1129
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
899
1130
|
);
|
|
900
1131
|
const lastDoc =
|
|
901
1132
|
querySnapshot.docs.length > 0
|
|
902
1133
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
903
1134
|
: null;
|
|
904
1135
|
|
|
905
|
-
console.log(
|
|
1136
|
+
console.log(
|
|
1137
|
+
`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`
|
|
1138
|
+
);
|
|
906
1139
|
|
|
907
1140
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
908
1141
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -910,22 +1143,24 @@ export class ProcedureService extends BaseService {
|
|
|
910
1143
|
}
|
|
911
1144
|
return { procedures, lastDoc };
|
|
912
1145
|
} catch (error) {
|
|
913
|
-
console.log(
|
|
1146
|
+
console.log("[PROCEDURE_SERVICE] Strategy 1 failed:", error);
|
|
914
1147
|
}
|
|
915
1148
|
}
|
|
916
1149
|
|
|
917
1150
|
// Strategy 2: Try name field search as fallback
|
|
918
1151
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
919
1152
|
try {
|
|
920
|
-
console.log(
|
|
1153
|
+
console.log(
|
|
1154
|
+
"[PROCEDURE_SERVICE] Strategy 2: Trying name field search"
|
|
1155
|
+
);
|
|
921
1156
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
922
1157
|
const constraints = getBaseConstraints();
|
|
923
|
-
constraints.push(where(
|
|
924
|
-
constraints.push(where(
|
|
925
|
-
constraints.push(orderBy(
|
|
1158
|
+
constraints.push(where("name", ">=", searchTerm));
|
|
1159
|
+
constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
|
|
1160
|
+
constraints.push(orderBy("name"));
|
|
926
1161
|
|
|
927
1162
|
if (filters.lastDoc) {
|
|
928
|
-
if (typeof filters.lastDoc.data ===
|
|
1163
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
929
1164
|
constraints.push(startAfter(filters.lastDoc));
|
|
930
1165
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
931
1166
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -935,17 +1170,22 @@ export class ProcedureService extends BaseService {
|
|
|
935
1170
|
}
|
|
936
1171
|
constraints.push(limit(filters.pagination || 10));
|
|
937
1172
|
|
|
938
|
-
const q = query(
|
|
1173
|
+
const q = query(
|
|
1174
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
1175
|
+
...constraints
|
|
1176
|
+
);
|
|
939
1177
|
const querySnapshot = await getDocs(q);
|
|
940
1178
|
const procedures = querySnapshot.docs.map(
|
|
941
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
1179
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
942
1180
|
);
|
|
943
1181
|
const lastDoc =
|
|
944
1182
|
querySnapshot.docs.length > 0
|
|
945
1183
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
946
1184
|
: null;
|
|
947
1185
|
|
|
948
|
-
console.log(
|
|
1186
|
+
console.log(
|
|
1187
|
+
`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`
|
|
1188
|
+
);
|
|
949
1189
|
|
|
950
1190
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
951
1191
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -953,20 +1193,20 @@ export class ProcedureService extends BaseService {
|
|
|
953
1193
|
}
|
|
954
1194
|
return { procedures, lastDoc };
|
|
955
1195
|
} catch (error) {
|
|
956
|
-
console.log(
|
|
1196
|
+
console.log("[PROCEDURE_SERVICE] Strategy 2 failed:", error);
|
|
957
1197
|
}
|
|
958
1198
|
}
|
|
959
1199
|
|
|
960
1200
|
// Strategy 3: orderBy createdAt with client-side filtering
|
|
961
1201
|
try {
|
|
962
1202
|
console.log(
|
|
963
|
-
|
|
1203
|
+
"[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering"
|
|
964
1204
|
);
|
|
965
1205
|
const constraints = getBaseConstraints();
|
|
966
|
-
constraints.push(orderBy(
|
|
1206
|
+
constraints.push(orderBy("createdAt", "desc"));
|
|
967
1207
|
|
|
968
1208
|
if (filters.lastDoc) {
|
|
969
|
-
if (typeof filters.lastDoc.data ===
|
|
1209
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
970
1210
|
constraints.push(startAfter(filters.lastDoc));
|
|
971
1211
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
972
1212
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -976,18 +1216,25 @@ export class ProcedureService extends BaseService {
|
|
|
976
1216
|
}
|
|
977
1217
|
constraints.push(limit(filters.pagination || 10));
|
|
978
1218
|
|
|
979
|
-
const q = query(
|
|
1219
|
+
const q = query(
|
|
1220
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
1221
|
+
...constraints
|
|
1222
|
+
);
|
|
980
1223
|
const querySnapshot = await getDocs(q);
|
|
981
1224
|
let procedures = querySnapshot.docs.map(
|
|
982
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
1225
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
983
1226
|
);
|
|
984
1227
|
|
|
985
1228
|
// Apply all client-side filters using centralized function
|
|
986
1229
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
987
1230
|
|
|
988
1231
|
const lastDoc =
|
|
989
|
-
querySnapshot.docs.length > 0
|
|
990
|
-
|
|
1232
|
+
querySnapshot.docs.length > 0
|
|
1233
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1234
|
+
: null;
|
|
1235
|
+
console.log(
|
|
1236
|
+
`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`
|
|
1237
|
+
);
|
|
991
1238
|
|
|
992
1239
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
993
1240
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -995,30 +1242,37 @@ export class ProcedureService extends BaseService {
|
|
|
995
1242
|
}
|
|
996
1243
|
return { procedures, lastDoc };
|
|
997
1244
|
} catch (error) {
|
|
998
|
-
console.log(
|
|
1245
|
+
console.log("[PROCEDURE_SERVICE] Strategy 3 failed:", error);
|
|
999
1246
|
}
|
|
1000
1247
|
|
|
1001
1248
|
// Strategy 4: Minimal query fallback
|
|
1002
1249
|
try {
|
|
1003
|
-
console.log(
|
|
1250
|
+
console.log("[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback");
|
|
1004
1251
|
const constraints: QueryConstraint[] = [
|
|
1005
|
-
where(
|
|
1006
|
-
orderBy(
|
|
1252
|
+
where("isActive", "==", true),
|
|
1253
|
+
orderBy("createdAt", "desc"),
|
|
1007
1254
|
limit(filters.pagination || 10),
|
|
1008
1255
|
];
|
|
1009
1256
|
|
|
1010
|
-
const q = query(
|
|
1257
|
+
const q = query(
|
|
1258
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
1259
|
+
...constraints
|
|
1260
|
+
);
|
|
1011
1261
|
const querySnapshot = await getDocs(q);
|
|
1012
1262
|
let procedures = querySnapshot.docs.map(
|
|
1013
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
1263
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
1014
1264
|
);
|
|
1015
1265
|
|
|
1016
1266
|
// Apply all client-side filters using centralized function
|
|
1017
1267
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1018
1268
|
|
|
1019
1269
|
const lastDoc =
|
|
1020
|
-
querySnapshot.docs.length > 0
|
|
1021
|
-
|
|
1270
|
+
querySnapshot.docs.length > 0
|
|
1271
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1272
|
+
: null;
|
|
1273
|
+
console.log(
|
|
1274
|
+
`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`
|
|
1275
|
+
);
|
|
1022
1276
|
|
|
1023
1277
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1024
1278
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -1026,14 +1280,16 @@ export class ProcedureService extends BaseService {
|
|
|
1026
1280
|
}
|
|
1027
1281
|
return { procedures, lastDoc };
|
|
1028
1282
|
} catch (error) {
|
|
1029
|
-
console.log(
|
|
1283
|
+
console.log("[PROCEDURE_SERVICE] Strategy 4 failed:", error);
|
|
1030
1284
|
}
|
|
1031
1285
|
|
|
1032
1286
|
// All strategies failed
|
|
1033
|
-
console.log(
|
|
1287
|
+
console.log(
|
|
1288
|
+
"[PROCEDURE_SERVICE] All strategies failed, returning empty result"
|
|
1289
|
+
);
|
|
1034
1290
|
return { procedures: [], lastDoc: null };
|
|
1035
1291
|
} catch (error) {
|
|
1036
|
-
console.error(
|
|
1292
|
+
console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
|
|
1037
1293
|
return { procedures: [], lastDoc: null };
|
|
1038
1294
|
}
|
|
1039
1295
|
}
|
|
@@ -1044,7 +1300,7 @@ export class ProcedureService extends BaseService {
|
|
|
1044
1300
|
*/
|
|
1045
1301
|
private applyInMemoryFilters(
|
|
1046
1302
|
procedures: Procedure[],
|
|
1047
|
-
filters: any
|
|
1303
|
+
filters: any
|
|
1048
1304
|
): (Procedure & { distance?: number })[] {
|
|
1049
1305
|
let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
|
|
1050
1306
|
|
|
@@ -1052,23 +1308,27 @@ export class ProcedureService extends BaseService {
|
|
|
1052
1308
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1053
1309
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1054
1310
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1055
|
-
const name = (procedure.name ||
|
|
1056
|
-
const nameLower = procedure.nameLower ||
|
|
1311
|
+
const name = (procedure.name || "").toLowerCase();
|
|
1312
|
+
const nameLower = procedure.nameLower || "";
|
|
1057
1313
|
return name.includes(searchTerm) || nameLower.includes(searchTerm);
|
|
1058
1314
|
});
|
|
1059
|
-
console.log(
|
|
1315
|
+
console.log(
|
|
1316
|
+
`[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`
|
|
1317
|
+
);
|
|
1060
1318
|
}
|
|
1061
1319
|
|
|
1062
1320
|
// Price filtering
|
|
1063
1321
|
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
|
|
1064
1322
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1065
1323
|
const price = procedure.price || 0;
|
|
1066
|
-
if (filters.minPrice !== undefined && price < filters.minPrice)
|
|
1067
|
-
|
|
1324
|
+
if (filters.minPrice !== undefined && price < filters.minPrice)
|
|
1325
|
+
return false;
|
|
1326
|
+
if (filters.maxPrice !== undefined && price > filters.maxPrice)
|
|
1327
|
+
return false;
|
|
1068
1328
|
return true;
|
|
1069
1329
|
});
|
|
1070
1330
|
console.log(
|
|
1071
|
-
`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}
|
|
1331
|
+
`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`
|
|
1072
1332
|
);
|
|
1073
1333
|
}
|
|
1074
1334
|
|
|
@@ -1076,64 +1336,69 @@ export class ProcedureService extends BaseService {
|
|
|
1076
1336
|
if (filters.minRating !== undefined || filters.maxRating !== undefined) {
|
|
1077
1337
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1078
1338
|
const rating = procedure.reviewInfo?.averageRating || 0;
|
|
1079
|
-
if (filters.minRating !== undefined && rating < filters.minRating)
|
|
1080
|
-
|
|
1339
|
+
if (filters.minRating !== undefined && rating < filters.minRating)
|
|
1340
|
+
return false;
|
|
1341
|
+
if (filters.maxRating !== undefined && rating > filters.maxRating)
|
|
1342
|
+
return false;
|
|
1081
1343
|
return true;
|
|
1082
1344
|
});
|
|
1083
1345
|
console.log(
|
|
1084
|
-
`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}
|
|
1346
|
+
`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`
|
|
1085
1347
|
);
|
|
1086
1348
|
}
|
|
1087
1349
|
|
|
1088
1350
|
// Treatment benefits filtering
|
|
1089
1351
|
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
1090
|
-
const
|
|
1352
|
+
const benefitIdsToMatch = filters.treatmentBenefits;
|
|
1091
1353
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1092
|
-
const
|
|
1093
|
-
return
|
|
1354
|
+
const procedureBenefitIds = procedure.treatmentBenefitIds || [];
|
|
1355
|
+
return benefitIdsToMatch.some((benefitId: string) =>
|
|
1356
|
+
procedureBenefitIds.includes(benefitId)
|
|
1357
|
+
);
|
|
1094
1358
|
});
|
|
1095
1359
|
console.log(
|
|
1096
|
-
`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}
|
|
1360
|
+
`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`
|
|
1097
1361
|
);
|
|
1098
1362
|
}
|
|
1099
1363
|
|
|
1100
1364
|
// Procedure family filtering
|
|
1101
1365
|
if (filters.procedureFamily) {
|
|
1102
1366
|
filteredProcedures = filteredProcedures.filter(
|
|
1103
|
-
(procedure) => procedure.family === filters.procedureFamily
|
|
1367
|
+
(procedure) => procedure.family === filters.procedureFamily
|
|
1104
1368
|
);
|
|
1105
1369
|
console.log(
|
|
1106
|
-
`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}
|
|
1370
|
+
`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`
|
|
1107
1371
|
);
|
|
1108
1372
|
}
|
|
1109
1373
|
|
|
1110
1374
|
// Category filtering
|
|
1111
1375
|
if (filters.procedureCategory) {
|
|
1112
1376
|
filteredProcedures = filteredProcedures.filter(
|
|
1113
|
-
(procedure) => procedure.category?.id === filters.procedureCategory
|
|
1377
|
+
(procedure) => procedure.category?.id === filters.procedureCategory
|
|
1114
1378
|
);
|
|
1115
1379
|
console.log(
|
|
1116
|
-
`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}
|
|
1380
|
+
`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`
|
|
1117
1381
|
);
|
|
1118
1382
|
}
|
|
1119
1383
|
|
|
1120
1384
|
// Subcategory filtering
|
|
1121
1385
|
if (filters.procedureSubcategory) {
|
|
1122
1386
|
filteredProcedures = filteredProcedures.filter(
|
|
1123
|
-
(procedure) =>
|
|
1387
|
+
(procedure) =>
|
|
1388
|
+
procedure.subcategory?.id === filters.procedureSubcategory
|
|
1124
1389
|
);
|
|
1125
1390
|
console.log(
|
|
1126
|
-
`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}
|
|
1391
|
+
`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`
|
|
1127
1392
|
);
|
|
1128
1393
|
}
|
|
1129
1394
|
|
|
1130
1395
|
// Technology filtering
|
|
1131
1396
|
if (filters.procedureTechnology) {
|
|
1132
1397
|
filteredProcedures = filteredProcedures.filter(
|
|
1133
|
-
(procedure) => procedure.technology?.id === filters.procedureTechnology
|
|
1398
|
+
(procedure) => procedure.technology?.id === filters.procedureTechnology
|
|
1134
1399
|
);
|
|
1135
1400
|
console.log(
|
|
1136
|
-
`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}
|
|
1401
|
+
`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`
|
|
1137
1402
|
);
|
|
1138
1403
|
}
|
|
1139
1404
|
|
|
@@ -1150,7 +1415,7 @@ export class ProcedureService extends BaseService {
|
|
|
1150
1415
|
const distance =
|
|
1151
1416
|
distanceBetween(
|
|
1152
1417
|
[location.latitude, location.longitude],
|
|
1153
|
-
[clinicLocation.latitude, clinicLocation.longitude]
|
|
1418
|
+
[clinicLocation.latitude, clinicLocation.longitude]
|
|
1154
1419
|
) / 1000; // Convert to km
|
|
1155
1420
|
|
|
1156
1421
|
// Attach distance for frontend sorting/display
|
|
@@ -1158,19 +1423,24 @@ export class ProcedureService extends BaseService {
|
|
|
1158
1423
|
|
|
1159
1424
|
return distance <= radiusInKm;
|
|
1160
1425
|
});
|
|
1161
|
-
console.log(
|
|
1426
|
+
console.log(
|
|
1427
|
+
`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`
|
|
1428
|
+
);
|
|
1162
1429
|
|
|
1163
1430
|
// Sort by distance when geo filtering is applied
|
|
1164
|
-
filteredProcedures.sort(
|
|
1431
|
+
filteredProcedures.sort(
|
|
1432
|
+
(a, b) => ((a as any).distance || 0) - ((b as any).distance || 0)
|
|
1433
|
+
);
|
|
1165
1434
|
}
|
|
1166
1435
|
|
|
1167
1436
|
return filteredProcedures as (Procedure & { distance?: number })[];
|
|
1168
1437
|
}
|
|
1169
1438
|
|
|
1170
|
-
private handleGeoQuery(
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1439
|
+
private handleGeoQuery(filters: any): Promise<{
|
|
1440
|
+
procedures: (Procedure & { distance?: number })[];
|
|
1441
|
+
lastDoc: any;
|
|
1442
|
+
}> {
|
|
1443
|
+
console.log("[PROCEDURE_SERVICE] Executing geo query with geohash bounds");
|
|
1174
1444
|
try {
|
|
1175
1445
|
const location = filters.location;
|
|
1176
1446
|
const radiusInKm = filters.radiusInKm;
|
|
@@ -1179,22 +1449,33 @@ export class ProcedureService extends BaseService {
|
|
|
1179
1449
|
return Promise.resolve({ procedures: [], lastDoc: null });
|
|
1180
1450
|
}
|
|
1181
1451
|
|
|
1182
|
-
const bounds = geohashQueryBounds(
|
|
1452
|
+
const bounds = geohashQueryBounds(
|
|
1453
|
+
[location.latitude, location.longitude],
|
|
1454
|
+
radiusInKm * 1000
|
|
1455
|
+
);
|
|
1183
1456
|
|
|
1184
1457
|
const fetches = bounds.map((b) => {
|
|
1185
1458
|
const constraints: QueryConstraint[] = [
|
|
1186
|
-
where(
|
|
1187
|
-
where(
|
|
1188
|
-
where(
|
|
1459
|
+
where("clinicInfo.location.geohash", ">=", b[0]),
|
|
1460
|
+
where("clinicInfo.location.geohash", "<=", b[1]),
|
|
1461
|
+
where(
|
|
1462
|
+
"isActive",
|
|
1463
|
+
"==",
|
|
1464
|
+
filters.isActive !== undefined ? filters.isActive : true
|
|
1465
|
+
),
|
|
1189
1466
|
];
|
|
1190
|
-
return getDocs(
|
|
1467
|
+
return getDocs(
|
|
1468
|
+
query(collection(this.db, PROCEDURES_COLLECTION), ...constraints)
|
|
1469
|
+
);
|
|
1191
1470
|
});
|
|
1192
1471
|
|
|
1193
1472
|
return Promise.all(fetches)
|
|
1194
1473
|
.then((snaps) => {
|
|
1195
1474
|
const collected: Procedure[] = [];
|
|
1196
1475
|
snaps.forEach((snap) => {
|
|
1197
|
-
snap.docs.forEach((d) =>
|
|
1476
|
+
snap.docs.forEach((d) =>
|
|
1477
|
+
collected.push({ ...(d.data() as Procedure), id: d.id })
|
|
1478
|
+
);
|
|
1198
1479
|
});
|
|
1199
1480
|
|
|
1200
1481
|
// Deduplicate by id
|
|
@@ -1212,26 +1493,29 @@ export class ProcedureService extends BaseService {
|
|
|
1212
1493
|
let startIndex = 0;
|
|
1213
1494
|
if (
|
|
1214
1495
|
filters.lastDoc &&
|
|
1215
|
-
typeof filters.lastDoc ===
|
|
1496
|
+
typeof filters.lastDoc === "object" &&
|
|
1216
1497
|
(filters.lastDoc as any).id
|
|
1217
1498
|
) {
|
|
1218
|
-
const idx = procedures.findIndex(
|
|
1499
|
+
const idx = procedures.findIndex(
|
|
1500
|
+
(p) => p.id === (filters.lastDoc as any).id
|
|
1501
|
+
);
|
|
1219
1502
|
if (idx >= 0) startIndex = idx + 1;
|
|
1220
1503
|
}
|
|
1221
1504
|
const page = procedures.slice(startIndex, startIndex + pageSize);
|
|
1222
|
-
const newLastDoc =
|
|
1505
|
+
const newLastDoc =
|
|
1506
|
+
page.length === pageSize ? page[page.length - 1] : null;
|
|
1223
1507
|
|
|
1224
1508
|
console.log(
|
|
1225
|
-
`[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km
|
|
1509
|
+
`[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`
|
|
1226
1510
|
);
|
|
1227
1511
|
return { procedures: page, lastDoc: newLastDoc };
|
|
1228
1512
|
})
|
|
1229
1513
|
.catch((err) => {
|
|
1230
|
-
console.error(
|
|
1514
|
+
console.error("[PROCEDURE_SERVICE] Geo bounds fetch failed:", err);
|
|
1231
1515
|
return { procedures: [], lastDoc: null };
|
|
1232
1516
|
});
|
|
1233
1517
|
} catch (error) {
|
|
1234
|
-
console.error(
|
|
1518
|
+
console.error("[PROCEDURE_SERVICE] Geo query failed:", error);
|
|
1235
1519
|
return Promise.resolve({ procedures: [], lastDoc: null });
|
|
1236
1520
|
}
|
|
1237
1521
|
}
|
|
@@ -1243,7 +1527,7 @@ export class ProcedureService extends BaseService {
|
|
|
1243
1527
|
* @returns The created procedure
|
|
1244
1528
|
*/
|
|
1245
1529
|
async createConsultationProcedure(
|
|
1246
|
-
data: Omit<CreateProcedureData,
|
|
1530
|
+
data: Omit<CreateProcedureData, "productId">
|
|
1247
1531
|
): Promise<Procedure> {
|
|
1248
1532
|
// Generate procedure ID first so we can use it for media uploads
|
|
1249
1533
|
const procedureId = this.generateId();
|
|
@@ -1257,7 +1541,7 @@ export class ProcedureService extends BaseService {
|
|
|
1257
1541
|
]);
|
|
1258
1542
|
|
|
1259
1543
|
if (!category || !subcategory || !technology) {
|
|
1260
|
-
throw new Error(
|
|
1544
|
+
throw new Error("One or more required base entities not found");
|
|
1261
1545
|
}
|
|
1262
1546
|
|
|
1263
1547
|
// Get clinic and practitioner information for aggregation
|
|
@@ -1268,7 +1552,11 @@ export class ProcedureService extends BaseService {
|
|
|
1268
1552
|
}
|
|
1269
1553
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
1270
1554
|
|
|
1271
|
-
const practitionerRef = doc(
|
|
1555
|
+
const practitionerRef = doc(
|
|
1556
|
+
this.db,
|
|
1557
|
+
PRACTITIONERS_COLLECTION,
|
|
1558
|
+
data.practitionerId
|
|
1559
|
+
);
|
|
1272
1560
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
1273
1561
|
if (!practitionerSnapshot.exists()) {
|
|
1274
1562
|
throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
|
|
@@ -1278,22 +1566,32 @@ export class ProcedureService extends BaseService {
|
|
|
1278
1566
|
// Process photos if provided
|
|
1279
1567
|
let processedPhotos: string[] = [];
|
|
1280
1568
|
if (data.photos && data.photos.length > 0) {
|
|
1281
|
-
processedPhotos = await this.processMediaArray(
|
|
1569
|
+
processedPhotos = await this.processMediaArray(
|
|
1570
|
+
data.photos,
|
|
1571
|
+
procedureId,
|
|
1572
|
+
"procedure-photos"
|
|
1573
|
+
);
|
|
1282
1574
|
}
|
|
1283
1575
|
|
|
1576
|
+
// Transform productsMetadata from validation format to ProcedureProduct format
|
|
1577
|
+
const transformedProductsMetadata = await this.transformProductsMetadata(
|
|
1578
|
+
data.productsMetadata,
|
|
1579
|
+
data.technologyId
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1284
1582
|
// Create aggregated clinic info for the procedure document
|
|
1285
1583
|
const clinicInfo = {
|
|
1286
1584
|
id: clinicSnapshot.id,
|
|
1287
1585
|
name: clinic.name,
|
|
1288
|
-
description: clinic.description ||
|
|
1586
|
+
description: clinic.description || "",
|
|
1289
1587
|
featuredPhoto:
|
|
1290
1588
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
1291
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
1589
|
+
? typeof clinic.featuredPhotos[0] === "string"
|
|
1292
1590
|
? clinic.featuredPhotos[0]
|
|
1293
|
-
:
|
|
1294
|
-
: typeof clinic.coverPhoto ===
|
|
1591
|
+
: ""
|
|
1592
|
+
: typeof clinic.coverPhoto === "string"
|
|
1295
1593
|
? clinic.coverPhoto
|
|
1296
|
-
:
|
|
1594
|
+
: "",
|
|
1297
1595
|
location: clinic.location,
|
|
1298
1596
|
contactInfo: clinic.contactInfo,
|
|
1299
1597
|
};
|
|
@@ -1302,31 +1600,33 @@ export class ProcedureService extends BaseService {
|
|
|
1302
1600
|
const doctorInfo = {
|
|
1303
1601
|
id: practitionerSnapshot.id,
|
|
1304
1602
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
1305
|
-
description: practitioner.basicInfo.bio ||
|
|
1603
|
+
description: practitioner.basicInfo.bio || "",
|
|
1306
1604
|
photo:
|
|
1307
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
1605
|
+
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
1308
1606
|
? practitioner.basicInfo.profileImageUrl
|
|
1309
|
-
:
|
|
1607
|
+
: "",
|
|
1310
1608
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
1311
1609
|
services: practitioner.procedures || [],
|
|
1312
1610
|
};
|
|
1313
1611
|
|
|
1314
1612
|
// Create a placeholder product for consultation procedures
|
|
1315
1613
|
const consultationProduct: Product = {
|
|
1316
|
-
id:
|
|
1317
|
-
name:
|
|
1318
|
-
description:
|
|
1319
|
-
brandId:
|
|
1320
|
-
brandName:
|
|
1614
|
+
id: "consultation-no-product",
|
|
1615
|
+
name: "No Product Required",
|
|
1616
|
+
description: "Consultation procedures do not require specific products",
|
|
1617
|
+
brandId: "consultation-brand",
|
|
1618
|
+
brandName: "Consultation",
|
|
1321
1619
|
technologyId: data.technologyId,
|
|
1322
1620
|
technologyName: technology.name,
|
|
1621
|
+
categoryId: technology.categoryId,
|
|
1622
|
+
subcategoryId: technology.subcategoryId,
|
|
1323
1623
|
isActive: true,
|
|
1324
1624
|
createdAt: new Date(),
|
|
1325
1625
|
updatedAt: new Date(),
|
|
1326
1626
|
};
|
|
1327
1627
|
|
|
1328
1628
|
// Create the procedure object
|
|
1329
|
-
const newProcedure: Omit<Procedure,
|
|
1629
|
+
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
1330
1630
|
id: procedureId,
|
|
1331
1631
|
...data,
|
|
1332
1632
|
nameLower: (data as any).nameLower || data.name.toLowerCase(),
|
|
@@ -1335,9 +1635,12 @@ export class ProcedureService extends BaseService {
|
|
|
1335
1635
|
subcategory,
|
|
1336
1636
|
technology,
|
|
1337
1637
|
product: consultationProduct, // Use placeholder product
|
|
1638
|
+
productsMetadata: transformedProductsMetadata,
|
|
1338
1639
|
blockingConditions: technology.blockingConditions,
|
|
1339
1640
|
contraindications: technology.contraindications || [],
|
|
1641
|
+
contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
|
|
1340
1642
|
treatmentBenefits: technology.benefits,
|
|
1643
|
+
treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
|
|
1341
1644
|
preRequirements: technology.requirements.pre,
|
|
1342
1645
|
postRequirements: technology.requirements.post,
|
|
1343
1646
|
certificationRequirement: technology.certificationRequirement,
|
|
@@ -1395,7 +1698,7 @@ export class ProcedureService extends BaseService {
|
|
|
1395
1698
|
name: data.name,
|
|
1396
1699
|
clinicId: data.clinicInfo?.id,
|
|
1397
1700
|
clinicName: data.clinicInfo?.name,
|
|
1398
|
-
address: data.clinicInfo?.location?.address ||
|
|
1701
|
+
address: data.clinicInfo?.location?.address || "",
|
|
1399
1702
|
latitude: data.clinicInfo?.location?.latitude,
|
|
1400
1703
|
longitude: data.clinicInfo?.location?.longitude,
|
|
1401
1704
|
};
|