@blackcode_sa/metaestetics-api 1.11.1 → 1.11.2
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 +324 -330
- package/dist/admin/index.d.ts +324 -330
- package/dist/backoffice/index.d.mts +67 -283
- package/dist/backoffice/index.d.ts +67 -283
- package/dist/backoffice/index.js +6 -114
- package/dist/backoffice/index.mjs +6 -112
- package/dist/index.d.mts +3037 -3100
- package/dist/index.d.ts +3037 -3100
- package/dist/index.js +129 -379
- package/dist/index.mjs +130 -379
- package/package.json +1 -1
- package/src/backoffice/expo-safe/index.ts +0 -2
- package/src/backoffice/services/__tests__/brand.service.test.ts +196 -0
- package/src/backoffice/services/__tests__/category.service.test.ts +201 -0
- package/src/backoffice/services/__tests__/product.service.test.ts +358 -0
- package/src/backoffice/services/__tests__/requirement.service.test.ts +226 -0
- package/src/backoffice/services/__tests__/subcategory.service.test.ts +181 -0
- package/src/backoffice/services/__tests__/technology.service.test.ts +1097 -0
- package/src/backoffice/services/technology.service.ts +10 -122
- package/src/backoffice/types/index.ts +0 -1
- package/src/backoffice/types/product.types.ts +1 -3
- package/src/backoffice/types/technology.types.ts +4 -4
- package/src/backoffice/validations/schemas.ts +9 -35
- package/src/services/appointment/appointment.service.ts +5 -0
- package/src/services/appointment/utils/appointment.utils.ts +113 -124
- package/src/services/procedure/procedure.service.ts +234 -434
- package/src/types/appointment/index.ts +37 -43
- package/src/types/clinic/index.ts +6 -1
- package/src/types/patient/medical-info.types.ts +3 -3
- package/src/types/procedure/index.ts +17 -20
- package/src/validations/appointment.schema.ts +118 -170
- package/src/validations/clinic.schema.ts +6 -1
- package/src/validations/patient/medical-info.schema.ts +2 -7
- package/src/backoffice/services/README.md +0 -40
- package/src/backoffice/services/constants.service.ts +0 -268
- package/src/backoffice/types/admin-constants.types.ts +0 -69
|
@@ -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
|
-
|
|
69
|
-
} from
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
72
|
-
import {
|
|
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,22 +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
|
|
267
|
-
nameLower:
|
|
268
|
-
(validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
225
|
+
nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
269
226
|
photos: processedPhotos,
|
|
270
227
|
category, // Embed full objects
|
|
271
228
|
subcategory,
|
|
@@ -273,9 +230,7 @@ export class ProcedureService extends BaseService {
|
|
|
273
230
|
product,
|
|
274
231
|
blockingConditions: technology.blockingConditions,
|
|
275
232
|
contraindications: technology.contraindications || [],
|
|
276
|
-
contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
|
|
277
233
|
treatmentBenefits: technology.benefits,
|
|
278
|
-
treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
|
|
279
234
|
preRequirements: technology.requirements.pre,
|
|
280
235
|
postRequirements: technology.requirements.post,
|
|
281
236
|
certificationRequirement: technology.certificationRequirement,
|
|
@@ -318,12 +273,12 @@ export class ProcedureService extends BaseService {
|
|
|
318
273
|
* @returns A promise that resolves to an array of the newly created procedures.
|
|
319
274
|
*/
|
|
320
275
|
async bulkCreateProcedures(
|
|
321
|
-
baseData: Omit<CreateProcedureData,
|
|
322
|
-
practitionerIds: string[]
|
|
276
|
+
baseData: Omit<CreateProcedureData, 'practitionerId'>,
|
|
277
|
+
practitionerIds: string[],
|
|
323
278
|
): Promise<Procedure[]> {
|
|
324
279
|
// 1. Validation
|
|
325
280
|
if (!practitionerIds || practitionerIds.length === 0) {
|
|
326
|
-
throw new Error(
|
|
281
|
+
throw new Error('Practitioner IDs array cannot be empty.');
|
|
327
282
|
}
|
|
328
283
|
|
|
329
284
|
// Add a dummy practitionerId for the validation schema to pass
|
|
@@ -331,28 +286,19 @@ export class ProcedureService extends BaseService {
|
|
|
331
286
|
const validatedData = createProcedureSchema.parse(validationData);
|
|
332
287
|
|
|
333
288
|
// 2. Fetch common data once to avoid redundant reads
|
|
334
|
-
const [category, subcategory, technology, product, clinicSnapshot] =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
this.technologyService.getById(validatedData.technologyId),
|
|
342
|
-
this.productService.getById(
|
|
343
|
-
validatedData.technologyId,
|
|
344
|
-
validatedData.productId
|
|
345
|
-
),
|
|
346
|
-
getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
|
|
347
|
-
]);
|
|
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
|
+
]);
|
|
348
296
|
|
|
349
297
|
if (!category || !subcategory || !technology || !product) {
|
|
350
|
-
throw new Error(
|
|
298
|
+
throw new Error('One or more required base entities not found');
|
|
351
299
|
}
|
|
352
300
|
if (!clinicSnapshot.exists()) {
|
|
353
|
-
throw new Error(
|
|
354
|
-
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
355
|
-
);
|
|
301
|
+
throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
|
|
356
302
|
}
|
|
357
303
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
358
304
|
|
|
@@ -363,7 +309,7 @@ export class ProcedureService extends BaseService {
|
|
|
363
309
|
processedPhotos = await this.processMediaArray(
|
|
364
310
|
validatedData.photos,
|
|
365
311
|
batchId,
|
|
366
|
-
|
|
312
|
+
'procedure-photos-batch',
|
|
367
313
|
);
|
|
368
314
|
}
|
|
369
315
|
|
|
@@ -374,7 +320,7 @@ export class ProcedureService extends BaseService {
|
|
|
374
320
|
const chunk = practitionerIds.slice(i, i + 30);
|
|
375
321
|
const practitionersQuery = query(
|
|
376
322
|
collection(this.db, PRACTITIONERS_COLLECTION),
|
|
377
|
-
where(documentId(),
|
|
323
|
+
where(documentId(), 'in', chunk),
|
|
378
324
|
);
|
|
379
325
|
const practitionersSnapshot = await getDocs(practitionersQuery);
|
|
380
326
|
practitionersSnapshot.docs.forEach((doc) => {
|
|
@@ -385,12 +331,8 @@ export class ProcedureService extends BaseService {
|
|
|
385
331
|
// Verify all practitioners were found
|
|
386
332
|
if (practitionersMap.size !== practitionerIds.length) {
|
|
387
333
|
const foundIds = Array.from(practitionersMap.keys());
|
|
388
|
-
const notFoundIds = practitionerIds.filter(
|
|
389
|
-
|
|
390
|
-
);
|
|
391
|
-
throw new Error(
|
|
392
|
-
`The following practitioners were not found: ${notFoundIds.join(", ")}`
|
|
393
|
-
);
|
|
334
|
+
const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
|
|
335
|
+
throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
|
|
394
336
|
}
|
|
395
337
|
|
|
396
338
|
// 5. Use a Firestore batch for atomic creation
|
|
@@ -399,15 +341,15 @@ export class ProcedureService extends BaseService {
|
|
|
399
341
|
const clinicInfo = {
|
|
400
342
|
id: clinicSnapshot.id,
|
|
401
343
|
name: clinic.name,
|
|
402
|
-
description: clinic.description ||
|
|
344
|
+
description: clinic.description || '',
|
|
403
345
|
featuredPhoto:
|
|
404
346
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
405
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
347
|
+
? typeof clinic.featuredPhotos[0] === 'string'
|
|
406
348
|
? clinic.featuredPhotos[0]
|
|
407
|
-
:
|
|
408
|
-
: typeof clinic.coverPhoto ===
|
|
349
|
+
: ''
|
|
350
|
+
: typeof clinic.coverPhoto === 'string'
|
|
409
351
|
? clinic.coverPhoto
|
|
410
|
-
:
|
|
352
|
+
: '',
|
|
411
353
|
location: clinic.location,
|
|
412
354
|
contactInfo: clinic.contactInfo,
|
|
413
355
|
};
|
|
@@ -418,11 +360,11 @@ export class ProcedureService extends BaseService {
|
|
|
418
360
|
const doctorInfo = {
|
|
419
361
|
id: practitioner.id,
|
|
420
362
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
421
|
-
description: practitioner.basicInfo.bio ||
|
|
363
|
+
description: practitioner.basicInfo.bio || '',
|
|
422
364
|
photo:
|
|
423
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
365
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
424
366
|
? practitioner.basicInfo.profileImageUrl
|
|
425
|
-
:
|
|
367
|
+
: '',
|
|
426
368
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
427
369
|
services: practitioner.procedures || [],
|
|
428
370
|
};
|
|
@@ -432,11 +374,10 @@ export class ProcedureService extends BaseService {
|
|
|
432
374
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
|
|
433
375
|
|
|
434
376
|
// Construct the new procedure, reusing common data
|
|
435
|
-
const newProcedure: Omit<Procedure,
|
|
377
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
436
378
|
id: procedureId,
|
|
437
379
|
...validatedData,
|
|
438
|
-
nameLower:
|
|
439
|
-
(validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
380
|
+
nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
440
381
|
practitionerId: practitionerId, // Override practitionerId with the correct one
|
|
441
382
|
photos: processedPhotos,
|
|
442
383
|
category,
|
|
@@ -445,10 +386,7 @@ export class ProcedureService extends BaseService {
|
|
|
445
386
|
product,
|
|
446
387
|
blockingConditions: technology.blockingConditions,
|
|
447
388
|
contraindications: technology.contraindications || [],
|
|
448
|
-
contraindicationIds:
|
|
449
|
-
technology.contraindications?.map((c) => c.id) || [],
|
|
450
389
|
treatmentBenefits: technology.benefits,
|
|
451
|
-
treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
|
|
452
390
|
preRequirements: technology.requirements.pre,
|
|
453
391
|
postRequirements: technology.requirements.post,
|
|
454
392
|
certificationRequirement: technology.certificationRequirement,
|
|
@@ -482,10 +420,7 @@ export class ProcedureService extends BaseService {
|
|
|
482
420
|
const fetchedProcedures: Procedure[] = [];
|
|
483
421
|
for (let i = 0; i < createdProcedureIds.length; i += 30) {
|
|
484
422
|
const chunk = createdProcedureIds.slice(i, i + 30);
|
|
485
|
-
const q = query(
|
|
486
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
487
|
-
where(documentId(), "in", chunk)
|
|
488
|
-
);
|
|
423
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), where(documentId(), 'in', chunk));
|
|
489
424
|
const snapshot = await getDocs(q);
|
|
490
425
|
snapshot.forEach((doc) => {
|
|
491
426
|
fetchedProcedures.push(doc.data() as Procedure);
|
|
@@ -516,13 +451,11 @@ export class ProcedureService extends BaseService {
|
|
|
516
451
|
* @param clinicBranchId - The ID of the clinic branch
|
|
517
452
|
* @returns List of procedures
|
|
518
453
|
*/
|
|
519
|
-
async getProceduresByClinicBranch(
|
|
520
|
-
clinicBranchId: string
|
|
521
|
-
): Promise<Procedure[]> {
|
|
454
|
+
async getProceduresByClinicBranch(clinicBranchId: string): Promise<Procedure[]> {
|
|
522
455
|
const q = query(
|
|
523
456
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
524
|
-
where(
|
|
525
|
-
where(
|
|
457
|
+
where('clinicBranchId', '==', clinicBranchId),
|
|
458
|
+
where('isActive', '==', true),
|
|
526
459
|
);
|
|
527
460
|
const snapshot = await getDocs(q);
|
|
528
461
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -533,13 +466,11 @@ export class ProcedureService extends BaseService {
|
|
|
533
466
|
* @param practitionerId - The ID of the practitioner
|
|
534
467
|
* @returns List of procedures
|
|
535
468
|
*/
|
|
536
|
-
async getProceduresByPractitioner(
|
|
537
|
-
practitionerId: string
|
|
538
|
-
): Promise<Procedure[]> {
|
|
469
|
+
async getProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
|
|
539
470
|
const q = query(
|
|
540
471
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
541
|
-
where(
|
|
542
|
-
where(
|
|
472
|
+
where('practitionerId', '==', practitionerId),
|
|
473
|
+
where('isActive', '==', true),
|
|
543
474
|
);
|
|
544
475
|
const snapshot = await getDocs(q);
|
|
545
476
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -550,13 +481,11 @@ export class ProcedureService extends BaseService {
|
|
|
550
481
|
* @param practitionerId - The ID of the practitioner
|
|
551
482
|
* @returns List of inactive procedures
|
|
552
483
|
*/
|
|
553
|
-
async getInactiveProceduresByPractitioner(
|
|
554
|
-
practitionerId: string
|
|
555
|
-
): Promise<Procedure[]> {
|
|
484
|
+
async getInactiveProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
|
|
556
485
|
const q = query(
|
|
557
486
|
collection(this.db, PROCEDURES_COLLECTION),
|
|
558
|
-
where(
|
|
559
|
-
where(
|
|
487
|
+
where('practitionerId', '==', practitionerId),
|
|
488
|
+
where('isActive', '==', false),
|
|
560
489
|
);
|
|
561
490
|
const snapshot = await getDocs(q);
|
|
562
491
|
return snapshot.docs.map((doc) => doc.data() as Procedure);
|
|
@@ -568,10 +497,7 @@ export class ProcedureService extends BaseService {
|
|
|
568
497
|
* @param data - The data to update the procedure with
|
|
569
498
|
* @returns The updated procedure
|
|
570
499
|
*/
|
|
571
|
-
async updateProcedure(
|
|
572
|
-
id: string,
|
|
573
|
-
data: UpdateProcedureData
|
|
574
|
-
): Promise<Procedure> {
|
|
500
|
+
async updateProcedure(id: string, data: UpdateProcedureData): Promise<Procedure> {
|
|
575
501
|
const validatedData = updateProcedureSchema.parse(data);
|
|
576
502
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
577
503
|
const procedureSnapshot = await getDoc(procedureRef);
|
|
@@ -595,54 +521,42 @@ export class ProcedureService extends BaseService {
|
|
|
595
521
|
updatedProcedureData.photos = await this.processMediaArray(
|
|
596
522
|
validatedData.photos,
|
|
597
523
|
id,
|
|
598
|
-
|
|
524
|
+
'procedure-photos',
|
|
599
525
|
);
|
|
600
526
|
}
|
|
601
527
|
|
|
602
528
|
// --- Prepare updates and fetch new related data if IDs change ---
|
|
603
529
|
|
|
604
530
|
// Handle Practitioner Change
|
|
605
|
-
if (
|
|
606
|
-
validatedData.practitionerId &&
|
|
607
|
-
validatedData.practitionerId !== oldPractitionerId
|
|
608
|
-
) {
|
|
531
|
+
if (validatedData.practitionerId && validatedData.practitionerId !== oldPractitionerId) {
|
|
609
532
|
practitionerChanged = true;
|
|
610
533
|
const newPractitionerRef = doc(
|
|
611
534
|
this.db,
|
|
612
535
|
PRACTITIONERS_COLLECTION,
|
|
613
|
-
validatedData.practitionerId
|
|
536
|
+
validatedData.practitionerId,
|
|
614
537
|
);
|
|
615
538
|
const newPractitionerSnap = await getDoc(newPractitionerRef);
|
|
616
539
|
if (!newPractitionerSnap.exists())
|
|
617
|
-
throw new Error(
|
|
618
|
-
`New Practitioner ${validatedData.practitionerId} not found`
|
|
619
|
-
);
|
|
540
|
+
throw new Error(`New Practitioner ${validatedData.practitionerId} not found`);
|
|
620
541
|
newPractitioner = newPractitionerSnap.data() as Practitioner;
|
|
621
542
|
// Update doctorInfo within the procedure document
|
|
622
543
|
updatedProcedureData.doctorInfo = {
|
|
623
544
|
id: newPractitioner.id,
|
|
624
545
|
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
625
|
-
description: newPractitioner.basicInfo.bio ||
|
|
546
|
+
description: newPractitioner.basicInfo.bio || '',
|
|
626
547
|
photo:
|
|
627
|
-
typeof newPractitioner.basicInfo.profileImageUrl ===
|
|
548
|
+
typeof newPractitioner.basicInfo.profileImageUrl === 'string'
|
|
628
549
|
? newPractitioner.basicInfo.profileImageUrl
|
|
629
|
-
:
|
|
550
|
+
: '', // Default to empty string if not a processed URL
|
|
630
551
|
rating: newPractitioner.reviewInfo?.averageRating || 0,
|
|
631
552
|
services: newPractitioner.procedures || [],
|
|
632
553
|
};
|
|
633
554
|
}
|
|
634
555
|
|
|
635
556
|
// Handle Clinic Change
|
|
636
|
-
if (
|
|
637
|
-
validatedData.clinicBranchId &&
|
|
638
|
-
validatedData.clinicBranchId !== oldClinicId
|
|
639
|
-
) {
|
|
557
|
+
if (validatedData.clinicBranchId && validatedData.clinicBranchId !== oldClinicId) {
|
|
640
558
|
clinicChanged = true;
|
|
641
|
-
const newClinicRef = doc(
|
|
642
|
-
this.db,
|
|
643
|
-
CLINICS_COLLECTION,
|
|
644
|
-
validatedData.clinicBranchId
|
|
645
|
-
);
|
|
559
|
+
const newClinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
|
|
646
560
|
const newClinicSnap = await getDoc(newClinicRef);
|
|
647
561
|
if (!newClinicSnap.exists())
|
|
648
562
|
throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
|
|
@@ -651,15 +565,15 @@ export class ProcedureService extends BaseService {
|
|
|
651
565
|
updatedProcedureData.clinicInfo = {
|
|
652
566
|
id: newClinic.id,
|
|
653
567
|
name: newClinic.name,
|
|
654
|
-
description: newClinic.description ||
|
|
568
|
+
description: newClinic.description || '',
|
|
655
569
|
featuredPhoto:
|
|
656
570
|
newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
|
|
657
|
-
? typeof newClinic.featuredPhotos[0] ===
|
|
571
|
+
? typeof newClinic.featuredPhotos[0] === 'string'
|
|
658
572
|
? newClinic.featuredPhotos[0]
|
|
659
|
-
:
|
|
660
|
-
: typeof newClinic.coverPhoto ===
|
|
573
|
+
: ''
|
|
574
|
+
: typeof newClinic.coverPhoto === 'string'
|
|
661
575
|
? newClinic.coverPhoto
|
|
662
|
-
:
|
|
576
|
+
: '',
|
|
663
577
|
location: newClinic.location,
|
|
664
578
|
contactInfo: newClinic.contactInfo,
|
|
665
579
|
};
|
|
@@ -671,11 +585,8 @@ export class ProcedureService extends BaseService {
|
|
|
671
585
|
updatedProcedureData.nameLower = validatedData.name.toLowerCase();
|
|
672
586
|
}
|
|
673
587
|
if (validatedData.categoryId) {
|
|
674
|
-
const category = await this.categoryService.getById(
|
|
675
|
-
|
|
676
|
-
);
|
|
677
|
-
if (!category)
|
|
678
|
-
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`);
|
|
679
590
|
updatedProcedureData.category = category;
|
|
680
591
|
finalCategoryId = category.id; // Update finalCategoryId if category changed
|
|
681
592
|
}
|
|
@@ -684,58 +595,42 @@ export class ProcedureService extends BaseService {
|
|
|
684
595
|
if (validatedData.subcategoryId && finalCategoryId) {
|
|
685
596
|
const subcategory = await this.subcategoryService.getById(
|
|
686
597
|
finalCategoryId,
|
|
687
|
-
validatedData.subcategoryId
|
|
598
|
+
validatedData.subcategoryId,
|
|
688
599
|
);
|
|
689
600
|
if (!subcategory)
|
|
690
601
|
throw new Error(
|
|
691
|
-
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}
|
|
602
|
+
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`,
|
|
692
603
|
);
|
|
693
604
|
updatedProcedureData.subcategory = subcategory;
|
|
694
605
|
} else if (validatedData.subcategoryId) {
|
|
695
|
-
console.warn(
|
|
696
|
-
"Attempted to update subcategory without a valid categoryId"
|
|
697
|
-
);
|
|
606
|
+
console.warn('Attempted to update subcategory without a valid categoryId');
|
|
698
607
|
}
|
|
699
608
|
|
|
700
609
|
let finalTechnologyId = existingProcedure.technology.id;
|
|
701
610
|
if (validatedData.technologyId) {
|
|
702
|
-
const technology = await this.technologyService.getById(
|
|
703
|
-
|
|
704
|
-
);
|
|
705
|
-
if (!technology)
|
|
706
|
-
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`);
|
|
707
613
|
updatedProcedureData.technology = technology;
|
|
708
614
|
finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
|
|
709
615
|
// Update related fields derived from technology
|
|
710
616
|
updatedProcedureData.blockingConditions = technology.blockingConditions;
|
|
711
|
-
updatedProcedureData.contraindications =
|
|
712
|
-
technology.contraindications || [];
|
|
713
|
-
updatedProcedureData.contraindicationIds =
|
|
714
|
-
technology.contraindications?.map((c) => c.id) || [];
|
|
715
617
|
updatedProcedureData.treatmentBenefits = technology.benefits;
|
|
716
|
-
updatedProcedureData.treatmentBenefitIds =
|
|
717
|
-
technology.benefits?.map((b) => b.id) || [];
|
|
718
618
|
updatedProcedureData.preRequirements = technology.requirements.pre;
|
|
719
619
|
updatedProcedureData.postRequirements = technology.requirements.post;
|
|
720
|
-
updatedProcedureData.certificationRequirement =
|
|
721
|
-
|
|
722
|
-
updatedProcedureData.documentationTemplates =
|
|
723
|
-
technology.documentationTemplates || [];
|
|
620
|
+
updatedProcedureData.certificationRequirement = technology.certificationRequirement;
|
|
621
|
+
updatedProcedureData.documentationTemplates = technology.documentationTemplates || [];
|
|
724
622
|
}
|
|
725
623
|
|
|
726
624
|
// Only fetch product if its ID is provided AND we have a valid finalTechnologyId
|
|
727
625
|
if (validatedData.productId && finalTechnologyId) {
|
|
728
|
-
const product = await this.productService.getById(
|
|
729
|
-
finalTechnologyId,
|
|
730
|
-
validatedData.productId
|
|
731
|
-
);
|
|
626
|
+
const product = await this.productService.getById(finalTechnologyId, validatedData.productId);
|
|
732
627
|
if (!product)
|
|
733
628
|
throw new Error(
|
|
734
|
-
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}
|
|
629
|
+
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}`,
|
|
735
630
|
);
|
|
736
631
|
updatedProcedureData.product = product;
|
|
737
632
|
} else if (validatedData.productId) {
|
|
738
|
-
console.warn(
|
|
633
|
+
console.warn('Attempted to update product without a valid technologyId');
|
|
739
634
|
}
|
|
740
635
|
|
|
741
636
|
// Update the procedure document
|
|
@@ -819,7 +714,7 @@ export class ProcedureService extends BaseService {
|
|
|
819
714
|
*/
|
|
820
715
|
async getAllProcedures(
|
|
821
716
|
pagination?: number,
|
|
822
|
-
lastDoc?: any
|
|
717
|
+
lastDoc?: any,
|
|
823
718
|
): Promise<{ procedures: Procedure[]; lastDoc: any }> {
|
|
824
719
|
try {
|
|
825
720
|
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
@@ -827,29 +722,24 @@ export class ProcedureService extends BaseService {
|
|
|
827
722
|
|
|
828
723
|
// Apply pagination if specified
|
|
829
724
|
if (pagination && pagination > 0) {
|
|
830
|
-
const { limit, startAfter } = await import(
|
|
725
|
+
const { limit, startAfter } = await import('firebase/firestore'); // Use dynamic import if needed top-level
|
|
831
726
|
|
|
832
727
|
if (lastDoc) {
|
|
833
728
|
proceduresQuery = query(
|
|
834
729
|
proceduresCollection,
|
|
835
|
-
orderBy(
|
|
730
|
+
orderBy('name'), // Use imported orderBy
|
|
836
731
|
startAfter(lastDoc),
|
|
837
|
-
limit(pagination)
|
|
732
|
+
limit(pagination),
|
|
838
733
|
);
|
|
839
734
|
} else {
|
|
840
|
-
proceduresQuery = query(
|
|
841
|
-
proceduresCollection,
|
|
842
|
-
orderBy("name"),
|
|
843
|
-
limit(pagination)
|
|
844
|
-
); // Use imported orderBy
|
|
735
|
+
proceduresQuery = query(proceduresCollection, orderBy('name'), limit(pagination)); // Use imported orderBy
|
|
845
736
|
}
|
|
846
737
|
} else {
|
|
847
|
-
proceduresQuery = query(proceduresCollection, orderBy(
|
|
738
|
+
proceduresQuery = query(proceduresCollection, orderBy('name')); // Use imported orderBy
|
|
848
739
|
}
|
|
849
740
|
|
|
850
741
|
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
851
|
-
const lastVisible =
|
|
852
|
-
proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
742
|
+
const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
853
743
|
|
|
854
744
|
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
855
745
|
const data = doc.data() as Procedure;
|
|
@@ -864,7 +754,7 @@ export class ProcedureService extends BaseService {
|
|
|
864
754
|
lastDoc: lastVisible,
|
|
865
755
|
};
|
|
866
756
|
} catch (error) {
|
|
867
|
-
console.error(
|
|
757
|
+
console.error('[PROCEDURE_SERVICE] Error getting all procedures:', error);
|
|
868
758
|
throw error;
|
|
869
759
|
}
|
|
870
760
|
}
|
|
@@ -876,7 +766,7 @@ export class ProcedureService extends BaseService {
|
|
|
876
766
|
*
|
|
877
767
|
* @param filters - Various filters to apply
|
|
878
768
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
879
|
-
* @param filters.
|
|
769
|
+
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
880
770
|
* @param filters.procedureFamily - Optional procedure family to filter by
|
|
881
771
|
* @param filters.procedureCategory - Optional procedure category to filter by
|
|
882
772
|
* @param filters.procedureSubcategory - Optional procedure subcategory to filter by
|
|
@@ -894,7 +784,7 @@ export class ProcedureService extends BaseService {
|
|
|
894
784
|
*/
|
|
895
785
|
async getProceduresByFilters(filters: {
|
|
896
786
|
nameSearch?: string;
|
|
897
|
-
treatmentBenefits?:
|
|
787
|
+
treatmentBenefits?: TreatmentBenefit[];
|
|
898
788
|
procedureFamily?: ProcedureFamily;
|
|
899
789
|
procedureCategory?: string;
|
|
900
790
|
procedureSubcategory?: string;
|
|
@@ -913,32 +803,26 @@ export class ProcedureService extends BaseService {
|
|
|
913
803
|
lastDoc: any;
|
|
914
804
|
}> {
|
|
915
805
|
try {
|
|
916
|
-
console.log(
|
|
917
|
-
"[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies"
|
|
918
|
-
);
|
|
806
|
+
console.log('[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies');
|
|
919
807
|
|
|
920
808
|
// Geo query debug i validacija
|
|
921
809
|
if (filters.location && filters.radiusInKm) {
|
|
922
|
-
console.log(
|
|
810
|
+
console.log('[PROCEDURE_SERVICE] Executing geo query:', {
|
|
923
811
|
location: filters.location,
|
|
924
812
|
radius: filters.radiusInKm,
|
|
925
|
-
serviceName:
|
|
813
|
+
serviceName: 'ProcedureService',
|
|
926
814
|
});
|
|
927
815
|
|
|
928
816
|
// Validacija location podataka
|
|
929
817
|
if (!filters.location.latitude || !filters.location.longitude) {
|
|
930
|
-
console.warn(
|
|
931
|
-
"[PROCEDURE_SERVICE] Invalid location data:",
|
|
932
|
-
filters.location
|
|
933
|
-
);
|
|
818
|
+
console.warn('[PROCEDURE_SERVICE] Invalid location data:', filters.location);
|
|
934
819
|
filters.location = undefined;
|
|
935
820
|
filters.radiusInKm = undefined;
|
|
936
821
|
}
|
|
937
822
|
}
|
|
938
823
|
|
|
939
824
|
// Handle geo queries separately (they work differently)
|
|
940
|
-
const isGeoQuery =
|
|
941
|
-
filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
825
|
+
const isGeoQuery = filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
942
826
|
if (isGeoQuery) {
|
|
943
827
|
return this.handleGeoQuery(filters);
|
|
944
828
|
}
|
|
@@ -949,55 +833,39 @@ export class ProcedureService extends BaseService {
|
|
|
949
833
|
|
|
950
834
|
// Active status filter
|
|
951
835
|
if (filters.isActive !== undefined) {
|
|
952
|
-
constraints.push(where(
|
|
836
|
+
constraints.push(where('isActive', '==', filters.isActive));
|
|
953
837
|
} else {
|
|
954
|
-
constraints.push(where(
|
|
838
|
+
constraints.push(where('isActive', '==', true));
|
|
955
839
|
}
|
|
956
840
|
|
|
957
841
|
// Filter constraints
|
|
958
842
|
if (filters.procedureFamily) {
|
|
959
|
-
constraints.push(where(
|
|
843
|
+
constraints.push(where('family', '==', filters.procedureFamily));
|
|
960
844
|
}
|
|
961
845
|
if (filters.procedureCategory) {
|
|
962
|
-
constraints.push(
|
|
963
|
-
where("category.id", "==", filters.procedureCategory)
|
|
964
|
-
);
|
|
846
|
+
constraints.push(where('category.id', '==', filters.procedureCategory));
|
|
965
847
|
}
|
|
966
848
|
if (filters.procedureSubcategory) {
|
|
967
|
-
constraints.push(
|
|
968
|
-
where("subcategory.id", "==", filters.procedureSubcategory)
|
|
969
|
-
);
|
|
849
|
+
constraints.push(where('subcategory.id', '==', filters.procedureSubcategory));
|
|
970
850
|
}
|
|
971
851
|
if (filters.procedureTechnology) {
|
|
972
|
-
constraints.push(
|
|
973
|
-
where("technology.id", "==", filters.procedureTechnology)
|
|
974
|
-
);
|
|
852
|
+
constraints.push(where('technology.id', '==', filters.procedureTechnology));
|
|
975
853
|
}
|
|
976
854
|
if (filters.minPrice !== undefined) {
|
|
977
|
-
constraints.push(where(
|
|
855
|
+
constraints.push(where('price', '>=', filters.minPrice));
|
|
978
856
|
}
|
|
979
857
|
if (filters.maxPrice !== undefined) {
|
|
980
|
-
constraints.push(where(
|
|
858
|
+
constraints.push(where('price', '<=', filters.maxPrice));
|
|
981
859
|
}
|
|
982
860
|
if (filters.minRating !== undefined) {
|
|
983
|
-
constraints.push(
|
|
984
|
-
where("reviewInfo.averageRating", ">=", filters.minRating)
|
|
985
|
-
);
|
|
861
|
+
constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
|
|
986
862
|
}
|
|
987
863
|
if (filters.maxRating !== undefined) {
|
|
988
|
-
constraints.push(
|
|
989
|
-
where("reviewInfo.averageRating", "<=", filters.maxRating)
|
|
990
|
-
);
|
|
864
|
+
constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
|
|
991
865
|
}
|
|
992
866
|
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
993
|
-
const
|
|
994
|
-
constraints.push(
|
|
995
|
-
where(
|
|
996
|
-
"treatmentBenefitIds",
|
|
997
|
-
"array-contains-any",
|
|
998
|
-
benefitIdsToMatch
|
|
999
|
-
)
|
|
1000
|
-
);
|
|
867
|
+
const benefitsToMatch = filters.treatmentBenefits;
|
|
868
|
+
constraints.push(where('treatmentBenefits', 'array-contains-any', benefitsToMatch));
|
|
1001
869
|
}
|
|
1002
870
|
|
|
1003
871
|
return constraints;
|
|
@@ -1006,17 +874,15 @@ export class ProcedureService extends BaseService {
|
|
|
1006
874
|
// Strategy 1: Try nameLower search if nameSearch exists
|
|
1007
875
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1008
876
|
try {
|
|
1009
|
-
console.log(
|
|
1010
|
-
"[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search"
|
|
1011
|
-
);
|
|
877
|
+
console.log('[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search');
|
|
1012
878
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1013
879
|
const constraints = getBaseConstraints();
|
|
1014
|
-
constraints.push(where(
|
|
1015
|
-
constraints.push(where(
|
|
1016
|
-
constraints.push(orderBy(
|
|
880
|
+
constraints.push(where('nameLower', '>=', searchTerm));
|
|
881
|
+
constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
|
|
882
|
+
constraints.push(orderBy('nameLower'));
|
|
1017
883
|
|
|
1018
884
|
if (filters.lastDoc) {
|
|
1019
|
-
if (typeof filters.lastDoc.data ===
|
|
885
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
1020
886
|
constraints.push(startAfter(filters.lastDoc));
|
|
1021
887
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
1022
888
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -1026,22 +892,17 @@ export class ProcedureService extends BaseService {
|
|
|
1026
892
|
}
|
|
1027
893
|
constraints.push(limit(filters.pagination || 10));
|
|
1028
894
|
|
|
1029
|
-
const q = query(
|
|
1030
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
1031
|
-
...constraints
|
|
1032
|
-
);
|
|
895
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1033
896
|
const querySnapshot = await getDocs(q);
|
|
1034
897
|
const procedures = querySnapshot.docs.map(
|
|
1035
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
898
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1036
899
|
);
|
|
1037
900
|
const lastDoc =
|
|
1038
901
|
querySnapshot.docs.length > 0
|
|
1039
902
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1040
903
|
: null;
|
|
1041
904
|
|
|
1042
|
-
console.log(
|
|
1043
|
-
`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`
|
|
1044
|
-
);
|
|
905
|
+
console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
|
|
1045
906
|
|
|
1046
907
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1047
908
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -1049,24 +910,22 @@ export class ProcedureService extends BaseService {
|
|
|
1049
910
|
}
|
|
1050
911
|
return { procedures, lastDoc };
|
|
1051
912
|
} catch (error) {
|
|
1052
|
-
console.log(
|
|
913
|
+
console.log('[PROCEDURE_SERVICE] Strategy 1 failed:', error);
|
|
1053
914
|
}
|
|
1054
915
|
}
|
|
1055
916
|
|
|
1056
917
|
// Strategy 2: Try name field search as fallback
|
|
1057
918
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1058
919
|
try {
|
|
1059
|
-
console.log(
|
|
1060
|
-
"[PROCEDURE_SERVICE] Strategy 2: Trying name field search"
|
|
1061
|
-
);
|
|
920
|
+
console.log('[PROCEDURE_SERVICE] Strategy 2: Trying name field search');
|
|
1062
921
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1063
922
|
const constraints = getBaseConstraints();
|
|
1064
|
-
constraints.push(where(
|
|
1065
|
-
constraints.push(where(
|
|
1066
|
-
constraints.push(orderBy(
|
|
923
|
+
constraints.push(where('name', '>=', searchTerm));
|
|
924
|
+
constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
|
|
925
|
+
constraints.push(orderBy('name'));
|
|
1067
926
|
|
|
1068
927
|
if (filters.lastDoc) {
|
|
1069
|
-
if (typeof filters.lastDoc.data ===
|
|
928
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
1070
929
|
constraints.push(startAfter(filters.lastDoc));
|
|
1071
930
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
1072
931
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -1076,22 +935,17 @@ export class ProcedureService extends BaseService {
|
|
|
1076
935
|
}
|
|
1077
936
|
constraints.push(limit(filters.pagination || 10));
|
|
1078
937
|
|
|
1079
|
-
const q = query(
|
|
1080
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
1081
|
-
...constraints
|
|
1082
|
-
);
|
|
938
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1083
939
|
const querySnapshot = await getDocs(q);
|
|
1084
940
|
const procedures = querySnapshot.docs.map(
|
|
1085
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
941
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1086
942
|
);
|
|
1087
943
|
const lastDoc =
|
|
1088
944
|
querySnapshot.docs.length > 0
|
|
1089
945
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1090
946
|
: null;
|
|
1091
947
|
|
|
1092
|
-
console.log(
|
|
1093
|
-
`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`
|
|
1094
|
-
);
|
|
948
|
+
console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
|
|
1095
949
|
|
|
1096
950
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1097
951
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -1099,20 +953,20 @@ export class ProcedureService extends BaseService {
|
|
|
1099
953
|
}
|
|
1100
954
|
return { procedures, lastDoc };
|
|
1101
955
|
} catch (error) {
|
|
1102
|
-
console.log(
|
|
956
|
+
console.log('[PROCEDURE_SERVICE] Strategy 2 failed:', error);
|
|
1103
957
|
}
|
|
1104
958
|
}
|
|
1105
959
|
|
|
1106
960
|
// Strategy 3: orderBy createdAt with client-side filtering
|
|
1107
961
|
try {
|
|
1108
962
|
console.log(
|
|
1109
|
-
|
|
963
|
+
'[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering',
|
|
1110
964
|
);
|
|
1111
965
|
const constraints = getBaseConstraints();
|
|
1112
|
-
constraints.push(orderBy(
|
|
966
|
+
constraints.push(orderBy('createdAt', 'desc'));
|
|
1113
967
|
|
|
1114
968
|
if (filters.lastDoc) {
|
|
1115
|
-
if (typeof filters.lastDoc.data ===
|
|
969
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
1116
970
|
constraints.push(startAfter(filters.lastDoc));
|
|
1117
971
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
1118
972
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -1122,25 +976,18 @@ export class ProcedureService extends BaseService {
|
|
|
1122
976
|
}
|
|
1123
977
|
constraints.push(limit(filters.pagination || 10));
|
|
1124
978
|
|
|
1125
|
-
const q = query(
|
|
1126
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
1127
|
-
...constraints
|
|
1128
|
-
);
|
|
979
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1129
980
|
const querySnapshot = await getDocs(q);
|
|
1130
981
|
let procedures = querySnapshot.docs.map(
|
|
1131
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
982
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1132
983
|
);
|
|
1133
984
|
|
|
1134
985
|
// Apply all client-side filters using centralized function
|
|
1135
986
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1136
987
|
|
|
1137
988
|
const lastDoc =
|
|
1138
|
-
querySnapshot.docs.length > 0
|
|
1139
|
-
|
|
1140
|
-
: null;
|
|
1141
|
-
console.log(
|
|
1142
|
-
`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`
|
|
1143
|
-
);
|
|
989
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
990
|
+
console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
|
|
1144
991
|
|
|
1145
992
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1146
993
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -1148,37 +995,30 @@ export class ProcedureService extends BaseService {
|
|
|
1148
995
|
}
|
|
1149
996
|
return { procedures, lastDoc };
|
|
1150
997
|
} catch (error) {
|
|
1151
|
-
console.log(
|
|
998
|
+
console.log('[PROCEDURE_SERVICE] Strategy 3 failed:', error);
|
|
1152
999
|
}
|
|
1153
1000
|
|
|
1154
1001
|
// Strategy 4: Minimal query fallback
|
|
1155
1002
|
try {
|
|
1156
|
-
console.log(
|
|
1003
|
+
console.log('[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback');
|
|
1157
1004
|
const constraints: QueryConstraint[] = [
|
|
1158
|
-
where(
|
|
1159
|
-
orderBy(
|
|
1005
|
+
where('isActive', '==', true),
|
|
1006
|
+
orderBy('createdAt', 'desc'),
|
|
1160
1007
|
limit(filters.pagination || 10),
|
|
1161
1008
|
];
|
|
1162
1009
|
|
|
1163
|
-
const q = query(
|
|
1164
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
1165
|
-
...constraints
|
|
1166
|
-
);
|
|
1010
|
+
const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
1167
1011
|
const querySnapshot = await getDocs(q);
|
|
1168
1012
|
let procedures = querySnapshot.docs.map(
|
|
1169
|
-
(doc) => ({ ...doc.data(), id: doc.id } as Procedure)
|
|
1013
|
+
(doc) => ({ ...doc.data(), id: doc.id } as Procedure),
|
|
1170
1014
|
);
|
|
1171
1015
|
|
|
1172
1016
|
// Apply all client-side filters using centralized function
|
|
1173
1017
|
procedures = this.applyInMemoryFilters(procedures, filters);
|
|
1174
1018
|
|
|
1175
1019
|
const lastDoc =
|
|
1176
|
-
querySnapshot.docs.length > 0
|
|
1177
|
-
|
|
1178
|
-
: null;
|
|
1179
|
-
console.log(
|
|
1180
|
-
`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`
|
|
1181
|
-
);
|
|
1020
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1021
|
+
console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
|
|
1182
1022
|
|
|
1183
1023
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1184
1024
|
if (procedures.length < (filters.pagination || 10)) {
|
|
@@ -1186,16 +1026,14 @@ export class ProcedureService extends BaseService {
|
|
|
1186
1026
|
}
|
|
1187
1027
|
return { procedures, lastDoc };
|
|
1188
1028
|
} catch (error) {
|
|
1189
|
-
console.log(
|
|
1029
|
+
console.log('[PROCEDURE_SERVICE] Strategy 4 failed:', error);
|
|
1190
1030
|
}
|
|
1191
1031
|
|
|
1192
1032
|
// All strategies failed
|
|
1193
|
-
console.log(
|
|
1194
|
-
"[PROCEDURE_SERVICE] All strategies failed, returning empty result"
|
|
1195
|
-
);
|
|
1033
|
+
console.log('[PROCEDURE_SERVICE] All strategies failed, returning empty result');
|
|
1196
1034
|
return { procedures: [], lastDoc: null };
|
|
1197
1035
|
} catch (error) {
|
|
1198
|
-
console.error(
|
|
1036
|
+
console.error('[PROCEDURE_SERVICE] Error filtering procedures:', error);
|
|
1199
1037
|
return { procedures: [], lastDoc: null };
|
|
1200
1038
|
}
|
|
1201
1039
|
}
|
|
@@ -1206,7 +1044,7 @@ export class ProcedureService extends BaseService {
|
|
|
1206
1044
|
*/
|
|
1207
1045
|
private applyInMemoryFilters(
|
|
1208
1046
|
procedures: Procedure[],
|
|
1209
|
-
filters: any
|
|
1047
|
+
filters: any,
|
|
1210
1048
|
): (Procedure & { distance?: number })[] {
|
|
1211
1049
|
let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
|
|
1212
1050
|
|
|
@@ -1214,27 +1052,23 @@ export class ProcedureService extends BaseService {
|
|
|
1214
1052
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1215
1053
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1216
1054
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1217
|
-
const name = (procedure.name ||
|
|
1218
|
-
const nameLower = procedure.nameLower ||
|
|
1055
|
+
const name = (procedure.name || '').toLowerCase();
|
|
1056
|
+
const nameLower = procedure.nameLower || '';
|
|
1219
1057
|
return name.includes(searchTerm) || nameLower.includes(searchTerm);
|
|
1220
1058
|
});
|
|
1221
|
-
console.log(
|
|
1222
|
-
`[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`
|
|
1223
|
-
);
|
|
1059
|
+
console.log(`[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`);
|
|
1224
1060
|
}
|
|
1225
1061
|
|
|
1226
1062
|
// Price filtering
|
|
1227
1063
|
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
|
|
1228
1064
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1229
1065
|
const price = procedure.price || 0;
|
|
1230
|
-
if (filters.minPrice !== undefined && price < filters.minPrice)
|
|
1231
|
-
|
|
1232
|
-
if (filters.maxPrice !== undefined && price > filters.maxPrice)
|
|
1233
|
-
return false;
|
|
1066
|
+
if (filters.minPrice !== undefined && price < filters.minPrice) return false;
|
|
1067
|
+
if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
|
|
1234
1068
|
return true;
|
|
1235
1069
|
});
|
|
1236
1070
|
console.log(
|
|
1237
|
-
`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}
|
|
1071
|
+
`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`,
|
|
1238
1072
|
);
|
|
1239
1073
|
}
|
|
1240
1074
|
|
|
@@ -1242,69 +1076,64 @@ export class ProcedureService extends BaseService {
|
|
|
1242
1076
|
if (filters.minRating !== undefined || filters.maxRating !== undefined) {
|
|
1243
1077
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1244
1078
|
const rating = procedure.reviewInfo?.averageRating || 0;
|
|
1245
|
-
if (filters.minRating !== undefined && rating < filters.minRating)
|
|
1246
|
-
|
|
1247
|
-
if (filters.maxRating !== undefined && rating > filters.maxRating)
|
|
1248
|
-
return false;
|
|
1079
|
+
if (filters.minRating !== undefined && rating < filters.minRating) return false;
|
|
1080
|
+
if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
|
|
1249
1081
|
return true;
|
|
1250
1082
|
});
|
|
1251
1083
|
console.log(
|
|
1252
|
-
`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}
|
|
1084
|
+
`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`,
|
|
1253
1085
|
);
|
|
1254
1086
|
}
|
|
1255
1087
|
|
|
1256
1088
|
// Treatment benefits filtering
|
|
1257
1089
|
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
1258
|
-
const
|
|
1090
|
+
const benefitsToMatch = filters.treatmentBenefits;
|
|
1259
1091
|
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
1260
|
-
const
|
|
1261
|
-
return
|
|
1262
|
-
procedureBenefitIds.includes(benefitId)
|
|
1263
|
-
);
|
|
1092
|
+
const procedureBenefits = procedure.treatmentBenefits || [];
|
|
1093
|
+
return benefitsToMatch.some((benefit: any) => procedureBenefits.includes(benefit));
|
|
1264
1094
|
});
|
|
1265
1095
|
console.log(
|
|
1266
|
-
`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}
|
|
1096
|
+
`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`,
|
|
1267
1097
|
);
|
|
1268
1098
|
}
|
|
1269
1099
|
|
|
1270
1100
|
// Procedure family filtering
|
|
1271
1101
|
if (filters.procedureFamily) {
|
|
1272
1102
|
filteredProcedures = filteredProcedures.filter(
|
|
1273
|
-
(procedure) => procedure.family === filters.procedureFamily
|
|
1103
|
+
(procedure) => procedure.family === filters.procedureFamily,
|
|
1274
1104
|
);
|
|
1275
1105
|
console.log(
|
|
1276
|
-
`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}
|
|
1106
|
+
`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`,
|
|
1277
1107
|
);
|
|
1278
1108
|
}
|
|
1279
1109
|
|
|
1280
1110
|
// Category filtering
|
|
1281
1111
|
if (filters.procedureCategory) {
|
|
1282
1112
|
filteredProcedures = filteredProcedures.filter(
|
|
1283
|
-
(procedure) => procedure.category?.id === filters.procedureCategory
|
|
1113
|
+
(procedure) => procedure.category?.id === filters.procedureCategory,
|
|
1284
1114
|
);
|
|
1285
1115
|
console.log(
|
|
1286
|
-
`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}
|
|
1116
|
+
`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`,
|
|
1287
1117
|
);
|
|
1288
1118
|
}
|
|
1289
1119
|
|
|
1290
1120
|
// Subcategory filtering
|
|
1291
1121
|
if (filters.procedureSubcategory) {
|
|
1292
1122
|
filteredProcedures = filteredProcedures.filter(
|
|
1293
|
-
(procedure) =>
|
|
1294
|
-
procedure.subcategory?.id === filters.procedureSubcategory
|
|
1123
|
+
(procedure) => procedure.subcategory?.id === filters.procedureSubcategory,
|
|
1295
1124
|
);
|
|
1296
1125
|
console.log(
|
|
1297
|
-
`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}
|
|
1126
|
+
`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`,
|
|
1298
1127
|
);
|
|
1299
1128
|
}
|
|
1300
1129
|
|
|
1301
1130
|
// Technology filtering
|
|
1302
1131
|
if (filters.procedureTechnology) {
|
|
1303
1132
|
filteredProcedures = filteredProcedures.filter(
|
|
1304
|
-
(procedure) => procedure.technology?.id === filters.procedureTechnology
|
|
1133
|
+
(procedure) => procedure.technology?.id === filters.procedureTechnology,
|
|
1305
1134
|
);
|
|
1306
1135
|
console.log(
|
|
1307
|
-
`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}
|
|
1136
|
+
`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`,
|
|
1308
1137
|
);
|
|
1309
1138
|
}
|
|
1310
1139
|
|
|
@@ -1321,7 +1150,7 @@ export class ProcedureService extends BaseService {
|
|
|
1321
1150
|
const distance =
|
|
1322
1151
|
distanceBetween(
|
|
1323
1152
|
[location.latitude, location.longitude],
|
|
1324
|
-
[clinicLocation.latitude, clinicLocation.longitude]
|
|
1153
|
+
[clinicLocation.latitude, clinicLocation.longitude],
|
|
1325
1154
|
) / 1000; // Convert to km
|
|
1326
1155
|
|
|
1327
1156
|
// Attach distance for frontend sorting/display
|
|
@@ -1329,24 +1158,19 @@ export class ProcedureService extends BaseService {
|
|
|
1329
1158
|
|
|
1330
1159
|
return distance <= radiusInKm;
|
|
1331
1160
|
});
|
|
1332
|
-
console.log(
|
|
1333
|
-
`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`
|
|
1334
|
-
);
|
|
1161
|
+
console.log(`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`);
|
|
1335
1162
|
|
|
1336
1163
|
// Sort by distance when geo filtering is applied
|
|
1337
|
-
filteredProcedures.sort(
|
|
1338
|
-
(a, b) => ((a as any).distance || 0) - ((b as any).distance || 0)
|
|
1339
|
-
);
|
|
1164
|
+
filteredProcedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
|
|
1340
1165
|
}
|
|
1341
1166
|
|
|
1342
1167
|
return filteredProcedures as (Procedure & { distance?: number })[];
|
|
1343
1168
|
}
|
|
1344
1169
|
|
|
1345
|
-
private handleGeoQuery(
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
console.log("[PROCEDURE_SERVICE] Executing geo query with geohash bounds");
|
|
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');
|
|
1350
1174
|
try {
|
|
1351
1175
|
const location = filters.location;
|
|
1352
1176
|
const radiusInKm = filters.radiusInKm;
|
|
@@ -1355,33 +1179,22 @@ export class ProcedureService extends BaseService {
|
|
|
1355
1179
|
return Promise.resolve({ procedures: [], lastDoc: null });
|
|
1356
1180
|
}
|
|
1357
1181
|
|
|
1358
|
-
const bounds = geohashQueryBounds(
|
|
1359
|
-
[location.latitude, location.longitude],
|
|
1360
|
-
radiusInKm * 1000
|
|
1361
|
-
);
|
|
1182
|
+
const bounds = geohashQueryBounds([location.latitude, location.longitude], radiusInKm * 1000);
|
|
1362
1183
|
|
|
1363
1184
|
const fetches = bounds.map((b) => {
|
|
1364
1185
|
const constraints: QueryConstraint[] = [
|
|
1365
|
-
where(
|
|
1366
|
-
where(
|
|
1367
|
-
where(
|
|
1368
|
-
"isActive",
|
|
1369
|
-
"==",
|
|
1370
|
-
filters.isActive !== undefined ? filters.isActive : true
|
|
1371
|
-
),
|
|
1186
|
+
where('clinicInfo.location.geohash', '>=', b[0]),
|
|
1187
|
+
where('clinicInfo.location.geohash', '<=', b[1]),
|
|
1188
|
+
where('isActive', '==', filters.isActive !== undefined ? filters.isActive : true),
|
|
1372
1189
|
];
|
|
1373
|
-
return getDocs(
|
|
1374
|
-
query(collection(this.db, PROCEDURES_COLLECTION), ...constraints)
|
|
1375
|
-
);
|
|
1190
|
+
return getDocs(query(collection(this.db, PROCEDURES_COLLECTION), ...constraints));
|
|
1376
1191
|
});
|
|
1377
1192
|
|
|
1378
1193
|
return Promise.all(fetches)
|
|
1379
1194
|
.then((snaps) => {
|
|
1380
1195
|
const collected: Procedure[] = [];
|
|
1381
1196
|
snaps.forEach((snap) => {
|
|
1382
|
-
snap.docs.forEach((d) =>
|
|
1383
|
-
collected.push({ ...(d.data() as Procedure), id: d.id })
|
|
1384
|
-
);
|
|
1197
|
+
snap.docs.forEach((d) => collected.push({ ...(d.data() as Procedure), id: d.id }));
|
|
1385
1198
|
});
|
|
1386
1199
|
|
|
1387
1200
|
// Deduplicate by id
|
|
@@ -1399,29 +1212,26 @@ export class ProcedureService extends BaseService {
|
|
|
1399
1212
|
let startIndex = 0;
|
|
1400
1213
|
if (
|
|
1401
1214
|
filters.lastDoc &&
|
|
1402
|
-
typeof filters.lastDoc ===
|
|
1215
|
+
typeof filters.lastDoc === 'object' &&
|
|
1403
1216
|
(filters.lastDoc as any).id
|
|
1404
1217
|
) {
|
|
1405
|
-
const idx = procedures.findIndex(
|
|
1406
|
-
(p) => p.id === (filters.lastDoc as any).id
|
|
1407
|
-
);
|
|
1218
|
+
const idx = procedures.findIndex((p) => p.id === (filters.lastDoc as any).id);
|
|
1408
1219
|
if (idx >= 0) startIndex = idx + 1;
|
|
1409
1220
|
}
|
|
1410
1221
|
const page = procedures.slice(startIndex, startIndex + pageSize);
|
|
1411
|
-
const newLastDoc =
|
|
1412
|
-
page.length === pageSize ? page[page.length - 1] : null;
|
|
1222
|
+
const newLastDoc = page.length === pageSize ? page[page.length - 1] : null;
|
|
1413
1223
|
|
|
1414
1224
|
console.log(
|
|
1415
|
-
`[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km
|
|
1225
|
+
`[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`,
|
|
1416
1226
|
);
|
|
1417
1227
|
return { procedures: page, lastDoc: newLastDoc };
|
|
1418
1228
|
})
|
|
1419
1229
|
.catch((err) => {
|
|
1420
|
-
console.error(
|
|
1230
|
+
console.error('[PROCEDURE_SERVICE] Geo bounds fetch failed:', err);
|
|
1421
1231
|
return { procedures: [], lastDoc: null };
|
|
1422
1232
|
});
|
|
1423
1233
|
} catch (error) {
|
|
1424
|
-
console.error(
|
|
1234
|
+
console.error('[PROCEDURE_SERVICE] Geo query failed:', error);
|
|
1425
1235
|
return Promise.resolve({ procedures: [], lastDoc: null });
|
|
1426
1236
|
}
|
|
1427
1237
|
}
|
|
@@ -1433,7 +1243,7 @@ export class ProcedureService extends BaseService {
|
|
|
1433
1243
|
* @returns The created procedure
|
|
1434
1244
|
*/
|
|
1435
1245
|
async createConsultationProcedure(
|
|
1436
|
-
data: Omit<CreateProcedureData,
|
|
1246
|
+
data: Omit<CreateProcedureData, 'productId'>,
|
|
1437
1247
|
): Promise<Procedure> {
|
|
1438
1248
|
// Generate procedure ID first so we can use it for media uploads
|
|
1439
1249
|
const procedureId = this.generateId();
|
|
@@ -1447,7 +1257,7 @@ export class ProcedureService extends BaseService {
|
|
|
1447
1257
|
]);
|
|
1448
1258
|
|
|
1449
1259
|
if (!category || !subcategory || !technology) {
|
|
1450
|
-
throw new Error(
|
|
1260
|
+
throw new Error('One or more required base entities not found');
|
|
1451
1261
|
}
|
|
1452
1262
|
|
|
1453
1263
|
// Get clinic and practitioner information for aggregation
|
|
@@ -1458,11 +1268,7 @@ export class ProcedureService extends BaseService {
|
|
|
1458
1268
|
}
|
|
1459
1269
|
const clinic = clinicSnapshot.data() as Clinic;
|
|
1460
1270
|
|
|
1461
|
-
const practitionerRef = doc(
|
|
1462
|
-
this.db,
|
|
1463
|
-
PRACTITIONERS_COLLECTION,
|
|
1464
|
-
data.practitionerId
|
|
1465
|
-
);
|
|
1271
|
+
const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, data.practitionerId);
|
|
1466
1272
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
1467
1273
|
if (!practitionerSnapshot.exists()) {
|
|
1468
1274
|
throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
|
|
@@ -1472,26 +1278,22 @@ export class ProcedureService extends BaseService {
|
|
|
1472
1278
|
// Process photos if provided
|
|
1473
1279
|
let processedPhotos: string[] = [];
|
|
1474
1280
|
if (data.photos && data.photos.length > 0) {
|
|
1475
|
-
processedPhotos = await this.processMediaArray(
|
|
1476
|
-
data.photos,
|
|
1477
|
-
procedureId,
|
|
1478
|
-
"procedure-photos"
|
|
1479
|
-
);
|
|
1281
|
+
processedPhotos = await this.processMediaArray(data.photos, procedureId, 'procedure-photos');
|
|
1480
1282
|
}
|
|
1481
1283
|
|
|
1482
1284
|
// Create aggregated clinic info for the procedure document
|
|
1483
1285
|
const clinicInfo = {
|
|
1484
1286
|
id: clinicSnapshot.id,
|
|
1485
1287
|
name: clinic.name,
|
|
1486
|
-
description: clinic.description ||
|
|
1288
|
+
description: clinic.description || '',
|
|
1487
1289
|
featuredPhoto:
|
|
1488
1290
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
1489
|
-
? typeof clinic.featuredPhotos[0] ===
|
|
1291
|
+
? typeof clinic.featuredPhotos[0] === 'string'
|
|
1490
1292
|
? clinic.featuredPhotos[0]
|
|
1491
|
-
:
|
|
1492
|
-
: typeof clinic.coverPhoto ===
|
|
1293
|
+
: ''
|
|
1294
|
+
: typeof clinic.coverPhoto === 'string'
|
|
1493
1295
|
? clinic.coverPhoto
|
|
1494
|
-
:
|
|
1296
|
+
: '',
|
|
1495
1297
|
location: clinic.location,
|
|
1496
1298
|
contactInfo: clinic.contactInfo,
|
|
1497
1299
|
};
|
|
@@ -1500,22 +1302,22 @@ export class ProcedureService extends BaseService {
|
|
|
1500
1302
|
const doctorInfo = {
|
|
1501
1303
|
id: practitionerSnapshot.id,
|
|
1502
1304
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
1503
|
-
description: practitioner.basicInfo.bio ||
|
|
1305
|
+
description: practitioner.basicInfo.bio || '',
|
|
1504
1306
|
photo:
|
|
1505
|
-
typeof practitioner.basicInfo.profileImageUrl ===
|
|
1307
|
+
typeof practitioner.basicInfo.profileImageUrl === 'string'
|
|
1506
1308
|
? practitioner.basicInfo.profileImageUrl
|
|
1507
|
-
:
|
|
1309
|
+
: '',
|
|
1508
1310
|
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
1509
1311
|
services: practitioner.procedures || [],
|
|
1510
1312
|
};
|
|
1511
1313
|
|
|
1512
1314
|
// Create a placeholder product for consultation procedures
|
|
1513
1315
|
const consultationProduct: Product = {
|
|
1514
|
-
id:
|
|
1515
|
-
name:
|
|
1516
|
-
description:
|
|
1517
|
-
brandId:
|
|
1518
|
-
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',
|
|
1519
1321
|
technologyId: data.technologyId,
|
|
1520
1322
|
technologyName: technology.name,
|
|
1521
1323
|
isActive: true,
|
|
@@ -1524,7 +1326,7 @@ export class ProcedureService extends BaseService {
|
|
|
1524
1326
|
};
|
|
1525
1327
|
|
|
1526
1328
|
// Create the procedure object
|
|
1527
|
-
const newProcedure: Omit<Procedure,
|
|
1329
|
+
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
1528
1330
|
id: procedureId,
|
|
1529
1331
|
...data,
|
|
1530
1332
|
nameLower: (data as any).nameLower || data.name.toLowerCase(),
|
|
@@ -1535,9 +1337,7 @@ export class ProcedureService extends BaseService {
|
|
|
1535
1337
|
product: consultationProduct, // Use placeholder product
|
|
1536
1338
|
blockingConditions: technology.blockingConditions,
|
|
1537
1339
|
contraindications: technology.contraindications || [],
|
|
1538
|
-
contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
|
|
1539
1340
|
treatmentBenefits: technology.benefits,
|
|
1540
|
-
treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
|
|
1541
1341
|
preRequirements: technology.requirements.pre,
|
|
1542
1342
|
postRequirements: technology.requirements.post,
|
|
1543
1343
|
certificationRequirement: technology.certificationRequirement,
|
|
@@ -1595,7 +1395,7 @@ export class ProcedureService extends BaseService {
|
|
|
1595
1395
|
name: data.name,
|
|
1596
1396
|
clinicId: data.clinicInfo?.id,
|
|
1597
1397
|
clinicName: data.clinicInfo?.name,
|
|
1598
|
-
address: data.clinicInfo?.location?.address ||
|
|
1398
|
+
address: data.clinicInfo?.location?.address || '',
|
|
1599
1399
|
latitude: data.clinicInfo?.location?.latitude,
|
|
1600
1400
|
longitude: data.clinicInfo?.location?.longitude,
|
|
1601
1401
|
};
|