@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.
@@ -6,16 +6,13 @@ import {
6
6
  uploadBytes,
7
7
  getDownloadURL,
8
8
  deleteObject,
9
- listAll,
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 = originalFileName || file.name;
80
- // Sanitize fileNameToUse if necessary, e.g., remove special characters or ensure a safe extension
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, // Store the full storage path
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 is a complex operation if it involves moving the file.
208
- * For now, this will only update the metadata. True access control might need storage path changes and Firestore rule updates.
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] Updating access level for media ID: ${mediaId} to ${newAccessLevel}`
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
- // IMPORTANT: This only updates the metadata field.
229
- // If your Firebase Storage rules depend on the path, this won't change file accessibility directly.
230
- // A full implementation might involve moving the file in storage to a new path (e.g., media/newAccessLevel/...),
231
- // updating the URL, and then updating the metadata. This is significantly more complex.
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
- const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
234
- const updateData = {
235
- accessLevel: newAccessLevel,
236
- updatedAt: Timestamp.now(),
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(`[MediaService] Media access level updated for ${mediaId}.`);
242
- // Return the updated metadata
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
- throw error;
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 from it.
288
- // For simplicity, if using cursor-based pagination with getDocs, you'd pass the snapshot.
289
- // If building query manually, it's more complex with orderBy fields.
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
- : clinicData.coverPhoto || "",
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
- const tokenSnapshot = await getDocs(q);
453
- if (!tokenSnapshot.empty) {
454
- return tokenSnapshot.docs[0].data() as PractitionerToken;
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
- : clinic.coverPhoto || "",
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
- : newClinic.coverPhoto || "",
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 '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 { Currency, PricingMeasure } from '../../backoffice/types/static/pricing.types';
5
- import type { ClinicInfo } from '../profile';
6
- import { ClinicReviewInfo } from '../reviews';
7
- import { ProcedureSummaryInfo } from '../procedure';
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 = 'clinic_groups';
10
- export const CLINIC_ADMINS_COLLECTION = 'clinic_admins';
11
- export const CLINICS_COLLECTION = 'clinics';
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 { PracticeType, Language, ClinicTag, ClinicPhotoTag } from './preferences.types';
16
+ import {
17
+ PracticeType,
18
+ Language,
19
+ ClinicTag,
20
+ ClinicPhotoTag,
21
+ } from "./preferences.types";
14
22
 
15
- export * from './preferences.types';
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 = 'active',
136
- USED = 'used',
137
- EXPIRED = '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 = 'no_subscription',
167
- BASIC = 'basic',
168
- PREMIUM = 'premium',
169
- ENTERPRISE = '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?: string | null;
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?: string | null;
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
- * Interface for service information
257
+ * Type that allows a field to be either a URL string or a File object
250
258
  */
251
- // export interface ServiceInfo { // <-- Comment out or remove old ServiceInfo
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: string[];
282
- coverPhoto: string | null;
283
- photosWithTags?: { url: string; tag: string }[];
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[]; // <-- Use the new type instead of servicesInfo
288
- reviewInfo: ClinicReviewInfo; // Aggregated review information
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?: string;
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: string | null;
309
- photosWithTags?: { url: string; tag: string }[];
310
- doctors: string[];
311
- procedures: string[];
312
- proceduresInfo?: ProcedureSummaryInfo[]; // <-- Add this
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: boolean;
315
- isVerified: boolean;
316
- logo?: string;
317
- featuredPhotos?: string[];
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?: string;
388
- coverPhoto: string | null;
389
- photosWithTags?: { url: string; tag: string }[];
390
- featuredPhotos?: string[];
379
+ logo?: MediaResource | null;
380
+ coverPhoto?: MediaResource | null;
381
+ photosWithTags?: { url: MediaResource; tag: string }[];
382
+ featuredPhotos?: MediaResource[];
391
383
  }
392
384
 
393
385
  /**
@@ -1,5 +1,5 @@
1
1
  import { User as FirebaseAuthUser } from "firebase/auth";
2
- import { Timestamp, FieldValue, serverTimestamp } from "firebase/firestore";
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 | (() => FieldValue);
18
- updatedAt: Timestamp | FieldValue | (() => FieldValue);
19
- lastLoginAt: Timestamp | FieldValue | (() => 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 | Timestamp | (() => FieldValue);
31
- updatedAt: FieldValue | Timestamp | (() => FieldValue);
32
- lastLoginAt: FieldValue | Timestamp | (() => FieldValue);
30
+ createdAt: FieldValue;
31
+ updatedAt: FieldValue;
32
+ lastLoginAt: FieldValue;
33
33
  }
34
34
 
35
35
  export const USERS_COLLECTION = "users";