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