@blackcode_sa/metaestetics-api 1.5.28 → 1.5.29

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.
Files changed (34) hide show
  1. package/dist/admin/index.d.mts +1199 -1
  2. package/dist/admin/index.d.ts +1199 -1
  3. package/dist/admin/index.js +1337 -2
  4. package/dist/admin/index.mjs +1333 -2
  5. package/dist/backoffice/index.d.mts +99 -7
  6. package/dist/backoffice/index.d.ts +99 -7
  7. package/dist/index.d.mts +4035 -2364
  8. package/dist/index.d.ts +4035 -2364
  9. package/dist/index.js +2616 -1929
  10. package/dist/index.mjs +2646 -1952
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
  13. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
  14. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
  15. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
  16. package/src/admin/index.ts +53 -4
  17. package/src/index.ts +28 -4
  18. package/src/services/clinic/clinic.service.ts +320 -107
  19. package/src/services/clinic/utils/clinic.utils.ts +66 -117
  20. package/src/services/clinic/utils/filter.utils.d.ts +23 -0
  21. package/src/services/clinic/utils/filter.utils.ts +264 -0
  22. package/src/services/practitioner/practitioner.service.ts +616 -5
  23. package/src/services/procedure/procedure.service.ts +599 -352
  24. package/src/services/reviews/reviews.service.ts +842 -0
  25. package/src/types/clinic/index.ts +24 -56
  26. package/src/types/practitioner/index.ts +34 -33
  27. package/src/types/procedure/index.ts +32 -0
  28. package/src/types/profile/index.ts +1 -1
  29. package/src/types/reviews/index.ts +126 -0
  30. package/src/validations/clinic.schema.ts +37 -64
  31. package/src/validations/practitioner.schema.ts +42 -32
  32. package/src/validations/procedure.schema.ts +11 -3
  33. package/src/validations/reviews.schema.ts +189 -0
  34. package/src/services/clinic/utils/review.utils.ts +0 -93
@@ -13,19 +13,29 @@ import {
13
13
  GeoPoint,
14
14
  QueryConstraint,
15
15
  FieldValue,
16
+ writeBatch,
17
+ arrayUnion,
18
+ arrayRemove,
16
19
  } from "firebase/firestore";
17
20
  import { BaseService } from "../base.service";
18
21
  import {
19
22
  Clinic,
20
23
  CreateClinicData,
21
24
  CLINICS_COLLECTION,
22
- ClinicReview,
23
25
  ClinicTag,
24
26
  ClinicTags,
25
27
  ClinicGroup,
28
+ CLINIC_GROUPS_COLLECTION,
26
29
  ClinicBranchSetupData,
27
30
  CLINIC_ADMINS_COLLECTION,
31
+ DoctorInfo,
32
+ // Remove incorrect imports below
33
+ // ProcedureSummaryInfo,
34
+ // ClinicInfo
28
35
  } from "../../types/clinic";
36
+ // Correct imports
37
+ import { ProcedureSummaryInfo } from "../../types/procedure";
38
+ import { ClinicInfo } from "../../types/profile";
29
39
  import { ClinicGroupService } from "./clinic-group.service";
30
40
  import { ClinicAdminService } from "./clinic-admin.service";
31
41
  import {
@@ -36,17 +46,18 @@ import {
36
46
  import {
37
47
  clinicSchema,
38
48
  createClinicSchema,
39
- clinicReviewSchema,
40
49
  } from "../../validations/clinic.schema";
41
50
  import { z } from "zod";
42
51
  import { Auth } from "firebase/auth";
43
52
  import { Firestore } from "firebase/firestore";
44
53
  import { FirebaseApp } from "firebase/app";
45
54
  import * as ClinicUtils from "./utils/clinic.utils";
46
- import * as ReviewUtils from "./utils/review.utils";
47
55
  import * as TagUtils from "./utils/tag.utils";
48
56
  import * as SearchUtils from "./utils/search.utils";
49
57
  import * as AdminUtils from "./utils/admin.utils";
58
+ import * as FilterUtils from "./utils/filter.utils";
59
+ import { ClinicReviewInfo } from "../../types/reviews";
60
+ import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
50
61
 
51
62
  export class ClinicService extends BaseService {
52
63
  private clinicGroupService: ClinicGroupService;
@@ -64,23 +75,290 @@ export class ClinicService extends BaseService {
64
75
  this.clinicGroupService = clinicGroupService;
65
76
  }
66
77
 
78
+ // --- Helper Functions ---
79
+
80
+ /**
81
+ * Creates an aggregated ClinicInfo object from Clinic data.
82
+ * @param clinic The clinic object
83
+ * @returns ClinicInfo object
84
+ */
85
+ private _createClinicInfoForAggregation(clinic: Clinic): ClinicInfo {
86
+ return {
87
+ id: clinic.id,
88
+ featuredPhoto:
89
+ clinic.featuredPhotos && clinic.featuredPhotos.length > 0
90
+ ? clinic.featuredPhotos[0]
91
+ : clinic.coverPhoto || "",
92
+ name: clinic.name,
93
+ description: clinic.description || "",
94
+ location: clinic.location,
95
+ contactInfo: clinic.contactInfo,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Updates the ClinicInfo within the clinicsInfo array for multiple practitioners.
101
+ * @param practitionerIds IDs of practitioners to update
102
+ * @param clinicInfo The updated ClinicInfo object
103
+ */
104
+ private async _updateClinicInfoInPractitioners(
105
+ practitionerIds: string[],
106
+ clinicInfo: ClinicInfo
107
+ ): Promise<void> {
108
+ const batch = writeBatch(this.db);
109
+ const clinicId = clinicInfo.id;
110
+
111
+ for (const practitionerId of practitionerIds) {
112
+ const practitionerRef = doc(
113
+ this.db,
114
+ PRACTITIONERS_COLLECTION,
115
+ practitionerId
116
+ );
117
+ // Remove old clinic info based on ID
118
+ batch.update(practitionerRef, {
119
+ clinicsInfo: arrayRemove(...[{ id: clinicId }]),
120
+ updatedAt: serverTimestamp(),
121
+ });
122
+ // Add updated clinic info
123
+ batch.update(practitionerRef, {
124
+ clinicsInfo: arrayUnion(clinicInfo),
125
+ updatedAt: serverTimestamp(),
126
+ });
127
+ }
128
+ try {
129
+ await batch.commit();
130
+ } catch (error) {
131
+ console.error(
132
+ `Error updating clinic info in practitioners for clinic ${clinicId}:`,
133
+ error
134
+ );
135
+ // Decide on error handling
136
+ }
137
+ }
138
+
139
+ // --- Core Service Methods (Updated) ---
140
+
67
141
  /**
68
- * Kreira novu kliniku
142
+ * Creates a new clinic.
143
+ * Initializes empty doctorsInfo and proceduresInfo.
144
+ * Aggregation into Clinic happens via PractitionerService and ProcedureService.
69
145
  */
70
146
  async createClinic(
71
147
  data: CreateClinicData,
72
148
  creatorAdminId: string
73
149
  ): Promise<Clinic> {
74
- return ClinicUtils.createClinic(
75
- this.db,
76
- data,
77
- creatorAdminId,
78
- this.clinicGroupService,
79
- this.clinicAdminService,
80
- this.app
81
- );
150
+ try {
151
+ const validatedData = createClinicSchema.parse(data);
152
+ const group = await this.clinicGroupService.getClinicGroup(
153
+ validatedData.clinicGroupId
154
+ );
155
+ if (!group)
156
+ throw new Error(
157
+ `Clinic group ${validatedData.clinicGroupId} not found`
158
+ );
159
+ const location = validatedData.location;
160
+ const hash = geohashForLocation([location.latitude, location.longitude]);
161
+ const defaultReviewInfo: ClinicReviewInfo = {
162
+ totalReviews: 0,
163
+ averageRating: 0,
164
+ cleanliness: 0,
165
+ facilities: 0,
166
+ staffFriendliness: 0,
167
+ waitingTime: 0,
168
+ accessibility: 0,
169
+ recommendationPercentage: 0,
170
+ };
171
+ const clinicId = this.generateId();
172
+
173
+ const clinicData: Omit<Clinic, "createdAt" | "updatedAt"> & {
174
+ createdAt: FieldValue;
175
+ updatedAt: FieldValue;
176
+ } = {
177
+ id: clinicId,
178
+ clinicGroupId: validatedData.clinicGroupId,
179
+ name: validatedData.name,
180
+ description: validatedData.description,
181
+ location: { ...location, geohash: hash },
182
+ contactInfo: validatedData.contactInfo,
183
+ workingHours: validatedData.workingHours,
184
+ tags: validatedData.tags,
185
+ featuredPhotos: validatedData.featuredPhotos || [],
186
+ coverPhoto: validatedData.coverPhoto,
187
+ photosWithTags: validatedData.photosWithTags,
188
+ doctors: [],
189
+ procedures: [],
190
+ doctorsInfo: [],
191
+ proceduresInfo: [],
192
+ reviewInfo: defaultReviewInfo,
193
+ admins: [creatorAdminId],
194
+ isActive: validatedData.isActive,
195
+ isVerified: validatedData.isVerified,
196
+ logo: validatedData.logo,
197
+ createdAt: serverTimestamp(),
198
+ updatedAt: serverTimestamp(),
199
+ };
200
+
201
+ // Re-validate before saving (ensure schema matches the final object)
202
+ clinicSchema.parse({
203
+ ...clinicData,
204
+ createdAt: Timestamp.now(),
205
+ updatedAt: Timestamp.now(),
206
+ });
207
+
208
+ const batch = writeBatch(this.db);
209
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
210
+ batch.set(clinicRef, clinicData);
211
+
212
+ const groupRef = doc(
213
+ this.db,
214
+ CLINIC_GROUPS_COLLECTION,
215
+ validatedData.clinicGroupId
216
+ );
217
+ const newClinicInfoForGroup = this._createClinicInfoForAggregation({
218
+ ...clinicData,
219
+ id: clinicId,
220
+ } as Clinic);
221
+ batch.update(groupRef, {
222
+ clinics: arrayUnion(clinicId),
223
+ clinicsInfo: arrayUnion(newClinicInfoForGroup),
224
+ updatedAt: serverTimestamp(),
225
+ });
226
+
227
+ const adminRef = doc(this.db, CLINIC_ADMINS_COLLECTION, creatorAdminId);
228
+ batch.update(adminRef, {
229
+ clinicsManaged: arrayUnion(clinicId),
230
+ updatedAt: serverTimestamp(),
231
+ });
232
+
233
+ await batch.commit();
234
+ const savedClinic = await this.getClinic(clinicId);
235
+ if (!savedClinic) throw new Error("Failed to retrieve created clinic");
236
+ return savedClinic;
237
+ } catch (error) {
238
+ if (error instanceof z.ZodError) {
239
+ throw new Error("Invalid clinic data: " + error.message);
240
+ }
241
+ console.error("Error creating clinic:", error);
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Updates a clinic and propagates changes (ClinicInfo) to associated practitioners.
248
+ */
249
+ async updateClinic(
250
+ clinicId: string,
251
+ data: Partial<Omit<Clinic, "id" | "createdAt" | "clinicGroupId">>,
252
+ adminId: string
253
+ ): Promise<Clinic> {
254
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
255
+ const clinicDoc = await getDoc(clinicRef);
256
+ if (!clinicDoc.exists()) {
257
+ throw new Error(`Clinic ${clinicId} not found`);
258
+ }
259
+
260
+ try {
261
+ const currentClinic = clinicDoc.data() as Clinic;
262
+ // Explicitly Omit fields managed by other services or internally
263
+ const { doctorsInfo, proceduresInfo, ...updatePayload } =
264
+ data as Partial<Clinic>;
265
+
266
+ if (updatePayload.location) {
267
+ const loc = updatePayload.location;
268
+ updatePayload.location = {
269
+ ...loc,
270
+ geohash: geohashForLocation([loc.latitude, loc.longitude]),
271
+ };
272
+ }
273
+
274
+ // Merge with current data for validation and aggregation, preserving arrays managed elsewhere
275
+ const finalStateForValidation = {
276
+ ...currentClinic,
277
+ ...updatePayload, // Apply safe updates
278
+ // Explicitly keep arrays managed by other services from current state
279
+ doctorsInfo: currentClinic.doctorsInfo,
280
+ proceduresInfo: currentClinic.proceduresInfo,
281
+ };
282
+
283
+ // Ensure required fields for validation are present
284
+ clinicSchema.parse({
285
+ ...finalStateForValidation,
286
+ updatedAt: Timestamp.now(), // Use current time for validation
287
+ });
288
+
289
+ // Prepare final update data for Firestore, including timestamp
290
+ const updateDataForFirestore = {
291
+ ...updatePayload,
292
+ updatedAt: serverTimestamp(),
293
+ };
294
+
295
+ const batch = writeBatch(this.db);
296
+ batch.update(clinicRef, updateDataForFirestore);
297
+
298
+ const groupRef = doc(
299
+ this.db,
300
+ CLINIC_GROUPS_COLLECTION,
301
+ currentClinic.clinicGroupId
302
+ );
303
+ // Use the validated final state for aggregation
304
+ const updatedClinicInfoForGroup = this._createClinicInfoForAggregation(
305
+ finalStateForValidation as Clinic
306
+ );
307
+ batch.update(groupRef, {
308
+ clinicsInfo: arrayRemove(...[{ id: clinicId }]),
309
+ updatedAt: serverTimestamp(),
310
+ });
311
+ batch.update(groupRef, {
312
+ clinicsInfo: arrayUnion(updatedClinicInfoForGroup),
313
+ updatedAt: serverTimestamp(),
314
+ });
315
+
316
+ const practitionerIds = currentClinic.doctors || [];
317
+ if (practitionerIds.length > 0) {
318
+ // Pass the aggregated info based on the final validated state
319
+ await this._updateClinicInfoInPractitioners(
320
+ practitionerIds,
321
+ updatedClinicInfoForGroup
322
+ );
323
+ }
324
+
325
+ await batch.commit();
326
+ const updatedClinic = await this.getClinic(clinicId);
327
+ if (!updatedClinic) throw new Error("Failed to retrieve updated clinic");
328
+ return updatedClinic;
329
+ } catch (error) {
330
+ if (error instanceof z.ZodError) {
331
+ throw new Error(
332
+ "Invalid clinic update data: " +
333
+ error.errors
334
+ .map((e) => `${e.path.join(".")} - ${e.message}`)
335
+ .join(", ")
336
+ );
337
+ }
338
+ console.error(`Error updating clinic ${clinicId}:`, error);
339
+ throw error;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Deactivates a clinic.
345
+ * Note: Does not currently remove ClinicInfo from practitioners (might be desired).
346
+ */
347
+ async deactivateClinic(clinicId: string, adminId: string): Promise<void> {
348
+ // Permission check omitted
349
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
350
+ await updateDoc(clinicRef, {
351
+ isActive: false,
352
+ updatedAt: serverTimestamp(),
353
+ });
354
+ // Consider whether to also update ClinicInfo in practitioners to reflect inactive status
82
355
  }
83
356
 
357
+ // --- Other Methods ---
358
+ // (getClinic, getClinicsByGroup, findClinicsInRadius, addTags, removeTags, getClinicsByAdmin, etc.)
359
+ // Review these methods to ensure they don't rely on outdated aggregation logic (e.g., filtering ServiceInfo)
360
+ // and update them to use proceduresInfo if necessary for filtering.
361
+
84
362
  /**
85
363
  * Dohvata kliniku po ID-u
86
364
  */
@@ -97,15 +375,21 @@ export class ClinicService extends BaseService {
97
375
 
98
376
  /**
99
377
  * Pretražuje klinike u određenom radijusu
378
+ * REVIEW: SearchUtils.findClinicsInRadius might need updating for filters.
100
379
  */
101
380
  async findClinicsInRadius(
102
381
  center: { latitude: number; longitude: number },
103
382
  radiusInKm: number,
104
383
  filters?: {
105
- services?: string[];
384
+ procedures?: string[];
106
385
  tags?: ClinicTag[];
386
+ // Add other relevant filters based on Clinic/ProcedureSummaryInfo fields
107
387
  }
108
388
  ): Promise<Clinic[]> {
389
+ console.warn(
390
+ "SearchUtils.findClinicsInRadius filter logic might need updating for proceduresInfo."
391
+ );
392
+ // Pass filters directly, assuming SearchUtils handles it.
109
393
  return SearchUtils.findClinicsInRadius(
110
394
  this.db,
111
395
  center,
@@ -114,52 +398,6 @@ export class ClinicService extends BaseService {
114
398
  );
115
399
  }
116
400
 
117
- /**
118
- * Ažurira kliniku
119
- */
120
- async updateClinic(
121
- clinicId: string,
122
- data: Partial<Clinic>,
123
- adminId: string
124
- ): Promise<Clinic> {
125
- return ClinicUtils.updateClinic(
126
- this.db,
127
- clinicId,
128
- data,
129
- adminId,
130
- this.clinicAdminService,
131
- this.app
132
- );
133
- }
134
-
135
- /**
136
- * Dodaje recenziju klinici
137
- */
138
- async addReview(
139
- clinicId: string,
140
- review: Omit<
141
- ClinicReview,
142
- "id" | "clinicId" | "createdAt" | "updatedAt" | "isVerified"
143
- >
144
- ): Promise<ClinicReview> {
145
- return ReviewUtils.addReview(this.db, clinicId, review, this.app);
146
- }
147
-
148
- /**
149
- * Deaktivira kliniku
150
- */
151
- async deactivateClinic(clinicId: string, adminId: string): Promise<void> {
152
- return ClinicUtils.deactivateClinic(
153
- this.db,
154
- clinicId,
155
- adminId,
156
- this.clinicAdminService
157
- );
158
- }
159
-
160
- /**
161
- * Dodaje tagove klinici
162
- */
163
401
  async addTags(
164
402
  clinicId: string,
165
403
  adminId: string,
@@ -177,9 +415,6 @@ export class ClinicService extends BaseService {
177
415
  );
178
416
  }
179
417
 
180
- /**
181
- * Uklanja tagove iz klinike
182
- */
183
418
  async removeTags(
184
419
  clinicId: string,
185
420
  adminId: string,
@@ -197,9 +432,6 @@ export class ClinicService extends BaseService {
197
432
  );
198
433
  }
199
434
 
200
- /**
201
- * Dohvata sve klinike gde je korisnik admin
202
- */
203
435
  async getClinicsByAdmin(
204
436
  adminId: string,
205
437
  options?: {
@@ -216,9 +448,6 @@ export class ClinicService extends BaseService {
216
448
  );
217
449
  }
218
450
 
219
- /**
220
- * Dohvata sve aktivne klinike gde je korisnik admin
221
- */
222
451
  async getActiveClinicsByAdmin(adminId: string): Promise<Clinic[]> {
223
452
  return ClinicUtils.getActiveClinicsByAdmin(
224
453
  this.db,
@@ -228,14 +457,6 @@ export class ClinicService extends BaseService {
228
457
  );
229
458
  }
230
459
 
231
- /**
232
- * Creates a new clinic branch for a clinic group
233
- *
234
- * @param clinicGroupId - The ID of the clinic group
235
- * @param setupData - The setup data for the clinic branch
236
- * @param adminId - The ID of the admin creating the branch
237
- * @returns The created clinic
238
- */
239
460
  async createClinicBranch(
240
461
  clinicGroupId: string,
241
462
  setupData: ClinicBranchSetupData,
@@ -276,7 +497,7 @@ export class ClinicService extends BaseService {
276
497
  coverPhoto: setupData.coverPhoto || null,
277
498
  photosWithTags: setupData.photosWithTags || [],
278
499
  doctors: [],
279
- services: [],
500
+ procedures: [],
280
501
  admins: [adminId],
281
502
  isActive: true,
282
503
  isVerified: false,
@@ -302,23 +523,10 @@ export class ClinicService extends BaseService {
302
523
  return clinic;
303
524
  }
304
525
 
305
- /**
306
- * Retrieves a clinic by its ID
307
- *
308
- * @param clinicId - ID of the clinic to retrieve
309
- * @returns The clinic if found, null otherwise
310
- */
311
526
  async getClinicById(clinicId: string): Promise<Clinic | null> {
312
527
  return ClinicUtils.getClinicById(this.db, clinicId);
313
528
  }
314
529
 
315
- /**
316
- * Retrieves all clinics with optional pagination
317
- *
318
- * @param pagination - Optional number of clinics per page (0 or undefined returns all)
319
- * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
320
- * @returns Array of clinics and the last document for pagination
321
- */
322
530
  async getAllClinics(
323
531
  pagination?: number,
324
532
  lastDoc?: any
@@ -326,33 +534,38 @@ export class ClinicService extends BaseService {
326
534
  return ClinicUtils.getAllClinics(this.db, pagination, lastDoc);
327
535
  }
328
536
 
329
- /**
330
- * Retrieves all clinics within a specified range from a location with optional pagination
331
- *
332
- * @param center - The center location coordinates {latitude, longitude}
333
- * @param rangeInKm - The range in kilometers to search within
334
- * @param pagination - Optional number of clinics per page (0 or undefined returns all)
335
- * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
336
- * @param filters - Optional filters to apply to the search (isActive, tags, etc.)
337
- * @returns Array of clinics with distance information and the last document for pagination
338
- */
339
537
  async getAllClinicsInRange(
340
538
  center: { latitude: number; longitude: number },
341
539
  rangeInKm: number,
342
540
  pagination?: number,
343
- lastDoc?: any,
344
- filters?: {
345
- isActive?: boolean;
346
- tags?: ClinicTag[];
347
- }
541
+ lastDoc?: any
348
542
  ): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
349
543
  return ClinicUtils.getAllClinicsInRange(
350
544
  this.db,
351
545
  center,
352
546
  rangeInKm,
353
547
  pagination,
354
- lastDoc,
355
- filters
548
+ lastDoc
356
549
  );
357
550
  }
551
+
552
+ async getClinicsByFilters(filters: {
553
+ center?: { latitude: number; longitude: number };
554
+ radiusInKm?: number;
555
+ tags?: ClinicTag[];
556
+ procedureFamily?: string;
557
+ procedureCategory?: string;
558
+ procedureSubcategory?: string;
559
+ procedureTechnology?: string;
560
+ minRating?: number;
561
+ maxRating?: number;
562
+ pagination?: number;
563
+ lastDoc?: any;
564
+ isActive?: boolean;
565
+ }): Promise<{
566
+ clinics: (Clinic & { distance?: number })[];
567
+ lastDoc: any;
568
+ }> {
569
+ return FilterUtils.getClinicsByFilters(this.db, filters);
570
+ }
358
571
  }