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