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