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