@blackcode_sa/metaestetics-api 1.4.8 → 1.4.10

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.
@@ -0,0 +1,188 @@
1
+ import {
2
+ getStorage,
3
+ ref,
4
+ uploadBytes,
5
+ getDownloadURL,
6
+ deleteObject,
7
+ } from "firebase/storage";
8
+ import { FirebaseApp } from "firebase/app";
9
+
10
+ /**
11
+ * Uploads a photo to Firebase Storage
12
+ * @param photo - The photo data URL or URL
13
+ * @param entityType - The type of entity (clinic-groups, clinics, etc.)
14
+ * @param entityId - The ID of the entity
15
+ * @param photoType - The type of photo (logo, featured, etc.)
16
+ * @param app - Firebase app instance
17
+ * @param fileName - Optional custom file name
18
+ * @returns The URL of the uploaded photo or null if upload failed
19
+ */
20
+ export async function uploadPhoto(
21
+ photo: string | null,
22
+ entityType: string,
23
+ entityId: string,
24
+ photoType: string,
25
+ app: FirebaseApp,
26
+ fileName?: string
27
+ ): Promise<string | null> {
28
+ // If photo is null or not a data URL, return it as is
29
+ if (!photo || typeof photo !== "string" || !photo.startsWith("data:")) {
30
+ return photo;
31
+ }
32
+
33
+ try {
34
+ console.log(
35
+ `[PHOTO_UTILS] Uploading ${photoType} for ${entityType}/${entityId}`
36
+ );
37
+
38
+ // Get Firebase Storage instance
39
+ const storage = getStorage(app);
40
+
41
+ // Create a reference to the storage location
42
+ const storageFileName = fileName || `${photoType}-${Date.now()}`;
43
+ const storageRef = ref(
44
+ storage,
45
+ `${entityType}/${entityId}/${storageFileName}`
46
+ );
47
+
48
+ // Extract base64 data from data URL
49
+ const base64Data = photo.split(",")[1];
50
+ const contentType = photo.split(";")[0].split(":")[1];
51
+
52
+ // Convert base64 to Blob
53
+ const byteCharacters = atob(base64Data);
54
+ const byteArrays = [];
55
+ for (let i = 0; i < byteCharacters.length; i++) {
56
+ byteArrays.push(byteCharacters.charCodeAt(i));
57
+ }
58
+ const blob = new Blob([new Uint8Array(byteArrays)], { type: contentType });
59
+
60
+ // Upload the file
61
+ await uploadBytes(storageRef, blob, { contentType });
62
+
63
+ // Get the download URL
64
+ const downloadUrl = await getDownloadURL(storageRef);
65
+
66
+ console.log(`[PHOTO_UTILS] ${photoType} uploaded successfully`, {
67
+ downloadUrl,
68
+ });
69
+ return downloadUrl;
70
+ } catch (error) {
71
+ console.error(`[PHOTO_UTILS] Error uploading ${photoType}:`, error);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Deletes a photo from Firebase Storage
78
+ * @param photoUrl - The URL of the photo to delete
79
+ * @param app - Firebase app instance
80
+ * @returns Whether the deletion was successful
81
+ */
82
+ export async function deletePhoto(
83
+ photoUrl: string,
84
+ app: FirebaseApp
85
+ ): Promise<boolean> {
86
+ if (!photoUrl || !photoUrl.includes("firebasestorage.googleapis.com")) {
87
+ return false;
88
+ }
89
+
90
+ try {
91
+ console.log(`[PHOTO_UTILS] Deleting photo`, { photoUrl });
92
+
93
+ // Get Firebase Storage instance
94
+ const storage = getStorage(app);
95
+
96
+ // Extract the storage path from the URL
97
+ const storageRef = ref(storage, photoUrl);
98
+
99
+ // Delete the file
100
+ await deleteObject(storageRef);
101
+
102
+ console.log(`[PHOTO_UTILS] Photo deleted successfully`);
103
+ return true;
104
+ } catch (error) {
105
+ console.error(`[PHOTO_UTILS] Error deleting photo:`, error);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Uploads multiple photos to Firebase Storage
112
+ * @param photos - Array of photo data URLs or URLs
113
+ * @param entityType - The type of entity (clinic-groups, clinics, etc.)
114
+ * @param entityId - The ID of the entity
115
+ * @param photoType - The type of photos (featured, gallery, etc.)
116
+ * @param app - Firebase app instance
117
+ * @returns Array of URLs of the uploaded photos
118
+ */
119
+ export async function uploadMultiplePhotos(
120
+ photos: string[],
121
+ entityType: string,
122
+ entityId: string,
123
+ photoType: string,
124
+ app: FirebaseApp
125
+ ): Promise<string[]> {
126
+ if (!photos || !Array.isArray(photos) || photos.length === 0) {
127
+ return [];
128
+ }
129
+
130
+ const uploadPromises = photos.map((photo, index) =>
131
+ uploadPhoto(photo, entityType, entityId, `${photoType}-${index}`, app)
132
+ );
133
+
134
+ try {
135
+ const results = await Promise.all(uploadPromises);
136
+ return results.filter((url): url is string => url !== null);
137
+ } catch (error) {
138
+ console.error(`[PHOTO_UTILS] Error uploading multiple photos:`, error);
139
+ return photos.filter((photo) => !photo.startsWith("data:"));
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Processes photos in an entity object, uploading any data URLs
145
+ * @param entity - The entity object containing photo fields
146
+ * @param entityType - The type of entity (clinic-groups, clinics, etc.)
147
+ * @param entityId - The ID of the entity
148
+ * @param photoFields - Array of field names that contain photos
149
+ * @param app - Firebase app instance
150
+ * @returns The entity with updated photo URLs
151
+ */
152
+ export async function processEntityPhotos<T extends Record<string, any>>(
153
+ entity: T,
154
+ entityType: string,
155
+ entityId: string,
156
+ photoFields: { field: keyof T; type: string }[],
157
+ app: FirebaseApp
158
+ ): Promise<T> {
159
+ const updatedEntity = { ...entity };
160
+
161
+ for (const { field, type } of photoFields) {
162
+ const value = entity[field];
163
+
164
+ if (!value) continue;
165
+
166
+ if (Array.isArray(value)) {
167
+ // Handle array of photos
168
+ updatedEntity[field] = (await uploadMultiplePhotos(
169
+ value,
170
+ entityType,
171
+ entityId,
172
+ type,
173
+ app
174
+ )) as any;
175
+ } else if (typeof value === "string") {
176
+ // Handle single photo
177
+ updatedEntity[field] = (await uploadPhoto(
178
+ value,
179
+ entityType,
180
+ entityId,
181
+ type,
182
+ app
183
+ )) as any;
184
+ }
185
+ }
186
+
187
+ return updatedEntity;
188
+ }
@@ -4,16 +4,20 @@ import {
4
4
  setDoc,
5
5
  Timestamp,
6
6
  Firestore,
7
+ getDoc,
8
+ addDoc,
7
9
  } from "firebase/firestore";
8
10
  import { Clinic, ClinicReview } from "../../../types/clinic";
9
11
  import { clinicReviewSchema } from "../../../validations/clinic.schema";
10
12
  import { getClinic, updateClinic } from "./clinic.utils";
13
+ import { FirebaseApp } from "firebase/app";
11
14
 
12
15
  /**
13
16
  * Adds a review to a clinic
14
17
  * @param db - Firestore database instance
15
- * @param clinicId - ID of the clinic to review
18
+ * @param clinicId - ID of the clinic to add the review to
16
19
  * @param review - Review data
20
+ * @param app - Firebase app instance
17
21
  * @returns The created review
18
22
  */
19
23
  export async function addReview(
@@ -22,20 +26,25 @@ export async function addReview(
22
26
  review: Omit<
23
27
  ClinicReview,
24
28
  "id" | "clinicId" | "createdAt" | "updatedAt" | "isVerified"
25
- >
29
+ >,
30
+ app: FirebaseApp
26
31
  ): Promise<ClinicReview> {
27
- const clinic = await getClinic(db, clinicId);
28
- if (!clinic) {
32
+ // Proveravamo da li klinika postoji
33
+ const clinicRef = doc(db, "clinics", clinicId);
34
+ const clinicSnap = await getDoc(clinicRef);
35
+
36
+ if (!clinicSnap.exists()) {
29
37
  throw new Error("Clinic not found");
30
38
  }
31
39
 
40
+ const clinic = clinicSnap.data();
41
+
42
+ // Kreiramo recenziju
32
43
  const now = Timestamp.now();
33
44
  const reviewData: ClinicReview = {
45
+ ...review,
34
46
  id: doc(collection(db, "clinic_reviews")).id,
35
47
  clinicId,
36
- patientId: review.patientId,
37
- rating: review.rating,
38
- comment: review.comment,
39
48
  createdAt: now,
40
49
  updatedAt: now,
41
50
  isVerified: false,
@@ -45,7 +54,7 @@ export async function addReview(
45
54
  clinicReviewSchema.parse(reviewData);
46
55
 
47
56
  // Čuvamo recenziju
48
- await setDoc(doc(db, "clinic_reviews", reviewData.id), reviewData);
57
+ await addDoc(collection(db, "clinic_reviews"), reviewData);
49
58
 
50
59
  // Ažuriramo prosečnu ocenu klinike
51
60
  const newRating = clinic.rating
@@ -68,18 +77,16 @@ export async function addReview(
68
77
  ...clinic.reviewsInfo,
69
78
  {
70
79
  id: reviewData.id,
71
- rating: review.rating,
72
- text: review.comment,
73
- patientId: review.patientId,
74
- patientName: "Patient", // This should be fetched from patient service
75
- patientPhoto: "", // This should be fetched from patient service
76
- createdAt: now,
77
- updatedAt: now,
80
+ patientId: reviewData.patientId,
81
+ rating: reviewData.rating,
82
+ comment: reviewData.comment,
83
+ createdAt: reviewData.createdAt,
78
84
  },
79
85
  ],
80
86
  },
81
- clinic.admins[0], // Using the first admin for the update
82
- { getClinicAdmin: async (id: string) => ({ isGroupOwner: true }) } // Mock admin service
87
+ "system", // System update, no admin ID needed
88
+ null, // No clinic admin service needed for system updates
89
+ app
83
90
  );
84
91
 
85
92
  return reviewData;
@@ -1,6 +1,7 @@
1
1
  import { Firestore } from "firebase/firestore";
2
2
  import { Clinic, ClinicTag } from "../../../types/clinic";
3
3
  import { getClinic, updateClinic } from "./clinic.utils";
4
+ import { FirebaseApp } from "firebase/app";
4
5
 
5
6
  /**
6
7
  * Adds tags to a clinic
@@ -9,6 +10,7 @@ import { getClinic, updateClinic } from "./clinic.utils";
9
10
  * @param adminId - ID of the admin making the change
10
11
  * @param newTags - Tags to add
11
12
  * @param clinicAdminService - Service for clinic admin operations
13
+ * @param app - Firebase app instance
12
14
  * @returns The updated clinic
13
15
  */
14
16
  export async function addTags(
@@ -18,7 +20,8 @@ export async function addTags(
18
20
  newTags: {
19
21
  tags?: ClinicTag[];
20
22
  },
21
- clinicAdminService: any
23
+ clinicAdminService: any,
24
+ app: FirebaseApp
22
25
  ): Promise<Clinic> {
23
26
  const clinic = await getClinic(db, clinicId);
24
27
  if (!clinic) {
@@ -31,7 +34,16 @@ export async function addTags(
31
34
  throw new Error("Admin not found");
32
35
  }
33
36
 
34
- if (!admin.isGroupOwner && !admin.clinicsManaged.includes(clinicId)) {
37
+ // Check if admin is either:
38
+ // 1. The owner of the clinic group that this clinic belongs to, OR
39
+ // 2. Has this clinic in their managed clinics list AND is listed in the clinic's admins array
40
+ const hasPermission =
41
+ (admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
42
+ (admin.clinicsManaged.includes(clinicId) &&
43
+ clinic.admins &&
44
+ clinic.admins.includes(adminId));
45
+
46
+ if (!hasPermission) {
35
47
  throw new Error("Admin does not have permission to update this clinic");
36
48
  }
37
49
 
@@ -45,7 +57,8 @@ export async function addTags(
45
57
  tags: updatedTags,
46
58
  },
47
59
  adminId,
48
- clinicAdminService
60
+ clinicAdminService,
61
+ app
49
62
  );
50
63
  }
51
64
 
@@ -56,6 +69,7 @@ export async function addTags(
56
69
  * @param adminId - ID of the admin making the change
57
70
  * @param tagsToRemove - Tags to remove
58
71
  * @param clinicAdminService - Service for clinic admin operations
72
+ * @param app - Firebase app instance
59
73
  * @returns The updated clinic
60
74
  */
61
75
  export async function removeTags(
@@ -65,7 +79,8 @@ export async function removeTags(
65
79
  tagsToRemove: {
66
80
  tags?: ClinicTag[];
67
81
  },
68
- clinicAdminService: any
82
+ clinicAdminService: any,
83
+ app: FirebaseApp
69
84
  ): Promise<Clinic> {
70
85
  const clinic = await getClinic(db, clinicId);
71
86
  if (!clinic) {
@@ -78,13 +93,22 @@ export async function removeTags(
78
93
  throw new Error("Admin not found");
79
94
  }
80
95
 
81
- if (!admin.isGroupOwner && !admin.clinicsManaged.includes(clinicId)) {
96
+ // Check if admin is either:
97
+ // 1. The owner of the clinic group that this clinic belongs to, OR
98
+ // 2. Has this clinic in their managed clinics list AND is listed in the clinic's admins array
99
+ const hasPermission =
100
+ (admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
101
+ (admin.clinicsManaged.includes(clinicId) &&
102
+ clinic.admins &&
103
+ clinic.admins.includes(adminId));
104
+
105
+ if (!hasPermission) {
82
106
  throw new Error("Admin does not have permission to update this clinic");
83
107
  }
84
108
 
85
- // Filter out tags that should be removed
109
+ // Remove specified tags
86
110
  const updatedTags = clinic.tags.filter(
87
- (tag) => !tagsToRemove.tags?.includes(tag)
111
+ (tag) => !tagsToRemove.tags || !tagsToRemove.tags.includes(tag)
88
112
  );
89
113
 
90
114
  return updateClinic(
@@ -94,6 +118,7 @@ export async function removeTags(
94
118
  tags: updatedTags,
95
119
  },
96
120
  adminId,
97
- clinicAdminService
121
+ clinicAdminService,
122
+ app
98
123
  );
99
124
  }
@@ -107,7 +107,7 @@ export interface ClinicInfo {
107
107
  export interface ClinicAdmin {
108
108
  id: string;
109
109
  userRef: string;
110
- clinicGroupId: string;
110
+ clinicGroupId: string; // Can be empty string initially for owners
111
111
  isGroupOwner: boolean;
112
112
  clinicsManaged: string[];
113
113
  clinicsManagedInfo: ClinicInfo[];
@@ -195,7 +195,7 @@ export interface ClinicGroup {
195
195
  admins: string[];
196
196
  adminsInfo: AdminInfo[];
197
197
  adminTokens: AdminToken[];
198
- ownerId: string;
198
+ ownerId: string | null;
199
199
  createdAt: Timestamp;
200
200
  updatedAt: Timestamp;
201
201
  isActive: boolean;
@@ -205,6 +205,7 @@ export interface ClinicGroup {
205
205
  subscriptionModel: SubscriptionModel;
206
206
  calendarSyncEnabled?: boolean;
207
207
  autoConfirmAppointments?: boolean;
208
+ businessIdentificationNumber?: string | null;
208
209
  }
209
210
 
210
211
  /**
@@ -216,7 +217,7 @@ export interface CreateClinicGroupData {
216
217
  hqLocation: ClinicLocation;
217
218
  contactInfo: ClinicContactInfo;
218
219
  contactPerson: ContactPerson;
219
- ownerId: string;
220
+ ownerId: string | null;
220
221
  isActive: boolean;
221
222
  logo?: string | null;
222
223
  practiceType?: PracticeType;
@@ -224,6 +225,7 @@ export interface CreateClinicGroupData {
224
225
  subscriptionModel?: SubscriptionModel;
225
226
  calendarSyncEnabled?: boolean;
226
227
  autoConfirmAppointments?: boolean;
228
+ businessIdentificationNumber?: string | null;
227
229
  }
228
230
 
229
231
  /**
@@ -355,12 +357,12 @@ export interface UpdateClinicData extends Partial<CreateClinicData> {
355
357
  */
356
358
  export interface CreateDefaultClinicGroupData {
357
359
  name: string;
358
- ownerId: string;
360
+ ownerId: string | null;
359
361
  contactPerson: ContactPerson;
360
362
  contactInfo: ClinicContactInfo;
361
363
  hqLocation: ClinicLocation;
362
364
  isActive: boolean;
363
- logo?: string;
365
+ logo?: string | null;
364
366
  practiceType?: PracticeType;
365
367
  languages?: Language[];
366
368
  subscriptionModel?: SubscriptionModel;
@@ -397,6 +399,7 @@ export interface ClinicGroupSetupData {
397
399
  logo: string;
398
400
  calendarSyncEnabled: boolean;
399
401
  autoConfirmAppointments: boolean;
402
+ businessIdentificationNumber: string | null;
400
403
  }
401
404
 
402
405
  /**
@@ -200,7 +200,7 @@ export const clinicGroupSchema = z.object({
200
200
  admins: z.array(z.string()),
201
201
  adminsInfo: z.array(adminInfoSchema),
202
202
  adminTokens: z.array(adminTokenSchema),
203
- ownerId: z.string(),
203
+ ownerId: z.string().nullable(),
204
204
  createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
205
205
  updatedAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
206
206
  isActive: z.boolean(),
@@ -210,6 +210,7 @@ export const clinicGroupSchema = z.object({
210
210
  subscriptionModel: z.nativeEnum(SubscriptionModel),
211
211
  calendarSyncEnabled: z.boolean().optional(),
212
212
  autoConfirmAppointments: z.boolean().optional(),
213
+ businessIdentificationNumber: z.string().optional().nullable(),
213
214
  });
214
215
 
215
216
  /**
@@ -290,7 +291,7 @@ export const createClinicGroupSchema = z.object({
290
291
  hqLocation: clinicLocationSchema,
291
292
  contactInfo: clinicContactInfoSchema,
292
293
  contactPerson: contactPersonSchema,
293
- ownerId: z.string(),
294
+ ownerId: z.string().nullable(),
294
295
  isActive: z.boolean(),
295
296
  logo: z.string().optional().nullable(),
296
297
  practiceType: z.nativeEnum(PracticeType).optional(),
@@ -301,6 +302,7 @@ export const createClinicGroupSchema = z.object({
301
302
  .default(SubscriptionModel.NO_SUBSCRIPTION),
302
303
  calendarSyncEnabled: z.boolean().optional(),
303
304
  autoConfirmAppointments: z.boolean().optional(),
305
+ businessIdentificationNumber: z.string().optional().nullable(),
304
306
  });
305
307
 
306
308
  /**
@@ -337,7 +339,7 @@ export const createClinicSchema = z.object({
337
339
  */
338
340
  export const createDefaultClinicGroupSchema = z.object({
339
341
  name: z.string(),
340
- ownerId: z.string(),
342
+ ownerId: z.string().nullable(),
341
343
  contactPerson: contactPersonSchema,
342
344
  contactInfo: clinicContactInfoSchema,
343
345
  hqLocation: clinicLocationSchema,
@@ -387,6 +389,7 @@ export const clinicGroupSetupSchema = z.object({
387
389
  logo: z.string(),
388
390
  calendarSyncEnabled: z.boolean(),
389
391
  autoConfirmAppointments: z.boolean(),
392
+ businessIdentificationNumber: z.string().optional().nullable(),
390
393
  });
391
394
 
392
395
  /**