@blackcode_sa/metaestetics-api 1.7.6 → 1.7.8
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 +7 -6
- package/dist/admin/index.d.ts +7 -6
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/index.d.mts +421 -130
- package/dist/index.d.ts +421 -130
- package/dist/index.js +1756 -1277
- package/dist/index.mjs +1548 -1053
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +6 -1
- package/src/services/auth.service.ts +15 -16
- package/src/services/clinic/clinic.service.ts +259 -76
- package/src/services/media/media.service.ts +135 -52
- package/src/services/practitioner/practitioner.service.ts +68 -5
- package/src/services/procedure/procedure.service.ts +12 -4
- package/src/types/clinic/index.ts +54 -62
- package/src/types/index.ts +7 -7
- package/src/validations/clinic.schema.ts +56 -26
- package/src/validations/schemas.ts +8 -28
|
@@ -6,16 +6,13 @@ import {
|
|
|
6
6
|
uploadBytes,
|
|
7
7
|
getDownloadURL,
|
|
8
8
|
deleteObject,
|
|
9
|
-
|
|
10
|
-
FirebaseStorage,
|
|
9
|
+
getBytes,
|
|
11
10
|
} from "firebase/storage";
|
|
12
11
|
import {
|
|
13
12
|
doc,
|
|
14
13
|
getDoc,
|
|
15
14
|
setDoc,
|
|
16
15
|
updateDoc,
|
|
17
|
-
arrayUnion,
|
|
18
|
-
arrayRemove,
|
|
19
16
|
collection,
|
|
20
17
|
query,
|
|
21
18
|
where,
|
|
@@ -39,17 +36,17 @@ export enum MediaAccessLevel {
|
|
|
39
36
|
* Media file metadata interface
|
|
40
37
|
*/
|
|
41
38
|
export interface MediaMetadata {
|
|
42
|
-
id: string;
|
|
43
|
-
name: string;
|
|
44
|
-
url: string;
|
|
45
|
-
contentType: string;
|
|
46
|
-
size: number;
|
|
47
|
-
createdAt: Timestamp;
|
|
48
|
-
accessLevel: MediaAccessLevel;
|
|
49
|
-
ownerId: string;
|
|
50
|
-
collectionName: string;
|
|
51
|
-
path: string;
|
|
52
|
-
updatedAt?: Timestamp;
|
|
39
|
+
id: string; // Unique ID for the media, also Firestore document ID
|
|
40
|
+
name: string; // Original file name or a descriptive name
|
|
41
|
+
url: string; // Publicly accessible download URL
|
|
42
|
+
contentType: string; // Mime type of the file
|
|
43
|
+
size: number; // Size of the file in bytes
|
|
44
|
+
createdAt: Timestamp; // Firestore Timestamp of creation
|
|
45
|
+
accessLevel: MediaAccessLevel; // Access level
|
|
46
|
+
ownerId: string; // ID of the entity that owns this media (e.g., patientId, clinicId)
|
|
47
|
+
collectionName: string; // Name of the collection this media belongs to (e.g., 'patient_profile_pictures', 'clinic_gallery')
|
|
48
|
+
path: string; // Full path in Firebase Storage
|
|
49
|
+
updatedAt?: Timestamp; // Firestore Timestamp of last update
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
const MEDIA_METADATA_COLLECTION = "media_metadata";
|
|
@@ -69,31 +66,28 @@ export class MediaService extends BaseService {
|
|
|
69
66
|
* @returns Promise with the media metadata.
|
|
70
67
|
*/
|
|
71
68
|
async uploadMedia(
|
|
72
|
-
file: File,
|
|
69
|
+
file: File | Blob,
|
|
73
70
|
ownerId: string,
|
|
74
71
|
accessLevel: MediaAccessLevel,
|
|
75
72
|
collectionName: string,
|
|
76
73
|
originalFileName?: string
|
|
77
74
|
): Promise<MediaMetadata> {
|
|
78
75
|
const mediaId = this.generateId();
|
|
79
|
-
const fileNameToUse =
|
|
80
|
-
|
|
76
|
+
const fileNameToUse =
|
|
77
|
+
originalFileName || (file instanceof File ? file.name : file.toString());
|
|
78
|
+
// Using collectionName in the path for better organization
|
|
81
79
|
const uniqueFileName = `${mediaId}-${fileNameToUse}`;
|
|
82
|
-
const filePath = `media/${accessLevel}/${ownerId}/${uniqueFileName}`;
|
|
80
|
+
const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
|
|
83
81
|
|
|
84
82
|
console.log(`[MediaService] Uploading file to: ${filePath}`);
|
|
85
|
-
|
|
86
83
|
const storageRef = ref(this.storage, filePath);
|
|
87
|
-
|
|
88
84
|
try {
|
|
89
85
|
const uploadResult = await uploadBytes(storageRef, file, {
|
|
90
86
|
contentType: file.type,
|
|
91
87
|
});
|
|
92
88
|
console.log("[MediaService] File uploaded successfully", uploadResult);
|
|
93
|
-
|
|
94
89
|
const downloadURL = await getDownloadURL(uploadResult.ref);
|
|
95
90
|
console.log("[MediaService] Got download URL:", downloadURL);
|
|
96
|
-
|
|
97
91
|
const metadata: MediaMetadata = {
|
|
98
92
|
id: mediaId,
|
|
99
93
|
name: fileNameToUse,
|
|
@@ -104,17 +98,14 @@ export class MediaService extends BaseService {
|
|
|
104
98
|
accessLevel: accessLevel,
|
|
105
99
|
ownerId: ownerId,
|
|
106
100
|
collectionName: collectionName,
|
|
107
|
-
path: filePath,
|
|
101
|
+
path: filePath,
|
|
108
102
|
};
|
|
109
|
-
|
|
110
103
|
const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
111
104
|
await setDoc(metadataDocRef, metadata);
|
|
112
105
|
console.log("[MediaService] Metadata stored in Firestore:", mediaId);
|
|
113
|
-
|
|
114
106
|
return metadata;
|
|
115
107
|
} catch (error) {
|
|
116
108
|
console.error("[MediaService] Error during media upload:", error);
|
|
117
|
-
// Consider more specific error handling or re-throwing a custom error
|
|
118
109
|
throw error;
|
|
119
110
|
}
|
|
120
111
|
}
|
|
@@ -204,20 +195,21 @@ export class MediaService extends BaseService {
|
|
|
204
195
|
}
|
|
205
196
|
|
|
206
197
|
/**
|
|
207
|
-
* Update media access level. This
|
|
208
|
-
*
|
|
198
|
+
* Update media access level. This involves moving the file in Firebase Storage
|
|
199
|
+
* to a new path reflecting the new access level, and updating its metadata.
|
|
209
200
|
* @param mediaId - ID of the media to update.
|
|
210
201
|
* @param newAccessLevel - New access level.
|
|
211
|
-
* @returns Promise with the updated media metadata.
|
|
202
|
+
* @returns Promise with the updated media metadata, or null if metadata not found.
|
|
212
203
|
*/
|
|
213
204
|
async updateMediaAccessLevel(
|
|
214
205
|
mediaId: string,
|
|
215
206
|
newAccessLevel: MediaAccessLevel
|
|
216
207
|
): Promise<MediaMetadata | null> {
|
|
217
208
|
console.log(
|
|
218
|
-
`[MediaService]
|
|
209
|
+
`[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
|
|
219
210
|
);
|
|
220
211
|
const metadata = await this.getMediaMetadata(mediaId);
|
|
212
|
+
|
|
221
213
|
if (!metadata) {
|
|
222
214
|
console.warn(
|
|
223
215
|
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
|
|
@@ -225,28 +217,124 @@ export class MediaService extends BaseService {
|
|
|
225
217
|
return null;
|
|
226
218
|
}
|
|
227
219
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
220
|
+
if (metadata.accessLevel === newAccessLevel) {
|
|
221
|
+
console.log(
|
|
222
|
+
`[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
|
|
223
|
+
);
|
|
224
|
+
const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
225
|
+
try {
|
|
226
|
+
await updateDoc(metadataDocRef, { updatedAt: Timestamp.now() });
|
|
227
|
+
return { ...metadata, updatedAt: Timestamp.now() };
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(
|
|
230
|
+
`[MediaService] Error updating timestamp for media ID ${mediaId}:`,
|
|
231
|
+
error
|
|
232
|
+
);
|
|
233
|
+
throw error; // Re-throw to indicate the update wasn't fully successful
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const oldStoragePath = metadata.path;
|
|
238
|
+
// Ensure the filename part remains consistent using metadata.id and metadata.name
|
|
239
|
+
const fileNamePart = `${metadata.id}-${metadata.name}`;
|
|
240
|
+
const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
|
|
232
241
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
console.log(
|
|
243
|
+
`[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const oldStorageFileRef = ref(this.storage, oldStoragePath);
|
|
247
|
+
const newStorageFileRef = ref(this.storage, newStoragePath);
|
|
238
248
|
|
|
239
249
|
try {
|
|
250
|
+
// 1. Download (get bytes of) the old file
|
|
251
|
+
console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
|
|
252
|
+
const fileBytes = await getBytes(oldStorageFileRef);
|
|
253
|
+
console.log(
|
|
254
|
+
`[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// 2. Upload the bytes to the new path
|
|
258
|
+
console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
|
|
259
|
+
await uploadBytes(newStorageFileRef, fileBytes, {
|
|
260
|
+
contentType: metadata.contentType,
|
|
261
|
+
});
|
|
262
|
+
console.log(
|
|
263
|
+
`[MediaService] Successfully uploaded bytes to ${newStoragePath}`
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// 3. Get the new download URL
|
|
267
|
+
const newDownloadURL = await getDownloadURL(newStorageFileRef);
|
|
268
|
+
console.log(
|
|
269
|
+
`[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// 4. Prepare metadata for update
|
|
273
|
+
const updateData = {
|
|
274
|
+
accessLevel: newAccessLevel,
|
|
275
|
+
path: newStoragePath,
|
|
276
|
+
url: newDownloadURL,
|
|
277
|
+
updatedAt: Timestamp.now(),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// 5. Update Firestore metadata
|
|
281
|
+
const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
282
|
+
console.log(
|
|
283
|
+
`[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
|
|
284
|
+
updateData
|
|
285
|
+
);
|
|
240
286
|
await updateDoc(metadataDocRef, updateData);
|
|
241
|
-
console.log(
|
|
242
|
-
|
|
287
|
+
console.log(
|
|
288
|
+
`[MediaService] Successfully updated Firestore metadata for ${mediaId}`
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// 6. Delete the old file from Firebase Storage (after metadata is updated)
|
|
292
|
+
try {
|
|
293
|
+
console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
|
|
294
|
+
await deleteObject(oldStorageFileRef);
|
|
295
|
+
console.log(
|
|
296
|
+
`[MediaService] Successfully deleted old file from ${oldStoragePath}`
|
|
297
|
+
);
|
|
298
|
+
} catch (deleteError) {
|
|
299
|
+
console.error(
|
|
300
|
+
`[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
|
|
301
|
+
deleteError
|
|
302
|
+
);
|
|
303
|
+
// Do not re-throw here, as the primary operation (move and metadata update) was successful.
|
|
304
|
+
// Log this issue for potential manual cleanup.
|
|
305
|
+
}
|
|
306
|
+
|
|
243
307
|
return { ...metadata, ...updateData } as MediaMetadata;
|
|
244
308
|
} catch (error) {
|
|
245
309
|
console.error(
|
|
246
|
-
`[MediaService] Error updating media access level for ${mediaId}:`,
|
|
310
|
+
`[MediaService] Error updating media access level and moving file for ${mediaId}:`,
|
|
247
311
|
error
|
|
248
312
|
);
|
|
249
|
-
|
|
313
|
+
// Attempt to clean up if new file was created but process failed before metadata update
|
|
314
|
+
// This is a best-effort cleanup. If newDownloadURL is not defined, it means upload failed.
|
|
315
|
+
// If newDownloadURL is defined but metadata update failed, new file might exist.
|
|
316
|
+
if (
|
|
317
|
+
newStorageFileRef &&
|
|
318
|
+
(error as any).code !== "storage/object-not-found" &&
|
|
319
|
+
(error as any).message?.includes("uploadBytes")
|
|
320
|
+
) {
|
|
321
|
+
// This check is a bit heuristic. Ideally, check if new file actually exists.
|
|
322
|
+
console.warn(
|
|
323
|
+
`[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
|
|
324
|
+
);
|
|
325
|
+
try {
|
|
326
|
+
await deleteObject(newStorageFileRef);
|
|
327
|
+
console.warn(
|
|
328
|
+
`[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
|
|
329
|
+
);
|
|
330
|
+
} catch (cleanupError) {
|
|
331
|
+
console.error(
|
|
332
|
+
`[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
|
|
333
|
+
cleanupError
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
throw error; // Re-throw the original error to the caller
|
|
250
338
|
}
|
|
251
339
|
}
|
|
252
340
|
|
|
@@ -284,14 +372,9 @@ export class MediaService extends BaseService {
|
|
|
284
372
|
if (startAfterId) {
|
|
285
373
|
const startAfterDoc = await this.getMediaMetadata(startAfterId);
|
|
286
374
|
if (startAfterDoc) {
|
|
287
|
-
// Firestore's startAfter needs a DocumentSnapshot or field values
|
|
288
|
-
// For
|
|
289
|
-
//
|
|
290
|
-
// This part needs refinement based on actual pagination strategy.
|
|
291
|
-
// For now, let's assume we'd fetch the doc snapshot if needed or simplify.
|
|
292
|
-
// console.log("[MediaService] Pagination: starting after document corresponding to ID", startAfterId);
|
|
293
|
-
// This simple approach of passing ID won't directly work with Firestore's startAfter without fetching the document snapshot first.
|
|
294
|
-
// A more robust solution would involve passing the actual DocumentSnapshot or specific field values of the last retrieved document.
|
|
375
|
+
// Placeholder: Firestore's startAfter needs a DocumentSnapshot or field values.
|
|
376
|
+
// For robust pagination, pass the actual DocumentSnapshot or use field values from it.
|
|
377
|
+
// e.g., qConstraints.push(startAfter(snapshotOfStartAfterDoc));
|
|
295
378
|
}
|
|
296
379
|
}
|
|
297
380
|
|
|
@@ -229,8 +229,12 @@ export class PractitionerService extends BaseService {
|
|
|
229
229
|
// Make sure we're using the right property for featuredPhoto
|
|
230
230
|
featuredPhoto:
|
|
231
231
|
clinicData.featuredPhotos && clinicData.featuredPhotos.length > 0
|
|
232
|
-
? clinicData.featuredPhotos[0]
|
|
233
|
-
|
|
232
|
+
? typeof clinicData.featuredPhotos[0] === "string"
|
|
233
|
+
? clinicData.featuredPhotos[0]
|
|
234
|
+
: ""
|
|
235
|
+
: (typeof clinicData.coverPhoto === "string"
|
|
236
|
+
? clinicData.coverPhoto
|
|
237
|
+
: "") || "",
|
|
234
238
|
description: clinicData.description || null,
|
|
235
239
|
});
|
|
236
240
|
}
|
|
@@ -442,6 +446,14 @@ export class PractitionerService extends BaseService {
|
|
|
442
446
|
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
443
447
|
);
|
|
444
448
|
|
|
449
|
+
console.log(
|
|
450
|
+
`[PRACTITIONER] Validating token for practitioner ${practitionerId}`,
|
|
451
|
+
{
|
|
452
|
+
tokenString,
|
|
453
|
+
timestamp: Timestamp.now().toDate(),
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
|
|
445
457
|
const q = query(
|
|
446
458
|
tokensRef,
|
|
447
459
|
where("token", "==", tokenString),
|
|
@@ -449,9 +461,31 @@ export class PractitionerService extends BaseService {
|
|
|
449
461
|
where("expiresAt", ">", Timestamp.now())
|
|
450
462
|
);
|
|
451
463
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
464
|
+
try {
|
|
465
|
+
const tokenSnapshot = await getDocs(q);
|
|
466
|
+
console.log(
|
|
467
|
+
`[PRACTITIONER] Token query results for practitioner ${practitionerId}`,
|
|
468
|
+
{
|
|
469
|
+
found: !tokenSnapshot.empty,
|
|
470
|
+
count: tokenSnapshot.size,
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (!tokenSnapshot.empty) {
|
|
475
|
+
const tokenData = tokenSnapshot.docs[0].data() as PractitionerToken;
|
|
476
|
+
console.log(`[PRACTITIONER] Valid token found`, {
|
|
477
|
+
tokenId: tokenData.id,
|
|
478
|
+
expiresAt: tokenData.expiresAt.toDate(),
|
|
479
|
+
});
|
|
480
|
+
return tokenData;
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error(
|
|
484
|
+
`[PRACTITIONER] Error validating token for practitioner ${practitionerId}:`,
|
|
485
|
+
error
|
|
486
|
+
);
|
|
487
|
+
// Re-throw the error to be handled by the caller
|
|
488
|
+
throw error;
|
|
455
489
|
}
|
|
456
490
|
}
|
|
457
491
|
|
|
@@ -726,19 +760,43 @@ export class PractitionerService extends BaseService {
|
|
|
726
760
|
userId: string
|
|
727
761
|
): Promise<Practitioner | null> {
|
|
728
762
|
// Find the token
|
|
763
|
+
console.log("[PRACTITIONER] Validating token for claiming profile", {
|
|
764
|
+
tokenString,
|
|
765
|
+
userId,
|
|
766
|
+
});
|
|
767
|
+
|
|
729
768
|
const token = await this.validateToken(tokenString);
|
|
769
|
+
|
|
730
770
|
if (!token) {
|
|
771
|
+
console.log(
|
|
772
|
+
"[PRACTITIONER] Token validation failed - token not found or not valid",
|
|
773
|
+
{
|
|
774
|
+
tokenString,
|
|
775
|
+
}
|
|
776
|
+
);
|
|
731
777
|
return null; // Token not found or not valid
|
|
732
778
|
}
|
|
733
779
|
|
|
780
|
+
console.log("[PRACTITIONER] Token successfully validated", {
|
|
781
|
+
tokenId: token.id,
|
|
782
|
+
practitionerId: token.practitionerId,
|
|
783
|
+
});
|
|
784
|
+
|
|
734
785
|
// Get the practitioner profile
|
|
735
786
|
const practitioner = await this.getPractitioner(token.practitionerId);
|
|
736
787
|
if (!practitioner) {
|
|
788
|
+
console.log("[PRACTITIONER] Practitioner not found", {
|
|
789
|
+
practitionerId: token.practitionerId,
|
|
790
|
+
});
|
|
737
791
|
return null; // Practitioner not found
|
|
738
792
|
}
|
|
739
793
|
|
|
740
794
|
// Ensure practitioner is in DRAFT status
|
|
741
795
|
if (practitioner.status !== PractitionerStatus.DRAFT) {
|
|
796
|
+
console.log("[PRACTITIONER] Practitioner status is not DRAFT", {
|
|
797
|
+
practitionerId: practitioner.id,
|
|
798
|
+
status: practitioner.status,
|
|
799
|
+
});
|
|
742
800
|
throw new Error("This practitioner profile has already been claimed");
|
|
743
801
|
}
|
|
744
802
|
|
|
@@ -757,6 +815,11 @@ export class PractitionerService extends BaseService {
|
|
|
757
815
|
// Mark the token as used
|
|
758
816
|
await this.markTokenAsUsed(token.id, token.practitionerId, userId);
|
|
759
817
|
|
|
818
|
+
console.log("[PRACTITIONER] Profile claimed successfully", {
|
|
819
|
+
practitionerId: updatedPractitioner.id,
|
|
820
|
+
userId,
|
|
821
|
+
});
|
|
822
|
+
|
|
760
823
|
return updatedPractitioner;
|
|
761
824
|
}
|
|
762
825
|
|
|
@@ -153,8 +153,12 @@ export class ProcedureService extends BaseService {
|
|
|
153
153
|
description: clinic.description || "",
|
|
154
154
|
featuredPhoto:
|
|
155
155
|
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
156
|
-
? clinic.featuredPhotos[0]
|
|
157
|
-
|
|
156
|
+
? typeof clinic.featuredPhotos[0] === "string"
|
|
157
|
+
? clinic.featuredPhotos[0]
|
|
158
|
+
: ""
|
|
159
|
+
: typeof clinic.coverPhoto === "string"
|
|
160
|
+
? clinic.coverPhoto
|
|
161
|
+
: "",
|
|
158
162
|
location: clinic.location,
|
|
159
163
|
contactInfo: clinic.contactInfo,
|
|
160
164
|
};
|
|
@@ -344,8 +348,12 @@ export class ProcedureService extends BaseService {
|
|
|
344
348
|
description: newClinic.description || "",
|
|
345
349
|
featuredPhoto:
|
|
346
350
|
newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
|
|
347
|
-
? newClinic.featuredPhotos[0]
|
|
348
|
-
|
|
351
|
+
? typeof newClinic.featuredPhotos[0] === "string"
|
|
352
|
+
? newClinic.featuredPhotos[0]
|
|
353
|
+
: ""
|
|
354
|
+
: typeof newClinic.coverPhoto === "string"
|
|
355
|
+
? newClinic.coverPhoto
|
|
356
|
+
: "",
|
|
349
357
|
location: newClinic.location,
|
|
350
358
|
contactInfo: newClinic.contactInfo,
|
|
351
359
|
};
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import { Timestamp, FieldValue } from
|
|
2
|
-
import type { ProcedureFamily } from
|
|
3
|
-
import type { TreatmentBenefit } from
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { Timestamp, FieldValue } from "firebase/firestore";
|
|
2
|
+
import type { ProcedureFamily } from "../../backoffice/types/static/procedure-family.types";
|
|
3
|
+
import type { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
|
|
4
|
+
import type {
|
|
5
|
+
Currency,
|
|
6
|
+
PricingMeasure,
|
|
7
|
+
} from "../../backoffice/types/static/pricing.types";
|
|
8
|
+
import type { ClinicInfo } from "../profile";
|
|
9
|
+
import { ClinicReviewInfo } from "../reviews";
|
|
10
|
+
import { ProcedureSummaryInfo } from "../procedure";
|
|
8
11
|
|
|
9
|
-
export const CLINIC_GROUPS_COLLECTION =
|
|
10
|
-
export const CLINIC_ADMINS_COLLECTION =
|
|
11
|
-
export const CLINICS_COLLECTION =
|
|
12
|
+
export const CLINIC_GROUPS_COLLECTION = "clinic_groups";
|
|
13
|
+
export const CLINIC_ADMINS_COLLECTION = "clinic_admins";
|
|
14
|
+
export const CLINICS_COLLECTION = "clinics";
|
|
12
15
|
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
PracticeType,
|
|
18
|
+
Language,
|
|
19
|
+
ClinicTag,
|
|
20
|
+
ClinicPhotoTag,
|
|
21
|
+
} from "./preferences.types";
|
|
14
22
|
|
|
15
|
-
export * from
|
|
23
|
+
export * from "./preferences.types";
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* Interface for clinic contact information
|
|
@@ -132,9 +140,9 @@ export interface UpdateClinicAdminData extends Partial<CreateClinicAdminData> {
|
|
|
132
140
|
* Enum for admin token status
|
|
133
141
|
*/
|
|
134
142
|
export enum AdminTokenStatus {
|
|
135
|
-
ACTIVE =
|
|
136
|
-
USED =
|
|
137
|
-
EXPIRED =
|
|
143
|
+
ACTIVE = "active",
|
|
144
|
+
USED = "used",
|
|
145
|
+
EXPIRED = "expired",
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
/**
|
|
@@ -163,10 +171,10 @@ export interface AdminInfo {
|
|
|
163
171
|
* Enum for subscription models
|
|
164
172
|
*/
|
|
165
173
|
export enum SubscriptionModel {
|
|
166
|
-
NO_SUBSCRIPTION =
|
|
167
|
-
BASIC =
|
|
168
|
-
PREMIUM =
|
|
169
|
-
ENTERPRISE =
|
|
174
|
+
NO_SUBSCRIPTION = "no_subscription",
|
|
175
|
+
BASIC = "basic",
|
|
176
|
+
PREMIUM = "premium",
|
|
177
|
+
ENTERPRISE = "enterprise",
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
/**
|
|
@@ -188,7 +196,7 @@ export interface ClinicGroup {
|
|
|
188
196
|
createdAt: Timestamp;
|
|
189
197
|
updatedAt: Timestamp;
|
|
190
198
|
isActive: boolean;
|
|
191
|
-
logo?:
|
|
199
|
+
logo?: MediaResource | null;
|
|
192
200
|
practiceType?: PracticeType;
|
|
193
201
|
languages?: Language[];
|
|
194
202
|
subscriptionModel: SubscriptionModel;
|
|
@@ -208,7 +216,7 @@ export interface CreateClinicGroupData {
|
|
|
208
216
|
contactPerson: ContactPerson;
|
|
209
217
|
ownerId: string | null;
|
|
210
218
|
isActive: boolean;
|
|
211
|
-
logo?:
|
|
219
|
+
logo?: MediaResource | null;
|
|
212
220
|
practiceType?: PracticeType;
|
|
213
221
|
languages?: Language[];
|
|
214
222
|
subscriptionModel?: SubscriptionModel;
|
|
@@ -239,32 +247,16 @@ export interface DoctorInfo {
|
|
|
239
247
|
id: string;
|
|
240
248
|
name: string;
|
|
241
249
|
description?: string;
|
|
242
|
-
photo: string;
|
|
250
|
+
photo: string | null;
|
|
243
251
|
rating: number;
|
|
244
252
|
services: string[]; // Used for search and filtering
|
|
245
253
|
// TODO: Add aggregated fields, like rating, reviews, services this doctor provides in this clinic and similar
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
/**
|
|
249
|
-
*
|
|
257
|
+
* Type that allows a field to be either a URL string or a File object
|
|
250
258
|
*/
|
|
251
|
-
|
|
252
|
-
// id: string;
|
|
253
|
-
// name: string;
|
|
254
|
-
// description?: string;
|
|
255
|
-
// photo: string;
|
|
256
|
-
// procedureFamily: ProcedureFamily;
|
|
257
|
-
// category: string;
|
|
258
|
-
// subCategory: string;
|
|
259
|
-
// technology: string;
|
|
260
|
-
// rating: number;
|
|
261
|
-
// doctors: string[]; // Used for search and filtering
|
|
262
|
-
// price: number;
|
|
263
|
-
// pricingMeasure: PricingMeasure;
|
|
264
|
-
// currency: Currency;
|
|
265
|
-
// duration: number; // Duration in minutes
|
|
266
|
-
// treatmentBenefits: TreatmentBenefit[];
|
|
267
|
-
// }
|
|
259
|
+
export type MediaResource = string | File | Blob;
|
|
268
260
|
|
|
269
261
|
/**
|
|
270
262
|
* Interface for clinic
|
|
@@ -278,20 +270,20 @@ export interface Clinic {
|
|
|
278
270
|
contactInfo: ClinicContactInfo;
|
|
279
271
|
workingHours: WorkingHours;
|
|
280
272
|
tags: ClinicTag[];
|
|
281
|
-
featuredPhotos:
|
|
282
|
-
coverPhoto:
|
|
283
|
-
photosWithTags?: { url:
|
|
273
|
+
featuredPhotos: MediaResource[];
|
|
274
|
+
coverPhoto: MediaResource | null;
|
|
275
|
+
photosWithTags?: { url: MediaResource; tag: string }[];
|
|
284
276
|
doctors: string[];
|
|
285
277
|
doctorsInfo: DoctorInfo[];
|
|
286
278
|
procedures: string[];
|
|
287
|
-
proceduresInfo: ProcedureSummaryInfo[];
|
|
288
|
-
reviewInfo: ClinicReviewInfo;
|
|
279
|
+
proceduresInfo: ProcedureSummaryInfo[];
|
|
280
|
+
reviewInfo: ClinicReviewInfo;
|
|
289
281
|
admins: string[];
|
|
290
282
|
createdAt: Timestamp;
|
|
291
283
|
updatedAt: Timestamp;
|
|
292
284
|
isActive: boolean;
|
|
293
285
|
isVerified: boolean;
|
|
294
|
-
logo?:
|
|
286
|
+
logo?: MediaResource | null;
|
|
295
287
|
}
|
|
296
288
|
|
|
297
289
|
/**
|
|
@@ -305,16 +297,16 @@ export interface CreateClinicData {
|
|
|
305
297
|
contactInfo: ClinicContactInfo;
|
|
306
298
|
workingHours: WorkingHours;
|
|
307
299
|
tags: ClinicTag[];
|
|
308
|
-
coverPhoto
|
|
309
|
-
photosWithTags?: { url:
|
|
310
|
-
doctors
|
|
311
|
-
procedures
|
|
312
|
-
proceduresInfo?: ProcedureSummaryInfo[];
|
|
300
|
+
coverPhoto?: MediaResource | null;
|
|
301
|
+
photosWithTags?: { url: MediaResource; tag: string }[];
|
|
302
|
+
doctors?: string[];
|
|
303
|
+
procedures?: string[];
|
|
304
|
+
proceduresInfo?: ProcedureSummaryInfo[];
|
|
313
305
|
admins: string[];
|
|
314
|
-
isActive
|
|
315
|
-
isVerified
|
|
316
|
-
logo?:
|
|
317
|
-
featuredPhotos?:
|
|
306
|
+
isActive?: boolean;
|
|
307
|
+
isVerified?: boolean;
|
|
308
|
+
logo?: MediaResource | null;
|
|
309
|
+
featuredPhotos?: MediaResource[];
|
|
318
310
|
}
|
|
319
311
|
|
|
320
312
|
/**
|
|
@@ -334,7 +326,7 @@ export interface CreateDefaultClinicGroupData {
|
|
|
334
326
|
contactInfo: ClinicContactInfo;
|
|
335
327
|
hqLocation: ClinicLocation;
|
|
336
328
|
isActive: boolean;
|
|
337
|
-
logo?: string | null;
|
|
329
|
+
logo?: string | File | null;
|
|
338
330
|
practiceType?: PracticeType;
|
|
339
331
|
languages?: Language[];
|
|
340
332
|
subscriptionModel?: SubscriptionModel;
|
|
@@ -355,7 +347,7 @@ export interface ClinicAdminSignupData {
|
|
|
355
347
|
clinicGroupData?: {
|
|
356
348
|
name: string;
|
|
357
349
|
hqLocation: ClinicLocation;
|
|
358
|
-
logo?: string;
|
|
350
|
+
logo?: string | File | null;
|
|
359
351
|
contactInfo: ClinicContactInfo;
|
|
360
352
|
subscriptionModel?: SubscriptionModel;
|
|
361
353
|
};
|
|
@@ -368,7 +360,7 @@ export interface ClinicGroupSetupData {
|
|
|
368
360
|
languages: Language[];
|
|
369
361
|
practiceType: PracticeType;
|
|
370
362
|
description: string;
|
|
371
|
-
logo: string;
|
|
363
|
+
logo: string | File | null;
|
|
372
364
|
calendarSyncEnabled: boolean;
|
|
373
365
|
autoConfirmAppointments: boolean;
|
|
374
366
|
businessIdentificationNumber: string | null;
|
|
@@ -384,10 +376,10 @@ export interface ClinicBranchSetupData {
|
|
|
384
376
|
contactInfo: ClinicContactInfo;
|
|
385
377
|
workingHours: WorkingHours;
|
|
386
378
|
tags: ClinicTag[];
|
|
387
|
-
logo?:
|
|
388
|
-
coverPhoto
|
|
389
|
-
photosWithTags?: { url:
|
|
390
|
-
featuredPhotos?:
|
|
379
|
+
logo?: MediaResource | null;
|
|
380
|
+
coverPhoto?: MediaResource | null;
|
|
381
|
+
photosWithTags?: { url: MediaResource; tag: string }[];
|
|
382
|
+
featuredPhotos?: MediaResource[];
|
|
391
383
|
}
|
|
392
384
|
|
|
393
385
|
/**
|
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { User as FirebaseAuthUser } from "firebase/auth";
|
|
2
|
-
import { Timestamp, FieldValue
|
|
2
|
+
import { Timestamp, FieldValue } from "firebase/firestore";
|
|
3
3
|
|
|
4
4
|
// User tipovi
|
|
5
5
|
export enum UserRole {
|
|
@@ -14,9 +14,9 @@ export interface User {
|
|
|
14
14
|
email: string | null;
|
|
15
15
|
roles: UserRole[];
|
|
16
16
|
isAnonymous: boolean;
|
|
17
|
-
createdAt: Timestamp | FieldValue
|
|
18
|
-
updatedAt: Timestamp | FieldValue
|
|
19
|
-
lastLoginAt: Timestamp | FieldValue
|
|
17
|
+
createdAt: Timestamp | FieldValue;
|
|
18
|
+
updatedAt: Timestamp | FieldValue;
|
|
19
|
+
lastLoginAt: Timestamp | FieldValue;
|
|
20
20
|
patientProfile?: string;
|
|
21
21
|
practitionerProfile?: string;
|
|
22
22
|
adminProfile?: string;
|
|
@@ -27,9 +27,9 @@ export interface CreateUserData {
|
|
|
27
27
|
email: string | null;
|
|
28
28
|
roles: UserRole[];
|
|
29
29
|
isAnonymous: boolean;
|
|
30
|
-
createdAt: FieldValue
|
|
31
|
-
updatedAt: FieldValue
|
|
32
|
-
lastLoginAt: FieldValue
|
|
30
|
+
createdAt: FieldValue;
|
|
31
|
+
updatedAt: FieldValue;
|
|
32
|
+
lastLoginAt: FieldValue;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export const USERS_COLLECTION = "users";
|