@blackcode_sa/metaestetics-api 1.8.18 → 1.10.0
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/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +212 -205
- package/dist/index.mjs +215 -210
- package/package.json +1 -1
- package/src/services/clinic/clinic.service.ts +91 -142
- package/src/services/clinic/utils/filter.utils.ts +180 -96
- package/src/services/procedure/procedure.service.ts +326 -362
|
@@ -20,57 +20,39 @@ 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
|
-
} from
|
|
69
|
-
import { Clinic, CLINICS_COLLECTION } from
|
|
70
|
-
import { ProcedureReviewInfo } from
|
|
71
|
-
import { distanceBetween, geohashQueryBounds } from
|
|
72
|
-
import { TreatmentBenefit } from
|
|
73
|
-
import { MediaService, MediaAccessLevel } from
|
|
50
|
+
} from '../../backoffice/types';
|
|
51
|
+
import { Clinic, CLINICS_COLLECTION } from '../../types/clinic';
|
|
52
|
+
import { ProcedureReviewInfo } from '../../types/reviews';
|
|
53
|
+
import { distanceBetween, geohashQueryBounds } from 'geofire-common';
|
|
54
|
+
import { TreatmentBenefit } from '../../backoffice/types/static/treatment-benefit.types';
|
|
55
|
+
import { MediaService, MediaAccessLevel } from '../media/media.service';
|
|
74
56
|
|
|
75
57
|
export class ProcedureService extends BaseService {
|
|
76
58
|
private categoryService: CategoryService;
|
|
@@ -87,7 +69,7 @@ export class ProcedureService extends BaseService {
|
|
|
87
69
|
subcategoryService: SubcategoryService,
|
|
88
70
|
technologyService: TechnologyService,
|
|
89
71
|
productService: ProductService,
|
|
90
|
-
mediaService: MediaService
|
|
72
|
+
mediaService: MediaService,
|
|
91
73
|
) {
|
|
92
74
|
super(db, auth, app);
|
|
93
75
|
this.categoryService = categoryService;
|
|
@@ -107,25 +89,23 @@ export class ProcedureService extends BaseService {
|
|
|
107
89
|
private async processMedia(
|
|
108
90
|
media: string | File | Blob | null | undefined,
|
|
109
91
|
ownerId: string,
|
|
110
|
-
collectionName: string
|
|
92
|
+
collectionName: string,
|
|
111
93
|
): Promise<string | null> {
|
|
112
94
|
if (!media) return null;
|
|
113
95
|
|
|
114
96
|
// If already a string URL, return it directly
|
|
115
|
-
if (typeof media ===
|
|
97
|
+
if (typeof media === 'string') {
|
|
116
98
|
return media;
|
|
117
99
|
}
|
|
118
100
|
|
|
119
101
|
// If it's a File, upload it using MediaService
|
|
120
102
|
if (media instanceof File || media instanceof Blob) {
|
|
121
|
-
console.log(
|
|
122
|
-
`[ProcedureService] Uploading ${collectionName} media for ${ownerId}`
|
|
123
|
-
);
|
|
103
|
+
console.log(`[ProcedureService] Uploading ${collectionName} media for ${ownerId}`);
|
|
124
104
|
const metadata = await this.mediaService.uploadMedia(
|
|
125
105
|
media,
|
|
126
106
|
ownerId,
|
|
127
107
|
MediaAccessLevel.PUBLIC,
|
|
128
|
-
collectionName
|
|
108
|
+
collectionName,
|
|
129
109
|
);
|
|
130
110
|
return metadata.url;
|
|
131
111
|
}
|
|
@@ -143,18 +123,14 @@ export class ProcedureService extends BaseService {
|
|
|
143
123
|
private async processMediaArray(
|
|
144
124
|
mediaArray: (string | File | Blob)[] | undefined,
|
|
145
125
|
ownerId: string,
|
|
146
|
-
collectionName: string
|
|
126
|
+
collectionName: string,
|
|
147
127
|
): Promise<string[]> {
|
|
148
128
|
if (!mediaArray || mediaArray.length === 0) return [];
|
|
149
129
|
|
|
150
130
|
const result: string[] = [];
|
|
151
131
|
|
|
152
132
|
for (const media of mediaArray) {
|
|
153
|
-
const processedUrl = await this.processMedia(
|
|
154
|
-
media,
|
|
155
|
-
ownerId,
|
|
156
|
-
collectionName
|
|
157
|
-
);
|
|
133
|
+
const processedUrl = await this.processMedia(media, ownerId, collectionName);
|
|
158
134
|
if (processedUrl) {
|
|
159
135
|
result.push(processedUrl);
|
|
160
136
|
}
|
|
@@ -177,45 +153,27 @@ export class ProcedureService extends BaseService {
|
|
|
177
153
|
// Get references to related entities (Category, Subcategory, Technology, Product)
|
|
178
154
|
const [category, subcategory, technology, product] = await Promise.all([
|
|
179
155
|
this.categoryService.getById(validatedData.categoryId),
|
|
180
|
-
this.subcategoryService.getById(
|
|
181
|
-
validatedData.categoryId,
|
|
182
|
-
validatedData.subcategoryId
|
|
183
|
-
),
|
|
156
|
+
this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
|
|
184
157
|
this.technologyService.getById(validatedData.technologyId),
|
|
185
|
-
this.productService.getById(
|
|
186
|
-
validatedData.technologyId,
|
|
187
|
-
validatedData.productId
|
|
188
|
-
),
|
|
158
|
+
this.productService.getById(validatedData.technologyId, validatedData.productId),
|
|
189
159
|
]);
|
|
190
160
|
|
|
191
161
|
if (!category || !subcategory || !technology || !product) {
|
|
192
|
-
throw new Error(
|
|
162
|
+
throw new Error('One or more required base entities not found');
|
|
193
163
|
}
|
|
194
164
|
|
|
195
165
|
// Get clinic and practitioner information for aggregation
|
|
196
|
-
const clinicRef = doc(
|
|
197
|
-
this.db,
|
|
198
|
-
CLINICS_COLLECTION,
|
|
199
|
-
validatedData.clinicBranchId
|
|
200
|
-
);
|
|
166
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
|
|
201
167
|
const clinicSnapshot = await getDoc(clinicRef);
|
|
202
168
|
if (!clinicSnapshot.exists()) {
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
205
|
-
);
|
|
169
|
+
throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
|
|
206
170
|
}
|
|
207
171
|
const clinic = clinicSnapshot.data() as Clinic; // Assert type
|
|
208
172
|
|
|
209
|
-
const practitionerRef = doc(
|
|
210
|
-
this.db,
|
|
211
|
-
PRACTITIONERS_COLLECTION,
|
|
212
|
-
validatedData.practitionerId
|
|
213
|
-
);
|
|
173
|
+
const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, validatedData.practitionerId);
|
|
214
174
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
215
175
|
if (!practitionerSnapshot.exists()) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
`Practitioner with ID ${validatedData.practitionerId} not found`
|
|
218
|
-
);
|
|
176
|
+
throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
|
|
219
177
|
}
|
|
220
178
|
const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
|
|
221
179
|
|
|
@@ -225,7 +183,7 @@ export class ProcedureService extends BaseService {
|
|
|
225
183
|
processedPhotos = await this.processMediaArray(
|
|
226
184
|
validatedData.photos,
|
|
227
185
|
procedureId,
|
|
228
|
-
|
|
186
|
+
'procedure-photos',
|
|
229
187
|
);
|
|
230
188
|
}
|
|
231
189
|
|
|
@@ -233,15 +191,15 @@ export class ProcedureService extends BaseService {
|
|
|
233
191
|
const clinicInfo = {
|
|
234
192
|
id: clinicSnapshot.id,
|
|
235
193
|
name: clinic.name,
|
|
236
|
-
description: clinic.description ||
|
|
194
|
+
description: clinic.description || '',
|
|
237
195
|
featuredPhoto:
|
|
238
196
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
239
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
197
|
+
? typeof clinic.featuredPhotos[0] === 'string'
|
|
240
198
|
? clinic.featuredPhotos[0]
|
|
241
|
-
:
|
|
242
|
-
: typeof clinic.coverPhoto ===
|
|
199
|
+
: ''
|
|
200
|
+
: typeof clinic.coverPhoto === 'string'
|
|
243
201
|
? clinic.coverPhoto
|
|
244
|
-
:
|
|
202
|
+
: '',
|
|
245
203
|
location: clinic.location,
|
|
246
204
|
contactInfo: clinic.contactInfo,
|
|
247
205
|
};
|
|
@@ -250,17 +208,17 @@ export class ProcedureService extends BaseService {
|
|
|
250
208
|
const doctorInfo = {
|
|
251
209
|
id: practitionerSnapshot.id,
|
|
252
210
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
253
|
-
description: practitioner.basicInfo.bio ||
|
|
211
|
+
description: practitioner.basicInfo.bio || '',
|
|
254
212
|
photo:
|
|
255
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
213
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
256
214
|
? practitioner.basicInfo.profileImageUrl
|
|
257
|
-
:
|
|
215
|
+
: '', // Default to empty string if not a processed URL
|
|
258
216
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
259
217
|
services: practitioner.procedures || [],
|
|
260
218
|
};
|
|
261
219
|
|
|
262
220
|
// Create the procedure object
|
|
263
|
-
const newProcedure: Omit<Procedure,
|
|
221
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
264
222
|
id: procedureId,
|
|
265
223
|
...validatedData,
|
|
266
224
|
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
@@ -314,12 +272,12 @@ export class ProcedureService extends BaseService {
|
|
|
314
272
|
* @returns A promise that resolves to an array of the newly created procedures.
|
|
315
273
|
*/
|
|
316
274
|
async bulkCreateProcedures(
|
|
317
|
-
baseData: Omit<CreateProcedureData,
|
|
318
|
-
practitionerIds: string[]
|
|
275
|
+
baseData: Omit<CreateProcedureData, 'practitionerId'>,
|
|
276
|
+
practitionerIds: string[],
|
|
319
277
|
): Promise<Procedure[]> {
|
|
320
278
|
// 1. Validation
|
|
321
279
|
if (!practitionerIds || practitionerIds.length === 0) {
|
|
322
|
-
throw new Error(
|
|
280
|
+
throw new Error('Practitioner IDs array cannot be empty.');
|
|
323
281
|
}
|
|
324
282
|
|
|
325
283
|
// Add a dummy practitionerId for the validation schema to pass
|
|
@@ -327,28 +285,19 @@ export class ProcedureService extends BaseService {
|
|
|
327
285
|
const validatedData = createProcedureSchema.parse(validationData);
|
|
328
286
|
|
|
329
287
|
// 2. Fetch common data once to avoid redundant reads
|
|
330
|
-
const [category, subcategory, technology, product, clinicSnapshot] =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
this.technologyService.getById(validatedData.technologyId),
|
|
338
|
-
this.productService.getById(
|
|
339
|
-
validatedData.technologyId,
|
|
340
|
-
validatedData.productId
|
|
341
|
-
),
|
|
342
|
-
getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
|
|
343
|
-
]);
|
|
288
|
+
const [category, subcategory, technology, product, clinicSnapshot] = await Promise.all([
|
|
289
|
+
this.categoryService.getById(validatedData.categoryId),
|
|
290
|
+
this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
|
|
291
|
+
this.technologyService.getById(validatedData.technologyId),
|
|
292
|
+
this.productService.getById(validatedData.technologyId, validatedData.productId),
|
|
293
|
+
getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
|
|
294
|
+
]);
|
|
344
295
|
|
|
345
296
|
if (!category || !subcategory || !technology || !product) {
|
|
346
|
-
throw new Error(
|
|
297
|
+
throw new Error('One or more required base entities not found');
|
|
347
298
|
}
|
|
348
299
|
if (!clinicSnapshot.exists()) {
|
|
349
|
-
throw new Error(
|
|
350
|
-
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
351
|
-
);
|
|
300
|
+
throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
|
|
352
301
|
}
|
|
353
302
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
354
303
|
|
|
@@ -359,7 +308,7 @@ export class ProcedureService extends BaseService {
|
|
|
359
308
|
processedPhotos = await this.processMediaArray(
|
|
360
309
|
validatedData.photos,
|
|
361
310
|
batchId,
|
|
362
|
-
|
|
311
|
+
'procedure-photos-batch',
|
|
363
312
|
);
|
|
364
313
|
}
|
|
365
314
|
|
|
@@ -370,7 +319,7 @@ export class ProcedureService extends BaseService {
|
|
|
370
319
|
const chunk = practitionerIds.slice(i, i + 30);
|
|
371
320
|
const practitionersQuery = query(
|
|
372
321
|
collection(this.db, PRACTITIONERS_COLLECTION),
|
|
373
|
-
where(documentId(),
|
|
322
|
+
where(documentId(), 'in', chunk),
|
|
374
323
|
);
|
|
375
324
|
const practitionersSnapshot = await getDocs(practitionersQuery);
|
|
376
325
|
practitionersSnapshot.docs.forEach((doc) => {
|
|
@@ -381,12 +330,8 @@ export class ProcedureService extends BaseService {
|
|
|
381
330
|
// Verify all practitioners were found
|
|
382
331
|
if (practitionersMap.size !== practitionerIds.length) {
|
|
383
332
|
const foundIds = Array.from(practitionersMap.keys());
|
|
384
|
-
const notFoundIds = practitionerIds.filter(
|
|
385
|
-
|
|
386
|
-
);
|
|
387
|
-
throw new Error(
|
|
388
|
-
`The following practitioners were not found: ${notFoundIds.join(", ")}`
|
|
389
|
-
);
|
|
333
|
+
const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
|
|
334
|
+
throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
|
|
390
335
|
}
|
|
391
336
|
|
|
392
337
|
// 5. Use a Firestore batch for atomic creation
|
|
@@ -395,15 +340,15 @@ export class ProcedureService extends BaseService {
|
|
|
395
340
|
const clinicInfo = {
|
|
396
341
|
id: clinicSnapshot.id,
|
|
397
342
|
name: clinic.name,
|
|
398
|
-
description: clinic.description ||
|
|
343
|
+
description: clinic.description || '',
|
|
399
344
|
featuredPhoto:
|
|
400
345
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
401
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
346
|
+
? typeof clinic.featuredPhotos[0] === 'string'
|
|
402
347
|
? clinic.featuredPhotos[0]
|
|
403
|
-
:
|
|
404
|
-
: typeof clinic.coverPhoto ===
|
|
348
|
+
: ''
|
|
349
|
+
: typeof clinic.coverPhoto === 'string'
|
|
405
350
|
? clinic.coverPhoto
|
|
406
|
-
:
|
|
351
|
+
: '',
|
|
407
352
|
location: clinic.location,
|
|
408
353
|
contactInfo: clinic.contactInfo,
|
|
409
354
|
};
|
|
@@ -414,11 +359,11 @@ export class ProcedureService extends BaseService {
|
|
|
414
359
|
const doctorInfo = {
|
|
415
360
|
id: practitioner.id,
|
|
416
361
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
417
|
-
description: practitioner.basicInfo.bio ||
|
|
362
|
+
description: practitioner.basicInfo.bio || '',
|
|
418
363
|
photo:
|
|
419
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
364
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
420
365
|
? practitioner.basicInfo.profileImageUrl
|
|
421
|
-
:
|
|
366
|
+
: '',
|
|
422
367
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
423
368
|
services: practitioner.procedures || [],
|
|
424
369
|
};
|
|
@@ -428,7 +373,7 @@ export class ProcedureService extends BaseService {
|
|
|
428
373
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
|
|
429
374
|
|
|
430
375
|
// Construct the new procedure, reusing common data
|
|
431
|
-
const newProcedure: Omit<Procedure,
|
|
376
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
432
377
|
id: procedureId,
|
|
433
378
|
...validatedData,
|
|
434
379
|
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
@@ -474,10 +419,7 @@ export class ProcedureService extends BaseService {
|
|
|
474
419
|
const fetchedProcedures: Procedure[] = [];
|
|
475
420
|
for (let i = 0; i < createdProcedureIds.length; i += 30) {
|
|
476
421
|
const chunk = createdProcedureIds.slice(i, i + 30);
|
|
477
|
-
const q = query(
|
|
478
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
479
|
-
where(documentId(), "in", chunk)
|
|
480
|
-
);
|
|
422
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), where(documentId(), 'in', chunk));
|
|
481
423
|
const snapshot = await getDocs(q);
|
|
482
424
|
snapshot.forEach((doc) => {
|
|
483
425
|
fetchedProcedures.push(doc.data() as Procedure);
|
|
@@ -508,13 +450,11 @@ export class ProcedureService extends BaseService {
|
|
|
508
450
|
* @param clinicBranchId - The ID of the clinic branch
|
|
509
451
|
* @returns List of procedures
|
|
510
452
|
*/
|
|
511
|
-
async getProceduresByClinicBranch(
|
|
512
|
-
clinicBranchId: string
|
|
513
|
-
): Promise<Procedure[]> {
|
|
453
|
+
async getProceduresByClinicBranch(clinicBranchId: string): Promise<Procedure[]> {
|
|
514
454
|
const q = query(
|
|
515
455
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
516
|
-
where(
|
|
517
|
-
where(
|
|
456
|
+
where('clinicBranchId', '==', clinicBranchId),
|
|
457
|
+
where('isActive', '==', true),
|
|
518
458
|
);
|
|
519
459
|
const snapshot = await getDocs(q);
|
|
520
460
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -525,13 +465,11 @@ export class ProcedureService extends BaseService {
|
|
|
525
465
|
* @param practitionerId - The ID of the practitioner
|
|
526
466
|
* @returns List of procedures
|
|
527
467
|
*/
|
|
528
|
-
async getProceduresByPractitioner(
|
|
529
|
-
practitionerId: string
|
|
530
|
-
): Promise<Procedure[]> {
|
|
468
|
+
async getProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
|
|
531
469
|
const q = query(
|
|
532
470
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
533
|
-
where(
|
|
534
|
-
where(
|
|
471
|
+
where('practitionerId', '==', practitionerId),
|
|
472
|
+
where('isActive', '==', true),
|
|
535
473
|
);
|
|
536
474
|
const snapshot = await getDocs(q);
|
|
537
475
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -542,13 +480,11 @@ export class ProcedureService extends BaseService {
|
|
|
542
480
|
* @param practitionerId - The ID of the practitioner
|
|
543
481
|
* @returns List of inactive procedures
|
|
544
482
|
*/
|
|
545
|
-
async getInactiveProceduresByPractitioner(
|
|
546
|
-
practitionerId: string
|
|
547
|
-
): Promise<Procedure[]> {
|
|
483
|
+
async getInactiveProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
|
|
548
484
|
const q = query(
|
|
549
485
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
550
|
-
where(
|
|
551
|
-
where(
|
|
486
|
+
where('practitionerId', '==', practitionerId),
|
|
487
|
+
where('isActive', '==', false),
|
|
552
488
|
);
|
|
553
489
|
const snapshot = await getDocs(q);
|
|
554
490
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -560,10 +496,7 @@ export class ProcedureService extends BaseService {
|
|
|
560
496
|
* @param data - The data to update the procedure with
|
|
561
497
|
* @returns The updated procedure
|
|
562
498
|
*/
|
|
563
|
-
async updateProcedure(
|
|
564
|
-
id: string,
|
|
565
|
-
data: UpdateProcedureData
|
|
566
|
-
): Promise<Procedure> {
|
|
499
|
+
async updateProcedure(id: string, data: UpdateProcedureData): Promise<Procedure> {
|
|
567
500
|
const validatedData = updateProcedureSchema.parse(data);
|
|
568
501
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
569
502
|
const procedureSnapshot = await getDoc(procedureRef);
|
|
@@ -587,54 +520,42 @@ export class ProcedureService extends BaseService {
|
|
|
587
520
|
updatedProcedureData.photos = await this.processMediaArray(
|
|
588
521
|
validatedData.photos,
|
|
589
522
|
id,
|
|
590
|
-
|
|
523
|
+
'procedure-photos',
|
|
591
524
|
);
|
|
592
525
|
}
|
|
593
526
|
|
|
594
527
|
// --- Prepare updates and fetch new related data if IDs change ---
|
|
595
528
|
|
|
596
529
|
// Handle Practitioner Change
|
|
597
|
-
if (
|
|
598
|
-
validatedData.practitionerId &&
|
|
599
|
-
validatedData.practitionerId !== oldPractitionerId
|
|
600
|
-
) {
|
|
530
|
+
if (validatedData.practitionerId && validatedData.practitionerId !== oldPractitionerId) {
|
|
601
531
|
practitionerChanged = true;
|
|
602
532
|
const newPractitionerRef = doc(
|
|
603
533
|
this.db,
|
|
604
534
|
PRACTITIONERS_COLLECTION,
|
|
605
|
-
validatedData.practitionerId
|
|
535
|
+
validatedData.practitionerId,
|
|
606
536
|
);
|
|
607
537
|
const newPractitionerSnap = await getDoc(newPractitionerRef);
|
|
608
538
|
if (!newPractitionerSnap.exists())
|
|
609
|
-
throw new Error(
|
|
610
|
-
`New Practitioner ${validatedData.practitionerId} not found`
|
|
611
|
-
);
|
|
539
|
+
throw new Error(`New Practitioner ${validatedData.practitionerId} not found`);
|
|
612
540
|
newPractitioner = newPractitionerSnap.data() as Practitioner;
|
|
613
541
|
// Update doctorInfo within the procedure document
|
|
614
542
|
updatedProcedureData.doctorInfo = {
|
|
615
543
|
id: newPractitioner.id,
|
|
616
544
|
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
617
|
-
description: newPractitioner.basicInfo.bio ||
|
|
545
|
+
description: newPractitioner.basicInfo.bio || '',
|
|
618
546
|
photo:
|
|
619
|
-
typeof newPractitioner.basicInfo.profileImageUrl ===
|
|
547
|
+
typeof newPractitioner.basicInfo.profileImageUrl === 'string'
|
|
620
548
|
? newPractitioner.basicInfo.profileImageUrl
|
|
621
|
-
:
|
|
549
|
+
: '', // Default to empty string if not a processed URL
|
|
622
550
|
rating: newPractitioner.reviewInfo?.averageRating || 0,
|
|
623
551
|
services: newPractitioner.procedures || [],
|
|
624
552
|
};
|
|
625
553
|
}
|
|
626
554
|
|
|
627
555
|
// Handle Clinic Change
|
|
628
|
-
if (
|
|
629
|
-
validatedData.clinicBranchId &&
|
|
630
|
-
validatedData.clinicBranchId !== oldClinicId
|
|
631
|
-
) {
|
|
556
|
+
if (validatedData.clinicBranchId && validatedData.clinicBranchId !== oldClinicId) {
|
|
632
557
|
clinicChanged = true;
|
|
633
|
-
const newClinicRef = doc(
|
|
634
|
-
this.db,
|
|
635
|
-
CLINICS_COLLECTION,
|
|
636
|
-
validatedData.clinicBranchId
|
|
637
|
-
);
|
|
558
|
+
const newClinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
|
|
638
559
|
const newClinicSnap = await getDoc(newClinicRef);
|
|
639
560
|
if (!newClinicSnap.exists())
|
|
640
561
|
throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
|
|
@@ -643,15 +564,15 @@ export class ProcedureService extends BaseService {
|
|
|
643
564
|
updatedProcedureData.clinicInfo = {
|
|
644
565
|
id: newClinic.id,
|
|
645
566
|
name: newClinic.name,
|
|
646
|
-
description: newClinic.description ||
|
|
567
|
+
description: newClinic.description || '',
|
|
647
568
|
featuredPhoto:
|
|
648
569
|
newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
|
|
649
|
-
? typeof newClinic.featuredPhotos[0] ===
|
|
570
|
+
? typeof newClinic.featuredPhotos[0] === 'string'
|
|
650
571
|
? newClinic.featuredPhotos[0]
|
|
651
|
-
:
|
|
652
|
-
: typeof newClinic.coverPhoto ===
|
|
572
|
+
: ''
|
|
573
|
+
: typeof newClinic.coverPhoto === 'string'
|
|
653
574
|
? newClinic.coverPhoto
|
|
654
|
-
:
|
|
575
|
+
: '',
|
|
655
576
|
location: newClinic.location,
|
|
656
577
|
contactInfo: newClinic.contactInfo,
|
|
657
578
|
};
|
|
@@ -663,11 +584,8 @@ export class ProcedureService extends BaseService {
|
|
|
663
584
|
updatedProcedureData.nameLower = validatedData.name.toLowerCase();
|
|
664
585
|
}
|
|
665
586
|
if (validatedData.categoryId) {
|
|
666
|
-
const category = await this.categoryService.getById(
|
|
667
|
-
|
|
668
|
-
);
|
|
669
|
-
if (!category)
|
|
670
|
-
throw new Error(`Category ${validatedData.categoryId} not found`);
|
|
587
|
+
const category = await this.categoryService.getById(validatedData.categoryId);
|
|
588
|
+
if (!category) throw new Error(`Category ${validatedData.categoryId} not found`);
|
|
671
589
|
updatedProcedureData.category = category;
|
|
672
590
|
finalCategoryId = category.id; // Update finalCategoryId if category changed
|
|
673
591
|
}
|
|
@@ -676,26 +594,21 @@ export class ProcedureService extends BaseService {
|
|
|
676
594
|
if (validatedData.subcategoryId && finalCategoryId) {
|
|
677
595
|
const subcategory = await this.subcategoryService.getById(
|
|
678
596
|
finalCategoryId,
|
|
679
|
-
validatedData.subcategoryId
|
|
597
|
+
validatedData.subcategoryId,
|
|
680
598
|
);
|
|
681
599
|
if (!subcategory)
|
|
682
600
|
throw new Error(
|
|
683
|
-
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}
|
|
601
|
+
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`,
|
|
684
602
|
);
|
|
685
603
|
updatedProcedureData.subcategory = subcategory;
|
|
686
604
|
} else if (validatedData.subcategoryId) {
|
|
687
|
-
console.warn(
|
|
688
|
-
"Attempted to update subcategory without a valid categoryId"
|
|
689
|
-
);
|
|
605
|
+
console.warn('Attempted to update subcategory without a valid categoryId');
|
|
690
606
|
}
|
|
691
607
|
|
|
692
608
|
let finalTechnologyId = existingProcedure.technology.id;
|
|
693
609
|
if (validatedData.technologyId) {
|
|
694
|
-
const technology = await this.technologyService.getById(
|
|
695
|
-
|
|
696
|
-
);
|
|
697
|
-
if (!technology)
|
|
698
|
-
throw new Error(`Technology ${validatedData.technologyId} not found`);
|
|
610
|
+
const technology = await this.technologyService.getById(validatedData.technologyId);
|
|
611
|
+
if (!technology) throw new Error(`Technology ${validatedData.technologyId} not found`);
|
|
699
612
|
updatedProcedureData.technology = technology;
|
|
700
613
|
finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
|
|
701
614
|
// Update related fields derived from technology
|
|
@@ -703,25 +616,20 @@ export class ProcedureService extends BaseService {
|
|
|
703
616
|
updatedProcedureData.treatmentBenefits = technology.benefits;
|
|
704
617
|
updatedProcedureData.preRequirements = technology.requirements.pre;
|
|
705
618
|
updatedProcedureData.postRequirements = technology.requirements.post;
|
|
706
|
-
updatedProcedureData.certificationRequirement =
|
|
707
|
-
|
|
708
|
-
updatedProcedureData.documentationTemplates =
|
|
709
|
-
technology.documentationTemplates || [];
|
|
619
|
+
updatedProcedureData.certificationRequirement = technology.certificationRequirement;
|
|
620
|
+
updatedProcedureData.documentationTemplates = technology.documentationTemplates || [];
|
|
710
621
|
}
|
|
711
622
|
|
|
712
623
|
// Only fetch product if its ID is provided AND we have a valid finalTechnologyId
|
|
713
624
|
if (validatedData.productId && finalTechnologyId) {
|
|
714
|
-
const product = await this.productService.getById(
|
|
715
|
-
finalTechnologyId,
|
|
716
|
-
validatedData.productId
|
|
717
|
-
);
|
|
625
|
+
const product = await this.productService.getById(finalTechnologyId, validatedData.productId);
|
|
718
626
|
if (!product)
|
|
719
627
|
throw new Error(
|
|
720
|
-
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}
|
|
628
|
+
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}`,
|
|
721
629
|
);
|
|
722
630
|
updatedProcedureData.product = product;
|
|
723
631
|
} else if (validatedData.productId) {
|
|
724
|
-
console.warn(
|
|
632
|
+
console.warn('Attempted to update product without a valid technologyId');
|
|
725
633
|
}
|
|
726
634
|
|
|
727
635
|
// Update the procedure document
|
|
@@ -805,7 +713,7 @@ export class ProcedureService extends BaseService {
|
|
|
805
713
|
*/
|
|
806
714
|
async getAllProcedures(
|
|
807
715
|
pagination?: number,
|
|
808
|
-
lastDoc?: any
|
|
716
|
+
lastDoc?: any,
|
|
809
717
|
): Promise<{ procedures: Procedure[]; lastDoc: any }> {
|
|
810
718
|
try {
|
|
811
719
|
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
@@ -813,29 +721,24 @@ export class ProcedureService extends BaseService {
|
|
|
813
721
|
|
|
814
722
|
// Apply pagination if specified
|
|
815
723
|
if (pagination && pagination > 0) {
|
|
816
|
-
const { limit, startAfter } = await import(
|
|
724
|
+
const { limit, startAfter } = await import('firebase/firestore'); // Use dynamic import if needed top-level
|
|
817
725
|
|
|
818
726
|
if (lastDoc) {
|
|
819
727
|
proceduresQuery = query(
|
|
820
728
|
proceduresCollection,
|
|
821
|
-
orderBy(
|
|
729
|
+
orderBy('name'), // Use imported orderBy
|
|
822
730
|
startAfter(lastDoc),
|
|
823
|
-
limit(pagination)
|
|
731
|
+
limit(pagination),
|
|
824
732
|
);
|
|
825
733
|
} else {
|
|
826
|
-
proceduresQuery = query(
|
|
827
|
-
proceduresCollection,
|
|
828
|
-
orderBy("name"),
|
|
829
|
-
limit(pagination)
|
|
830
|
-
); // Use imported orderBy
|
|
734
|
+
proceduresQuery = query(proceduresCollection, orderBy('name'), limit(pagination)); // Use imported orderBy
|
|
831
735
|
}
|
|
832
736
|
} else {
|
|
833
|
-
proceduresQuery = query(proceduresCollection, orderBy(
|
|
737
|
+
proceduresQuery = query(proceduresCollection, orderBy('name')); // Use imported orderBy
|
|
834
738
|
}
|
|
835
739
|
|
|
836
740
|
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
837
|
-
const lastVisible =
|
|
838
|
-
proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
741
|
+
const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
839
742
|
|
|
840
743
|
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
841
744
|
const data = doc.data() as Procedure;
|
|
@@ -850,7 +753,7 @@ export class ProcedureService extends BaseService {
|
|
|
850
753
|
lastDoc: lastVisible,
|
|
851
754
|
};
|
|
852
755
|
} catch (error) {
|
|
853
|
-
console.error(
|
|
756
|
+
console.error('[PROCEDURE_SERVICE] Error getting all procedures:', error);
|
|
854
757
|
throw error;
|
|
855
758
|
}
|
|
856
759
|
}
|
|
@@ -899,16 +802,16 @@ export class ProcedureService extends BaseService {
|
|
|
899
802
|
lastDoc: any;
|
|
900
803
|
}> {
|
|
901
804
|
try {
|
|
902
|
-
console.log(
|
|
805
|
+
console.log('[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies');
|
|
903
806
|
|
|
904
807
|
// Geo query debug i validacija
|
|
905
808
|
if (filters.location && filters.radiusInKm) {
|
|
906
809
|
console.log('[PROCEDURE_SERVICE] Executing geo query:', {
|
|
907
810
|
location: filters.location,
|
|
908
811
|
radius: filters.radiusInKm,
|
|
909
|
-
serviceName: 'ProcedureService'
|
|
812
|
+
serviceName: 'ProcedureService',
|
|
910
813
|
});
|
|
911
|
-
|
|
814
|
+
|
|
912
815
|
// Validacija location podataka
|
|
913
816
|
if (!filters.location.latitude || !filters.location.longitude) {
|
|
914
817
|
console.warn('[PROCEDURE_SERVICE] Invalid location data:', filters.location);
|
|
@@ -926,42 +829,42 @@ export class ProcedureService extends BaseService {
|
|
|
926
829
|
// Base constraints (used in all strategies)
|
|
927
830
|
const getBaseConstraints = () => {
|
|
928
831
|
const constraints: QueryConstraint[] = [];
|
|
929
|
-
|
|
832
|
+
|
|
930
833
|
// Active status filter
|
|
931
834
|
if (filters.isActive !== undefined) {
|
|
932
|
-
constraints.push(where(
|
|
835
|
+
constraints.push(where('isActive', '==', filters.isActive));
|
|
933
836
|
} else {
|
|
934
|
-
constraints.push(where(
|
|
837
|
+
constraints.push(where('isActive', '==', true));
|
|
935
838
|
}
|
|
936
839
|
|
|
937
840
|
// Filter constraints
|
|
938
841
|
if (filters.procedureFamily) {
|
|
939
|
-
constraints.push(where(
|
|
842
|
+
constraints.push(where('family', '==', filters.procedureFamily));
|
|
940
843
|
}
|
|
941
844
|
if (filters.procedureCategory) {
|
|
942
|
-
constraints.push(where(
|
|
845
|
+
constraints.push(where('category.id', '==', filters.procedureCategory));
|
|
943
846
|
}
|
|
944
847
|
if (filters.procedureSubcategory) {
|
|
945
|
-
constraints.push(where(
|
|
848
|
+
constraints.push(where('subcategory.id', '==', filters.procedureSubcategory));
|
|
946
849
|
}
|
|
947
850
|
if (filters.procedureTechnology) {
|
|
948
|
-
constraints.push(where(
|
|
851
|
+
constraints.push(where('technology.id', '==', filters.procedureTechnology));
|
|
949
852
|
}
|
|
950
853
|
if (filters.minPrice !== undefined) {
|
|
951
|
-
constraints.push(where(
|
|
854
|
+
constraints.push(where('price', '>=', filters.minPrice));
|
|
952
855
|
}
|
|
953
856
|
if (filters.maxPrice !== undefined) {
|
|
954
|
-
constraints.push(where(
|
|
857
|
+
constraints.push(where('price', '<=', filters.maxPrice));
|
|
955
858
|
}
|
|
956
859
|
if (filters.minRating !== undefined) {
|
|
957
|
-
constraints.push(where(
|
|
860
|
+
constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
|
|
958
861
|
}
|
|
959
862
|
if (filters.maxRating !== undefined) {
|
|
960
|
-
constraints.push(where(
|
|
863
|
+
constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
|
|
961
864
|
}
|
|
962
865
|
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
963
866
|
const benefitsToMatch = filters.treatmentBenefits;
|
|
964
|
-
constraints.push(where(
|
|
867
|
+
constraints.push(where('treatmentBenefits', 'array-contains-any', benefitsToMatch));
|
|
965
868
|
}
|
|
966
869
|
|
|
967
870
|
return constraints;
|
|
@@ -970,15 +873,15 @@ export class ProcedureService extends BaseService {
|
|
|
970
873
|
// Strategy 1: Try nameLower search if nameSearch exists
|
|
971
874
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
972
875
|
try {
|
|
973
|
-
console.log(
|
|
876
|
+
console.log('[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search');
|
|
974
877
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
975
878
|
const constraints = getBaseConstraints();
|
|
976
|
-
constraints.push(where(
|
|
977
|
-
constraints.push(where(
|
|
978
|
-
constraints.push(orderBy(
|
|
879
|
+
constraints.push(where('nameLower', '>=', searchTerm));
|
|
880
|
+
constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
|
|
881
|
+
constraints.push(orderBy('nameLower'));
|
|
979
882
|
|
|
980
883
|
if (filters.lastDoc) {
|
|
981
|
-
if (typeof filters.lastDoc.data ===
|
|
884
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
982
885
|
constraints.push(startAfter(filters.lastDoc));
|
|
983
886
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
984
887
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -990,33 +893,38 @@ export class ProcedureService extends BaseService {
|
|
|
990
893
|
|
|
991
894
|
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
992
895
|
const querySnapshot = await getDocs(q);
|
|
993
|
-
const procedures = querySnapshot.docs.map(
|
|
994
|
-
|
|
995
|
-
|
|
896
|
+
const procedures = querySnapshot.docs.map(
|
|
897
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
898
|
+
);
|
|
899
|
+
const lastDoc =
|
|
900
|
+
querySnapshot.docs.length > 0
|
|
901
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
902
|
+
: null;
|
|
903
|
+
|
|
996
904
|
console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
|
|
997
|
-
|
|
905
|
+
|
|
998
906
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
999
907
|
if (procedures.length < (filters.pagination || 10)) {
|
|
1000
908
|
return { procedures, lastDoc: null };
|
|
1001
909
|
}
|
|
1002
910
|
return { procedures, lastDoc };
|
|
1003
911
|
} catch (error) {
|
|
1004
|
-
console.log(
|
|
912
|
+
console.log('[PROCEDURE_SERVICE] Strategy 1 failed:', error);
|
|
1005
913
|
}
|
|
1006
914
|
}
|
|
1007
915
|
|
|
1008
916
|
// Strategy 2: Try name field search as fallback
|
|
1009
917
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1010
918
|
try {
|
|
1011
|
-
console.log(
|
|
919
|
+
console.log('[PROCEDURE_SERVICE] Strategy 2: Trying name field search');
|
|
1012
920
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1013
921
|
const constraints = getBaseConstraints();
|
|
1014
|
-
constraints.push(where(
|
|
1015
|
-
constraints.push(where(
|
|
1016
|
-
constraints.push(orderBy(
|
|
922
|
+
constraints.push(where('name', '>=', searchTerm));
|
|
923
|
+
constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
|
|
924
|
+
constraints.push(orderBy('name'));
|
|
1017
925
|
|
|
1018
926
|
if (filters.lastDoc) {
|
|
1019
|
-
if (typeof filters.lastDoc.data ===
|
|
927
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
1020
928
|
constraints.push(startAfter(filters.lastDoc));
|
|
1021
929
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
1022
930
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -1028,29 +936,36 @@ export class ProcedureService extends BaseService {
|
|
|
1028
936
|
|
|
1029
937
|
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1030
938
|
const querySnapshot = await getDocs(q);
|
|
1031
|
-
const procedures = querySnapshot.docs.map(
|
|
1032
|
-
|
|
1033
|
-
|
|
939
|
+
const procedures = querySnapshot.docs.map(
|
|
940
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
941
|
+
);
|
|
942
|
+
const lastDoc =
|
|
943
|
+
querySnapshot.docs.length > 0
|
|
944
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
945
|
+
: null;
|
|
946
|
+
|
|
1034
947
|
console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
|
|
1035
|
-
|
|
948
|
+
|
|
1036
949
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1037
950
|
if (procedures.length < (filters.pagination || 10)) {
|
|
1038
951
|
return { procedures, lastDoc: null };
|
|
1039
952
|
}
|
|
1040
953
|
return { procedures, lastDoc };
|
|
1041
954
|
} catch (error) {
|
|
1042
|
-
console.log(
|
|
955
|
+
console.log('[PROCEDURE_SERVICE] Strategy 2 failed:', error);
|
|
1043
956
|
}
|
|
1044
957
|
}
|
|
1045
958
|
|
|
1046
959
|
// Strategy 3: orderBy createdAt with client-side filtering
|
|
1047
960
|
try {
|
|
1048
|
-
console.log(
|
|
961
|
+
console.log(
|
|
962
|
+
'[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering',
|
|
963
|
+
);
|
|
1049
964
|
const constraints = getBaseConstraints();
|
|
1050
|
-
constraints.push(orderBy(
|
|
965
|
+
constraints.push(orderBy('createdAt', 'desc'));
|
|
1051
966
|
|
|
1052
967
|
if (filters.lastDoc) {
|
|
1053
|
-
if (typeof filters.lastDoc.data ===
|
|
968
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
1054
969
|
constraints.push(startAfter(filters.lastDoc));
|
|
1055
970
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
1056
971
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -1062,57 +977,62 @@ export class ProcedureService extends BaseService {
|
|
|
1062
977
|
|
|
1063
978
|
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1064
979
|
const querySnapshot = await getDocs(q);
|
|
1065
|
-
let procedures = querySnapshot.docs.map(
|
|
980
|
+
let procedures = querySnapshot.docs.map(
|
|
981
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
982
|
+
);
|
|
1066
983
|
|
|
1067
984
|
// Apply all client-side filters using centralized function
|
|
1068
985
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1069
986
|
|
|
1070
|
-
const lastDoc =
|
|
987
|
+
const lastDoc =
|
|
988
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1071
989
|
console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
|
|
1072
|
-
|
|
990
|
+
|
|
1073
991
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1074
992
|
if (procedures.length < (filters.pagination || 10)) {
|
|
1075
993
|
return { procedures, lastDoc: null };
|
|
1076
994
|
}
|
|
1077
995
|
return { procedures, lastDoc };
|
|
1078
996
|
} catch (error) {
|
|
1079
|
-
console.log(
|
|
997
|
+
console.log('[PROCEDURE_SERVICE] Strategy 3 failed:', error);
|
|
1080
998
|
}
|
|
1081
999
|
|
|
1082
1000
|
// Strategy 4: Minimal query fallback
|
|
1083
1001
|
try {
|
|
1084
|
-
console.log(
|
|
1002
|
+
console.log('[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback');
|
|
1085
1003
|
const constraints: QueryConstraint[] = [
|
|
1086
|
-
where(
|
|
1087
|
-
orderBy(
|
|
1088
|
-
limit(filters.pagination || 10)
|
|
1004
|
+
where('isActive', '==', true),
|
|
1005
|
+
orderBy('createdAt', 'desc'),
|
|
1006
|
+
limit(filters.pagination || 10),
|
|
1089
1007
|
];
|
|
1090
1008
|
|
|
1091
1009
|
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1092
1010
|
const querySnapshot = await getDocs(q);
|
|
1093
|
-
let procedures = querySnapshot.docs.map(
|
|
1011
|
+
let procedures = querySnapshot.docs.map(
|
|
1012
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1013
|
+
);
|
|
1094
1014
|
|
|
1095
1015
|
// Apply all client-side filters using centralized function
|
|
1096
1016
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1097
1017
|
|
|
1098
|
-
const lastDoc =
|
|
1018
|
+
const lastDoc =
|
|
1019
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1099
1020
|
console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
|
|
1100
|
-
|
|
1021
|
+
|
|
1101
1022
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1102
1023
|
if (procedures.length < (filters.pagination || 10)) {
|
|
1103
1024
|
return { procedures, lastDoc: null };
|
|
1104
1025
|
}
|
|
1105
1026
|
return { procedures, lastDoc };
|
|
1106
1027
|
} catch (error) {
|
|
1107
|
-
console.log(
|
|
1028
|
+
console.log('[PROCEDURE_SERVICE] Strategy 4 failed:', error);
|
|
1108
1029
|
}
|
|
1109
1030
|
|
|
1110
1031
|
// All strategies failed
|
|
1111
|
-
console.log(
|
|
1032
|
+
console.log('[PROCEDURE_SERVICE] All strategies failed, returning empty result');
|
|
1112
1033
|
return { procedures: [], lastDoc: null };
|
|
1113
|
-
|
|
1114
1034
|
} catch (error) {
|
|
1115
|
-
console.error(
|
|
1035
|
+
console.error('[PROCEDURE_SERVICE] Error filtering procedures:', error);
|
|
1116
1036
|
return { procedures: [], lastDoc: null };
|
|
1117
1037
|
}
|
|
1118
1038
|
}
|
|
@@ -1121,13 +1041,16 @@ export class ProcedureService extends BaseService {
|
|
|
1121
1041
|
* Applies in-memory filters to procedures array
|
|
1122
1042
|
* Used when Firestore queries fail or for complex filtering
|
|
1123
1043
|
*/
|
|
1124
|
-
private applyInMemoryFilters(
|
|
1044
|
+
private applyInMemoryFilters(
|
|
1045
|
+
procedures: Procedure[],
|
|
1046
|
+
filters: any,
|
|
1047
|
+
): (Procedure & { distance?: number })[] {
|
|
1125
1048
|
let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
|
|
1126
1049
|
|
|
1127
1050
|
// Name search filter
|
|
1128
1051
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1129
1052
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1130
|
-
filteredProcedures = filteredProcedures.filter(procedure => {
|
|
1053
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1131
1054
|
const name = (procedure.name || '').toLowerCase();
|
|
1132
1055
|
const nameLower = procedure.nameLower || '';
|
|
1133
1056
|
return name.includes(searchTerm) || nameLower.includes(searchTerm);
|
|
@@ -1137,82 +1060,105 @@ export class ProcedureService extends BaseService {
|
|
|
1137
1060
|
|
|
1138
1061
|
// Price filtering
|
|
1139
1062
|
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
|
|
1140
|
-
filteredProcedures = filteredProcedures.filter(procedure => {
|
|
1063
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1141
1064
|
const price = procedure.price || 0;
|
|
1142
1065
|
if (filters.minPrice !== undefined && price < filters.minPrice) return false;
|
|
1143
1066
|
if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
|
|
1144
1067
|
return true;
|
|
1145
1068
|
});
|
|
1146
|
-
console.log(
|
|
1069
|
+
console.log(
|
|
1070
|
+
`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`,
|
|
1071
|
+
);
|
|
1147
1072
|
}
|
|
1148
1073
|
|
|
1149
1074
|
// Rating filtering
|
|
1150
1075
|
if (filters.minRating !== undefined || filters.maxRating !== undefined) {
|
|
1151
|
-
filteredProcedures = filteredProcedures.filter(procedure => {
|
|
1076
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1152
1077
|
const rating = procedure.reviewInfo?.averageRating || 0;
|
|
1153
1078
|
if (filters.minRating !== undefined && rating < filters.minRating) return false;
|
|
1154
1079
|
if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
|
|
1155
1080
|
return true;
|
|
1156
1081
|
});
|
|
1157
|
-
console.log(
|
|
1082
|
+
console.log(
|
|
1083
|
+
`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`,
|
|
1084
|
+
);
|
|
1158
1085
|
}
|
|
1159
1086
|
|
|
1160
1087
|
// Treatment benefits filtering
|
|
1161
1088
|
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
1162
1089
|
const benefitsToMatch = filters.treatmentBenefits;
|
|
1163
|
-
filteredProcedures = filteredProcedures.filter(procedure => {
|
|
1090
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1164
1091
|
const procedureBenefits = procedure.treatmentBenefits || [];
|
|
1165
1092
|
return benefitsToMatch.some((benefit: any) => procedureBenefits.includes(benefit));
|
|
1166
1093
|
});
|
|
1167
|
-
console.log(
|
|
1094
|
+
console.log(
|
|
1095
|
+
`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`,
|
|
1096
|
+
);
|
|
1168
1097
|
}
|
|
1169
1098
|
|
|
1170
1099
|
// Procedure family filtering
|
|
1171
1100
|
if (filters.procedureFamily) {
|
|
1172
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1173
|
-
|
|
1101
|
+
filteredProcedures = filteredProcedures.filter(
|
|
1102
|
+
(procedure) => procedure.family === filters.procedureFamily,
|
|
1103
|
+
);
|
|
1104
|
+
console.log(
|
|
1105
|
+
`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`,
|
|
1106
|
+
);
|
|
1174
1107
|
}
|
|
1175
1108
|
|
|
1176
1109
|
// Category filtering
|
|
1177
1110
|
if (filters.procedureCategory) {
|
|
1178
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1179
|
-
|
|
1111
|
+
filteredProcedures = filteredProcedures.filter(
|
|
1112
|
+
(procedure) => procedure.category?.id === filters.procedureCategory,
|
|
1113
|
+
);
|
|
1114
|
+
console.log(
|
|
1115
|
+
`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`,
|
|
1116
|
+
);
|
|
1180
1117
|
}
|
|
1181
1118
|
|
|
1182
1119
|
// Subcategory filtering
|
|
1183
1120
|
if (filters.procedureSubcategory) {
|
|
1184
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1185
|
-
|
|
1121
|
+
filteredProcedures = filteredProcedures.filter(
|
|
1122
|
+
(procedure) => procedure.subcategory?.id === filters.procedureSubcategory,
|
|
1123
|
+
);
|
|
1124
|
+
console.log(
|
|
1125
|
+
`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`,
|
|
1126
|
+
);
|
|
1186
1127
|
}
|
|
1187
1128
|
|
|
1188
1129
|
// Technology filtering
|
|
1189
1130
|
if (filters.procedureTechnology) {
|
|
1190
|
-
filteredProcedures = filteredProcedures.filter(
|
|
1191
|
-
|
|
1131
|
+
filteredProcedures = filteredProcedures.filter(
|
|
1132
|
+
(procedure) => procedure.technology?.id === filters.procedureTechnology,
|
|
1133
|
+
);
|
|
1134
|
+
console.log(
|
|
1135
|
+
`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`,
|
|
1136
|
+
);
|
|
1192
1137
|
}
|
|
1193
1138
|
|
|
1194
1139
|
// Geo-radius filter
|
|
1195
1140
|
if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
|
|
1196
1141
|
const location = filters.location;
|
|
1197
1142
|
const radiusInKm = filters.radiusInKm;
|
|
1198
|
-
filteredProcedures = filteredProcedures.filter(procedure => {
|
|
1143
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1199
1144
|
const clinicLocation = procedure.clinicInfo?.location;
|
|
1200
1145
|
if (!clinicLocation?.latitude || !clinicLocation?.longitude) {
|
|
1201
1146
|
return false;
|
|
1202
1147
|
}
|
|
1203
|
-
|
|
1204
|
-
const distance =
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1148
|
+
|
|
1149
|
+
const distance =
|
|
1150
|
+
distanceBetween(
|
|
1151
|
+
[location.latitude, location.longitude],
|
|
1152
|
+
[clinicLocation.latitude, clinicLocation.longitude],
|
|
1153
|
+
) / 1000; // Convert to km
|
|
1154
|
+
|
|
1209
1155
|
// Attach distance for frontend sorting/display
|
|
1210
1156
|
(procedure as any).distance = distance;
|
|
1211
|
-
|
|
1157
|
+
|
|
1212
1158
|
return distance <= radiusInKm;
|
|
1213
1159
|
});
|
|
1214
1160
|
console.log(`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`);
|
|
1215
|
-
|
|
1161
|
+
|
|
1216
1162
|
// Sort by distance when geo filtering is applied
|
|
1217
1163
|
filteredProcedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
|
|
1218
1164
|
}
|
|
@@ -1220,45 +1166,69 @@ export class ProcedureService extends BaseService {
|
|
|
1220
1166
|
return filteredProcedures as (Procedure & { distance?: number })[];
|
|
1221
1167
|
}
|
|
1222
1168
|
|
|
1223
|
-
private handleGeoQuery(
|
|
1224
|
-
|
|
1225
|
-
|
|
1169
|
+
private handleGeoQuery(
|
|
1170
|
+
filters: any,
|
|
1171
|
+
): Promise<{ procedures: (Procedure & { distance?: number })[]; lastDoc: any }> {
|
|
1172
|
+
console.log('[PROCEDURE_SERVICE] Executing geo query with geohash bounds');
|
|
1226
1173
|
try {
|
|
1227
|
-
// Enhanced geo query implementation with proper debugging
|
|
1228
1174
|
const location = filters.location;
|
|
1229
1175
|
const radiusInKm = filters.radiusInKm;
|
|
1230
|
-
|
|
1231
|
-
console.log('[PROCEDURE_SERVICE] Geo query parameters:', {
|
|
1232
|
-
latitude: location.latitude,
|
|
1233
|
-
longitude: location.longitude,
|
|
1234
|
-
radiusInKm: radiusInKm,
|
|
1235
|
-
pagination: filters.pagination || 10
|
|
1236
|
-
});
|
|
1237
1176
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
// Apply all filters using centralized function (includes geo filtering)
|
|
1252
|
-
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1253
|
-
|
|
1254
|
-
console.log(`[PROCEDURE_SERVICE] Geo query success: ${procedures.length} procedures within ${radiusInKm}km`);
|
|
1255
|
-
|
|
1256
|
-
// Fix Load More for geo queries
|
|
1257
|
-
const lastDoc = procedures.length < (filters.pagination || 10) ? null : querySnapshot.docs[querySnapshot.docs.length - 1];
|
|
1258
|
-
|
|
1259
|
-
return { procedures, lastDoc };
|
|
1177
|
+
if (!location || !radiusInKm) {
|
|
1178
|
+
return Promise.resolve({ procedures: [], lastDoc: null });
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const bounds = geohashQueryBounds([location.latitude, location.longitude], radiusInKm * 1000);
|
|
1182
|
+
|
|
1183
|
+
const fetches = bounds.map((b) => {
|
|
1184
|
+
const constraints: QueryConstraint[] = [
|
|
1185
|
+
where('clinicInfo.location.geohash', '>=', b[0]),
|
|
1186
|
+
where('clinicInfo.location.geohash', '<=', b[1]),
|
|
1187
|
+
where('isActive', '==', filters.isActive !== undefined ? filters.isActive : true),
|
|
1188
|
+
];
|
|
1189
|
+
return getDocs(query(collection(this.db, PROCEDURES_COLLECTION), ...constraints));
|
|
1260
1190
|
});
|
|
1261
|
-
|
|
1191
|
+
|
|
1192
|
+
return Promise.all(fetches)
|
|
1193
|
+
.then((snaps) => {
|
|
1194
|
+
const collected: Procedure[] = [];
|
|
1195
|
+
snaps.forEach((snap) => {
|
|
1196
|
+
snap.docs.forEach((d) => collected.push({ ...(d.data() as Procedure), id: d.id }));
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Deduplicate by id
|
|
1200
|
+
const uniqueMap = new Map<string, Procedure>();
|
|
1201
|
+
for (const p of collected) {
|
|
1202
|
+
uniqueMap.set(p.id, p);
|
|
1203
|
+
}
|
|
1204
|
+
let procedures = Array.from(uniqueMap.values());
|
|
1205
|
+
|
|
1206
|
+
// Apply remaining filters including precise distance and sorting
|
|
1207
|
+
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1208
|
+
|
|
1209
|
+
// Manual pagination
|
|
1210
|
+
const pageSize = filters.pagination || 10;
|
|
1211
|
+
let startIndex = 0;
|
|
1212
|
+
if (
|
|
1213
|
+
filters.lastDoc &&
|
|
1214
|
+
typeof filters.lastDoc === 'object' &&
|
|
1215
|
+
(filters.lastDoc as any).id
|
|
1216
|
+
) {
|
|
1217
|
+
const idx = procedures.findIndex((p) => p.id === (filters.lastDoc as any).id);
|
|
1218
|
+
if (idx >= 0) startIndex = idx + 1;
|
|
1219
|
+
}
|
|
1220
|
+
const page = procedures.slice(startIndex, startIndex + pageSize);
|
|
1221
|
+
const newLastDoc = page.length === pageSize ? page[page.length - 1] : null;
|
|
1222
|
+
|
|
1223
|
+
console.log(
|
|
1224
|
+
`[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`,
|
|
1225
|
+
);
|
|
1226
|
+
return { procedures: page, lastDoc: newLastDoc };
|
|
1227
|
+
})
|
|
1228
|
+
.catch((err) => {
|
|
1229
|
+
console.error('[PROCEDURE_SERVICE] Geo bounds fetch failed:', err);
|
|
1230
|
+
return { procedures: [], lastDoc: null };
|
|
1231
|
+
});
|
|
1262
1232
|
} catch (error) {
|
|
1263
1233
|
console.error('[PROCEDURE_SERVICE] Geo query failed:', error);
|
|
1264
1234
|
return Promise.resolve({ procedures: [], lastDoc: null });
|
|
@@ -1272,7 +1242,7 @@ export class ProcedureService extends BaseService {
|
|
|
1272
1242
|
* @returns The created procedure
|
|
1273
1243
|
*/
|
|
1274
1244
|
async createConsultationProcedure(
|
|
1275
|
-
data: Omit<CreateProcedureData,
|
|
1245
|
+
data: Omit<CreateProcedureData, 'productId'>,
|
|
1276
1246
|
): Promise<Procedure> {
|
|
1277
1247
|
// Generate procedure ID first so we can use it for media uploads
|
|
1278
1248
|
const procedureId = this.generateId();
|
|
@@ -1286,7 +1256,7 @@ export class ProcedureService extends BaseService {
|
|
|
1286
1256
|
]);
|
|
1287
1257
|
|
|
1288
1258
|
if (!category || !subcategory || !technology) {
|
|
1289
|
-
throw new Error(
|
|
1259
|
+
throw new Error('One or more required base entities not found');
|
|
1290
1260
|
}
|
|
1291
1261
|
|
|
1292
1262
|
// Get clinic and practitioner information for aggregation
|
|
@@ -1297,11 +1267,7 @@ export class ProcedureService extends BaseService {
|
|
|
1297
1267
|
}
|
|
1298
1268
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
1299
1269
|
|
|
1300
|
-
const practitionerRef = doc(
|
|
1301
|
-
this.db,
|
|
1302
|
-
PRACTITIONERS_COLLECTION,
|
|
1303
|
-
data.practitionerId
|
|
1304
|
-
);
|
|
1270
|
+
const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, data.practitionerId);
|
|
1305
1271
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
1306
1272
|
if (!practitionerSnapshot.exists()) {
|
|
1307
1273
|
throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
|
|
@@ -1311,26 +1277,22 @@ export class ProcedureService extends BaseService {
|
|
|
1311
1277
|
// Process photos if provided
|
|
1312
1278
|
let processedPhotos: string[] = [];
|
|
1313
1279
|
if (data.photos && data.photos.length > 0) {
|
|
1314
|
-
processedPhotos = await this.processMediaArray(
|
|
1315
|
-
data.photos,
|
|
1316
|
-
procedureId,
|
|
1317
|
-
"procedure-photos"
|
|
1318
|
-
);
|
|
1280
|
+
processedPhotos = await this.processMediaArray(data.photos, procedureId, 'procedure-photos');
|
|
1319
1281
|
}
|
|
1320
1282
|
|
|
1321
1283
|
// Create aggregated clinic info for the procedure document
|
|
1322
1284
|
const clinicInfo = {
|
|
1323
1285
|
id: clinicSnapshot.id,
|
|
1324
1286
|
name: clinic.name,
|
|
1325
|
-
description: clinic.description ||
|
|
1287
|
+
description: clinic.description || '',
|
|
1326
1288
|
featuredPhoto:
|
|
1327
1289
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
1328
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
1290
|
+
? typeof clinic.featuredPhotos[0] === 'string'
|
|
1329
1291
|
? clinic.featuredPhotos[0]
|
|
1330
|
-
:
|
|
1331
|
-
: typeof clinic.coverPhoto ===
|
|
1292
|
+
: ''
|
|
1293
|
+
: typeof clinic.coverPhoto === 'string'
|
|
1332
1294
|
? clinic.coverPhoto
|
|
1333
|
-
:
|
|
1295
|
+
: '',
|
|
1334
1296
|
location: clinic.location,
|
|
1335
1297
|
contactInfo: clinic.contactInfo,
|
|
1336
1298
|
};
|
|
@@ -1339,22 +1301,22 @@ export class ProcedureService extends BaseService {
|
|
|
1339
1301
|
const doctorInfo = {
|
|
1340
1302
|
id: practitionerSnapshot.id,
|
|
1341
1303
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
1342
|
-
description: practitioner.basicInfo.bio ||
|
|
1304
|
+
description: practitioner.basicInfo.bio || '',
|
|
1343
1305
|
photo:
|
|
1344
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
1306
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
1345
1307
|
? practitioner.basicInfo.profileImageUrl
|
|
1346
|
-
:
|
|
1308
|
+
: '',
|
|
1347
1309
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
1348
1310
|
services: practitioner.procedures || [],
|
|
1349
1311
|
};
|
|
1350
1312
|
|
|
1351
1313
|
// Create a placeholder product for consultation procedures
|
|
1352
1314
|
const consultationProduct: Product = {
|
|
1353
|
-
id:
|
|
1354
|
-
name:
|
|
1355
|
-
description:
|
|
1356
|
-
brandId:
|
|
1357
|
-
brandName:
|
|
1315
|
+
id: 'consultation-no-product',
|
|
1316
|
+
name: 'No Product Required',
|
|
1317
|
+
description: 'Consultation procedures do not require specific products',
|
|
1318
|
+
brandId: 'consultation-brand',
|
|
1319
|
+
brandName: 'Consultation',
|
|
1358
1320
|
technologyId: data.technologyId,
|
|
1359
1321
|
technologyName: technology.name,
|
|
1360
1322
|
isActive: true,
|
|
@@ -1363,7 +1325,7 @@ export class ProcedureService extends BaseService {
|
|
|
1363
1325
|
};
|
|
1364
1326
|
|
|
1365
1327
|
// Create the procedure object
|
|
1366
|
-
const newProcedure: Omit<Procedure,
|
|
1328
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
1367
1329
|
id: procedureId,
|
|
1368
1330
|
...data,
|
|
1369
1331
|
nameLower: (data as any).nameLower || data.name.toLowerCase(),
|
|
@@ -1412,18 +1374,20 @@ export class ProcedureService extends BaseService {
|
|
|
1412
1374
|
* This is optimized for mobile map usage to reduce payload size.
|
|
1413
1375
|
* @returns Array of minimal procedure info for map
|
|
1414
1376
|
*/
|
|
1415
|
-
async getProceduresForMap(): Promise<
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1377
|
+
async getProceduresForMap(): Promise<
|
|
1378
|
+
{
|
|
1379
|
+
id: string;
|
|
1380
|
+
name: string;
|
|
1381
|
+
clinicId: string | undefined;
|
|
1382
|
+
clinicName: string | undefined;
|
|
1383
|
+
address: string;
|
|
1384
|
+
latitude: number | undefined;
|
|
1385
|
+
longitude: number | undefined;
|
|
1386
|
+
}[]
|
|
1387
|
+
> {
|
|
1424
1388
|
const proceduresRef = collection(this.db, PROCEDURES_COLLECTION);
|
|
1425
1389
|
const snapshot = await getDocs(proceduresRef);
|
|
1426
|
-
const proceduresForMap = snapshot.docs.map(doc => {
|
|
1390
|
+
const proceduresForMap = snapshot.docs.map((doc) => {
|
|
1427
1391
|
const data = doc.data();
|
|
1428
1392
|
return {
|
|
1429
1393
|
id: doc.id,
|