@blackcode_sa/metaestetics-api 1.12.64 → 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 +80 -11
- package/dist/index.mjs +80 -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 -1682
- 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,697 +1,697 @@
|
|
|
1
|
-
/// <reference types="axios" />
|
|
2
|
-
import { Firestore, Timestamp } from "firebase/firestore";
|
|
3
|
-
import axios from "axios";
|
|
4
|
-
import {
|
|
5
|
-
SyncedCalendar,
|
|
6
|
-
SyncedCalendarProvider,
|
|
7
|
-
UpdateSyncedCalendarData,
|
|
8
|
-
} from "../../../types/calendar/synced-calendar.types";
|
|
9
|
-
import {
|
|
10
|
-
CalendarEvent,
|
|
11
|
-
CalendarEventStatus,
|
|
12
|
-
CalendarEventType,
|
|
13
|
-
CalendarSyncStatus,
|
|
14
|
-
} from "../../../types/calendar";
|
|
15
|
-
import {
|
|
16
|
-
updateLastSyncedTimestampUtil,
|
|
17
|
-
updatePractitionerSyncedCalendarUtil,
|
|
18
|
-
updatePatientSyncedCalendarUtil,
|
|
19
|
-
updateClinicSyncedCalendarUtil,
|
|
20
|
-
} from "./synced-calendar.utils";
|
|
21
|
-
|
|
22
|
-
// API URL and client configuration - these should be environment variables in a real application
|
|
23
|
-
export const GOOGLE_CALENDAR_API_URL = "https://www.googleapis.com/calendar/v3";
|
|
24
|
-
const GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
25
|
-
const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
26
|
-
const CLIENT_ID = "your-client-id"; // Replace with your actual client ID
|
|
27
|
-
const CLIENT_SECRET = "your-client-secret"; // Replace with your actual client secret
|
|
28
|
-
const REDIRECT_URI = "your-redirect-uri"; // Replace with your actual redirect URI
|
|
29
|
-
|
|
30
|
-
// Error interface for better error handling
|
|
31
|
-
interface ApiError {
|
|
32
|
-
message: string;
|
|
33
|
-
status?: number;
|
|
34
|
-
response?: {
|
|
35
|
-
data?: any;
|
|
36
|
-
status?: number;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Helper function for making API requests
|
|
42
|
-
* @param method - HTTP method
|
|
43
|
-
* @param url - Request URL
|
|
44
|
-
* @param headers - Request headers
|
|
45
|
-
* @param data - Request body data
|
|
46
|
-
* @param params - URL query parameters
|
|
47
|
-
* @returns Response data
|
|
48
|
-
*/
|
|
49
|
-
export async function makeRequest(
|
|
50
|
-
method: string,
|
|
51
|
-
url: string,
|
|
52
|
-
headers: Record<string, string>,
|
|
53
|
-
data?: any,
|
|
54
|
-
params?: Record<string, string>
|
|
55
|
-
): Promise<any> {
|
|
56
|
-
// Construct URL with query parameters if provided
|
|
57
|
-
const queryParams = params
|
|
58
|
-
? "?" +
|
|
59
|
-
Object.entries(params)
|
|
60
|
-
.map(
|
|
61
|
-
([key, value]) =>
|
|
62
|
-
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
63
|
-
)
|
|
64
|
-
.join("&")
|
|
65
|
-
: "";
|
|
66
|
-
|
|
67
|
-
const finalUrl = url + queryParams;
|
|
68
|
-
|
|
69
|
-
// Example implementation with fetch
|
|
70
|
-
const options: RequestInit = {
|
|
71
|
-
method,
|
|
72
|
-
headers,
|
|
73
|
-
body: data ? JSON.stringify(data) : undefined,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const response = await fetch(finalUrl, options);
|
|
77
|
-
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
const error: any = new Error(
|
|
80
|
-
`Request failed with status ${response.status}`
|
|
81
|
-
);
|
|
82
|
-
error.response = response;
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return response.json();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Authenticates with Google Calendar API
|
|
91
|
-
* @param authCode - Authorization code from Google OAuth
|
|
92
|
-
* @returns Access token and refresh token
|
|
93
|
-
*/
|
|
94
|
-
export async function authenticateWithGoogleCalendarUtil(
|
|
95
|
-
authCode: string
|
|
96
|
-
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
|
97
|
-
try {
|
|
98
|
-
// Exchange authorization code for tokens
|
|
99
|
-
const data = {
|
|
100
|
-
code: authCode,
|
|
101
|
-
client_id: CLIENT_ID,
|
|
102
|
-
client_secret: CLIENT_SECRET,
|
|
103
|
-
redirect_uri: REDIRECT_URI,
|
|
104
|
-
grant_type: "authorization_code",
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const response = await makeRequest(
|
|
108
|
-
"post",
|
|
109
|
-
GOOGLE_OAUTH_URL,
|
|
110
|
-
{ "Content-Type": "application/json" },
|
|
111
|
-
data
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
accessToken: response.access_token,
|
|
116
|
-
refreshToken: response.refresh_token,
|
|
117
|
-
expiresIn: response.expires_in,
|
|
118
|
-
};
|
|
119
|
-
} catch (error) {
|
|
120
|
-
const apiError = error as ApiError;
|
|
121
|
-
console.error(
|
|
122
|
-
"Error authenticating with Google Calendar:",
|
|
123
|
-
apiError.message || "Unknown error"
|
|
124
|
-
);
|
|
125
|
-
throw new Error(
|
|
126
|
-
`Failed to authenticate with Google Calendar: ${
|
|
127
|
-
apiError.message || "Unknown error"
|
|
128
|
-
}`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Refreshes the Google Calendar API access token
|
|
135
|
-
* @param refreshToken - Refresh token
|
|
136
|
-
* @returns New access token and expiry
|
|
137
|
-
*/
|
|
138
|
-
export async function refreshGoogleCalendarTokenUtil(
|
|
139
|
-
refreshToken: string
|
|
140
|
-
): Promise<{ accessToken: string; expiresIn: number }> {
|
|
141
|
-
try {
|
|
142
|
-
// Use refresh token to get a new access token
|
|
143
|
-
const data = {
|
|
144
|
-
refresh_token: refreshToken,
|
|
145
|
-
client_id: CLIENT_ID,
|
|
146
|
-
client_secret: CLIENT_SECRET,
|
|
147
|
-
grant_type: "refresh_token",
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const response = await makeRequest(
|
|
151
|
-
"post",
|
|
152
|
-
GOOGLE_OAUTH_URL,
|
|
153
|
-
{ "Content-Type": "application/json" },
|
|
154
|
-
data
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
accessToken: response.access_token,
|
|
159
|
-
expiresIn: response.expires_in,
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
const apiError = error as ApiError;
|
|
163
|
-
console.error(
|
|
164
|
-
"Error refreshing Google Calendar token:",
|
|
165
|
-
apiError.message || "Unknown error"
|
|
166
|
-
);
|
|
167
|
-
throw new Error(
|
|
168
|
-
`Failed to refresh Google Calendar token: ${
|
|
169
|
-
apiError.message || "Unknown error"
|
|
170
|
-
}`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Lists available Google Calendars for a user
|
|
177
|
-
* @param accessToken - Google API access token
|
|
178
|
-
* @returns List of available calendars
|
|
179
|
-
*/
|
|
180
|
-
export async function listGoogleCalendarsUtil(
|
|
181
|
-
accessToken: string
|
|
182
|
-
): Promise<Array<{ id: string; name: string }>> {
|
|
183
|
-
try {
|
|
184
|
-
// Call Google Calendar API to list calendars
|
|
185
|
-
const response = await makeRequest(
|
|
186
|
-
"get",
|
|
187
|
-
`${GOOGLE_CALENDAR_API_URL}/users/me/calendarList`,
|
|
188
|
-
{ Authorization: `Bearer ${accessToken}` }
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
// Map the response to our format
|
|
192
|
-
return response.items.map((calendar: any) => ({
|
|
193
|
-
id: calendar.id,
|
|
194
|
-
name: calendar.summary,
|
|
195
|
-
}));
|
|
196
|
-
} catch (error) {
|
|
197
|
-
const apiError = error as ApiError;
|
|
198
|
-
console.error(
|
|
199
|
-
"Error listing Google Calendars:",
|
|
200
|
-
apiError.message || "Unknown error"
|
|
201
|
-
);
|
|
202
|
-
throw new Error(
|
|
203
|
-
`Failed to list Google Calendars: ${apiError.message || "Unknown error"}`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Ensures the synced calendar token is valid and refreshes if needed
|
|
210
|
-
* @param db - Firestore instance
|
|
211
|
-
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
212
|
-
* @param entityId - ID of the entity
|
|
213
|
-
* @param syncedCalendar - Synced calendar data
|
|
214
|
-
* @returns Valid access token
|
|
215
|
-
*/
|
|
216
|
-
async function ensureValidToken(
|
|
217
|
-
db: Firestore,
|
|
218
|
-
entityType: "practitioner" | "patient" | "clinic",
|
|
219
|
-
entityId: string,
|
|
220
|
-
syncedCalendar: SyncedCalendar
|
|
221
|
-
): Promise<string> {
|
|
222
|
-
// Check if token is expired or will expire soon (within 5 minutes)
|
|
223
|
-
const expiryTime = syncedCalendar.tokenExpiry.toDate();
|
|
224
|
-
const now = new Date();
|
|
225
|
-
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
|
|
226
|
-
|
|
227
|
-
if (expiryTime < fiveMinutesFromNow) {
|
|
228
|
-
// Token is expired or will expire soon, refresh it
|
|
229
|
-
const { accessToken, expiresIn } = await refreshGoogleCalendarTokenUtil(
|
|
230
|
-
syncedCalendar.refreshToken
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
// Calculate new expiry time
|
|
234
|
-
const tokenExpiry = new Date();
|
|
235
|
-
tokenExpiry.setSeconds(tokenExpiry.getSeconds() + expiresIn);
|
|
236
|
-
|
|
237
|
-
// Update the synced calendar with the new token
|
|
238
|
-
const updateData: Omit<UpdateSyncedCalendarData, "updatedAt"> = {
|
|
239
|
-
accessToken,
|
|
240
|
-
tokenExpiry: Timestamp.fromDate(tokenExpiry),
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// Update the synced calendar in Firestore
|
|
244
|
-
switch (entityType) {
|
|
245
|
-
case "practitioner":
|
|
246
|
-
await updatePractitionerSyncedCalendarUtil(
|
|
247
|
-
db,
|
|
248
|
-
entityId,
|
|
249
|
-
syncedCalendar.id,
|
|
250
|
-
updateData
|
|
251
|
-
);
|
|
252
|
-
break;
|
|
253
|
-
case "patient":
|
|
254
|
-
await updatePatientSyncedCalendarUtil(
|
|
255
|
-
db,
|
|
256
|
-
entityId,
|
|
257
|
-
syncedCalendar.id,
|
|
258
|
-
updateData
|
|
259
|
-
);
|
|
260
|
-
break;
|
|
261
|
-
case "clinic":
|
|
262
|
-
await updateClinicSyncedCalendarUtil(
|
|
263
|
-
db,
|
|
264
|
-
entityId,
|
|
265
|
-
syncedCalendar.id,
|
|
266
|
-
updateData
|
|
267
|
-
);
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return accessToken;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Token is still valid
|
|
275
|
-
return syncedCalendar.accessToken;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Syncs events from our system to Google Calendar
|
|
280
|
-
* @param db - Firestore instance
|
|
281
|
-
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
282
|
-
* @param entityId - ID of the entity
|
|
283
|
-
* @param syncedCalendar - Synced calendar to use
|
|
284
|
-
* @param events - Events to sync
|
|
285
|
-
* @param existingSyncId - Optional existing sync ID for updating an event
|
|
286
|
-
* @returns Result of the sync operation
|
|
287
|
-
*/
|
|
288
|
-
export async function syncEventsToGoogleCalendarUtil(
|
|
289
|
-
db: Firestore,
|
|
290
|
-
entityType: "practitioner" | "patient" | "clinic",
|
|
291
|
-
entityId: string,
|
|
292
|
-
syncedCalendar: SyncedCalendar,
|
|
293
|
-
events: CalendarEvent[],
|
|
294
|
-
existingSyncId?: string
|
|
295
|
-
): Promise<{
|
|
296
|
-
success: boolean;
|
|
297
|
-
syncedEvents: number;
|
|
298
|
-
errors: any[];
|
|
299
|
-
eventIds: string[];
|
|
300
|
-
}> {
|
|
301
|
-
try {
|
|
302
|
-
// Refresh token if needed
|
|
303
|
-
const { accessToken } = await refreshGoogleCalendarTokenUtil(
|
|
304
|
-
syncedCalendar.refreshToken
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
let syncedCount = 0;
|
|
308
|
-
const errors: any[] = [];
|
|
309
|
-
const eventIds: string[] = [];
|
|
310
|
-
|
|
311
|
-
// Process each event
|
|
312
|
-
for (const event of events) {
|
|
313
|
-
try {
|
|
314
|
-
// For patients: Sync all INTERNAL events except CANCELED and REJECTED
|
|
315
|
-
// For doctors: Only sync INTERNAL events with CONFIRMED status
|
|
316
|
-
// For clinics: No longer syncing
|
|
317
|
-
|
|
318
|
-
// Skip events that are external (we don't sync external events back)
|
|
319
|
-
if (event.syncStatus === CalendarSyncStatus.EXTERNAL) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Apply entity-specific sync rules
|
|
324
|
-
if (
|
|
325
|
-
entityType === "practitioner" &&
|
|
326
|
-
event.status !== CalendarEventStatus.CONFIRMED
|
|
327
|
-
) {
|
|
328
|
-
// For doctors, only sync CONFIRMED events
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
entityType === "patient" &&
|
|
334
|
-
(event.status === CalendarEventStatus.CANCELED ||
|
|
335
|
-
event.status === CalendarEventStatus.REJECTED)
|
|
336
|
-
) {
|
|
337
|
-
// For patients, don't sync CANCELED or REJECTED events
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (entityType === "clinic") {
|
|
342
|
-
// No longer syncing clinic events
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Convert our event to Google Calendar format
|
|
347
|
-
const googleEvent = convertCalendarEventToGoogleEventUtil(event);
|
|
348
|
-
const headers = {
|
|
349
|
-
Authorization: `Bearer ${accessToken}`,
|
|
350
|
-
"Content-Type": "application/json",
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
let responseId = "";
|
|
354
|
-
|
|
355
|
-
// Check if we have an existing sync ID to update
|
|
356
|
-
if (existingSyncId) {
|
|
357
|
-
// Update the existing event using the provided ID
|
|
358
|
-
const response = await makeRequest(
|
|
359
|
-
"put",
|
|
360
|
-
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSyncId}`,
|
|
361
|
-
headers,
|
|
362
|
-
googleEvent
|
|
363
|
-
);
|
|
364
|
-
responseId = response.id;
|
|
365
|
-
} else {
|
|
366
|
-
// Check if the event already has a synced calendar event ID for this calendar
|
|
367
|
-
const existingSync = event.syncedCalendarEventId?.find(
|
|
368
|
-
(sync) =>
|
|
369
|
-
sync.syncedCalendarProvider === SyncedCalendarProvider.GOOGLE &&
|
|
370
|
-
// We should check if this is the same calendar we're syncing with, but that information isn't stored
|
|
371
|
-
// For now, we'll just use the first Google Calendar sync ID
|
|
372
|
-
sync.syncedCalendarProvider === syncedCalendar.provider
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
if (existingSync) {
|
|
376
|
-
// Update existing event
|
|
377
|
-
const response = await makeRequest(
|
|
378
|
-
"put",
|
|
379
|
-
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSync.eventId}`,
|
|
380
|
-
headers,
|
|
381
|
-
googleEvent
|
|
382
|
-
);
|
|
383
|
-
responseId = response.id;
|
|
384
|
-
} else {
|
|
385
|
-
// Create new event
|
|
386
|
-
const response = await makeRequest(
|
|
387
|
-
"post",
|
|
388
|
-
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
389
|
-
headers,
|
|
390
|
-
googleEvent
|
|
391
|
-
);
|
|
392
|
-
responseId = response.id;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (responseId) {
|
|
397
|
-
eventIds.push(responseId);
|
|
398
|
-
syncedCount++;
|
|
399
|
-
}
|
|
400
|
-
} catch (error) {
|
|
401
|
-
const apiError = error as ApiError;
|
|
402
|
-
errors.push({
|
|
403
|
-
eventId: event.id,
|
|
404
|
-
error: apiError.message || "Unknown error",
|
|
405
|
-
status: apiError.response?.status,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Update the last synced timestamp
|
|
411
|
-
await updateLastSyncedTimestampUtil(
|
|
412
|
-
db,
|
|
413
|
-
entityType,
|
|
414
|
-
entityId,
|
|
415
|
-
syncedCalendar.id
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
success: errors.length === 0,
|
|
420
|
-
syncedEvents: syncedCount,
|
|
421
|
-
errors,
|
|
422
|
-
eventIds,
|
|
423
|
-
};
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error("Error syncing with Google Calendar:", error);
|
|
426
|
-
return {
|
|
427
|
-
success: false,
|
|
428
|
-
syncedEvents: 0,
|
|
429
|
-
errors: [{ error: (error as Error).message || "Unknown error" }],
|
|
430
|
-
eventIds: [],
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Fetches events from Google Calendar
|
|
437
|
-
* @param db - Firestore instance
|
|
438
|
-
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
439
|
-
* @param entityId - ID of the entity
|
|
440
|
-
* @param syncedCalendar - Synced calendar data
|
|
441
|
-
* @param startDate - Start date for fetching events
|
|
442
|
-
* @param endDate - End date for fetching events
|
|
443
|
-
* @returns Events fetched from Google Calendar
|
|
444
|
-
*/
|
|
445
|
-
export async function fetchEventsFromGoogleCalendarUtil(
|
|
446
|
-
db: Firestore,
|
|
447
|
-
entityType: "practitioner" | "patient" | "clinic",
|
|
448
|
-
entityId: string,
|
|
449
|
-
syncedCalendar: SyncedCalendar,
|
|
450
|
-
startDate: Date,
|
|
451
|
-
endDate: Date
|
|
452
|
-
): Promise<any[]> {
|
|
453
|
-
try {
|
|
454
|
-
// Ensure we have a valid token
|
|
455
|
-
const accessToken = await ensureValidToken(
|
|
456
|
-
db,
|
|
457
|
-
entityType,
|
|
458
|
-
entityId,
|
|
459
|
-
syncedCalendar
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
// Format dates for Google Calendar API
|
|
463
|
-
const timeMin = startDate.toISOString();
|
|
464
|
-
const timeMax = endDate.toISOString();
|
|
465
|
-
|
|
466
|
-
// Call Google Calendar API to fetch events
|
|
467
|
-
const response = await makeRequest(
|
|
468
|
-
"get",
|
|
469
|
-
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
470
|
-
{ Authorization: `Bearer ${accessToken}` },
|
|
471
|
-
undefined,
|
|
472
|
-
{
|
|
473
|
-
timeMin,
|
|
474
|
-
timeMax,
|
|
475
|
-
singleEvents: "true",
|
|
476
|
-
orderBy: "startTime",
|
|
477
|
-
}
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
// Update the last synced timestamp
|
|
481
|
-
await updateLastSyncedTimestampUtil(
|
|
482
|
-
db,
|
|
483
|
-
entityType,
|
|
484
|
-
entityId,
|
|
485
|
-
syncedCalendar.id
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
return response.items;
|
|
489
|
-
} catch (error) {
|
|
490
|
-
const apiError = error as ApiError;
|
|
491
|
-
console.error(
|
|
492
|
-
"Error fetching events from Google Calendar:",
|
|
493
|
-
apiError.message || "Unknown error"
|
|
494
|
-
);
|
|
495
|
-
throw new Error(
|
|
496
|
-
`Failed to fetch events from Google Calendar: ${
|
|
497
|
-
apiError.message || "Unknown error"
|
|
498
|
-
}`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Converts a Google Calendar event to our system's format
|
|
505
|
-
* @param googleEvent - Google Calendar event
|
|
506
|
-
* @param entityId - ID of the entity (practitioner, patient, clinic)
|
|
507
|
-
* @param entityType - Type of entity
|
|
508
|
-
* @returns Converted calendar event
|
|
509
|
-
*/
|
|
510
|
-
export function convertGoogleEventToCalendarEventUtil(
|
|
511
|
-
googleEvent: any,
|
|
512
|
-
entityId: string,
|
|
513
|
-
entityType: "practitioner" | "patient" | "clinic"
|
|
514
|
-
): Partial<CalendarEvent> {
|
|
515
|
-
// Extract start and end times
|
|
516
|
-
const start = googleEvent.start.dateTime
|
|
517
|
-
? new Date(googleEvent.start.dateTime)
|
|
518
|
-
: new Date(googleEvent.start.date);
|
|
519
|
-
const end = googleEvent.end.dateTime
|
|
520
|
-
? new Date(googleEvent.end.dateTime)
|
|
521
|
-
: new Date(googleEvent.end.date);
|
|
522
|
-
|
|
523
|
-
// Create the calendar event
|
|
524
|
-
const calendarEvent: Partial<CalendarEvent> = {
|
|
525
|
-
eventName: googleEvent.summary || "External Event",
|
|
526
|
-
eventLocation: googleEvent.location,
|
|
527
|
-
eventTime: {
|
|
528
|
-
start: Timestamp.fromDate(start),
|
|
529
|
-
end: Timestamp.fromDate(end),
|
|
530
|
-
},
|
|
531
|
-
description: googleEvent.description || "",
|
|
532
|
-
// External events are always set as CONFIRMED - status updates will happen externally
|
|
533
|
-
status: CalendarEventStatus.CONFIRMED,
|
|
534
|
-
// All external events are marked as EXTERNAL to indicate they originated outside our system
|
|
535
|
-
syncStatus: CalendarSyncStatus.EXTERNAL,
|
|
536
|
-
// All external events are treated as BLOCKING events
|
|
537
|
-
eventType: CalendarEventType.BLOCKING,
|
|
538
|
-
// Store the original Google Calendar event ID
|
|
539
|
-
syncedCalendarEventId: [
|
|
540
|
-
{
|
|
541
|
-
eventId: googleEvent.id,
|
|
542
|
-
syncedCalendarProvider: SyncedCalendarProvider.GOOGLE,
|
|
543
|
-
syncedAt: Timestamp.now(),
|
|
544
|
-
},
|
|
545
|
-
],
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
// Add entity-specific fields
|
|
549
|
-
switch (entityType) {
|
|
550
|
-
case "practitioner":
|
|
551
|
-
calendarEvent.practitionerProfileId = entityId;
|
|
552
|
-
break;
|
|
553
|
-
case "patient":
|
|
554
|
-
calendarEvent.patientProfileId = entityId;
|
|
555
|
-
break;
|
|
556
|
-
case "clinic":
|
|
557
|
-
calendarEvent.clinicBranchId = entityId;
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return calendarEvent;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Converts our system's calendar event to Google Calendar format
|
|
566
|
-
* @param calendarEvent - Our system's calendar event
|
|
567
|
-
* @returns Google Calendar event
|
|
568
|
-
*/
|
|
569
|
-
export function convertCalendarEventToGoogleEventUtil(
|
|
570
|
-
calendarEvent: CalendarEvent
|
|
571
|
-
): any {
|
|
572
|
-
// Create the Google Calendar event
|
|
573
|
-
const googleEvent: any = {
|
|
574
|
-
summary: calendarEvent.eventName,
|
|
575
|
-
location: calendarEvent.eventLocation,
|
|
576
|
-
description: calendarEvent.description,
|
|
577
|
-
start: {
|
|
578
|
-
dateTime: calendarEvent.eventTime.start.toDate().toISOString(),
|
|
579
|
-
timeZone: "UTC",
|
|
580
|
-
},
|
|
581
|
-
end: {
|
|
582
|
-
dateTime: calendarEvent.eventTime.end.toDate().toISOString(),
|
|
583
|
-
timeZone: "UTC",
|
|
584
|
-
},
|
|
585
|
-
// Add reminders
|
|
586
|
-
reminders: {
|
|
587
|
-
useDefault: false,
|
|
588
|
-
overrides: [
|
|
589
|
-
{ method: "email", minutes: 24 * 60 }, // 1 day before
|
|
590
|
-
{ method: "popup", minutes: 30 }, // 30 minutes before
|
|
591
|
-
],
|
|
592
|
-
},
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
// Add status mapping
|
|
596
|
-
switch (calendarEvent.status) {
|
|
597
|
-
case CalendarEventStatus.CONFIRMED:
|
|
598
|
-
googleEvent.status = "confirmed";
|
|
599
|
-
break;
|
|
600
|
-
case CalendarEventStatus.CANCELED:
|
|
601
|
-
googleEvent.status = "cancelled";
|
|
602
|
-
break;
|
|
603
|
-
case CalendarEventStatus.PENDING:
|
|
604
|
-
// Google Calendar doesn't have a direct equivalent for pending
|
|
605
|
-
// We'll use tentative as the closest match
|
|
606
|
-
googleEvent.status = "tentative";
|
|
607
|
-
break;
|
|
608
|
-
default:
|
|
609
|
-
googleEvent.status = "confirmed";
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Add attendees if this is an appointment
|
|
613
|
-
if (calendarEvent.eventType === CalendarEventType.APPOINTMENT) {
|
|
614
|
-
googleEvent.attendees = [];
|
|
615
|
-
|
|
616
|
-
// In a real implementation, you would fetch the email addresses
|
|
617
|
-
// of the practitioner and patient from their profiles
|
|
618
|
-
if (calendarEvent.practitionerProfileId) {
|
|
619
|
-
googleEvent.attendees.push({
|
|
620
|
-
email: "practitioner@example.com", // This would be fetched from the practitioner profile
|
|
621
|
-
displayName: "Dr. Practitioner", // This would be fetched from the practitioner profile
|
|
622
|
-
responseStatus: "accepted",
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if (calendarEvent.patientProfileId) {
|
|
627
|
-
googleEvent.attendees.push({
|
|
628
|
-
email: "patient@example.com", // This would be fetched from the patient profile
|
|
629
|
-
displayName: "Patient", // This would be fetched from the patient profile
|
|
630
|
-
responseStatus: "needsAction",
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return googleEvent;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Deletes an event from Google Calendar
|
|
640
|
-
* @param db - Firestore instance
|
|
641
|
-
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
642
|
-
* @param entityId - ID of the entity
|
|
643
|
-
* @param syncedCalendar - Synced calendar data
|
|
644
|
-
* @param eventId - ID of the event in Google Calendar
|
|
645
|
-
* @returns Success status
|
|
646
|
-
*/
|
|
647
|
-
export async function deleteGoogleCalendarEventUtil(
|
|
648
|
-
db: Firestore,
|
|
649
|
-
entityType: "practitioner" | "patient" | "clinic",
|
|
650
|
-
entityId: string,
|
|
651
|
-
syncedCalendar: SyncedCalendar,
|
|
652
|
-
eventId: string
|
|
653
|
-
): Promise<boolean> {
|
|
654
|
-
try {
|
|
655
|
-
// Ensure we have a valid token
|
|
656
|
-
const accessToken = await ensureValidToken(
|
|
657
|
-
db,
|
|
658
|
-
entityType,
|
|
659
|
-
entityId,
|
|
660
|
-
syncedCalendar
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
// Delete the event from Google Calendar
|
|
664
|
-
await makeRequest(
|
|
665
|
-
"delete",
|
|
666
|
-
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
|
|
667
|
-
{ Authorization: `Bearer ${accessToken}` }
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
return true;
|
|
671
|
-
} catch (error) {
|
|
672
|
-
const apiError = error as ApiError;
|
|
673
|
-
console.error(
|
|
674
|
-
"Error deleting event from Google Calendar:",
|
|
675
|
-
apiError.message || "Unknown error"
|
|
676
|
-
);
|
|
677
|
-
throw new Error(
|
|
678
|
-
`Failed to delete event from Google Calendar: ${
|
|
679
|
-
apiError.message || "Unknown error"
|
|
680
|
-
}`
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Gets the OAuth URL for Google Calendar
|
|
687
|
-
* @param scopes - OAuth scopes to request
|
|
688
|
-
* @returns OAuth URL
|
|
689
|
-
*/
|
|
690
|
-
export function getGoogleCalendarOAuthUrlUtil(
|
|
691
|
-
scopes: string[] = ["https://www.googleapis.com/auth/calendar"]
|
|
692
|
-
): string {
|
|
693
|
-
const scopeString = encodeURIComponent(scopes.join(" "));
|
|
694
|
-
return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
|
|
695
|
-
REDIRECT_URI
|
|
696
|
-
)}&response_type=code&scope=${scopeString}&access_type=offline&prompt=consent`;
|
|
697
|
-
}
|
|
1
|
+
/// <reference types="axios" />
|
|
2
|
+
import { Firestore, Timestamp } from "firebase/firestore";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import {
|
|
5
|
+
SyncedCalendar,
|
|
6
|
+
SyncedCalendarProvider,
|
|
7
|
+
UpdateSyncedCalendarData,
|
|
8
|
+
} from "../../../types/calendar/synced-calendar.types";
|
|
9
|
+
import {
|
|
10
|
+
CalendarEvent,
|
|
11
|
+
CalendarEventStatus,
|
|
12
|
+
CalendarEventType,
|
|
13
|
+
CalendarSyncStatus,
|
|
14
|
+
} from "../../../types/calendar";
|
|
15
|
+
import {
|
|
16
|
+
updateLastSyncedTimestampUtil,
|
|
17
|
+
updatePractitionerSyncedCalendarUtil,
|
|
18
|
+
updatePatientSyncedCalendarUtil,
|
|
19
|
+
updateClinicSyncedCalendarUtil,
|
|
20
|
+
} from "./synced-calendar.utils";
|
|
21
|
+
|
|
22
|
+
// API URL and client configuration - these should be environment variables in a real application
|
|
23
|
+
export const GOOGLE_CALENDAR_API_URL = "https://www.googleapis.com/calendar/v3";
|
|
24
|
+
const GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
25
|
+
const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
26
|
+
const CLIENT_ID = "your-client-id"; // Replace with your actual client ID
|
|
27
|
+
const CLIENT_SECRET = "your-client-secret"; // Replace with your actual client secret
|
|
28
|
+
const REDIRECT_URI = "your-redirect-uri"; // Replace with your actual redirect URI
|
|
29
|
+
|
|
30
|
+
// Error interface for better error handling
|
|
31
|
+
interface ApiError {
|
|
32
|
+
message: string;
|
|
33
|
+
status?: number;
|
|
34
|
+
response?: {
|
|
35
|
+
data?: any;
|
|
36
|
+
status?: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper function for making API requests
|
|
42
|
+
* @param method - HTTP method
|
|
43
|
+
* @param url - Request URL
|
|
44
|
+
* @param headers - Request headers
|
|
45
|
+
* @param data - Request body data
|
|
46
|
+
* @param params - URL query parameters
|
|
47
|
+
* @returns Response data
|
|
48
|
+
*/
|
|
49
|
+
export async function makeRequest(
|
|
50
|
+
method: string,
|
|
51
|
+
url: string,
|
|
52
|
+
headers: Record<string, string>,
|
|
53
|
+
data?: any,
|
|
54
|
+
params?: Record<string, string>
|
|
55
|
+
): Promise<any> {
|
|
56
|
+
// Construct URL with query parameters if provided
|
|
57
|
+
const queryParams = params
|
|
58
|
+
? "?" +
|
|
59
|
+
Object.entries(params)
|
|
60
|
+
.map(
|
|
61
|
+
([key, value]) =>
|
|
62
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
63
|
+
)
|
|
64
|
+
.join("&")
|
|
65
|
+
: "";
|
|
66
|
+
|
|
67
|
+
const finalUrl = url + queryParams;
|
|
68
|
+
|
|
69
|
+
// Example implementation with fetch
|
|
70
|
+
const options: RequestInit = {
|
|
71
|
+
method,
|
|
72
|
+
headers,
|
|
73
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const response = await fetch(finalUrl, options);
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const error: any = new Error(
|
|
80
|
+
`Request failed with status ${response.status}`
|
|
81
|
+
);
|
|
82
|
+
error.response = response;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return response.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Authenticates with Google Calendar API
|
|
91
|
+
* @param authCode - Authorization code from Google OAuth
|
|
92
|
+
* @returns Access token and refresh token
|
|
93
|
+
*/
|
|
94
|
+
export async function authenticateWithGoogleCalendarUtil(
|
|
95
|
+
authCode: string
|
|
96
|
+
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
|
97
|
+
try {
|
|
98
|
+
// Exchange authorization code for tokens
|
|
99
|
+
const data = {
|
|
100
|
+
code: authCode,
|
|
101
|
+
client_id: CLIENT_ID,
|
|
102
|
+
client_secret: CLIENT_SECRET,
|
|
103
|
+
redirect_uri: REDIRECT_URI,
|
|
104
|
+
grant_type: "authorization_code",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const response = await makeRequest(
|
|
108
|
+
"post",
|
|
109
|
+
GOOGLE_OAUTH_URL,
|
|
110
|
+
{ "Content-Type": "application/json" },
|
|
111
|
+
data
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
accessToken: response.access_token,
|
|
116
|
+
refreshToken: response.refresh_token,
|
|
117
|
+
expiresIn: response.expires_in,
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const apiError = error as ApiError;
|
|
121
|
+
console.error(
|
|
122
|
+
"Error authenticating with Google Calendar:",
|
|
123
|
+
apiError.message || "Unknown error"
|
|
124
|
+
);
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Failed to authenticate with Google Calendar: ${
|
|
127
|
+
apiError.message || "Unknown error"
|
|
128
|
+
}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Refreshes the Google Calendar API access token
|
|
135
|
+
* @param refreshToken - Refresh token
|
|
136
|
+
* @returns New access token and expiry
|
|
137
|
+
*/
|
|
138
|
+
export async function refreshGoogleCalendarTokenUtil(
|
|
139
|
+
refreshToken: string
|
|
140
|
+
): Promise<{ accessToken: string; expiresIn: number }> {
|
|
141
|
+
try {
|
|
142
|
+
// Use refresh token to get a new access token
|
|
143
|
+
const data = {
|
|
144
|
+
refresh_token: refreshToken,
|
|
145
|
+
client_id: CLIENT_ID,
|
|
146
|
+
client_secret: CLIENT_SECRET,
|
|
147
|
+
grant_type: "refresh_token",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const response = await makeRequest(
|
|
151
|
+
"post",
|
|
152
|
+
GOOGLE_OAUTH_URL,
|
|
153
|
+
{ "Content-Type": "application/json" },
|
|
154
|
+
data
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
accessToken: response.access_token,
|
|
159
|
+
expiresIn: response.expires_in,
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const apiError = error as ApiError;
|
|
163
|
+
console.error(
|
|
164
|
+
"Error refreshing Google Calendar token:",
|
|
165
|
+
apiError.message || "Unknown error"
|
|
166
|
+
);
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Failed to refresh Google Calendar token: ${
|
|
169
|
+
apiError.message || "Unknown error"
|
|
170
|
+
}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Lists available Google Calendars for a user
|
|
177
|
+
* @param accessToken - Google API access token
|
|
178
|
+
* @returns List of available calendars
|
|
179
|
+
*/
|
|
180
|
+
export async function listGoogleCalendarsUtil(
|
|
181
|
+
accessToken: string
|
|
182
|
+
): Promise<Array<{ id: string; name: string }>> {
|
|
183
|
+
try {
|
|
184
|
+
// Call Google Calendar API to list calendars
|
|
185
|
+
const response = await makeRequest(
|
|
186
|
+
"get",
|
|
187
|
+
`${GOOGLE_CALENDAR_API_URL}/users/me/calendarList`,
|
|
188
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Map the response to our format
|
|
192
|
+
return response.items.map((calendar: any) => ({
|
|
193
|
+
id: calendar.id,
|
|
194
|
+
name: calendar.summary,
|
|
195
|
+
}));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const apiError = error as ApiError;
|
|
198
|
+
console.error(
|
|
199
|
+
"Error listing Google Calendars:",
|
|
200
|
+
apiError.message || "Unknown error"
|
|
201
|
+
);
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Failed to list Google Calendars: ${apiError.message || "Unknown error"}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Ensures the synced calendar token is valid and refreshes if needed
|
|
210
|
+
* @param db - Firestore instance
|
|
211
|
+
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
212
|
+
* @param entityId - ID of the entity
|
|
213
|
+
* @param syncedCalendar - Synced calendar data
|
|
214
|
+
* @returns Valid access token
|
|
215
|
+
*/
|
|
216
|
+
async function ensureValidToken(
|
|
217
|
+
db: Firestore,
|
|
218
|
+
entityType: "practitioner" | "patient" | "clinic",
|
|
219
|
+
entityId: string,
|
|
220
|
+
syncedCalendar: SyncedCalendar
|
|
221
|
+
): Promise<string> {
|
|
222
|
+
// Check if token is expired or will expire soon (within 5 minutes)
|
|
223
|
+
const expiryTime = syncedCalendar.tokenExpiry.toDate();
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
|
|
226
|
+
|
|
227
|
+
if (expiryTime < fiveMinutesFromNow) {
|
|
228
|
+
// Token is expired or will expire soon, refresh it
|
|
229
|
+
const { accessToken, expiresIn } = await refreshGoogleCalendarTokenUtil(
|
|
230
|
+
syncedCalendar.refreshToken
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Calculate new expiry time
|
|
234
|
+
const tokenExpiry = new Date();
|
|
235
|
+
tokenExpiry.setSeconds(tokenExpiry.getSeconds() + expiresIn);
|
|
236
|
+
|
|
237
|
+
// Update the synced calendar with the new token
|
|
238
|
+
const updateData: Omit<UpdateSyncedCalendarData, "updatedAt"> = {
|
|
239
|
+
accessToken,
|
|
240
|
+
tokenExpiry: Timestamp.fromDate(tokenExpiry),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Update the synced calendar in Firestore
|
|
244
|
+
switch (entityType) {
|
|
245
|
+
case "practitioner":
|
|
246
|
+
await updatePractitionerSyncedCalendarUtil(
|
|
247
|
+
db,
|
|
248
|
+
entityId,
|
|
249
|
+
syncedCalendar.id,
|
|
250
|
+
updateData
|
|
251
|
+
);
|
|
252
|
+
break;
|
|
253
|
+
case "patient":
|
|
254
|
+
await updatePatientSyncedCalendarUtil(
|
|
255
|
+
db,
|
|
256
|
+
entityId,
|
|
257
|
+
syncedCalendar.id,
|
|
258
|
+
updateData
|
|
259
|
+
);
|
|
260
|
+
break;
|
|
261
|
+
case "clinic":
|
|
262
|
+
await updateClinicSyncedCalendarUtil(
|
|
263
|
+
db,
|
|
264
|
+
entityId,
|
|
265
|
+
syncedCalendar.id,
|
|
266
|
+
updateData
|
|
267
|
+
);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return accessToken;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Token is still valid
|
|
275
|
+
return syncedCalendar.accessToken;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Syncs events from our system to Google Calendar
|
|
280
|
+
* @param db - Firestore instance
|
|
281
|
+
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
282
|
+
* @param entityId - ID of the entity
|
|
283
|
+
* @param syncedCalendar - Synced calendar to use
|
|
284
|
+
* @param events - Events to sync
|
|
285
|
+
* @param existingSyncId - Optional existing sync ID for updating an event
|
|
286
|
+
* @returns Result of the sync operation
|
|
287
|
+
*/
|
|
288
|
+
export async function syncEventsToGoogleCalendarUtil(
|
|
289
|
+
db: Firestore,
|
|
290
|
+
entityType: "practitioner" | "patient" | "clinic",
|
|
291
|
+
entityId: string,
|
|
292
|
+
syncedCalendar: SyncedCalendar,
|
|
293
|
+
events: CalendarEvent[],
|
|
294
|
+
existingSyncId?: string
|
|
295
|
+
): Promise<{
|
|
296
|
+
success: boolean;
|
|
297
|
+
syncedEvents: number;
|
|
298
|
+
errors: any[];
|
|
299
|
+
eventIds: string[];
|
|
300
|
+
}> {
|
|
301
|
+
try {
|
|
302
|
+
// Refresh token if needed
|
|
303
|
+
const { accessToken } = await refreshGoogleCalendarTokenUtil(
|
|
304
|
+
syncedCalendar.refreshToken
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
let syncedCount = 0;
|
|
308
|
+
const errors: any[] = [];
|
|
309
|
+
const eventIds: string[] = [];
|
|
310
|
+
|
|
311
|
+
// Process each event
|
|
312
|
+
for (const event of events) {
|
|
313
|
+
try {
|
|
314
|
+
// For patients: Sync all INTERNAL events except CANCELED and REJECTED
|
|
315
|
+
// For doctors: Only sync INTERNAL events with CONFIRMED status
|
|
316
|
+
// For clinics: No longer syncing
|
|
317
|
+
|
|
318
|
+
// Skip events that are external (we don't sync external events back)
|
|
319
|
+
if (event.syncStatus === CalendarSyncStatus.EXTERNAL) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Apply entity-specific sync rules
|
|
324
|
+
if (
|
|
325
|
+
entityType === "practitioner" &&
|
|
326
|
+
event.status !== CalendarEventStatus.CONFIRMED
|
|
327
|
+
) {
|
|
328
|
+
// For doctors, only sync CONFIRMED events
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
entityType === "patient" &&
|
|
334
|
+
(event.status === CalendarEventStatus.CANCELED ||
|
|
335
|
+
event.status === CalendarEventStatus.REJECTED)
|
|
336
|
+
) {
|
|
337
|
+
// For patients, don't sync CANCELED or REJECTED events
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (entityType === "clinic") {
|
|
342
|
+
// No longer syncing clinic events
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Convert our event to Google Calendar format
|
|
347
|
+
const googleEvent = convertCalendarEventToGoogleEventUtil(event);
|
|
348
|
+
const headers = {
|
|
349
|
+
Authorization: `Bearer ${accessToken}`,
|
|
350
|
+
"Content-Type": "application/json",
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let responseId = "";
|
|
354
|
+
|
|
355
|
+
// Check if we have an existing sync ID to update
|
|
356
|
+
if (existingSyncId) {
|
|
357
|
+
// Update the existing event using the provided ID
|
|
358
|
+
const response = await makeRequest(
|
|
359
|
+
"put",
|
|
360
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSyncId}`,
|
|
361
|
+
headers,
|
|
362
|
+
googleEvent
|
|
363
|
+
);
|
|
364
|
+
responseId = response.id;
|
|
365
|
+
} else {
|
|
366
|
+
// Check if the event already has a synced calendar event ID for this calendar
|
|
367
|
+
const existingSync = event.syncedCalendarEventId?.find(
|
|
368
|
+
(sync) =>
|
|
369
|
+
sync.syncedCalendarProvider === SyncedCalendarProvider.GOOGLE &&
|
|
370
|
+
// We should check if this is the same calendar we're syncing with, but that information isn't stored
|
|
371
|
+
// For now, we'll just use the first Google Calendar sync ID
|
|
372
|
+
sync.syncedCalendarProvider === syncedCalendar.provider
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
if (existingSync) {
|
|
376
|
+
// Update existing event
|
|
377
|
+
const response = await makeRequest(
|
|
378
|
+
"put",
|
|
379
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSync.eventId}`,
|
|
380
|
+
headers,
|
|
381
|
+
googleEvent
|
|
382
|
+
);
|
|
383
|
+
responseId = response.id;
|
|
384
|
+
} else {
|
|
385
|
+
// Create new event
|
|
386
|
+
const response = await makeRequest(
|
|
387
|
+
"post",
|
|
388
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
389
|
+
headers,
|
|
390
|
+
googleEvent
|
|
391
|
+
);
|
|
392
|
+
responseId = response.id;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (responseId) {
|
|
397
|
+
eventIds.push(responseId);
|
|
398
|
+
syncedCount++;
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const apiError = error as ApiError;
|
|
402
|
+
errors.push({
|
|
403
|
+
eventId: event.id,
|
|
404
|
+
error: apiError.message || "Unknown error",
|
|
405
|
+
status: apiError.response?.status,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Update the last synced timestamp
|
|
411
|
+
await updateLastSyncedTimestampUtil(
|
|
412
|
+
db,
|
|
413
|
+
entityType,
|
|
414
|
+
entityId,
|
|
415
|
+
syncedCalendar.id
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
success: errors.length === 0,
|
|
420
|
+
syncedEvents: syncedCount,
|
|
421
|
+
errors,
|
|
422
|
+
eventIds,
|
|
423
|
+
};
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error("Error syncing with Google Calendar:", error);
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
syncedEvents: 0,
|
|
429
|
+
errors: [{ error: (error as Error).message || "Unknown error" }],
|
|
430
|
+
eventIds: [],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Fetches events from Google Calendar
|
|
437
|
+
* @param db - Firestore instance
|
|
438
|
+
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
439
|
+
* @param entityId - ID of the entity
|
|
440
|
+
* @param syncedCalendar - Synced calendar data
|
|
441
|
+
* @param startDate - Start date for fetching events
|
|
442
|
+
* @param endDate - End date for fetching events
|
|
443
|
+
* @returns Events fetched from Google Calendar
|
|
444
|
+
*/
|
|
445
|
+
export async function fetchEventsFromGoogleCalendarUtil(
|
|
446
|
+
db: Firestore,
|
|
447
|
+
entityType: "practitioner" | "patient" | "clinic",
|
|
448
|
+
entityId: string,
|
|
449
|
+
syncedCalendar: SyncedCalendar,
|
|
450
|
+
startDate: Date,
|
|
451
|
+
endDate: Date
|
|
452
|
+
): Promise<any[]> {
|
|
453
|
+
try {
|
|
454
|
+
// Ensure we have a valid token
|
|
455
|
+
const accessToken = await ensureValidToken(
|
|
456
|
+
db,
|
|
457
|
+
entityType,
|
|
458
|
+
entityId,
|
|
459
|
+
syncedCalendar
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// Format dates for Google Calendar API
|
|
463
|
+
const timeMin = startDate.toISOString();
|
|
464
|
+
const timeMax = endDate.toISOString();
|
|
465
|
+
|
|
466
|
+
// Call Google Calendar API to fetch events
|
|
467
|
+
const response = await makeRequest(
|
|
468
|
+
"get",
|
|
469
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
470
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
471
|
+
undefined,
|
|
472
|
+
{
|
|
473
|
+
timeMin,
|
|
474
|
+
timeMax,
|
|
475
|
+
singleEvents: "true",
|
|
476
|
+
orderBy: "startTime",
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// Update the last synced timestamp
|
|
481
|
+
await updateLastSyncedTimestampUtil(
|
|
482
|
+
db,
|
|
483
|
+
entityType,
|
|
484
|
+
entityId,
|
|
485
|
+
syncedCalendar.id
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return response.items;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const apiError = error as ApiError;
|
|
491
|
+
console.error(
|
|
492
|
+
"Error fetching events from Google Calendar:",
|
|
493
|
+
apiError.message || "Unknown error"
|
|
494
|
+
);
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Failed to fetch events from Google Calendar: ${
|
|
497
|
+
apiError.message || "Unknown error"
|
|
498
|
+
}`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Converts a Google Calendar event to our system's format
|
|
505
|
+
* @param googleEvent - Google Calendar event
|
|
506
|
+
* @param entityId - ID of the entity (practitioner, patient, clinic)
|
|
507
|
+
* @param entityType - Type of entity
|
|
508
|
+
* @returns Converted calendar event
|
|
509
|
+
*/
|
|
510
|
+
export function convertGoogleEventToCalendarEventUtil(
|
|
511
|
+
googleEvent: any,
|
|
512
|
+
entityId: string,
|
|
513
|
+
entityType: "practitioner" | "patient" | "clinic"
|
|
514
|
+
): Partial<CalendarEvent> {
|
|
515
|
+
// Extract start and end times
|
|
516
|
+
const start = googleEvent.start.dateTime
|
|
517
|
+
? new Date(googleEvent.start.dateTime)
|
|
518
|
+
: new Date(googleEvent.start.date);
|
|
519
|
+
const end = googleEvent.end.dateTime
|
|
520
|
+
? new Date(googleEvent.end.dateTime)
|
|
521
|
+
: new Date(googleEvent.end.date);
|
|
522
|
+
|
|
523
|
+
// Create the calendar event
|
|
524
|
+
const calendarEvent: Partial<CalendarEvent> = {
|
|
525
|
+
eventName: googleEvent.summary || "External Event",
|
|
526
|
+
eventLocation: googleEvent.location,
|
|
527
|
+
eventTime: {
|
|
528
|
+
start: Timestamp.fromDate(start),
|
|
529
|
+
end: Timestamp.fromDate(end),
|
|
530
|
+
},
|
|
531
|
+
description: googleEvent.description || "",
|
|
532
|
+
// External events are always set as CONFIRMED - status updates will happen externally
|
|
533
|
+
status: CalendarEventStatus.CONFIRMED,
|
|
534
|
+
// All external events are marked as EXTERNAL to indicate they originated outside our system
|
|
535
|
+
syncStatus: CalendarSyncStatus.EXTERNAL,
|
|
536
|
+
// All external events are treated as BLOCKING events
|
|
537
|
+
eventType: CalendarEventType.BLOCKING,
|
|
538
|
+
// Store the original Google Calendar event ID
|
|
539
|
+
syncedCalendarEventId: [
|
|
540
|
+
{
|
|
541
|
+
eventId: googleEvent.id,
|
|
542
|
+
syncedCalendarProvider: SyncedCalendarProvider.GOOGLE,
|
|
543
|
+
syncedAt: Timestamp.now(),
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Add entity-specific fields
|
|
549
|
+
switch (entityType) {
|
|
550
|
+
case "practitioner":
|
|
551
|
+
calendarEvent.practitionerProfileId = entityId;
|
|
552
|
+
break;
|
|
553
|
+
case "patient":
|
|
554
|
+
calendarEvent.patientProfileId = entityId;
|
|
555
|
+
break;
|
|
556
|
+
case "clinic":
|
|
557
|
+
calendarEvent.clinicBranchId = entityId;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return calendarEvent;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Converts our system's calendar event to Google Calendar format
|
|
566
|
+
* @param calendarEvent - Our system's calendar event
|
|
567
|
+
* @returns Google Calendar event
|
|
568
|
+
*/
|
|
569
|
+
export function convertCalendarEventToGoogleEventUtil(
|
|
570
|
+
calendarEvent: CalendarEvent
|
|
571
|
+
): any {
|
|
572
|
+
// Create the Google Calendar event
|
|
573
|
+
const googleEvent: any = {
|
|
574
|
+
summary: calendarEvent.eventName,
|
|
575
|
+
location: calendarEvent.eventLocation,
|
|
576
|
+
description: calendarEvent.description,
|
|
577
|
+
start: {
|
|
578
|
+
dateTime: calendarEvent.eventTime.start.toDate().toISOString(),
|
|
579
|
+
timeZone: "UTC",
|
|
580
|
+
},
|
|
581
|
+
end: {
|
|
582
|
+
dateTime: calendarEvent.eventTime.end.toDate().toISOString(),
|
|
583
|
+
timeZone: "UTC",
|
|
584
|
+
},
|
|
585
|
+
// Add reminders
|
|
586
|
+
reminders: {
|
|
587
|
+
useDefault: false,
|
|
588
|
+
overrides: [
|
|
589
|
+
{ method: "email", minutes: 24 * 60 }, // 1 day before
|
|
590
|
+
{ method: "popup", minutes: 30 }, // 30 minutes before
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Add status mapping
|
|
596
|
+
switch (calendarEvent.status) {
|
|
597
|
+
case CalendarEventStatus.CONFIRMED:
|
|
598
|
+
googleEvent.status = "confirmed";
|
|
599
|
+
break;
|
|
600
|
+
case CalendarEventStatus.CANCELED:
|
|
601
|
+
googleEvent.status = "cancelled";
|
|
602
|
+
break;
|
|
603
|
+
case CalendarEventStatus.PENDING:
|
|
604
|
+
// Google Calendar doesn't have a direct equivalent for pending
|
|
605
|
+
// We'll use tentative as the closest match
|
|
606
|
+
googleEvent.status = "tentative";
|
|
607
|
+
break;
|
|
608
|
+
default:
|
|
609
|
+
googleEvent.status = "confirmed";
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Add attendees if this is an appointment
|
|
613
|
+
if (calendarEvent.eventType === CalendarEventType.APPOINTMENT) {
|
|
614
|
+
googleEvent.attendees = [];
|
|
615
|
+
|
|
616
|
+
// In a real implementation, you would fetch the email addresses
|
|
617
|
+
// of the practitioner and patient from their profiles
|
|
618
|
+
if (calendarEvent.practitionerProfileId) {
|
|
619
|
+
googleEvent.attendees.push({
|
|
620
|
+
email: "practitioner@example.com", // This would be fetched from the practitioner profile
|
|
621
|
+
displayName: "Dr. Practitioner", // This would be fetched from the practitioner profile
|
|
622
|
+
responseStatus: "accepted",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (calendarEvent.patientProfileId) {
|
|
627
|
+
googleEvent.attendees.push({
|
|
628
|
+
email: "patient@example.com", // This would be fetched from the patient profile
|
|
629
|
+
displayName: "Patient", // This would be fetched from the patient profile
|
|
630
|
+
responseStatus: "needsAction",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return googleEvent;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Deletes an event from Google Calendar
|
|
640
|
+
* @param db - Firestore instance
|
|
641
|
+
* @param entityType - Type of entity (practitioner, patient, clinic)
|
|
642
|
+
* @param entityId - ID of the entity
|
|
643
|
+
* @param syncedCalendar - Synced calendar data
|
|
644
|
+
* @param eventId - ID of the event in Google Calendar
|
|
645
|
+
* @returns Success status
|
|
646
|
+
*/
|
|
647
|
+
export async function deleteGoogleCalendarEventUtil(
|
|
648
|
+
db: Firestore,
|
|
649
|
+
entityType: "practitioner" | "patient" | "clinic",
|
|
650
|
+
entityId: string,
|
|
651
|
+
syncedCalendar: SyncedCalendar,
|
|
652
|
+
eventId: string
|
|
653
|
+
): Promise<boolean> {
|
|
654
|
+
try {
|
|
655
|
+
// Ensure we have a valid token
|
|
656
|
+
const accessToken = await ensureValidToken(
|
|
657
|
+
db,
|
|
658
|
+
entityType,
|
|
659
|
+
entityId,
|
|
660
|
+
syncedCalendar
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
// Delete the event from Google Calendar
|
|
664
|
+
await makeRequest(
|
|
665
|
+
"delete",
|
|
666
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
|
|
667
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
return true;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
const apiError = error as ApiError;
|
|
673
|
+
console.error(
|
|
674
|
+
"Error deleting event from Google Calendar:",
|
|
675
|
+
apiError.message || "Unknown error"
|
|
676
|
+
);
|
|
677
|
+
throw new Error(
|
|
678
|
+
`Failed to delete event from Google Calendar: ${
|
|
679
|
+
apiError.message || "Unknown error"
|
|
680
|
+
}`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Gets the OAuth URL for Google Calendar
|
|
687
|
+
* @param scopes - OAuth scopes to request
|
|
688
|
+
* @returns OAuth URL
|
|
689
|
+
*/
|
|
690
|
+
export function getGoogleCalendarOAuthUrlUtil(
|
|
691
|
+
scopes: string[] = ["https://www.googleapis.com/auth/calendar"]
|
|
692
|
+
): string {
|
|
693
|
+
const scopeString = encodeURIComponent(scopes.join(" "));
|
|
694
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
|
|
695
|
+
REDIRECT_URI
|
|
696
|
+
)}&response_type=code&scope=${scopeString}&access_type=offline&prompt=consent`;
|
|
697
|
+
}
|