@blackcode_sa/metaestetics-api 1.12.65 → 1.12.66
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 +2 -0
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.js +45 -4
- package/dist/admin/index.mjs +45 -4
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -11
- package/dist/index.mjs +53 -11
- package/package.json +119 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +75 -75
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +40 -40
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +318 -318
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +395 -395
- package/src/backoffice/services/technology.service.ts +1083 -1083
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +62 -62
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +163 -163
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2505 -2505
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +13 -13
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +1715 -1715
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +683 -636
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +489 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +44 -44
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +265 -265
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -130
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +493 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -189
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,519 +1,519 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collection,
|
|
3
|
-
doc,
|
|
4
|
-
getDoc,
|
|
5
|
-
getDocs,
|
|
6
|
-
query,
|
|
7
|
-
where,
|
|
8
|
-
updateDoc,
|
|
9
|
-
setDoc,
|
|
10
|
-
deleteDoc,
|
|
11
|
-
Timestamp,
|
|
12
|
-
serverTimestamp,
|
|
13
|
-
QueryConstraint,
|
|
14
|
-
orderBy,
|
|
15
|
-
limit,
|
|
16
|
-
and,
|
|
17
|
-
or,
|
|
18
|
-
} from "firebase/firestore";
|
|
19
|
-
import { BaseService } from "../base.service";
|
|
20
|
-
import {
|
|
21
|
-
PractitionerInvite,
|
|
22
|
-
CreatePractitionerInviteData,
|
|
23
|
-
UpdatePractitionerInviteData,
|
|
24
|
-
PractitionerInviteFilters,
|
|
25
|
-
PractitionerInviteStatus,
|
|
26
|
-
PRACTITIONER_INVITES_COLLECTION,
|
|
27
|
-
} from "../../types/clinic/practitioner-invite.types";
|
|
28
|
-
import { ClinicInfo, PractitionerProfileInfo } from "../../types/profile";
|
|
29
|
-
import {
|
|
30
|
-
PRACTITIONERS_COLLECTION,
|
|
31
|
-
Practitioner,
|
|
32
|
-
} from "../../types/practitioner";
|
|
33
|
-
import { CLINICS_COLLECTION, Clinic } from "../../types/clinic";
|
|
34
|
-
import { Auth } from "firebase/auth";
|
|
35
|
-
import { Firestore } from "firebase/firestore";
|
|
36
|
-
import { FirebaseApp } from "firebase/app";
|
|
37
|
-
|
|
38
|
-
export class PractitionerInviteService extends BaseService {
|
|
39
|
-
constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
|
|
40
|
-
super(db, auth, app);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Creates a new practitioner invite
|
|
45
|
-
* @param practitionerId - Practitioner ID
|
|
46
|
-
* @param clinicId - Clinic ID
|
|
47
|
-
* @param proposedWorkingHours - Proposed working hours
|
|
48
|
-
* @param invitedBy - Admin ID who creates the invite
|
|
49
|
-
* @param message - Optional message
|
|
50
|
-
* @returns Created invite
|
|
51
|
-
*/
|
|
52
|
-
async createInviteAdmin(
|
|
53
|
-
practitionerId: string,
|
|
54
|
-
clinicId: string,
|
|
55
|
-
proposedWorkingHours: any,
|
|
56
|
-
invitedBy: string,
|
|
57
|
-
message?: string
|
|
58
|
-
): Promise<PractitionerInvite> {
|
|
59
|
-
try {
|
|
60
|
-
const inviteId = this.generateId();
|
|
61
|
-
|
|
62
|
-
// Fetch practitioner and clinic information
|
|
63
|
-
const [practitioner, clinic] = await Promise.all([
|
|
64
|
-
this.getPractitionerById(practitionerId),
|
|
65
|
-
this.getClinicById(clinicId),
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
|
-
if (!practitioner) {
|
|
69
|
-
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!clinic) {
|
|
73
|
-
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Check if there's already a pending invite for this practitioner-clinic pair
|
|
77
|
-
const existingInvite = await this.findExistingInvite(
|
|
78
|
-
practitionerId,
|
|
79
|
-
clinicId
|
|
80
|
-
);
|
|
81
|
-
if (
|
|
82
|
-
existingInvite &&
|
|
83
|
-
existingInvite.status === PractitionerInviteStatus.PENDING
|
|
84
|
-
) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
"There's already a pending invite for this practitioner at this clinic"
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Build practitioner info using existing aggregation type
|
|
91
|
-
const practitionerInfo: PractitionerProfileInfo = {
|
|
92
|
-
id: practitioner.id,
|
|
93
|
-
practitionerPhoto:
|
|
94
|
-
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
95
|
-
? practitioner.basicInfo.profileImageUrl
|
|
96
|
-
: null,
|
|
97
|
-
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
98
|
-
email: practitioner.basicInfo.email,
|
|
99
|
-
phone: practitioner.basicInfo.phoneNumber || null,
|
|
100
|
-
certification: practitioner.certification,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// Build clinic info using existing aggregation type
|
|
104
|
-
const clinicInfo: ClinicInfo = {
|
|
105
|
-
id: clinic.id,
|
|
106
|
-
featuredPhoto:
|
|
107
|
-
typeof clinic.coverPhoto === "string" ? clinic.coverPhoto : "",
|
|
108
|
-
name: clinic.name,
|
|
109
|
-
description: clinic.description || null,
|
|
110
|
-
location: clinic.location,
|
|
111
|
-
contactInfo: clinic.contactInfo,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const inviteData: CreatePractitionerInviteData = {
|
|
115
|
-
practitionerId,
|
|
116
|
-
clinicId,
|
|
117
|
-
practitionerInfo,
|
|
118
|
-
clinicInfo,
|
|
119
|
-
proposedWorkingHours,
|
|
120
|
-
invitedBy,
|
|
121
|
-
message: message || null,
|
|
122
|
-
status: PractitionerInviteStatus.PENDING,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const now = Timestamp.now();
|
|
126
|
-
const invite: PractitionerInvite = {
|
|
127
|
-
id: inviteId,
|
|
128
|
-
...inviteData,
|
|
129
|
-
status: PractitionerInviteStatus.PENDING,
|
|
130
|
-
createdAt: now,
|
|
131
|
-
updatedAt: now,
|
|
132
|
-
acceptedAt: null,
|
|
133
|
-
rejectedAt: null,
|
|
134
|
-
cancelledAt: null,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
138
|
-
await setDoc(docRef, invite);
|
|
139
|
-
|
|
140
|
-
return invite;
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.error(
|
|
143
|
-
"[PractitionerInviteService] Error creating invite:",
|
|
144
|
-
error
|
|
145
|
-
);
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Gets all invites for a specific doctor/practitioner
|
|
152
|
-
* @param practitionerId - Practitioner ID
|
|
153
|
-
* @param statusFilter - Optional status filter
|
|
154
|
-
* @returns Array of invites
|
|
155
|
-
*/
|
|
156
|
-
async getAllInvitesDoctor(
|
|
157
|
-
practitionerId: string,
|
|
158
|
-
statusFilter?: PractitionerInviteStatus[]
|
|
159
|
-
): Promise<PractitionerInvite[]> {
|
|
160
|
-
try {
|
|
161
|
-
const constraints: QueryConstraint[] = [
|
|
162
|
-
where("practitionerId", "==", practitionerId),
|
|
163
|
-
orderBy("createdAt", "desc"),
|
|
164
|
-
];
|
|
165
|
-
|
|
166
|
-
if (statusFilter && statusFilter.length > 0) {
|
|
167
|
-
constraints.push(where("status", "in", statusFilter));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const q = query(
|
|
171
|
-
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
172
|
-
...constraints
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const querySnapshot = await getDocs(q);
|
|
176
|
-
return querySnapshot.docs.map((doc) => doc.data() as PractitionerInvite);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
console.error(
|
|
179
|
-
"[PractitionerInviteService] Error getting doctor invites:",
|
|
180
|
-
error
|
|
181
|
-
);
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Gets all invites for a specific clinic
|
|
188
|
-
* @param clinicId - Clinic ID
|
|
189
|
-
* @param statusFilter - Optional status filter
|
|
190
|
-
* @returns Array of invites
|
|
191
|
-
*/
|
|
192
|
-
async getAllInvitesClinic(
|
|
193
|
-
clinicId: string,
|
|
194
|
-
statusFilter?: PractitionerInviteStatus[]
|
|
195
|
-
): Promise<PractitionerInvite[]> {
|
|
196
|
-
try {
|
|
197
|
-
const constraints: QueryConstraint[] = [
|
|
198
|
-
where("clinicId", "==", clinicId),
|
|
199
|
-
orderBy("createdAt", "desc"),
|
|
200
|
-
];
|
|
201
|
-
|
|
202
|
-
if (statusFilter && statusFilter.length > 0) {
|
|
203
|
-
constraints.push(where("status", "in", statusFilter));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const q = query(
|
|
207
|
-
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
208
|
-
...constraints
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
const querySnapshot = await getDocs(q);
|
|
212
|
-
return querySnapshot.docs.map((doc) => doc.data() as PractitionerInvite);
|
|
213
|
-
} catch (error) {
|
|
214
|
-
console.error(
|
|
215
|
-
"[PractitionerInviteService] Error getting clinic invites:",
|
|
216
|
-
error
|
|
217
|
-
);
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Doctor accepts an invite
|
|
224
|
-
* @param inviteId - Invite ID
|
|
225
|
-
* @returns Updated invite
|
|
226
|
-
*/
|
|
227
|
-
async acceptInviteDoctor(inviteId: string): Promise<PractitionerInvite> {
|
|
228
|
-
try {
|
|
229
|
-
const invite = await this.getInviteById(inviteId);
|
|
230
|
-
if (!invite) {
|
|
231
|
-
throw new Error(`Invite with ID ${inviteId} not found`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
235
|
-
throw new Error("Only pending invites can be accepted");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const updateData = {
|
|
239
|
-
status: PractitionerInviteStatus.ACCEPTED,
|
|
240
|
-
acceptedAt: Timestamp.now(),
|
|
241
|
-
updatedAt: serverTimestamp(),
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
245
|
-
await updateDoc(docRef, updateData);
|
|
246
|
-
|
|
247
|
-
// Return updated invite
|
|
248
|
-
return (await this.getInviteById(inviteId)) as PractitionerInvite;
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.error(
|
|
251
|
-
"[PractitionerInviteService] Error accepting invite:",
|
|
252
|
-
error
|
|
253
|
-
);
|
|
254
|
-
throw error;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Doctor rejects an invite
|
|
260
|
-
* @param inviteId - Invite ID
|
|
261
|
-
* @param rejectionReason - Optional reason for rejection
|
|
262
|
-
* @returns Updated invite
|
|
263
|
-
*/
|
|
264
|
-
async rejectInviteDoctor(
|
|
265
|
-
inviteId: string,
|
|
266
|
-
rejectionReason?: string
|
|
267
|
-
): Promise<PractitionerInvite> {
|
|
268
|
-
try {
|
|
269
|
-
const invite = await this.getInviteById(inviteId);
|
|
270
|
-
if (!invite) {
|
|
271
|
-
throw new Error(`Invite with ID ${inviteId} not found`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
275
|
-
throw new Error("Only pending invites can be rejected");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const updateData = {
|
|
279
|
-
status: PractitionerInviteStatus.REJECTED,
|
|
280
|
-
rejectionReason: rejectionReason || null,
|
|
281
|
-
rejectedAt: Timestamp.now(),
|
|
282
|
-
updatedAt: serverTimestamp(),
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
286
|
-
await updateDoc(docRef, updateData);
|
|
287
|
-
|
|
288
|
-
// Return updated invite
|
|
289
|
-
return (await this.getInviteById(inviteId)) as PractitionerInvite;
|
|
290
|
-
} catch (error) {
|
|
291
|
-
console.error(
|
|
292
|
-
"[PractitionerInviteService] Error rejecting invite:",
|
|
293
|
-
error
|
|
294
|
-
);
|
|
295
|
-
throw error;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Admin cancels an invite
|
|
301
|
-
* @param inviteId - Invite ID
|
|
302
|
-
* @param cancelReason - Optional reason for cancellation
|
|
303
|
-
* @returns Updated invite
|
|
304
|
-
*/
|
|
305
|
-
async cancelInviteAdmin(
|
|
306
|
-
inviteId: string,
|
|
307
|
-
cancelReason?: string
|
|
308
|
-
): Promise<PractitionerInvite> {
|
|
309
|
-
try {
|
|
310
|
-
const invite = await this.getInviteById(inviteId);
|
|
311
|
-
if (!invite) {
|
|
312
|
-
throw new Error(`Invite with ID ${inviteId} not found`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
316
|
-
throw new Error("Only pending invites can be cancelled");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const updateData = {
|
|
320
|
-
status: PractitionerInviteStatus.CANCELLED,
|
|
321
|
-
cancelReason: cancelReason || null,
|
|
322
|
-
cancelledAt: Timestamp.now(),
|
|
323
|
-
updatedAt: serverTimestamp(),
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
327
|
-
await updateDoc(docRef, updateData);
|
|
328
|
-
|
|
329
|
-
// Return updated invite
|
|
330
|
-
return (await this.getInviteById(inviteId)) as PractitionerInvite;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
console.error(
|
|
333
|
-
"[PractitionerInviteService] Error cancelling invite:",
|
|
334
|
-
error
|
|
335
|
-
);
|
|
336
|
-
throw error;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Gets an invite by ID
|
|
342
|
-
* @param inviteId - Invite ID
|
|
343
|
-
* @returns Invite or null if not found
|
|
344
|
-
*/
|
|
345
|
-
async getInviteById(inviteId: string): Promise<PractitionerInvite | null> {
|
|
346
|
-
try {
|
|
347
|
-
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
348
|
-
const docSnap = await getDoc(docRef);
|
|
349
|
-
|
|
350
|
-
if (docSnap.exists()) {
|
|
351
|
-
return docSnap.data() as PractitionerInvite;
|
|
352
|
-
}
|
|
353
|
-
return null;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
console.error(
|
|
356
|
-
"[PractitionerInviteService] Error getting invite by ID:",
|
|
357
|
-
error
|
|
358
|
-
);
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Gets invites with advanced filtering options
|
|
365
|
-
* @param filters - Filter options
|
|
366
|
-
* @returns Array of filtered invites
|
|
367
|
-
*/
|
|
368
|
-
async getInvitesWithFilters(
|
|
369
|
-
filters: PractitionerInviteFilters
|
|
370
|
-
): Promise<PractitionerInvite[]> {
|
|
371
|
-
try {
|
|
372
|
-
const constraints: QueryConstraint[] = [];
|
|
373
|
-
|
|
374
|
-
// Add filters
|
|
375
|
-
if (filters.practitionerId) {
|
|
376
|
-
constraints.push(where("practitionerId", "==", filters.practitionerId));
|
|
377
|
-
}
|
|
378
|
-
if (filters.clinicId) {
|
|
379
|
-
constraints.push(where("clinicId", "==", filters.clinicId));
|
|
380
|
-
}
|
|
381
|
-
if (filters.invitedBy) {
|
|
382
|
-
constraints.push(where("invitedBy", "==", filters.invitedBy));
|
|
383
|
-
}
|
|
384
|
-
if (filters.status && filters.status.length > 0) {
|
|
385
|
-
constraints.push(where("status", "in", filters.status));
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Add ordering
|
|
389
|
-
const orderField = filters.orderBy || "createdAt";
|
|
390
|
-
const orderDirection = filters.orderDirection || "desc";
|
|
391
|
-
constraints.push(orderBy(orderField, orderDirection));
|
|
392
|
-
|
|
393
|
-
// Add limit
|
|
394
|
-
if (filters.limit) {
|
|
395
|
-
constraints.push(limit(filters.limit));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const q = query(
|
|
399
|
-
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
400
|
-
...constraints
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const querySnapshot = await getDocs(q);
|
|
404
|
-
let invites = querySnapshot.docs.map(
|
|
405
|
-
(doc) => doc.data() as PractitionerInvite
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
// Apply date filters (client-side filtering due to Firestore limitations)
|
|
409
|
-
if (filters.fromDate) {
|
|
410
|
-
invites = invites.filter(
|
|
411
|
-
(invite) => invite.createdAt >= filters.fromDate!
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
if (filters.toDate) {
|
|
415
|
-
invites = invites.filter(
|
|
416
|
-
(invite) => invite.createdAt <= filters.toDate!
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return invites;
|
|
421
|
-
} catch (error) {
|
|
422
|
-
console.error(
|
|
423
|
-
"[PractitionerInviteService] Error getting invites with filters:",
|
|
424
|
-
error
|
|
425
|
-
);
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Deletes an invite (admin only)
|
|
432
|
-
* @param inviteId - Invite ID
|
|
433
|
-
*/
|
|
434
|
-
async deleteInvite(inviteId: string): Promise<void> {
|
|
435
|
-
try {
|
|
436
|
-
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
437
|
-
await deleteDoc(docRef);
|
|
438
|
-
} catch (error) {
|
|
439
|
-
console.error(
|
|
440
|
-
"[PractitionerInviteService] Error deleting invite:",
|
|
441
|
-
error
|
|
442
|
-
);
|
|
443
|
-
throw error;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Private helper methods
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Gets practitioner by ID
|
|
451
|
-
* @param practitionerId - Practitioner ID
|
|
452
|
-
* @returns Practitioner or null
|
|
453
|
-
*/
|
|
454
|
-
private async getPractitionerById(
|
|
455
|
-
practitionerId: string
|
|
456
|
-
): Promise<Practitioner | null> {
|
|
457
|
-
try {
|
|
458
|
-
const docRef = doc(this.db, PRACTITIONERS_COLLECTION, practitionerId);
|
|
459
|
-
const docSnap = await getDoc(docRef);
|
|
460
|
-
return docSnap.exists() ? (docSnap.data() as Practitioner) : null;
|
|
461
|
-
} catch (error) {
|
|
462
|
-
console.error(
|
|
463
|
-
"[PractitionerInviteService] Error getting practitioner:",
|
|
464
|
-
error
|
|
465
|
-
);
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Gets clinic by ID
|
|
472
|
-
* @param clinicId - Clinic ID
|
|
473
|
-
* @returns Clinic or null
|
|
474
|
-
*/
|
|
475
|
-
private async getClinicById(clinicId: string): Promise<Clinic | null> {
|
|
476
|
-
try {
|
|
477
|
-
const docRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
478
|
-
const docSnap = await getDoc(docRef);
|
|
479
|
-
return docSnap.exists() ? (docSnap.data() as Clinic) : null;
|
|
480
|
-
} catch (error) {
|
|
481
|
-
console.error("[PractitionerInviteService] Error getting clinic:", error);
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Finds existing invite between practitioner and clinic
|
|
488
|
-
* @param practitionerId - Practitioner ID
|
|
489
|
-
* @param clinicId - Clinic ID
|
|
490
|
-
* @returns Existing invite or null
|
|
491
|
-
*/
|
|
492
|
-
private async findExistingInvite(
|
|
493
|
-
practitionerId: string,
|
|
494
|
-
clinicId: string
|
|
495
|
-
): Promise<PractitionerInvite | null> {
|
|
496
|
-
try {
|
|
497
|
-
const q = query(
|
|
498
|
-
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
499
|
-
where("practitionerId", "==", practitionerId),
|
|
500
|
-
where("clinicId", "==", clinicId),
|
|
501
|
-
orderBy("createdAt", "desc"),
|
|
502
|
-
limit(1)
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
const querySnapshot = await getDocs(q);
|
|
506
|
-
if (querySnapshot.empty) {
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return querySnapshot.docs[0].data() as PractitionerInvite;
|
|
511
|
-
} catch (error) {
|
|
512
|
-
console.error(
|
|
513
|
-
"[PractitionerInviteService] Error finding existing invite:",
|
|
514
|
-
error
|
|
515
|
-
);
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
collection,
|
|
3
|
+
doc,
|
|
4
|
+
getDoc,
|
|
5
|
+
getDocs,
|
|
6
|
+
query,
|
|
7
|
+
where,
|
|
8
|
+
updateDoc,
|
|
9
|
+
setDoc,
|
|
10
|
+
deleteDoc,
|
|
11
|
+
Timestamp,
|
|
12
|
+
serverTimestamp,
|
|
13
|
+
QueryConstraint,
|
|
14
|
+
orderBy,
|
|
15
|
+
limit,
|
|
16
|
+
and,
|
|
17
|
+
or,
|
|
18
|
+
} from "firebase/firestore";
|
|
19
|
+
import { BaseService } from "../base.service";
|
|
20
|
+
import {
|
|
21
|
+
PractitionerInvite,
|
|
22
|
+
CreatePractitionerInviteData,
|
|
23
|
+
UpdatePractitionerInviteData,
|
|
24
|
+
PractitionerInviteFilters,
|
|
25
|
+
PractitionerInviteStatus,
|
|
26
|
+
PRACTITIONER_INVITES_COLLECTION,
|
|
27
|
+
} from "../../types/clinic/practitioner-invite.types";
|
|
28
|
+
import { ClinicInfo, PractitionerProfileInfo } from "../../types/profile";
|
|
29
|
+
import {
|
|
30
|
+
PRACTITIONERS_COLLECTION,
|
|
31
|
+
Practitioner,
|
|
32
|
+
} from "../../types/practitioner";
|
|
33
|
+
import { CLINICS_COLLECTION, Clinic } from "../../types/clinic";
|
|
34
|
+
import { Auth } from "firebase/auth";
|
|
35
|
+
import { Firestore } from "firebase/firestore";
|
|
36
|
+
import { FirebaseApp } from "firebase/app";
|
|
37
|
+
|
|
38
|
+
export class PractitionerInviteService extends BaseService {
|
|
39
|
+
constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
|
|
40
|
+
super(db, auth, app);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new practitioner invite
|
|
45
|
+
* @param practitionerId - Practitioner ID
|
|
46
|
+
* @param clinicId - Clinic ID
|
|
47
|
+
* @param proposedWorkingHours - Proposed working hours
|
|
48
|
+
* @param invitedBy - Admin ID who creates the invite
|
|
49
|
+
* @param message - Optional message
|
|
50
|
+
* @returns Created invite
|
|
51
|
+
*/
|
|
52
|
+
async createInviteAdmin(
|
|
53
|
+
practitionerId: string,
|
|
54
|
+
clinicId: string,
|
|
55
|
+
proposedWorkingHours: any,
|
|
56
|
+
invitedBy: string,
|
|
57
|
+
message?: string
|
|
58
|
+
): Promise<PractitionerInvite> {
|
|
59
|
+
try {
|
|
60
|
+
const inviteId = this.generateId();
|
|
61
|
+
|
|
62
|
+
// Fetch practitioner and clinic information
|
|
63
|
+
const [practitioner, clinic] = await Promise.all([
|
|
64
|
+
this.getPractitionerById(practitionerId),
|
|
65
|
+
this.getClinicById(clinicId),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
if (!practitioner) {
|
|
69
|
+
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!clinic) {
|
|
73
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if there's already a pending invite for this practitioner-clinic pair
|
|
77
|
+
const existingInvite = await this.findExistingInvite(
|
|
78
|
+
practitionerId,
|
|
79
|
+
clinicId
|
|
80
|
+
);
|
|
81
|
+
if (
|
|
82
|
+
existingInvite &&
|
|
83
|
+
existingInvite.status === PractitionerInviteStatus.PENDING
|
|
84
|
+
) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"There's already a pending invite for this practitioner at this clinic"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build practitioner info using existing aggregation type
|
|
91
|
+
const practitionerInfo: PractitionerProfileInfo = {
|
|
92
|
+
id: practitioner.id,
|
|
93
|
+
practitionerPhoto:
|
|
94
|
+
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
95
|
+
? practitioner.basicInfo.profileImageUrl
|
|
96
|
+
: null,
|
|
97
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
98
|
+
email: practitioner.basicInfo.email,
|
|
99
|
+
phone: practitioner.basicInfo.phoneNumber || null,
|
|
100
|
+
certification: practitioner.certification,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Build clinic info using existing aggregation type
|
|
104
|
+
const clinicInfo: ClinicInfo = {
|
|
105
|
+
id: clinic.id,
|
|
106
|
+
featuredPhoto:
|
|
107
|
+
typeof clinic.coverPhoto === "string" ? clinic.coverPhoto : "",
|
|
108
|
+
name: clinic.name,
|
|
109
|
+
description: clinic.description || null,
|
|
110
|
+
location: clinic.location,
|
|
111
|
+
contactInfo: clinic.contactInfo,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const inviteData: CreatePractitionerInviteData = {
|
|
115
|
+
practitionerId,
|
|
116
|
+
clinicId,
|
|
117
|
+
practitionerInfo,
|
|
118
|
+
clinicInfo,
|
|
119
|
+
proposedWorkingHours,
|
|
120
|
+
invitedBy,
|
|
121
|
+
message: message || null,
|
|
122
|
+
status: PractitionerInviteStatus.PENDING,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const now = Timestamp.now();
|
|
126
|
+
const invite: PractitionerInvite = {
|
|
127
|
+
id: inviteId,
|
|
128
|
+
...inviteData,
|
|
129
|
+
status: PractitionerInviteStatus.PENDING,
|
|
130
|
+
createdAt: now,
|
|
131
|
+
updatedAt: now,
|
|
132
|
+
acceptedAt: null,
|
|
133
|
+
rejectedAt: null,
|
|
134
|
+
cancelledAt: null,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
138
|
+
await setDoc(docRef, invite);
|
|
139
|
+
|
|
140
|
+
return invite;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(
|
|
143
|
+
"[PractitionerInviteService] Error creating invite:",
|
|
144
|
+
error
|
|
145
|
+
);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Gets all invites for a specific doctor/practitioner
|
|
152
|
+
* @param practitionerId - Practitioner ID
|
|
153
|
+
* @param statusFilter - Optional status filter
|
|
154
|
+
* @returns Array of invites
|
|
155
|
+
*/
|
|
156
|
+
async getAllInvitesDoctor(
|
|
157
|
+
practitionerId: string,
|
|
158
|
+
statusFilter?: PractitionerInviteStatus[]
|
|
159
|
+
): Promise<PractitionerInvite[]> {
|
|
160
|
+
try {
|
|
161
|
+
const constraints: QueryConstraint[] = [
|
|
162
|
+
where("practitionerId", "==", practitionerId),
|
|
163
|
+
orderBy("createdAt", "desc"),
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
if (statusFilter && statusFilter.length > 0) {
|
|
167
|
+
constraints.push(where("status", "in", statusFilter));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const q = query(
|
|
171
|
+
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
172
|
+
...constraints
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const querySnapshot = await getDocs(q);
|
|
176
|
+
return querySnapshot.docs.map((doc) => doc.data() as PractitionerInvite);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(
|
|
179
|
+
"[PractitionerInviteService] Error getting doctor invites:",
|
|
180
|
+
error
|
|
181
|
+
);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets all invites for a specific clinic
|
|
188
|
+
* @param clinicId - Clinic ID
|
|
189
|
+
* @param statusFilter - Optional status filter
|
|
190
|
+
* @returns Array of invites
|
|
191
|
+
*/
|
|
192
|
+
async getAllInvitesClinic(
|
|
193
|
+
clinicId: string,
|
|
194
|
+
statusFilter?: PractitionerInviteStatus[]
|
|
195
|
+
): Promise<PractitionerInvite[]> {
|
|
196
|
+
try {
|
|
197
|
+
const constraints: QueryConstraint[] = [
|
|
198
|
+
where("clinicId", "==", clinicId),
|
|
199
|
+
orderBy("createdAt", "desc"),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
if (statusFilter && statusFilter.length > 0) {
|
|
203
|
+
constraints.push(where("status", "in", statusFilter));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const q = query(
|
|
207
|
+
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
208
|
+
...constraints
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const querySnapshot = await getDocs(q);
|
|
212
|
+
return querySnapshot.docs.map((doc) => doc.data() as PractitionerInvite);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(
|
|
215
|
+
"[PractitionerInviteService] Error getting clinic invites:",
|
|
216
|
+
error
|
|
217
|
+
);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Doctor accepts an invite
|
|
224
|
+
* @param inviteId - Invite ID
|
|
225
|
+
* @returns Updated invite
|
|
226
|
+
*/
|
|
227
|
+
async acceptInviteDoctor(inviteId: string): Promise<PractitionerInvite> {
|
|
228
|
+
try {
|
|
229
|
+
const invite = await this.getInviteById(inviteId);
|
|
230
|
+
if (!invite) {
|
|
231
|
+
throw new Error(`Invite with ID ${inviteId} not found`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
235
|
+
throw new Error("Only pending invites can be accepted");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const updateData = {
|
|
239
|
+
status: PractitionerInviteStatus.ACCEPTED,
|
|
240
|
+
acceptedAt: Timestamp.now(),
|
|
241
|
+
updatedAt: serverTimestamp(),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
245
|
+
await updateDoc(docRef, updateData);
|
|
246
|
+
|
|
247
|
+
// Return updated invite
|
|
248
|
+
return (await this.getInviteById(inviteId)) as PractitionerInvite;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(
|
|
251
|
+
"[PractitionerInviteService] Error accepting invite:",
|
|
252
|
+
error
|
|
253
|
+
);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Doctor rejects an invite
|
|
260
|
+
* @param inviteId - Invite ID
|
|
261
|
+
* @param rejectionReason - Optional reason for rejection
|
|
262
|
+
* @returns Updated invite
|
|
263
|
+
*/
|
|
264
|
+
async rejectInviteDoctor(
|
|
265
|
+
inviteId: string,
|
|
266
|
+
rejectionReason?: string
|
|
267
|
+
): Promise<PractitionerInvite> {
|
|
268
|
+
try {
|
|
269
|
+
const invite = await this.getInviteById(inviteId);
|
|
270
|
+
if (!invite) {
|
|
271
|
+
throw new Error(`Invite with ID ${inviteId} not found`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
275
|
+
throw new Error("Only pending invites can be rejected");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const updateData = {
|
|
279
|
+
status: PractitionerInviteStatus.REJECTED,
|
|
280
|
+
rejectionReason: rejectionReason || null,
|
|
281
|
+
rejectedAt: Timestamp.now(),
|
|
282
|
+
updatedAt: serverTimestamp(),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
286
|
+
await updateDoc(docRef, updateData);
|
|
287
|
+
|
|
288
|
+
// Return updated invite
|
|
289
|
+
return (await this.getInviteById(inviteId)) as PractitionerInvite;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error(
|
|
292
|
+
"[PractitionerInviteService] Error rejecting invite:",
|
|
293
|
+
error
|
|
294
|
+
);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Admin cancels an invite
|
|
301
|
+
* @param inviteId - Invite ID
|
|
302
|
+
* @param cancelReason - Optional reason for cancellation
|
|
303
|
+
* @returns Updated invite
|
|
304
|
+
*/
|
|
305
|
+
async cancelInviteAdmin(
|
|
306
|
+
inviteId: string,
|
|
307
|
+
cancelReason?: string
|
|
308
|
+
): Promise<PractitionerInvite> {
|
|
309
|
+
try {
|
|
310
|
+
const invite = await this.getInviteById(inviteId);
|
|
311
|
+
if (!invite) {
|
|
312
|
+
throw new Error(`Invite with ID ${inviteId} not found`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
316
|
+
throw new Error("Only pending invites can be cancelled");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const updateData = {
|
|
320
|
+
status: PractitionerInviteStatus.CANCELLED,
|
|
321
|
+
cancelReason: cancelReason || null,
|
|
322
|
+
cancelledAt: Timestamp.now(),
|
|
323
|
+
updatedAt: serverTimestamp(),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
327
|
+
await updateDoc(docRef, updateData);
|
|
328
|
+
|
|
329
|
+
// Return updated invite
|
|
330
|
+
return (await this.getInviteById(inviteId)) as PractitionerInvite;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(
|
|
333
|
+
"[PractitionerInviteService] Error cancelling invite:",
|
|
334
|
+
error
|
|
335
|
+
);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Gets an invite by ID
|
|
342
|
+
* @param inviteId - Invite ID
|
|
343
|
+
* @returns Invite or null if not found
|
|
344
|
+
*/
|
|
345
|
+
async getInviteById(inviteId: string): Promise<PractitionerInvite | null> {
|
|
346
|
+
try {
|
|
347
|
+
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
348
|
+
const docSnap = await getDoc(docRef);
|
|
349
|
+
|
|
350
|
+
if (docSnap.exists()) {
|
|
351
|
+
return docSnap.data() as PractitionerInvite;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(
|
|
356
|
+
"[PractitionerInviteService] Error getting invite by ID:",
|
|
357
|
+
error
|
|
358
|
+
);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gets invites with advanced filtering options
|
|
365
|
+
* @param filters - Filter options
|
|
366
|
+
* @returns Array of filtered invites
|
|
367
|
+
*/
|
|
368
|
+
async getInvitesWithFilters(
|
|
369
|
+
filters: PractitionerInviteFilters
|
|
370
|
+
): Promise<PractitionerInvite[]> {
|
|
371
|
+
try {
|
|
372
|
+
const constraints: QueryConstraint[] = [];
|
|
373
|
+
|
|
374
|
+
// Add filters
|
|
375
|
+
if (filters.practitionerId) {
|
|
376
|
+
constraints.push(where("practitionerId", "==", filters.practitionerId));
|
|
377
|
+
}
|
|
378
|
+
if (filters.clinicId) {
|
|
379
|
+
constraints.push(where("clinicId", "==", filters.clinicId));
|
|
380
|
+
}
|
|
381
|
+
if (filters.invitedBy) {
|
|
382
|
+
constraints.push(where("invitedBy", "==", filters.invitedBy));
|
|
383
|
+
}
|
|
384
|
+
if (filters.status && filters.status.length > 0) {
|
|
385
|
+
constraints.push(where("status", "in", filters.status));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Add ordering
|
|
389
|
+
const orderField = filters.orderBy || "createdAt";
|
|
390
|
+
const orderDirection = filters.orderDirection || "desc";
|
|
391
|
+
constraints.push(orderBy(orderField, orderDirection));
|
|
392
|
+
|
|
393
|
+
// Add limit
|
|
394
|
+
if (filters.limit) {
|
|
395
|
+
constraints.push(limit(filters.limit));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const q = query(
|
|
399
|
+
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
400
|
+
...constraints
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const querySnapshot = await getDocs(q);
|
|
404
|
+
let invites = querySnapshot.docs.map(
|
|
405
|
+
(doc) => doc.data() as PractitionerInvite
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Apply date filters (client-side filtering due to Firestore limitations)
|
|
409
|
+
if (filters.fromDate) {
|
|
410
|
+
invites = invites.filter(
|
|
411
|
+
(invite) => invite.createdAt >= filters.fromDate!
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (filters.toDate) {
|
|
415
|
+
invites = invites.filter(
|
|
416
|
+
(invite) => invite.createdAt <= filters.toDate!
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return invites;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error(
|
|
423
|
+
"[PractitionerInviteService] Error getting invites with filters:",
|
|
424
|
+
error
|
|
425
|
+
);
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Deletes an invite (admin only)
|
|
432
|
+
* @param inviteId - Invite ID
|
|
433
|
+
*/
|
|
434
|
+
async deleteInvite(inviteId: string): Promise<void> {
|
|
435
|
+
try {
|
|
436
|
+
const docRef = doc(this.db, PRACTITIONER_INVITES_COLLECTION, inviteId);
|
|
437
|
+
await deleteDoc(docRef);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error(
|
|
440
|
+
"[PractitionerInviteService] Error deleting invite:",
|
|
441
|
+
error
|
|
442
|
+
);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Private helper methods
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Gets practitioner by ID
|
|
451
|
+
* @param practitionerId - Practitioner ID
|
|
452
|
+
* @returns Practitioner or null
|
|
453
|
+
*/
|
|
454
|
+
private async getPractitionerById(
|
|
455
|
+
practitionerId: string
|
|
456
|
+
): Promise<Practitioner | null> {
|
|
457
|
+
try {
|
|
458
|
+
const docRef = doc(this.db, PRACTITIONERS_COLLECTION, practitionerId);
|
|
459
|
+
const docSnap = await getDoc(docRef);
|
|
460
|
+
return docSnap.exists() ? (docSnap.data() as Practitioner) : null;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error(
|
|
463
|
+
"[PractitionerInviteService] Error getting practitioner:",
|
|
464
|
+
error
|
|
465
|
+
);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Gets clinic by ID
|
|
472
|
+
* @param clinicId - Clinic ID
|
|
473
|
+
* @returns Clinic or null
|
|
474
|
+
*/
|
|
475
|
+
private async getClinicById(clinicId: string): Promise<Clinic | null> {
|
|
476
|
+
try {
|
|
477
|
+
const docRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
478
|
+
const docSnap = await getDoc(docRef);
|
|
479
|
+
return docSnap.exists() ? (docSnap.data() as Clinic) : null;
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error("[PractitionerInviteService] Error getting clinic:", error);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Finds existing invite between practitioner and clinic
|
|
488
|
+
* @param practitionerId - Practitioner ID
|
|
489
|
+
* @param clinicId - Clinic ID
|
|
490
|
+
* @returns Existing invite or null
|
|
491
|
+
*/
|
|
492
|
+
private async findExistingInvite(
|
|
493
|
+
practitionerId: string,
|
|
494
|
+
clinicId: string
|
|
495
|
+
): Promise<PractitionerInvite | null> {
|
|
496
|
+
try {
|
|
497
|
+
const q = query(
|
|
498
|
+
collection(this.db, PRACTITIONER_INVITES_COLLECTION),
|
|
499
|
+
where("practitionerId", "==", practitionerId),
|
|
500
|
+
where("clinicId", "==", clinicId),
|
|
501
|
+
orderBy("createdAt", "desc"),
|
|
502
|
+
limit(1)
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const querySnapshot = await getDocs(q);
|
|
506
|
+
if (querySnapshot.empty) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return querySnapshot.docs[0].data() as PractitionerInvite;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error(
|
|
513
|
+
"[PractitionerInviteService] Error finding existing invite:",
|
|
514
|
+
error
|
|
515
|
+
);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|