@blackcode_sa/metaestetics-api 1.13.3 → 1.13.5

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 (293) hide show
  1. package/dist/admin/index.d.mts +15 -28
  2. package/dist/admin/index.d.ts +15 -28
  3. package/dist/index.d.mts +18 -30
  4. package/dist/index.d.ts +18 -30
  5. package/dist/index.js +11 -3
  6. package/dist/index.mjs +11 -3
  7. package/package.json +121 -119
  8. package/src/__mocks__/firstore.ts +10 -10
  9. package/src/admin/aggregation/README.md +79 -79
  10. package/src/admin/aggregation/appointment/README.md +128 -128
  11. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  12. package/src/admin/aggregation/appointment/index.ts +1 -1
  13. package/src/admin/aggregation/clinic/README.md +52 -52
  14. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  15. package/src/admin/aggregation/clinic/index.ts +1 -1
  16. package/src/admin/aggregation/forms/README.md +13 -13
  17. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  18. package/src/admin/aggregation/forms/index.ts +1 -1
  19. package/src/admin/aggregation/index.ts +8 -8
  20. package/src/admin/aggregation/patient/README.md +27 -27
  21. package/src/admin/aggregation/patient/index.ts +1 -1
  22. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  23. package/src/admin/aggregation/practitioner/README.md +42 -42
  24. package/src/admin/aggregation/practitioner/index.ts +1 -1
  25. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  26. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  28. package/src/admin/aggregation/procedure/README.md +43 -43
  29. package/src/admin/aggregation/procedure/index.ts +1 -1
  30. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  31. package/src/admin/aggregation/reviews/index.ts +1 -1
  32. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  33. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  34. package/src/admin/analytics/index.ts +2 -2
  35. package/src/admin/booking/README.md +125 -125
  36. package/src/admin/booking/booking.admin.ts +1037 -1037
  37. package/src/admin/booking/booking.calculator.ts +712 -712
  38. package/src/admin/booking/booking.types.ts +59 -59
  39. package/src/admin/booking/index.ts +3 -3
  40. package/src/admin/booking/timezones-problem.md +185 -185
  41. package/src/admin/calendar/README.md +7 -7
  42. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  43. package/src/admin/calendar/index.ts +1 -1
  44. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  45. package/src/admin/documentation-templates/index.ts +1 -1
  46. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  47. package/src/admin/free-consultation/index.ts +1 -1
  48. package/src/admin/index.ts +81 -81
  49. package/src/admin/logger/index.ts +78 -78
  50. package/src/admin/mailing/README.md +95 -95
  51. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  52. package/src/admin/mailing/appointment/index.ts +1 -1
  53. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  54. package/src/admin/mailing/base.mailing.service.ts +208 -208
  55. package/src/admin/mailing/index.ts +3 -3
  56. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  57. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  58. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  59. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  60. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  61. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  62. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  63. package/src/admin/notifications/index.ts +1 -1
  64. package/src/admin/notifications/notifications.admin.ts +710 -710
  65. package/src/admin/requirements/README.md +128 -128
  66. package/src/admin/requirements/index.ts +1 -1
  67. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  68. package/src/admin/users/index.ts +1 -1
  69. package/src/admin/users/user-profile.admin.ts +405 -405
  70. package/src/backoffice/constants/certification.constants.ts +13 -13
  71. package/src/backoffice/constants/index.ts +1 -1
  72. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  73. package/src/backoffice/errors/index.ts +1 -1
  74. package/src/backoffice/expo-safe/README.md +26 -26
  75. package/src/backoffice/expo-safe/index.ts +41 -41
  76. package/src/backoffice/index.ts +5 -5
  77. package/src/backoffice/services/FIXES_README.md +102 -102
  78. package/src/backoffice/services/README.md +57 -57
  79. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  80. package/src/backoffice/services/analytics.service.summary.md +143 -143
  81. package/src/backoffice/services/brand.service.ts +256 -256
  82. package/src/backoffice/services/category.service.ts +384 -384
  83. package/src/backoffice/services/constants.service.ts +385 -385
  84. package/src/backoffice/services/documentation-template.service.ts +202 -202
  85. package/src/backoffice/services/index.ts +10 -10
  86. package/src/backoffice/services/migrate-products.ts +116 -116
  87. package/src/backoffice/services/product.service.ts +553 -553
  88. package/src/backoffice/services/requirement.service.ts +235 -235
  89. package/src/backoffice/services/subcategory.service.ts +461 -461
  90. package/src/backoffice/services/technology.service.ts +1151 -1151
  91. package/src/backoffice/types/README.md +12 -12
  92. package/src/backoffice/types/admin-constants.types.ts +69 -69
  93. package/src/backoffice/types/brand.types.ts +29 -29
  94. package/src/backoffice/types/category.types.ts +67 -67
  95. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  96. package/src/backoffice/types/index.ts +10 -10
  97. package/src/backoffice/types/procedure-product.types.ts +38 -38
  98. package/src/backoffice/types/product.types.ts +240 -240
  99. package/src/backoffice/types/requirement.types.ts +63 -63
  100. package/src/backoffice/types/static/README.md +18 -18
  101. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  102. package/src/backoffice/types/static/certification.types.ts +37 -37
  103. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  104. package/src/backoffice/types/static/index.ts +6 -6
  105. package/src/backoffice/types/static/pricing.types.ts +16 -16
  106. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  107. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  108. package/src/backoffice/types/subcategory.types.ts +34 -34
  109. package/src/backoffice/types/technology.types.ts +168 -168
  110. package/src/backoffice/validations/index.ts +1 -1
  111. package/src/backoffice/validations/schemas.ts +164 -164
  112. package/src/config/__mocks__/firebase.ts +99 -99
  113. package/src/config/firebase.ts +78 -78
  114. package/src/config/index.ts +9 -9
  115. package/src/errors/auth.error.ts +6 -6
  116. package/src/errors/auth.errors.ts +200 -200
  117. package/src/errors/clinic.errors.ts +32 -32
  118. package/src/errors/firebase.errors.ts +47 -47
  119. package/src/errors/user.errors.ts +99 -99
  120. package/src/index.backup.ts +407 -407
  121. package/src/index.ts +6 -6
  122. package/src/locales/en.ts +31 -31
  123. package/src/recommender/admin/index.ts +1 -1
  124. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  125. package/src/recommender/front/index.ts +1 -1
  126. package/src/recommender/front/services/onboarding.service.ts +5 -5
  127. package/src/recommender/front/services/recommender.service.ts +3 -3
  128. package/src/recommender/index.ts +1 -1
  129. package/src/services/PATIENTAUTH.MD +197 -197
  130. package/src/services/README.md +106 -106
  131. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  132. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  133. package/src/services/__tests__/auth.service.test.ts +346 -346
  134. package/src/services/__tests__/base.service.test.ts +77 -77
  135. package/src/services/__tests__/user.service.test.ts +528 -528
  136. package/src/services/analytics/ARCHITECTURE.md +199 -199
  137. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  138. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  139. package/src/services/analytics/QUICK_START.md +393 -393
  140. package/src/services/analytics/README.md +304 -304
  141. package/src/services/analytics/SUMMARY.md +141 -141
  142. package/src/services/analytics/TRENDS.md +380 -380
  143. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  144. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  145. package/src/services/analytics/analytics.service.ts +2142 -2142
  146. package/src/services/analytics/index.ts +4 -4
  147. package/src/services/analytics/review-analytics.service.ts +941 -941
  148. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  149. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  150. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  151. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  152. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  153. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  154. package/src/services/appointment/README.md +17 -17
  155. package/src/services/appointment/appointment.service.ts +2558 -2558
  156. package/src/services/appointment/index.ts +1 -1
  157. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  158. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  159. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  160. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  161. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  162. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  163. package/src/services/auth/auth.service.ts +989 -989
  164. package/src/services/auth/auth.v2.service.ts +961 -961
  165. package/src/services/auth/index.ts +7 -7
  166. package/src/services/auth/utils/error.utils.ts +90 -90
  167. package/src/services/auth/utils/firebase.utils.ts +49 -49
  168. package/src/services/auth/utils/index.ts +21 -21
  169. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  170. package/src/services/base.service.ts +41 -41
  171. package/src/services/calendar/calendar.service.ts +1077 -1077
  172. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  173. package/src/services/calendar/calendar.v3.service.ts +313 -313
  174. package/src/services/calendar/externalCalendar.service.ts +178 -178
  175. package/src/services/calendar/index.ts +5 -5
  176. package/src/services/calendar/synced-calendars.service.ts +743 -743
  177. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  178. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  179. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  180. package/src/services/calendar/utils/docs.utils.ts +157 -157
  181. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  182. package/src/services/calendar/utils/index.ts +8 -8
  183. package/src/services/calendar/utils/patient.utils.ts +198 -198
  184. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  185. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  186. package/src/services/clinic/README.md +204 -204
  187. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  188. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  189. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  190. package/src/services/clinic/billing-transactions.service.ts +217 -217
  191. package/src/services/clinic/clinic-admin.service.ts +202 -202
  192. package/src/services/clinic/clinic-group.service.ts +310 -310
  193. package/src/services/clinic/clinic.service.ts +708 -708
  194. package/src/services/clinic/index.ts +5 -5
  195. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  196. package/src/services/clinic/utils/admin.utils.ts +551 -551
  197. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  198. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  199. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  200. package/src/services/clinic/utils/filter.utils.ts +446 -446
  201. package/src/services/clinic/utils/index.ts +11 -11
  202. package/src/services/clinic/utils/photos.utils.ts +188 -188
  203. package/src/services/clinic/utils/search.utils.ts +84 -84
  204. package/src/services/clinic/utils/tag.utils.ts +124 -124
  205. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  206. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  207. package/src/services/documentation-templates/index.ts +2 -2
  208. package/src/services/index.ts +14 -14
  209. package/src/services/media/index.ts +1 -1
  210. package/src/services/media/media.service.ts +418 -418
  211. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  212. package/src/services/notifications/index.ts +1 -1
  213. package/src/services/notifications/notification.service.ts +215 -215
  214. package/src/services/patient/README.md +48 -48
  215. package/src/services/patient/To-Do.md +43 -43
  216. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  217. package/src/services/patient/index.ts +2 -2
  218. package/src/services/patient/patient.service.ts +883 -883
  219. package/src/services/patient/patientRequirements.service.ts +285 -285
  220. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  221. package/src/services/patient/utils/clinic.utils.ts +80 -80
  222. package/src/services/patient/utils/docs.utils.ts +142 -142
  223. package/src/services/patient/utils/index.ts +9 -9
  224. package/src/services/patient/utils/location.utils.ts +126 -126
  225. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  226. package/src/services/patient/utils/medical.utils.ts +458 -458
  227. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  228. package/src/services/patient/utils/profile.utils.ts +510 -510
  229. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  230. package/src/services/patient/utils/token.utils.ts +211 -211
  231. package/src/services/practitioner/README.md +145 -145
  232. package/src/services/practitioner/index.ts +1 -1
  233. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  234. package/src/services/procedure/README.md +163 -163
  235. package/src/services/procedure/index.ts +1 -1
  236. package/src/services/procedure/procedure.service.ts +2200 -2191
  237. package/src/services/reviews/index.ts +1 -1
  238. package/src/services/reviews/reviews.service.ts +734 -734
  239. package/src/services/user/index.ts +1 -1
  240. package/src/services/user/user.service.ts +489 -489
  241. package/src/services/user/user.v2.service.ts +466 -466
  242. package/src/types/analytics/analytics.types.ts +597 -597
  243. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  244. package/src/types/analytics/index.ts +4 -4
  245. package/src/types/analytics/stored-analytics.types.ts +137 -137
  246. package/src/types/appointment/index.ts +480 -480
  247. package/src/types/calendar/index.ts +258 -258
  248. package/src/types/calendar/synced-calendar.types.ts +66 -66
  249. package/src/types/clinic/index.ts +498 -489
  250. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  251. package/src/types/clinic/preferences.types.ts +159 -159
  252. package/src/types/clinic/to-do +3 -3
  253. package/src/types/documentation-templates/index.ts +308 -308
  254. package/src/types/index.ts +47 -47
  255. package/src/types/notifications/README.md +77 -77
  256. package/src/types/notifications/index.ts +286 -286
  257. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  258. package/src/types/patient/allergies.ts +58 -58
  259. package/src/types/patient/index.ts +275 -275
  260. package/src/types/patient/medical-info.types.ts +152 -152
  261. package/src/types/patient/patient-requirements.ts +92 -92
  262. package/src/types/patient/token.types.ts +61 -61
  263. package/src/types/practitioner/index.ts +206 -206
  264. package/src/types/procedure/index.ts +181 -181
  265. package/src/types/profile/index.ts +39 -39
  266. package/src/types/reviews/index.ts +132 -132
  267. package/src/types/tz-lookup.d.ts +4 -4
  268. package/src/types/user/index.ts +38 -38
  269. package/src/utils/TIMESTAMPS.md +176 -176
  270. package/src/utils/TimestampUtils.ts +241 -241
  271. package/src/utils/index.ts +1 -1
  272. package/src/validations/appointment.schema.ts +574 -574
  273. package/src/validations/calendar.schema.ts +225 -225
  274. package/src/validations/clinic.schema.ts +494 -493
  275. package/src/validations/common.schema.ts +25 -25
  276. package/src/validations/documentation-templates/index.ts +1 -1
  277. package/src/validations/documentation-templates/template.schema.ts +220 -220
  278. package/src/validations/documentation-templates.schema.ts +10 -10
  279. package/src/validations/index.ts +20 -20
  280. package/src/validations/media.schema.ts +10 -10
  281. package/src/validations/notification.schema.ts +90 -90
  282. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  283. package/src/validations/patient/medical-info.schema.ts +125 -125
  284. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  285. package/src/validations/patient/token.schema.ts +29 -29
  286. package/src/validations/patient.schema.ts +217 -217
  287. package/src/validations/practitioner.schema.ts +222 -222
  288. package/src/validations/procedure-product.schema.ts +41 -41
  289. package/src/validations/procedure.schema.ts +124 -124
  290. package/src/validations/profile-info.schema.ts +41 -41
  291. package/src/validations/reviews.schema.ts +195 -195
  292. package/src/validations/schemas.ts +104 -104
  293. package/src/validations/shared.schema.ts +78 -78
@@ -1,552 +1,552 @@
1
- import {
2
- Firestore,
3
- collection,
4
- doc,
5
- getDoc,
6
- getDocs,
7
- query,
8
- where,
9
- setDoc,
10
- updateDoc,
11
- serverTimestamp,
12
- Timestamp,
13
- orderBy,
14
- limit,
15
- startAfter,
16
- QueryConstraint,
17
- DocumentSnapshot,
18
- } from 'firebase/firestore';
19
- import {
20
- Appointment,
21
- AppointmentStatus,
22
- CreateAppointmentData,
23
- UpdateAppointmentData,
24
- APPOINTMENTS_COLLECTION,
25
- SearchAppointmentsParams,
26
- PaymentStatus,
27
- } from '../../../types/appointment';
28
- import { CalendarEvent, CALENDAR_COLLECTION } from '../../../types/calendar';
29
- import { ProcedureSummaryInfo } from '../../../types/procedure';
30
- import { ClinicInfo, PatientProfileInfo, PractitionerProfileInfo } from '../../../types/profile';
31
- import { BlockingCondition } from '../../../backoffice/types/static/blocking-condition.types';
32
- import { Requirement } from '../../../backoffice/types/requirement.types';
33
- import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
34
- import { CLINICS_COLLECTION } from '../../../types/clinic';
35
- import { PATIENTS_COLLECTION } from '../../../types/patient';
36
- import { PROCEDURES_COLLECTION } from '../../../types/procedure';
37
- import { Technology, TECHNOLOGIES_COLLECTION } from '../../../backoffice/types/technology.types';
38
- import type { ContraindicationDynamic } from '../../../backoffice';
39
-
40
- /**
41
- * Fetches all the necessary information for an appointment by IDs.
42
- *
43
- * @param db Firestore instance
44
- * @param clinicId Clinic ID
45
- * @param practitionerId Practitioner ID
46
- * @param patientId Patient ID
47
- * @param procedureId Procedure ID
48
- * @returns Object containing the aggregated information
49
- */
50
- export async function fetchAggregatedInfoUtil(
51
- db: Firestore,
52
- clinicId: string,
53
- practitionerId: string,
54
- patientId: string,
55
- procedureId: string,
56
- ): Promise<{
57
- clinicInfo: ClinicInfo;
58
- practitionerInfo: PractitionerProfileInfo;
59
- patientInfo: PatientProfileInfo;
60
- procedureInfo: ProcedureSummaryInfo;
61
- blockingConditions: BlockingCondition[];
62
- contraindications: ContraindicationDynamic[];
63
- preProcedureRequirements: Requirement[];
64
- postProcedureRequirements: Requirement[];
65
- }> {
66
- try {
67
- // Fetch all data in parallel for efficiency
68
- const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] = await Promise.all([
69
- getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
70
- getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
71
- getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
72
- getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
73
- ]);
74
-
75
- // Check if all required entities exist
76
- if (!clinicDoc.exists()) {
77
- throw new Error(`Clinic with ID ${clinicId} not found`);
78
- }
79
- if (!practitionerDoc.exists()) {
80
- throw new Error(`Practitioner with ID ${practitionerId} not found`);
81
- }
82
- if (!patientDoc.exists()) {
83
- throw new Error(`Patient with ID ${patientId} not found`);
84
- }
85
- if (!procedureDoc.exists()) {
86
- throw new Error(`Procedure with ID ${procedureId} not found`);
87
- }
88
-
89
- const clinicData = clinicDoc.data();
90
- const practitionerData = practitionerDoc.data();
91
- const patientData = patientDoc.data();
92
- const procedureData = procedureDoc.data();
93
-
94
- // Extract relevant info for ClinicInfo
95
- const clinicInfo: ClinicInfo = {
96
- id: clinicId,
97
- featuredPhoto: clinicData.featuredPhotos?.[0] || '',
98
- name: clinicData.name,
99
- description: clinicData.description || null,
100
- location: clinicData.location,
101
- contactInfo: clinicData.contactInfo,
102
- };
103
-
104
- // Extract relevant info for PractitionerProfileInfo
105
- const practitionerInfo: PractitionerProfileInfo = {
106
- id: practitionerId,
107
- practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
108
- name: `${practitionerData.basicInfo?.firstName || ''} ${
109
- practitionerData.basicInfo?.lastName || ''
110
- }`.trim(),
111
- email: practitionerData.basicInfo?.email || '',
112
- phone: practitionerData.basicInfo?.phoneNumber || null,
113
- certification: practitionerData.certification,
114
- };
115
-
116
- // Extract relevant info for PatientProfileInfo
117
- // Note: This may need adjustment depending on how patient data is structured
118
- const patientInfo: PatientProfileInfo = {
119
- id: patientId,
120
- fullName: patientData.displayName || '',
121
- email: patientData.email || '',
122
- phone: patientData.phoneNumber || null,
123
- dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
124
- gender: patientData.gender || 'other',
125
- };
126
-
127
- // Extract procedureInfo from the procedure document
128
- // Assuming procedureData already has a procedureInfo property or similar structure
129
- const procedureInfo: ProcedureSummaryInfo = {
130
- id: procedureId,
131
- name: procedureData.name,
132
- description: procedureData.description,
133
- photo: procedureData.photo || '',
134
- family: procedureData.family,
135
- categoryName: procedureData.category?.name || '',
136
- subcategoryName: procedureData.subcategory?.name || '',
137
- technologyName: procedureData.technology?.name || '',
138
- brandName: procedureData.product?.brandName || '', // Safe: optional chaining
139
- productName: procedureData.product?.name || '', // Safe: optional chaining
140
- price: procedureData.price || 0,
141
- pricingMeasure: procedureData.pricingMeasure,
142
- currency: procedureData.currency,
143
- duration: procedureData.duration || 0,
144
- clinicId: clinicId,
145
- clinicName: clinicInfo.name,
146
- practitionerId: practitionerId,
147
- practitionerName: practitionerInfo.name,
148
- };
149
-
150
- // Fetch the technology document to get procedure requirements
151
- let technologyId = '';
152
- if (procedureData.technology?.id) {
153
- technologyId = procedureData.technology.id;
154
- }
155
-
156
- let blockingConditions: BlockingCondition[] = [];
157
- let contraindications: ContraindicationDynamic[] = [];
158
- let preProcedureRequirements: Requirement[] = [];
159
- let postProcedureRequirements: Requirement[] = [];
160
-
161
- // If we have a technology ID, fetch its details
162
- if (technologyId) {
163
- const technologyDoc = await getDoc(doc(db, TECHNOLOGIES_COLLECTION, technologyId));
164
- if (technologyDoc.exists()) {
165
- const technologyData = technologyDoc.data() as Technology;
166
-
167
- // Extract technology-related info
168
- blockingConditions = technologyData.blockingConditions || [];
169
- contraindications = technologyData.contraindications || [];
170
- preProcedureRequirements = technologyData.requirements?.pre || [];
171
- postProcedureRequirements = technologyData.requirements?.post || [];
172
- }
173
- } else {
174
- // Fallback to procedure-level data if technology not available
175
- blockingConditions = procedureData.blockingConditions || [];
176
- contraindications = procedureData.contraindications || [];
177
- preProcedureRequirements = procedureData.preRequirements || [];
178
- postProcedureRequirements = procedureData.postRequirements || [];
179
- }
180
-
181
- return {
182
- clinicInfo,
183
- practitionerInfo,
184
- patientInfo,
185
- procedureInfo,
186
- blockingConditions,
187
- contraindications,
188
- preProcedureRequirements,
189
- postProcedureRequirements,
190
- };
191
- } catch (error) {
192
- console.error('Error fetching aggregated info:', error);
193
- throw error;
194
- }
195
- }
196
-
197
- /**
198
- * Creates a new appointment in Firestore.
199
- *
200
- * @param db Firestore instance
201
- * @param data Data needed to create the appointment
202
- * @param aggregatedInfo Already fetched and aggregated info
203
- * @param generateId Function to generate a unique ID
204
- * @returns The created Appointment
205
- */
206
- // export async function createAppointmentUtil(
207
- // db: Firestore,
208
- // data: CreateAppointmentData,
209
- // aggregatedInfo: {
210
- // clinicInfo: ClinicInfo;
211
- // practitionerInfo: PractitionerProfileInfo;
212
- // patientInfo: PatientProfileInfo;
213
- // procedureInfo: ProcedureSummaryInfo;
214
- // blockingConditions: BlockingCondition[];
215
- // contraindications: ContraindicationDynamic[];
216
- // preProcedureRequirements: Requirement[];
217
- // postProcedureRequirements: Requirement[];
218
- // },
219
- // generateId: () => string
220
- // ): Promise<Appointment> {
221
- // try {
222
- // const appointmentId = generateId();
223
-
224
- // // Create appointment object
225
- // const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
226
- // createdAt: any;
227
- // updatedAt: any;
228
- // } = {
229
- // id: appointmentId,
230
- // calendarEventId: data.calendarEventId,
231
- // clinicBranchId: data.clinicBranchId,
232
- // clinicInfo: aggregatedInfo.clinicInfo,
233
- // practitionerId: data.practitionerId,
234
- // practitionerInfo: aggregatedInfo.practitionerInfo,
235
- // patientId: data.patientId,
236
- // patientInfo: aggregatedInfo.patientInfo,
237
- // procedureId: data.procedureId,
238
- // procedureInfo: aggregatedInfo.procedureInfo,
239
- // status: data.initialStatus,
240
- // bookingTime: Timestamp.now(),
241
- // appointmentStartTime: data.appointmentStartTime,
242
- // appointmentEndTime: data.appointmentEndTime,
243
- // patientNotes: data.patientNotes || null,
244
- // cost: data.cost,
245
- // currency: data.currency,
246
- // paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
247
- // blockingConditions: aggregatedInfo.blockingConditions,
248
- // contraindications: aggregatedInfo.contraindications,
249
- // preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
250
- // postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
251
- // completedPreRequirements: [],
252
- // completedPostRequirements: [],
253
- // createdAt: serverTimestamp(),
254
- // updatedAt: serverTimestamp(),
255
- // };
256
-
257
- // // Add additional fields for confirmation if appointment is already confirmed
258
- // if (data.initialStatus === AppointmentStatus.CONFIRMED) {
259
- // appointment.confirmationTime = Timestamp.now();
260
- // }
261
-
262
- // // Save to Firestore
263
- // await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
264
-
265
- // // Update the calendar event with the appointment ID
266
- // const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
267
- // await updateDoc(calendarEventRef, {
268
- // appointmentId: appointmentId,
269
- // updatedAt: serverTimestamp(),
270
- // });
271
-
272
- // // Return the created appointment
273
- // // Convert serverTimestamp to regular Timestamp for immediate use
274
- // const now = Timestamp.now();
275
- // return {
276
- // ...appointment,
277
- // createdAt: now,
278
- // updatedAt: now,
279
- // } as Appointment;
280
- // } catch (error) {
281
- // console.error("Error creating appointment:", error);
282
- // throw error;
283
- // }
284
- // }
285
-
286
- /**
287
- * Updates an existing appointment in Firestore.
288
- *
289
- * @param db Firestore instance
290
- * @param appointmentId ID of the appointment to update
291
- * @param data Update data for the appointment
292
- * @returns The updated Appointment
293
- */
294
- export async function updateAppointmentUtil(
295
- db: Firestore,
296
- appointmentId: string,
297
- data: UpdateAppointmentData,
298
- ): Promise<Appointment> {
299
- try {
300
- const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
301
- const appointmentDoc = await getDoc(appointmentRef);
302
-
303
- if (!appointmentDoc.exists()) {
304
- throw new Error(`Appointment with ID ${appointmentId} not found`);
305
- }
306
-
307
- const currentAppointment = appointmentDoc.data() as Appointment;
308
-
309
- // Handle requirement completion tracking
310
- let completedPreRequirements = currentAppointment.completedPreRequirements || [];
311
- let completedPostRequirements = currentAppointment.completedPostRequirements || [];
312
-
313
- if (data.completedPreRequirements) {
314
- // Validate that all IDs exist in the pre-requirements
315
- const validPreReqIds = currentAppointment.preProcedureRequirements.map(req => req.id);
316
-
317
- // Only perform validation and merging if the input is an array
318
- if (Array.isArray(data.completedPreRequirements)) {
319
- const invalidPreReqIds = data.completedPreRequirements.filter(
320
- id => !validPreReqIds.includes(id),
321
- );
322
-
323
- if (invalidPreReqIds.length > 0) {
324
- throw new Error(`Invalid pre-requirement IDs: ${invalidPreReqIds.join(', ')}`);
325
- }
326
-
327
- // Update the completed pre-requirements
328
- completedPreRequirements = [
329
- ...new Set([...completedPreRequirements, ...data.completedPreRequirements]),
330
- ];
331
- }
332
- }
333
-
334
- if (data.completedPostRequirements) {
335
- // Validate that all IDs exist in the post-requirements
336
- const validPostReqIds = currentAppointment.postProcedureRequirements.map(req => req.id);
337
-
338
- if (Array.isArray(data.completedPostRequirements)) {
339
- const invalidPostReqIds = data.completedPostRequirements.filter(
340
- id => !validPostReqIds.includes(id),
341
- );
342
-
343
- if (invalidPostReqIds.length > 0) {
344
- throw new Error(`Invalid post-requirement IDs: ${invalidPostReqIds.join(', ')}`);
345
- }
346
-
347
- // Update the completed post-requirements
348
- completedPostRequirements = [
349
- ...new Set([...completedPostRequirements, ...data.completedPostRequirements]),
350
- ];
351
- }
352
- }
353
-
354
- // Prepare update data
355
- const updateData: any = {
356
- ...data,
357
- completedPreRequirements: Array.isArray(data.completedPreRequirements)
358
- ? completedPreRequirements
359
- : data.completedPreRequirements,
360
- completedPostRequirements: Array.isArray(data.completedPostRequirements)
361
- ? completedPostRequirements
362
- : data.completedPostRequirements,
363
- updatedAt: serverTimestamp(),
364
- };
365
-
366
- // Remove undefined fields
367
- Object.keys(updateData).forEach(key => {
368
- if (updateData[key] === undefined) {
369
- delete updateData[key];
370
- }
371
- });
372
-
373
- // Handle status changes
374
- if (data.status && data.status !== currentAppointment.status) {
375
- // Handle confirmation
376
- if (data.status === AppointmentStatus.CONFIRMED && !updateData.confirmationTime) {
377
- updateData.confirmationTime = Timestamp.now();
378
- }
379
-
380
- // Update the related calendar event status if needed
381
- if (currentAppointment.calendarEventId) {
382
- await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
383
- }
384
- }
385
-
386
- // Update the appointment
387
- await updateDoc(appointmentRef, updateData);
388
-
389
- // Fetch the updated appointment
390
- const updatedAppointmentDoc = await getDoc(appointmentRef);
391
- if (!updatedAppointmentDoc.exists()) {
392
- throw new Error(`Failed to retrieve updated appointment ${appointmentId}`);
393
- }
394
-
395
- return updatedAppointmentDoc.data() as Appointment;
396
- } catch (error) {
397
- console.error(`Error updating appointment ${appointmentId}:`, error);
398
- throw error;
399
- }
400
- }
401
-
402
- /**
403
- * Updates the status of a calendar event based on appointment status changes.
404
- *
405
- * @param db Firestore instance
406
- * @param calendarEventId ID of the calendar event
407
- * @param appointmentStatus New appointment status
408
- */
409
- async function updateCalendarEventStatus(
410
- db: Firestore,
411
- calendarEventId: string,
412
- appointmentStatus: AppointmentStatus,
413
- ): Promise<void> {
414
- try {
415
- const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
416
- const calendarEventDoc = await getDoc(calendarEventRef);
417
-
418
- if (!calendarEventDoc.exists()) {
419
- console.warn(`Calendar event with ID ${calendarEventId} not found`);
420
- return;
421
- }
422
-
423
- // Map appointment status to calendar event status
424
- let calendarStatus;
425
- switch (appointmentStatus) {
426
- case AppointmentStatus.CONFIRMED:
427
- calendarStatus = 'confirmed';
428
- break;
429
- case AppointmentStatus.CANCELED_PATIENT:
430
- case AppointmentStatus.CANCELED_CLINIC:
431
- calendarStatus = 'canceled';
432
- break;
433
- case AppointmentStatus.RESCHEDULED_BY_CLINIC:
434
- calendarStatus = 'rescheduled';
435
- break;
436
- case AppointmentStatus.COMPLETED:
437
- calendarStatus = 'completed';
438
- break;
439
- default:
440
- // For other states, don't update the calendar status
441
- return;
442
- }
443
-
444
- await updateDoc(calendarEventRef, {
445
- status: calendarStatus,
446
- updatedAt: serverTimestamp(),
447
- });
448
- } catch (error) {
449
- console.error(`Error updating calendar event ${calendarEventId}:`, error);
450
- // Don't throw error to avoid failing the appointment update
451
- }
452
- }
453
-
454
- /**
455
- * Gets an appointment by its ID.
456
- *
457
- * @param db Firestore instance
458
- * @param appointmentId Appointment ID
459
- * @returns The appointment or null if not found
460
- */
461
- export async function getAppointmentByIdUtil(
462
- db: Firestore,
463
- appointmentId: string,
464
- ): Promise<Appointment | null> {
465
- try {
466
- const appointmentDoc = await getDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId));
467
-
468
- if (!appointmentDoc.exists()) {
469
- return null;
470
- }
471
-
472
- return appointmentDoc.data() as Appointment;
473
- } catch (error) {
474
- console.error(`Error getting appointment ${appointmentId}:`, error);
475
- throw error;
476
- }
477
- }
478
-
479
- /**
480
- * Searches for appointments based on various criteria.
481
- *
482
- * @param db Firestore instance
483
- * @param params Search parameters
484
- * @returns Found appointments and the last document for pagination
485
- */
486
- export async function searchAppointmentsUtil(
487
- db: Firestore,
488
- params: SearchAppointmentsParams,
489
- ): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
490
- try {
491
- const constraints: QueryConstraint[] = [];
492
-
493
- // Add filters based on provided params
494
- if (params.patientId) {
495
- constraints.push(where('patientId', '==', params.patientId));
496
- }
497
-
498
- if (params.practitionerId) {
499
- constraints.push(where('practitionerId', '==', params.practitionerId));
500
- }
501
-
502
- if (params.clinicBranchId) {
503
- constraints.push(where('clinicBranchId', '==', params.clinicBranchId));
504
- }
505
-
506
- if (params.startDate) {
507
- constraints.push(where('appointmentStartTime', '>=', Timestamp.fromDate(params.startDate)));
508
- }
509
-
510
- if (params.endDate) {
511
- constraints.push(where('appointmentStartTime', '<=', Timestamp.fromDate(params.endDate)));
512
- }
513
-
514
- if (params.status) {
515
- if (Array.isArray(params.status)) {
516
- // If multiple statuses, use in operator
517
- constraints.push(where('status', 'in', params.status));
518
- } else {
519
- // Single status
520
- constraints.push(where('status', '==', params.status));
521
- }
522
- }
523
-
524
- // Add ordering
525
- constraints.push(orderBy('appointmentStartTime', 'asc'));
526
-
527
- // Add pagination if specified
528
- if (params.limit) {
529
- constraints.push(limit(params.limit));
530
- }
531
-
532
- if (params.startAfter) {
533
- constraints.push(startAfter(params.startAfter));
534
- }
535
-
536
- // Execute query
537
- const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
538
- const querySnapshot = await getDocs(q);
539
-
540
- // Extract results
541
- const appointments = querySnapshot.docs.map(doc => doc.data() as Appointment);
542
-
543
- // Get last document for pagination
544
- const lastDoc =
545
- querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
546
-
547
- return { appointments, lastDoc };
548
- } catch (error) {
549
- console.error('Error searching appointments:', error);
550
- throw error;
551
- }
552
- }
1
+ import {
2
+ Firestore,
3
+ collection,
4
+ doc,
5
+ getDoc,
6
+ getDocs,
7
+ query,
8
+ where,
9
+ setDoc,
10
+ updateDoc,
11
+ serverTimestamp,
12
+ Timestamp,
13
+ orderBy,
14
+ limit,
15
+ startAfter,
16
+ QueryConstraint,
17
+ DocumentSnapshot,
18
+ } from 'firebase/firestore';
19
+ import {
20
+ Appointment,
21
+ AppointmentStatus,
22
+ CreateAppointmentData,
23
+ UpdateAppointmentData,
24
+ APPOINTMENTS_COLLECTION,
25
+ SearchAppointmentsParams,
26
+ PaymentStatus,
27
+ } from '../../../types/appointment';
28
+ import { CalendarEvent, CALENDAR_COLLECTION } from '../../../types/calendar';
29
+ import { ProcedureSummaryInfo } from '../../../types/procedure';
30
+ import { ClinicInfo, PatientProfileInfo, PractitionerProfileInfo } from '../../../types/profile';
31
+ import { BlockingCondition } from '../../../backoffice/types/static/blocking-condition.types';
32
+ import { Requirement } from '../../../backoffice/types/requirement.types';
33
+ import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
34
+ import { CLINICS_COLLECTION } from '../../../types/clinic';
35
+ import { PATIENTS_COLLECTION } from '../../../types/patient';
36
+ import { PROCEDURES_COLLECTION } from '../../../types/procedure';
37
+ import { Technology, TECHNOLOGIES_COLLECTION } from '../../../backoffice/types/technology.types';
38
+ import type { ContraindicationDynamic } from '../../../backoffice';
39
+
40
+ /**
41
+ * Fetches all the necessary information for an appointment by IDs.
42
+ *
43
+ * @param db Firestore instance
44
+ * @param clinicId Clinic ID
45
+ * @param practitionerId Practitioner ID
46
+ * @param patientId Patient ID
47
+ * @param procedureId Procedure ID
48
+ * @returns Object containing the aggregated information
49
+ */
50
+ export async function fetchAggregatedInfoUtil(
51
+ db: Firestore,
52
+ clinicId: string,
53
+ practitionerId: string,
54
+ patientId: string,
55
+ procedureId: string,
56
+ ): Promise<{
57
+ clinicInfo: ClinicInfo;
58
+ practitionerInfo: PractitionerProfileInfo;
59
+ patientInfo: PatientProfileInfo;
60
+ procedureInfo: ProcedureSummaryInfo;
61
+ blockingConditions: BlockingCondition[];
62
+ contraindications: ContraindicationDynamic[];
63
+ preProcedureRequirements: Requirement[];
64
+ postProcedureRequirements: Requirement[];
65
+ }> {
66
+ try {
67
+ // Fetch all data in parallel for efficiency
68
+ const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] = await Promise.all([
69
+ getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
70
+ getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
71
+ getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
72
+ getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
73
+ ]);
74
+
75
+ // Check if all required entities exist
76
+ if (!clinicDoc.exists()) {
77
+ throw new Error(`Clinic with ID ${clinicId} not found`);
78
+ }
79
+ if (!practitionerDoc.exists()) {
80
+ throw new Error(`Practitioner with ID ${practitionerId} not found`);
81
+ }
82
+ if (!patientDoc.exists()) {
83
+ throw new Error(`Patient with ID ${patientId} not found`);
84
+ }
85
+ if (!procedureDoc.exists()) {
86
+ throw new Error(`Procedure with ID ${procedureId} not found`);
87
+ }
88
+
89
+ const clinicData = clinicDoc.data();
90
+ const practitionerData = practitionerDoc.data();
91
+ const patientData = patientDoc.data();
92
+ const procedureData = procedureDoc.data();
93
+
94
+ // Extract relevant info for ClinicInfo
95
+ const clinicInfo: ClinicInfo = {
96
+ id: clinicId,
97
+ featuredPhoto: clinicData.featuredPhotos?.[0] || '',
98
+ name: clinicData.name,
99
+ description: clinicData.description || null,
100
+ location: clinicData.location,
101
+ contactInfo: clinicData.contactInfo,
102
+ };
103
+
104
+ // Extract relevant info for PractitionerProfileInfo
105
+ const practitionerInfo: PractitionerProfileInfo = {
106
+ id: practitionerId,
107
+ practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
108
+ name: `${practitionerData.basicInfo?.firstName || ''} ${
109
+ practitionerData.basicInfo?.lastName || ''
110
+ }`.trim(),
111
+ email: practitionerData.basicInfo?.email || '',
112
+ phone: practitionerData.basicInfo?.phoneNumber || null,
113
+ certification: practitionerData.certification,
114
+ };
115
+
116
+ // Extract relevant info for PatientProfileInfo
117
+ // Note: This may need adjustment depending on how patient data is structured
118
+ const patientInfo: PatientProfileInfo = {
119
+ id: patientId,
120
+ fullName: patientData.displayName || '',
121
+ email: patientData.email || '',
122
+ phone: patientData.phoneNumber || null,
123
+ dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
124
+ gender: patientData.gender || 'other',
125
+ };
126
+
127
+ // Extract procedureInfo from the procedure document
128
+ // Assuming procedureData already has a procedureInfo property or similar structure
129
+ const procedureInfo: ProcedureSummaryInfo = {
130
+ id: procedureId,
131
+ name: procedureData.name,
132
+ description: procedureData.description,
133
+ photo: procedureData.photo || '',
134
+ family: procedureData.family,
135
+ categoryName: procedureData.category?.name || '',
136
+ subcategoryName: procedureData.subcategory?.name || '',
137
+ technologyName: procedureData.technology?.name || '',
138
+ brandName: procedureData.product?.brandName || '', // Safe: optional chaining
139
+ productName: procedureData.product?.name || '', // Safe: optional chaining
140
+ price: procedureData.price || 0,
141
+ pricingMeasure: procedureData.pricingMeasure,
142
+ currency: procedureData.currency,
143
+ duration: procedureData.duration || 0,
144
+ clinicId: clinicId,
145
+ clinicName: clinicInfo.name,
146
+ practitionerId: practitionerId,
147
+ practitionerName: practitionerInfo.name,
148
+ };
149
+
150
+ // Fetch the technology document to get procedure requirements
151
+ let technologyId = '';
152
+ if (procedureData.technology?.id) {
153
+ technologyId = procedureData.technology.id;
154
+ }
155
+
156
+ let blockingConditions: BlockingCondition[] = [];
157
+ let contraindications: ContraindicationDynamic[] = [];
158
+ let preProcedureRequirements: Requirement[] = [];
159
+ let postProcedureRequirements: Requirement[] = [];
160
+
161
+ // If we have a technology ID, fetch its details
162
+ if (technologyId) {
163
+ const technologyDoc = await getDoc(doc(db, TECHNOLOGIES_COLLECTION, technologyId));
164
+ if (technologyDoc.exists()) {
165
+ const technologyData = technologyDoc.data() as Technology;
166
+
167
+ // Extract technology-related info
168
+ blockingConditions = technologyData.blockingConditions || [];
169
+ contraindications = technologyData.contraindications || [];
170
+ preProcedureRequirements = technologyData.requirements?.pre || [];
171
+ postProcedureRequirements = technologyData.requirements?.post || [];
172
+ }
173
+ } else {
174
+ // Fallback to procedure-level data if technology not available
175
+ blockingConditions = procedureData.blockingConditions || [];
176
+ contraindications = procedureData.contraindications || [];
177
+ preProcedureRequirements = procedureData.preRequirements || [];
178
+ postProcedureRequirements = procedureData.postRequirements || [];
179
+ }
180
+
181
+ return {
182
+ clinicInfo,
183
+ practitionerInfo,
184
+ patientInfo,
185
+ procedureInfo,
186
+ blockingConditions,
187
+ contraindications,
188
+ preProcedureRequirements,
189
+ postProcedureRequirements,
190
+ };
191
+ } catch (error) {
192
+ console.error('Error fetching aggregated info:', error);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Creates a new appointment in Firestore.
199
+ *
200
+ * @param db Firestore instance
201
+ * @param data Data needed to create the appointment
202
+ * @param aggregatedInfo Already fetched and aggregated info
203
+ * @param generateId Function to generate a unique ID
204
+ * @returns The created Appointment
205
+ */
206
+ // export async function createAppointmentUtil(
207
+ // db: Firestore,
208
+ // data: CreateAppointmentData,
209
+ // aggregatedInfo: {
210
+ // clinicInfo: ClinicInfo;
211
+ // practitionerInfo: PractitionerProfileInfo;
212
+ // patientInfo: PatientProfileInfo;
213
+ // procedureInfo: ProcedureSummaryInfo;
214
+ // blockingConditions: BlockingCondition[];
215
+ // contraindications: ContraindicationDynamic[];
216
+ // preProcedureRequirements: Requirement[];
217
+ // postProcedureRequirements: Requirement[];
218
+ // },
219
+ // generateId: () => string
220
+ // ): Promise<Appointment> {
221
+ // try {
222
+ // const appointmentId = generateId();
223
+
224
+ // // Create appointment object
225
+ // const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
226
+ // createdAt: any;
227
+ // updatedAt: any;
228
+ // } = {
229
+ // id: appointmentId,
230
+ // calendarEventId: data.calendarEventId,
231
+ // clinicBranchId: data.clinicBranchId,
232
+ // clinicInfo: aggregatedInfo.clinicInfo,
233
+ // practitionerId: data.practitionerId,
234
+ // practitionerInfo: aggregatedInfo.practitionerInfo,
235
+ // patientId: data.patientId,
236
+ // patientInfo: aggregatedInfo.patientInfo,
237
+ // procedureId: data.procedureId,
238
+ // procedureInfo: aggregatedInfo.procedureInfo,
239
+ // status: data.initialStatus,
240
+ // bookingTime: Timestamp.now(),
241
+ // appointmentStartTime: data.appointmentStartTime,
242
+ // appointmentEndTime: data.appointmentEndTime,
243
+ // patientNotes: data.patientNotes || null,
244
+ // cost: data.cost,
245
+ // currency: data.currency,
246
+ // paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
247
+ // blockingConditions: aggregatedInfo.blockingConditions,
248
+ // contraindications: aggregatedInfo.contraindications,
249
+ // preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
250
+ // postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
251
+ // completedPreRequirements: [],
252
+ // completedPostRequirements: [],
253
+ // createdAt: serverTimestamp(),
254
+ // updatedAt: serverTimestamp(),
255
+ // };
256
+
257
+ // // Add additional fields for confirmation if appointment is already confirmed
258
+ // if (data.initialStatus === AppointmentStatus.CONFIRMED) {
259
+ // appointment.confirmationTime = Timestamp.now();
260
+ // }
261
+
262
+ // // Save to Firestore
263
+ // await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
264
+
265
+ // // Update the calendar event with the appointment ID
266
+ // const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
267
+ // await updateDoc(calendarEventRef, {
268
+ // appointmentId: appointmentId,
269
+ // updatedAt: serverTimestamp(),
270
+ // });
271
+
272
+ // // Return the created appointment
273
+ // // Convert serverTimestamp to regular Timestamp for immediate use
274
+ // const now = Timestamp.now();
275
+ // return {
276
+ // ...appointment,
277
+ // createdAt: now,
278
+ // updatedAt: now,
279
+ // } as Appointment;
280
+ // } catch (error) {
281
+ // console.error("Error creating appointment:", error);
282
+ // throw error;
283
+ // }
284
+ // }
285
+
286
+ /**
287
+ * Updates an existing appointment in Firestore.
288
+ *
289
+ * @param db Firestore instance
290
+ * @param appointmentId ID of the appointment to update
291
+ * @param data Update data for the appointment
292
+ * @returns The updated Appointment
293
+ */
294
+ export async function updateAppointmentUtil(
295
+ db: Firestore,
296
+ appointmentId: string,
297
+ data: UpdateAppointmentData,
298
+ ): Promise<Appointment> {
299
+ try {
300
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
301
+ const appointmentDoc = await getDoc(appointmentRef);
302
+
303
+ if (!appointmentDoc.exists()) {
304
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
305
+ }
306
+
307
+ const currentAppointment = appointmentDoc.data() as Appointment;
308
+
309
+ // Handle requirement completion tracking
310
+ let completedPreRequirements = currentAppointment.completedPreRequirements || [];
311
+ let completedPostRequirements = currentAppointment.completedPostRequirements || [];
312
+
313
+ if (data.completedPreRequirements) {
314
+ // Validate that all IDs exist in the pre-requirements
315
+ const validPreReqIds = currentAppointment.preProcedureRequirements.map(req => req.id);
316
+
317
+ // Only perform validation and merging if the input is an array
318
+ if (Array.isArray(data.completedPreRequirements)) {
319
+ const invalidPreReqIds = data.completedPreRequirements.filter(
320
+ id => !validPreReqIds.includes(id),
321
+ );
322
+
323
+ if (invalidPreReqIds.length > 0) {
324
+ throw new Error(`Invalid pre-requirement IDs: ${invalidPreReqIds.join(', ')}`);
325
+ }
326
+
327
+ // Update the completed pre-requirements
328
+ completedPreRequirements = [
329
+ ...new Set([...completedPreRequirements, ...data.completedPreRequirements]),
330
+ ];
331
+ }
332
+ }
333
+
334
+ if (data.completedPostRequirements) {
335
+ // Validate that all IDs exist in the post-requirements
336
+ const validPostReqIds = currentAppointment.postProcedureRequirements.map(req => req.id);
337
+
338
+ if (Array.isArray(data.completedPostRequirements)) {
339
+ const invalidPostReqIds = data.completedPostRequirements.filter(
340
+ id => !validPostReqIds.includes(id),
341
+ );
342
+
343
+ if (invalidPostReqIds.length > 0) {
344
+ throw new Error(`Invalid post-requirement IDs: ${invalidPostReqIds.join(', ')}`);
345
+ }
346
+
347
+ // Update the completed post-requirements
348
+ completedPostRequirements = [
349
+ ...new Set([...completedPostRequirements, ...data.completedPostRequirements]),
350
+ ];
351
+ }
352
+ }
353
+
354
+ // Prepare update data
355
+ const updateData: any = {
356
+ ...data,
357
+ completedPreRequirements: Array.isArray(data.completedPreRequirements)
358
+ ? completedPreRequirements
359
+ : data.completedPreRequirements,
360
+ completedPostRequirements: Array.isArray(data.completedPostRequirements)
361
+ ? completedPostRequirements
362
+ : data.completedPostRequirements,
363
+ updatedAt: serverTimestamp(),
364
+ };
365
+
366
+ // Remove undefined fields
367
+ Object.keys(updateData).forEach(key => {
368
+ if (updateData[key] === undefined) {
369
+ delete updateData[key];
370
+ }
371
+ });
372
+
373
+ // Handle status changes
374
+ if (data.status && data.status !== currentAppointment.status) {
375
+ // Handle confirmation
376
+ if (data.status === AppointmentStatus.CONFIRMED && !updateData.confirmationTime) {
377
+ updateData.confirmationTime = Timestamp.now();
378
+ }
379
+
380
+ // Update the related calendar event status if needed
381
+ if (currentAppointment.calendarEventId) {
382
+ await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
383
+ }
384
+ }
385
+
386
+ // Update the appointment
387
+ await updateDoc(appointmentRef, updateData);
388
+
389
+ // Fetch the updated appointment
390
+ const updatedAppointmentDoc = await getDoc(appointmentRef);
391
+ if (!updatedAppointmentDoc.exists()) {
392
+ throw new Error(`Failed to retrieve updated appointment ${appointmentId}`);
393
+ }
394
+
395
+ return updatedAppointmentDoc.data() as Appointment;
396
+ } catch (error) {
397
+ console.error(`Error updating appointment ${appointmentId}:`, error);
398
+ throw error;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Updates the status of a calendar event based on appointment status changes.
404
+ *
405
+ * @param db Firestore instance
406
+ * @param calendarEventId ID of the calendar event
407
+ * @param appointmentStatus New appointment status
408
+ */
409
+ async function updateCalendarEventStatus(
410
+ db: Firestore,
411
+ calendarEventId: string,
412
+ appointmentStatus: AppointmentStatus,
413
+ ): Promise<void> {
414
+ try {
415
+ const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
416
+ const calendarEventDoc = await getDoc(calendarEventRef);
417
+
418
+ if (!calendarEventDoc.exists()) {
419
+ console.warn(`Calendar event with ID ${calendarEventId} not found`);
420
+ return;
421
+ }
422
+
423
+ // Map appointment status to calendar event status
424
+ let calendarStatus;
425
+ switch (appointmentStatus) {
426
+ case AppointmentStatus.CONFIRMED:
427
+ calendarStatus = 'confirmed';
428
+ break;
429
+ case AppointmentStatus.CANCELED_PATIENT:
430
+ case AppointmentStatus.CANCELED_CLINIC:
431
+ calendarStatus = 'canceled';
432
+ break;
433
+ case AppointmentStatus.RESCHEDULED_BY_CLINIC:
434
+ calendarStatus = 'rescheduled';
435
+ break;
436
+ case AppointmentStatus.COMPLETED:
437
+ calendarStatus = 'completed';
438
+ break;
439
+ default:
440
+ // For other states, don't update the calendar status
441
+ return;
442
+ }
443
+
444
+ await updateDoc(calendarEventRef, {
445
+ status: calendarStatus,
446
+ updatedAt: serverTimestamp(),
447
+ });
448
+ } catch (error) {
449
+ console.error(`Error updating calendar event ${calendarEventId}:`, error);
450
+ // Don't throw error to avoid failing the appointment update
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Gets an appointment by its ID.
456
+ *
457
+ * @param db Firestore instance
458
+ * @param appointmentId Appointment ID
459
+ * @returns The appointment or null if not found
460
+ */
461
+ export async function getAppointmentByIdUtil(
462
+ db: Firestore,
463
+ appointmentId: string,
464
+ ): Promise<Appointment | null> {
465
+ try {
466
+ const appointmentDoc = await getDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId));
467
+
468
+ if (!appointmentDoc.exists()) {
469
+ return null;
470
+ }
471
+
472
+ return appointmentDoc.data() as Appointment;
473
+ } catch (error) {
474
+ console.error(`Error getting appointment ${appointmentId}:`, error);
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Searches for appointments based on various criteria.
481
+ *
482
+ * @param db Firestore instance
483
+ * @param params Search parameters
484
+ * @returns Found appointments and the last document for pagination
485
+ */
486
+ export async function searchAppointmentsUtil(
487
+ db: Firestore,
488
+ params: SearchAppointmentsParams,
489
+ ): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
490
+ try {
491
+ const constraints: QueryConstraint[] = [];
492
+
493
+ // Add filters based on provided params
494
+ if (params.patientId) {
495
+ constraints.push(where('patientId', '==', params.patientId));
496
+ }
497
+
498
+ if (params.practitionerId) {
499
+ constraints.push(where('practitionerId', '==', params.practitionerId));
500
+ }
501
+
502
+ if (params.clinicBranchId) {
503
+ constraints.push(where('clinicBranchId', '==', params.clinicBranchId));
504
+ }
505
+
506
+ if (params.startDate) {
507
+ constraints.push(where('appointmentStartTime', '>=', Timestamp.fromDate(params.startDate)));
508
+ }
509
+
510
+ if (params.endDate) {
511
+ constraints.push(where('appointmentStartTime', '<=', Timestamp.fromDate(params.endDate)));
512
+ }
513
+
514
+ if (params.status) {
515
+ if (Array.isArray(params.status)) {
516
+ // If multiple statuses, use in operator
517
+ constraints.push(where('status', 'in', params.status));
518
+ } else {
519
+ // Single status
520
+ constraints.push(where('status', '==', params.status));
521
+ }
522
+ }
523
+
524
+ // Add ordering
525
+ constraints.push(orderBy('appointmentStartTime', 'asc'));
526
+
527
+ // Add pagination if specified
528
+ if (params.limit) {
529
+ constraints.push(limit(params.limit));
530
+ }
531
+
532
+ if (params.startAfter) {
533
+ constraints.push(startAfter(params.startAfter));
534
+ }
535
+
536
+ // Execute query
537
+ const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
538
+ const querySnapshot = await getDocs(q);
539
+
540
+ // Extract results
541
+ const appointments = querySnapshot.docs.map(doc => doc.data() as Appointment);
542
+
543
+ // Get last document for pagination
544
+ const lastDoc =
545
+ querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
546
+
547
+ return { appointments, lastDoc };
548
+ } catch (error) {
549
+ console.error('Error searching appointments:', error);
550
+ throw error;
551
+ }
552
+ }