@blackcode_sa/metaestetics-api 1.4.18 → 1.5.1
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/index.d.mts +7900 -4726
- package/dist/index.d.ts +7900 -4726
- package/dist/index.js +3029 -111
- package/dist/index.mjs +3059 -111
- package/package.json +4 -3
- package/src/index.ts +57 -2
- package/src/services/calendar/calendar-refactored.service.ts +1531 -0
- package/src/services/calendar/calendar.service.ts +1077 -0
- package/src/services/calendar/synced-calendars.service.ts +743 -0
- package/src/services/calendar/utils/appointment.utils.ts +314 -0
- package/src/services/calendar/utils/calendar-event.utils.ts +510 -0
- package/src/services/calendar/utils/clinic.utils.ts +237 -0
- package/src/services/calendar/utils/docs.utils.ts +157 -0
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -0
- package/src/services/calendar/utils/index.ts +8 -0
- package/src/services/calendar/utils/patient.utils.ts +198 -0
- package/src/services/calendar/utils/practitioner.utils.ts +221 -0
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -0
- package/src/services/practitioner/practitioner.service.ts +346 -1
- package/src/types/calendar/index.ts +187 -0
- package/src/types/calendar/synced-calendar.types.ts +66 -0
- package/src/types/clinic/index.ts +1 -12
- package/src/types/index.ts +4 -0
- package/src/types/practitioner/index.ts +81 -0
- package/src/types/profile/index.ts +39 -0
- package/src/validations/calendar.schema.ts +223 -0
- package/src/validations/practitioner.schema.ts +66 -0
- package/src/validations/profile-info.schema.ts +41 -0
|
@@ -17,11 +17,20 @@ import {
|
|
|
17
17
|
CreatePractitionerData,
|
|
18
18
|
UpdatePractitionerData,
|
|
19
19
|
PRACTITIONERS_COLLECTION,
|
|
20
|
+
REGISTER_TOKENS_COLLECTION,
|
|
21
|
+
PractitionerStatus,
|
|
22
|
+
CreateDraftPractitionerData,
|
|
23
|
+
PractitionerToken,
|
|
24
|
+
CreatePractitionerTokenData,
|
|
25
|
+
PractitionerTokenStatus,
|
|
20
26
|
} from "../../types/practitioner";
|
|
21
27
|
import { ClinicService } from "../clinic/clinic.service";
|
|
22
28
|
import {
|
|
23
29
|
practitionerSchema,
|
|
24
30
|
createPractitionerSchema,
|
|
31
|
+
createDraftPractitionerSchema,
|
|
32
|
+
practitionerTokenSchema,
|
|
33
|
+
createPractitionerTokenSchema,
|
|
25
34
|
} from "../../validations/practitioner.schema";
|
|
26
35
|
import { z } from "zod";
|
|
27
36
|
import { Auth } from "firebase/auth";
|
|
@@ -92,8 +101,10 @@ export class PractitionerService extends BaseService {
|
|
|
92
101
|
basicInfo: validatedData.basicInfo,
|
|
93
102
|
certification: validatedData.certification,
|
|
94
103
|
clinics: validatedData.clinics || [],
|
|
104
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
95
105
|
isActive: validatedData.isActive,
|
|
96
106
|
isVerified: validatedData.isVerified,
|
|
107
|
+
status: validatedData.status || PractitionerStatus.ACTIVE,
|
|
97
108
|
createdAt: serverTimestamp(),
|
|
98
109
|
updatedAt: serverTimestamp(),
|
|
99
110
|
};
|
|
@@ -125,6 +136,278 @@ export class PractitionerService extends BaseService {
|
|
|
125
136
|
}
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Kreira novi draft profil zdravstvenog radnika bez povezanog korisnika
|
|
141
|
+
* Koristi se od strane administratora klinike za kreiranje profila i kasnije pozivanje
|
|
142
|
+
* @param data Podaci za kreiranje draft profila
|
|
143
|
+
* @param createdBy ID administratora koji kreira profil
|
|
144
|
+
* @param clinicId ID klinike za koju se kreira profil
|
|
145
|
+
* @returns Objekt koji sadrži kreirani draft profil i token za registraciju
|
|
146
|
+
*/
|
|
147
|
+
async createDraftPractitioner(
|
|
148
|
+
data: CreateDraftPractitionerData,
|
|
149
|
+
createdBy: string,
|
|
150
|
+
clinicId: string
|
|
151
|
+
): Promise<{ practitioner: Practitioner; token: PractitionerToken }> {
|
|
152
|
+
try {
|
|
153
|
+
// Validacija ulaznih podataka
|
|
154
|
+
const validatedData = createDraftPractitionerSchema.parse(data);
|
|
155
|
+
|
|
156
|
+
// Provera da li klinika postoji
|
|
157
|
+
const clinic = await this.getClinicService().getClinic(clinicId);
|
|
158
|
+
if (!clinic) {
|
|
159
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Priprema podataka za kreiranje profila
|
|
163
|
+
const clinics = data.clinics || [clinicId];
|
|
164
|
+
|
|
165
|
+
// Provera da li sve dodatno navedene klinike postoje
|
|
166
|
+
if (data.clinics) {
|
|
167
|
+
for (const cId of data.clinics) {
|
|
168
|
+
if (cId !== clinicId) {
|
|
169
|
+
const otherClinic = await this.getClinicService().getClinic(cId);
|
|
170
|
+
if (!otherClinic) {
|
|
171
|
+
throw new Error(`Clinic ${cId} not found`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const practitionerId = this.generateId();
|
|
178
|
+
const practitionerData: Omit<Practitioner, "createdAt" | "updatedAt"> & {
|
|
179
|
+
createdAt: ReturnType<typeof serverTimestamp>;
|
|
180
|
+
updatedAt: ReturnType<typeof serverTimestamp>;
|
|
181
|
+
} = {
|
|
182
|
+
id: practitionerId,
|
|
183
|
+
userRef: "", // Prazno - biće popunjeno kada korisnik kreira nalog
|
|
184
|
+
basicInfo: validatedData.basicInfo,
|
|
185
|
+
certification: validatedData.certification,
|
|
186
|
+
clinics: clinics,
|
|
187
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
188
|
+
isActive:
|
|
189
|
+
validatedData.isActive !== undefined ? validatedData.isActive : false,
|
|
190
|
+
isVerified:
|
|
191
|
+
validatedData.isVerified !== undefined
|
|
192
|
+
? validatedData.isVerified
|
|
193
|
+
: false,
|
|
194
|
+
status: PractitionerStatus.DRAFT,
|
|
195
|
+
createdAt: serverTimestamp(),
|
|
196
|
+
updatedAt: serverTimestamp(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Validacija kompletnog objekta
|
|
200
|
+
// Koristimo privremeni userRef za validaciju, biće prazan u bazi
|
|
201
|
+
practitionerSchema.parse({
|
|
202
|
+
...practitionerData,
|
|
203
|
+
userRef: "temp-for-validation",
|
|
204
|
+
createdAt: Timestamp.now(),
|
|
205
|
+
updatedAt: Timestamp.now(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Čuvamo u Firestore
|
|
209
|
+
await setDoc(
|
|
210
|
+
doc(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
|
|
211
|
+
practitionerData
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const savedPractitioner = await this.getPractitioner(practitionerData.id);
|
|
215
|
+
if (!savedPractitioner) {
|
|
216
|
+
throw new Error("Failed to create draft practitioner profile");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Automatski kreiramo token za registraciju
|
|
220
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
221
|
+
|
|
222
|
+
// Default expiration is 7 days from now
|
|
223
|
+
const expiration = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
224
|
+
|
|
225
|
+
const token: PractitionerToken = {
|
|
226
|
+
id: this.generateId(),
|
|
227
|
+
token: tokenString,
|
|
228
|
+
practitionerId: practitionerId,
|
|
229
|
+
email: practitionerData.basicInfo.email,
|
|
230
|
+
clinicId: clinicId,
|
|
231
|
+
status: PractitionerTokenStatus.ACTIVE,
|
|
232
|
+
createdBy: createdBy,
|
|
233
|
+
createdAt: Timestamp.now(),
|
|
234
|
+
expiresAt: Timestamp.fromDate(expiration),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Validate token object
|
|
238
|
+
practitionerTokenSchema.parse(token);
|
|
239
|
+
|
|
240
|
+
// Store the token in the practitioner document's register_tokens subcollection
|
|
241
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
242
|
+
await setDoc(doc(this.db, tokenPath), token);
|
|
243
|
+
|
|
244
|
+
// Ovde bi bilo slanje emaila sa tokenom, ali to ćemo implementirati kasnije
|
|
245
|
+
// TODO: Implement email sending with Cloud Functions
|
|
246
|
+
|
|
247
|
+
return { practitioner: savedPractitioner, token };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof z.ZodError) {
|
|
250
|
+
throw new Error("Invalid practitioner data: " + error.message);
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Creates a token for inviting practitioner to claim their profile
|
|
258
|
+
* @param data Data for creating token
|
|
259
|
+
* @param createdBy ID of the user creating the token
|
|
260
|
+
* @returns Created token
|
|
261
|
+
*/
|
|
262
|
+
async createPractitionerToken(
|
|
263
|
+
data: CreatePractitionerTokenData,
|
|
264
|
+
createdBy: string
|
|
265
|
+
): Promise<PractitionerToken> {
|
|
266
|
+
try {
|
|
267
|
+
// Validate data
|
|
268
|
+
const validatedData = createPractitionerTokenSchema.parse(data);
|
|
269
|
+
|
|
270
|
+
// Check if practitioner exists and is in DRAFT status
|
|
271
|
+
const practitioner = await this.getPractitioner(
|
|
272
|
+
validatedData.practitionerId
|
|
273
|
+
);
|
|
274
|
+
if (!practitioner) {
|
|
275
|
+
throw new Error("Practitioner not found");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (practitioner.status !== PractitionerStatus.DRAFT) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
"Can only create tokens for practitioners in DRAFT status"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check if clinic exists and practitioner belongs to it
|
|
285
|
+
const clinic = await this.getClinicService().getClinic(
|
|
286
|
+
validatedData.clinicId
|
|
287
|
+
);
|
|
288
|
+
if (!clinic) {
|
|
289
|
+
throw new Error(`Clinic ${validatedData.clinicId} not found`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!practitioner.clinics.includes(validatedData.clinicId)) {
|
|
293
|
+
throw new Error("Practitioner is not associated with this clinic");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Default expiration is 7 days from now if not specified
|
|
297
|
+
const expiration =
|
|
298
|
+
validatedData.expiresAt ||
|
|
299
|
+
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
300
|
+
|
|
301
|
+
// Generate a token (6 characters) using generateId from BaseService
|
|
302
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
303
|
+
|
|
304
|
+
const token: PractitionerToken = {
|
|
305
|
+
id: this.generateId(),
|
|
306
|
+
token: tokenString,
|
|
307
|
+
practitionerId: validatedData.practitionerId,
|
|
308
|
+
email: validatedData.email,
|
|
309
|
+
clinicId: validatedData.clinicId,
|
|
310
|
+
status: PractitionerTokenStatus.ACTIVE,
|
|
311
|
+
createdBy: createdBy,
|
|
312
|
+
createdAt: Timestamp.now(),
|
|
313
|
+
expiresAt: Timestamp.fromDate(expiration),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Validate token object
|
|
317
|
+
practitionerTokenSchema.parse(token);
|
|
318
|
+
|
|
319
|
+
// Store the token in the practitioner document's register_tokens subcollection
|
|
320
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
321
|
+
await setDoc(doc(this.db, tokenPath), token);
|
|
322
|
+
|
|
323
|
+
return token;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error instanceof z.ZodError) {
|
|
326
|
+
throw new Error("Invalid token data: " + error.message);
|
|
327
|
+
}
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Gets active tokens for a practitioner
|
|
334
|
+
* @param practitionerId ID of the practitioner
|
|
335
|
+
* @returns Array of active tokens
|
|
336
|
+
*/
|
|
337
|
+
async getPractitionerActiveTokens(
|
|
338
|
+
practitionerId: string
|
|
339
|
+
): Promise<PractitionerToken[]> {
|
|
340
|
+
const tokensRef = collection(
|
|
341
|
+
this.db,
|
|
342
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const q = query(
|
|
346
|
+
tokensRef,
|
|
347
|
+
where("status", "==", PractitionerTokenStatus.ACTIVE),
|
|
348
|
+
where("expiresAt", ">", Timestamp.now())
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const querySnapshot = await getDocs(q);
|
|
352
|
+
return querySnapshot.docs.map((doc) => doc.data() as PractitionerToken);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Gets a token by its string value and validates it
|
|
357
|
+
* @param tokenString The token string to find
|
|
358
|
+
* @returns The token if found and valid, null otherwise
|
|
359
|
+
*/
|
|
360
|
+
async validateToken(tokenString: string): Promise<PractitionerToken | null> {
|
|
361
|
+
// We need to search through all practitioners' register_tokens subcollections
|
|
362
|
+
const practitionersRef = collection(this.db, PRACTITIONERS_COLLECTION);
|
|
363
|
+
const practitionersSnapshot = await getDocs(practitionersRef);
|
|
364
|
+
|
|
365
|
+
for (const practitionerDoc of practitionersSnapshot.docs) {
|
|
366
|
+
const practitionerId = practitionerDoc.id;
|
|
367
|
+
const tokensRef = collection(
|
|
368
|
+
this.db,
|
|
369
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const q = query(
|
|
373
|
+
tokensRef,
|
|
374
|
+
where("token", "==", tokenString),
|
|
375
|
+
where("status", "==", PractitionerTokenStatus.ACTIVE),
|
|
376
|
+
where("expiresAt", ">", Timestamp.now())
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const tokenSnapshot = await getDocs(q);
|
|
380
|
+
if (!tokenSnapshot.empty) {
|
|
381
|
+
return tokenSnapshot.docs[0].data() as PractitionerToken;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Marks a token as used
|
|
390
|
+
* @param tokenId ID of the token
|
|
391
|
+
* @param practitionerId ID of the practitioner
|
|
392
|
+
* @param userId ID of the user using the token
|
|
393
|
+
*/
|
|
394
|
+
async markTokenAsUsed(
|
|
395
|
+
tokenId: string,
|
|
396
|
+
practitionerId: string,
|
|
397
|
+
userId: string
|
|
398
|
+
): Promise<void> {
|
|
399
|
+
const tokenRef = doc(
|
|
400
|
+
this.db,
|
|
401
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
await updateDoc(tokenRef, {
|
|
405
|
+
status: PractitionerTokenStatus.USED,
|
|
406
|
+
usedBy: userId,
|
|
407
|
+
usedAt: Timestamp.now(),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
128
411
|
/**
|
|
129
412
|
* Dohvata zdravstvenog radnika po ID-u
|
|
130
413
|
*/
|
|
@@ -166,7 +449,24 @@ export class PractitionerService extends BaseService {
|
|
|
166
449
|
const q = query(
|
|
167
450
|
collection(this.db, PRACTITIONERS_COLLECTION),
|
|
168
451
|
where("clinics", "array-contains", clinicId),
|
|
169
|
-
where("isActive", "==", true)
|
|
452
|
+
where("isActive", "==", true),
|
|
453
|
+
where("status", "==", PractitionerStatus.ACTIVE)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const querySnapshot = await getDocs(q);
|
|
457
|
+
return querySnapshot.docs.map((doc) => doc.data() as Practitioner);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Dohvata sve draft zdravstvene radnike za određenu kliniku
|
|
462
|
+
*/
|
|
463
|
+
async getDraftPractitionersByClinic(
|
|
464
|
+
clinicId: string
|
|
465
|
+
): Promise<Practitioner[]> {
|
|
466
|
+
const q = query(
|
|
467
|
+
collection(this.db, PRACTITIONERS_COLLECTION),
|
|
468
|
+
where("clinics", "array-contains", clinicId),
|
|
469
|
+
where("status", "==", PractitionerStatus.DRAFT)
|
|
170
470
|
);
|
|
171
471
|
|
|
172
472
|
const querySnapshot = await getDocs(q);
|
|
@@ -302,4 +602,49 @@ export class PractitionerService extends BaseService {
|
|
|
302
602
|
|
|
303
603
|
await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId));
|
|
304
604
|
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Validates a registration token and claims the associated draft practitioner profile
|
|
608
|
+
* @param tokenString The token provided by the practitioner
|
|
609
|
+
* @param userId The ID of the user claiming the profile
|
|
610
|
+
* @returns The claimed practitioner profile or null if token is invalid
|
|
611
|
+
*/
|
|
612
|
+
async validateTokenAndClaimProfile(
|
|
613
|
+
tokenString: string,
|
|
614
|
+
userId: string
|
|
615
|
+
): Promise<Practitioner | null> {
|
|
616
|
+
// Find the token
|
|
617
|
+
const token = await this.validateToken(tokenString);
|
|
618
|
+
if (!token) {
|
|
619
|
+
return null; // Token not found or not valid
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Get the practitioner profile
|
|
623
|
+
const practitioner = await this.getPractitioner(token.practitionerId);
|
|
624
|
+
if (!practitioner) {
|
|
625
|
+
return null; // Practitioner not found
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Ensure practitioner is in DRAFT status
|
|
629
|
+
if (practitioner.status !== PractitionerStatus.DRAFT) {
|
|
630
|
+
throw new Error("This practitioner profile has already been claimed");
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check if user already has a practitioner profile
|
|
634
|
+
const existingPractitioner = await this.getPractitionerByUserRef(userId);
|
|
635
|
+
if (existingPractitioner) {
|
|
636
|
+
throw new Error("User already has a practitioner profile");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Claim the profile by linking it to the user
|
|
640
|
+
const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
|
|
641
|
+
userRef: userId,
|
|
642
|
+
status: PractitionerStatus.ACTIVE,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Mark the token as used
|
|
646
|
+
await this.markTokenAsUsed(token.id, token.practitionerId, userId);
|
|
647
|
+
|
|
648
|
+
return updatedPractitioner;
|
|
649
|
+
}
|
|
305
650
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Timestamp, FieldValue } from "firebase/firestore";
|
|
2
|
+
import type { ClinicLocation } from "../clinic";
|
|
3
|
+
import type {
|
|
4
|
+
Category,
|
|
5
|
+
ProcedureFamily,
|
|
6
|
+
Product,
|
|
7
|
+
Subcategory,
|
|
8
|
+
Technology,
|
|
9
|
+
} from "../../backoffice";
|
|
10
|
+
import type { SyncedCalendarProvider } from "./synced-calendar.types";
|
|
11
|
+
import type { Gender } from "../patient";
|
|
12
|
+
import type { PractitionerCertification } from "../practitioner";
|
|
13
|
+
import type { Currency } from "../../backoffice/types/static/pricing.types";
|
|
14
|
+
import type {
|
|
15
|
+
ClinicInfo,
|
|
16
|
+
PractitionerProfileInfo,
|
|
17
|
+
PatientProfileInfo,
|
|
18
|
+
} from "../profile";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enum for calendar event status
|
|
22
|
+
*/
|
|
23
|
+
export enum CalendarEventStatus {
|
|
24
|
+
PENDING = "pending", // When event is created, but not confirmed
|
|
25
|
+
CONFIRMED = "confirmed", // When event is confirmed and ready to be used
|
|
26
|
+
REJECTED = "rejected", // When event is rejected by the clinic administrator or patient
|
|
27
|
+
CANCELED = "canceled", // When event is canceled by the patient
|
|
28
|
+
RESCHEDULED = "rescheduled", // When event is rescheduled by the clinic administrator
|
|
29
|
+
COMPLETED = "completed", // When event is completed
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Enum for calendar event sync status
|
|
34
|
+
*/
|
|
35
|
+
export enum CalendarSyncStatus {
|
|
36
|
+
INTERNAL = "internal",
|
|
37
|
+
EXTERNAL = "external",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enum for calendar event types
|
|
42
|
+
*/
|
|
43
|
+
export enum CalendarEventType {
|
|
44
|
+
APPOINTMENT = "appointment",
|
|
45
|
+
BLOCKING = "blocking",
|
|
46
|
+
BREAK = "break",
|
|
47
|
+
FREE_DAY = "free_day",
|
|
48
|
+
OTHER = "other",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Interface for calendar event time
|
|
53
|
+
*/
|
|
54
|
+
export interface CalendarEventTime {
|
|
55
|
+
start: Timestamp;
|
|
56
|
+
end: Timestamp;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ProcedureInfo {
|
|
60
|
+
// TO-DO: Create detailed procedure info when procedures are integrated
|
|
61
|
+
name: string;
|
|
62
|
+
description: string;
|
|
63
|
+
duration: number;
|
|
64
|
+
price: number;
|
|
65
|
+
currency: Currency;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ProcedureCategorization {
|
|
69
|
+
procedureFamily: ProcedureFamily;
|
|
70
|
+
procedureCategory: Category;
|
|
71
|
+
procedureSubcategory: Subcategory;
|
|
72
|
+
procedureTechnology: Technology;
|
|
73
|
+
procedureProduct: Product;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SyncedCalendarEvent {
|
|
77
|
+
eventId: string;
|
|
78
|
+
syncedCalendarProvider: SyncedCalendarProvider;
|
|
79
|
+
syncedAt: Timestamp; // Timestamp when the event was last synced with the external calendar
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Interface for calendar event
|
|
84
|
+
*/
|
|
85
|
+
export interface CalendarEvent {
|
|
86
|
+
id: string;
|
|
87
|
+
clinicBranchId?: string | null;
|
|
88
|
+
clinicBranchInfo?: ClinicInfo | null;
|
|
89
|
+
practitionerProfileId?: string | null;
|
|
90
|
+
practitionerProfileInfo?: PractitionerProfileInfo | null;
|
|
91
|
+
patientProfileId?: string | null;
|
|
92
|
+
patientProfileInfo?: PatientProfileInfo | null;
|
|
93
|
+
procedureId?: string | null;
|
|
94
|
+
procedureInfo?: ProcedureInfo | null;
|
|
95
|
+
procedureCategorization?: ProcedureCategorization | null;
|
|
96
|
+
appointmentId?: string | null; // Created when calendar event is confirmed and it's type is APPOINTMENT
|
|
97
|
+
syncedCalendarEventId?: SyncedCalendarEvent[] | null;
|
|
98
|
+
eventName: string;
|
|
99
|
+
eventLocation?: ClinicLocation;
|
|
100
|
+
eventTime: CalendarEventTime;
|
|
101
|
+
description?: string;
|
|
102
|
+
status: CalendarEventStatus;
|
|
103
|
+
syncStatus: CalendarSyncStatus;
|
|
104
|
+
eventType: CalendarEventType;
|
|
105
|
+
createdAt: Timestamp;
|
|
106
|
+
updatedAt: Timestamp;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Interface for creating a calendar event
|
|
111
|
+
*/
|
|
112
|
+
export interface CreateCalendarEventData {
|
|
113
|
+
id: string;
|
|
114
|
+
clinicBranchId?: string | null;
|
|
115
|
+
clinicBranchInfo?: ClinicInfo | null;
|
|
116
|
+
practitionerProfileId?: string | null;
|
|
117
|
+
practitionerProfileInfo?: PractitionerProfileInfo | null;
|
|
118
|
+
patientProfileId?: string | null;
|
|
119
|
+
patientProfileInfo?: PatientProfileInfo | null;
|
|
120
|
+
procedureId?: string | null;
|
|
121
|
+
appointmentId?: string | null; // Not needed if event type is not APPOINTMENT, or if there is no automatic appointment confirmation
|
|
122
|
+
syncedCalendarEventId?: SyncedCalendarEvent[] | null;
|
|
123
|
+
eventName: string;
|
|
124
|
+
eventLocation?: ClinicLocation;
|
|
125
|
+
eventTime: CalendarEventTime;
|
|
126
|
+
description?: string;
|
|
127
|
+
status: CalendarEventStatus;
|
|
128
|
+
syncStatus: CalendarSyncStatus;
|
|
129
|
+
eventType: CalendarEventType;
|
|
130
|
+
createdAt: FieldValue;
|
|
131
|
+
updatedAt: FieldValue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Interface for updating a calendar event
|
|
136
|
+
*/
|
|
137
|
+
export interface UpdateCalendarEventData {
|
|
138
|
+
syncedCalendarEventId?: SyncedCalendarEvent[] | null;
|
|
139
|
+
appointmentId?: string | null; // Updated when calendar event is confirmed and it's type is APPOINTMENT, plus Appointment is created
|
|
140
|
+
eventName?: string;
|
|
141
|
+
eventTime?: CalendarEventTime;
|
|
142
|
+
description?: string;
|
|
143
|
+
status?: CalendarEventStatus;
|
|
144
|
+
syncStatus?: CalendarSyncStatus;
|
|
145
|
+
eventType?: CalendarEventType;
|
|
146
|
+
updatedAt: FieldValue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Interface for available time slot
|
|
151
|
+
*/
|
|
152
|
+
export interface TimeSlot {
|
|
153
|
+
start: Date;
|
|
154
|
+
end: Date;
|
|
155
|
+
isAvailable: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Interface for appointment creation parameters
|
|
160
|
+
*/
|
|
161
|
+
export interface CreateAppointmentParams {
|
|
162
|
+
clinicId: string;
|
|
163
|
+
doctorId: string;
|
|
164
|
+
patientId: string;
|
|
165
|
+
procedureId: string;
|
|
166
|
+
eventLocation: ClinicLocation;
|
|
167
|
+
eventTime: CalendarEventTime;
|
|
168
|
+
description?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Interface for appointment update parameters
|
|
173
|
+
*/
|
|
174
|
+
export interface UpdateAppointmentParams {
|
|
175
|
+
appointmentId: string;
|
|
176
|
+
clinicId: string;
|
|
177
|
+
doctorId: string;
|
|
178
|
+
patientId: string;
|
|
179
|
+
eventTime?: CalendarEventTime;
|
|
180
|
+
description?: string;
|
|
181
|
+
status?: CalendarEventStatus;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Collection names for calendar
|
|
186
|
+
*/
|
|
187
|
+
export const CALENDAR_COLLECTION = "calendar";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Timestamp, FieldValue } from "firebase/firestore";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enum for synced calendar provider
|
|
5
|
+
*/
|
|
6
|
+
export enum SyncedCalendarProvider {
|
|
7
|
+
GOOGLE = "google",
|
|
8
|
+
OUTLOOK = "outlook",
|
|
9
|
+
APPLE = "apple",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Interface for synced calendar
|
|
14
|
+
*/
|
|
15
|
+
export interface SyncedCalendar {
|
|
16
|
+
id: string;
|
|
17
|
+
provider: SyncedCalendarProvider;
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
accessToken: string;
|
|
21
|
+
refreshToken: string;
|
|
22
|
+
tokenExpiry: Timestamp;
|
|
23
|
+
calendarId: string;
|
|
24
|
+
isActive: boolean;
|
|
25
|
+
lastSyncedAt?: Timestamp;
|
|
26
|
+
createdAt: Timestamp;
|
|
27
|
+
updatedAt: Timestamp;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Interface for creating a synced calendar
|
|
32
|
+
*/
|
|
33
|
+
export interface CreateSyncedCalendarData {
|
|
34
|
+
id: string;
|
|
35
|
+
provider: SyncedCalendarProvider;
|
|
36
|
+
name: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
accessToken: string;
|
|
39
|
+
refreshToken: string;
|
|
40
|
+
tokenExpiry: Timestamp;
|
|
41
|
+
calendarId: string;
|
|
42
|
+
isActive: boolean;
|
|
43
|
+
lastSyncedAt?: Timestamp;
|
|
44
|
+
createdAt: FieldValue;
|
|
45
|
+
updatedAt: FieldValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Interface for updating a synced calendar
|
|
50
|
+
*/
|
|
51
|
+
export interface UpdateSyncedCalendarData {
|
|
52
|
+
name?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
accessToken?: string;
|
|
55
|
+
refreshToken?: string;
|
|
56
|
+
tokenExpiry?: Timestamp;
|
|
57
|
+
calendarId?: string;
|
|
58
|
+
isActive?: boolean;
|
|
59
|
+
lastSyncedAt?: Timestamp;
|
|
60
|
+
updatedAt: FieldValue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Collection name for synced calendars
|
|
65
|
+
*/
|
|
66
|
+
export const SYNCED_CALENDARS_COLLECTION = "syncedCalendars";
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
Currency,
|
|
6
6
|
PricingMeasure,
|
|
7
7
|
} from "../../backoffice/types/static/pricing.types";
|
|
8
|
+
import type { ClinicInfo } from "../profile";
|
|
8
9
|
|
|
9
10
|
export const CLINIC_GROUPS_COLLECTION = "clinic_groups";
|
|
10
11
|
export const CLINIC_ADMINS_COLLECTION = "clinic_admins";
|
|
@@ -94,18 +95,6 @@ export interface ContactPerson {
|
|
|
94
95
|
phoneNumber?: string | null;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
/**
|
|
98
|
-
* Interface for clinic information
|
|
99
|
-
*/
|
|
100
|
-
export interface ClinicInfo {
|
|
101
|
-
id: string;
|
|
102
|
-
featuredPhoto: string;
|
|
103
|
-
name: string;
|
|
104
|
-
description?: string;
|
|
105
|
-
location: ClinicLocation;
|
|
106
|
-
contactInfo: ClinicContactInfo;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
98
|
/**
|
|
110
99
|
* Interface for clinic admin
|
|
111
100
|
*/
|
package/src/types/index.ts
CHANGED
|
@@ -41,3 +41,7 @@ export type FirebaseUser = FirebaseAuthUser;
|
|
|
41
41
|
export * from "./documentation-templates";
|
|
42
42
|
export const DOCUMENTATION_TEMPLATES_COLLECTION = "documentation-templates";
|
|
43
43
|
export const FILLED_DOCUMENTS_COLLECTION = "filled-documents";
|
|
44
|
+
|
|
45
|
+
// Calendar
|
|
46
|
+
export * from "./calendar";
|
|
47
|
+
export const CALENDAR_COLLECTION = "calendar";
|