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