@blackcode_sa/metaestetics-api 1.7.6 → 1.7.7
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 +1689 -1262
- package/dist/index.mjs +1493 -1050
- 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 +6 -2
- 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
package/package.json
CHANGED
|
@@ -457,7 +457,12 @@ export class BookingAdmin {
|
|
|
457
457
|
const clinicInfo: ClinicInfo = {
|
|
458
458
|
id: clinicSnap.id,
|
|
459
459
|
name: clinicData.name,
|
|
460
|
-
featuredPhoto:
|
|
460
|
+
featuredPhoto:
|
|
461
|
+
(typeof clinicData.coverPhoto === "string"
|
|
462
|
+
? clinicData.coverPhoto
|
|
463
|
+
: "") ||
|
|
464
|
+
(typeof clinicData.logo === "string" ? clinicData.logo : "") ||
|
|
465
|
+
"",
|
|
461
466
|
description: clinicData.description,
|
|
462
467
|
location: clinicData.location,
|
|
463
468
|
contactInfo: clinicData.contactInfo,
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
import { PractitionerService } from "./practitioner/practitioner.service";
|
|
72
72
|
import { practitionerSignupSchema } from "../validations/practitioner.schema";
|
|
73
73
|
import { CertificationLevel } from "../backoffice/types/static/certification.types";
|
|
74
|
+
import { MediaService } from "./media/media.service";
|
|
74
75
|
|
|
75
76
|
export class AuthService extends BaseService {
|
|
76
77
|
private googleProvider = new GoogleAuthProvider();
|
|
@@ -199,12 +200,14 @@ export class AuthService extends BaseService {
|
|
|
199
200
|
this.app,
|
|
200
201
|
clinicAdminService
|
|
201
202
|
);
|
|
203
|
+
const mediaService = new MediaService(this.db, this.auth, this.app);
|
|
202
204
|
const clinicService = new ClinicService(
|
|
203
205
|
this.db,
|
|
204
206
|
this.auth,
|
|
205
207
|
this.app,
|
|
206
208
|
clinicGroupService,
|
|
207
|
-
clinicAdminService
|
|
209
|
+
clinicAdminService,
|
|
210
|
+
mediaService
|
|
208
211
|
);
|
|
209
212
|
|
|
210
213
|
// Set services to resolve circular dependencies
|
|
@@ -510,12 +513,14 @@ export class AuthService extends BaseService {
|
|
|
510
513
|
this.app,
|
|
511
514
|
clinicAdminService
|
|
512
515
|
);
|
|
516
|
+
const mediaService = new MediaService(this.db, this.auth, this.app);
|
|
513
517
|
const clinicService = new ClinicService(
|
|
514
518
|
this.db,
|
|
515
519
|
this.auth,
|
|
516
520
|
this.app,
|
|
517
521
|
clinicGroupService,
|
|
518
|
-
clinicAdminService
|
|
522
|
+
clinicAdminService,
|
|
523
|
+
mediaService
|
|
519
524
|
);
|
|
520
525
|
|
|
521
526
|
// Set services to resolve circular dependencies
|
|
@@ -991,21 +996,15 @@ export class AuthService extends BaseService {
|
|
|
991
996
|
|
|
992
997
|
// We need to create a full PractitionerBasicInfo object
|
|
993
998
|
const basicInfo: PractitionerBasicInfo = {
|
|
994
|
-
firstName:
|
|
995
|
-
|
|
996
|
-
? data.firstName
|
|
997
|
-
: data.firstName.padEnd(2, " "),
|
|
998
|
-
lastName:
|
|
999
|
-
data.lastName.length >= 2
|
|
1000
|
-
? data.lastName
|
|
1001
|
-
: data.lastName.padEnd(2, " "),
|
|
999
|
+
firstName: data.firstName,
|
|
1000
|
+
lastName: data.lastName,
|
|
1002
1001
|
email: data.email,
|
|
1003
|
-
phoneNumber: "
|
|
1002
|
+
phoneNumber: data.profileData.basicInfo?.phoneNumber || "",
|
|
1004
1003
|
profileImageUrl: data.profileData.basicInfo?.profileImageUrl || "",
|
|
1005
1004
|
gender: data.profileData.basicInfo?.gender || "other", // Default to "other" if not provided
|
|
1006
1005
|
bio: data.profileData.basicInfo?.bio || "",
|
|
1007
|
-
title:
|
|
1008
|
-
dateOfBirth:
|
|
1006
|
+
title: "Practitioner", // Default title
|
|
1007
|
+
dateOfBirth: new Date(), // Default to today
|
|
1009
1008
|
languages: ["English"], // Default language
|
|
1010
1009
|
};
|
|
1011
1010
|
|
|
@@ -1014,9 +1013,9 @@ export class AuthService extends BaseService {
|
|
|
1014
1013
|
.certification || {
|
|
1015
1014
|
level: CertificationLevel.AESTHETICIAN,
|
|
1016
1015
|
specialties: [],
|
|
1017
|
-
licenseNumber: "
|
|
1018
|
-
issuingAuthority: "
|
|
1019
|
-
issueDate:
|
|
1016
|
+
licenseNumber: "Pending",
|
|
1017
|
+
issuingAuthority: "Pending",
|
|
1018
|
+
issueDate: new Date(),
|
|
1020
1019
|
verificationStatus: "pending",
|
|
1021
1020
|
};
|
|
1022
1021
|
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
import {
|
|
44
44
|
clinicSchema,
|
|
45
45
|
createClinicSchema,
|
|
46
|
+
updateClinicSchema,
|
|
47
|
+
clinicBranchSetupSchema,
|
|
46
48
|
} from "../../validations/clinic.schema";
|
|
47
49
|
import { z } from "zod";
|
|
48
50
|
import { Auth } from "firebase/auth";
|
|
@@ -55,41 +57,175 @@ import * as AdminUtils from "./utils/admin.utils";
|
|
|
55
57
|
import * as FilterUtils from "./utils/filter.utils";
|
|
56
58
|
import { ClinicReviewInfo } from "../../types/reviews";
|
|
57
59
|
import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
|
|
60
|
+
import { MediaService, MediaAccessLevel } from "../media/media.service";
|
|
58
61
|
|
|
59
62
|
export class ClinicService extends BaseService {
|
|
60
63
|
private clinicGroupService: ClinicGroupService;
|
|
61
64
|
private clinicAdminService: ClinicAdminService;
|
|
65
|
+
private mediaService: MediaService;
|
|
62
66
|
|
|
63
67
|
constructor(
|
|
64
68
|
db: Firestore,
|
|
65
69
|
auth: Auth,
|
|
66
70
|
app: FirebaseApp,
|
|
67
71
|
clinicGroupService: ClinicGroupService,
|
|
68
|
-
clinicAdminService: ClinicAdminService
|
|
72
|
+
clinicAdminService: ClinicAdminService,
|
|
73
|
+
mediaService: MediaService
|
|
69
74
|
) {
|
|
70
75
|
super(db, auth, app);
|
|
71
76
|
this.clinicAdminService = clinicAdminService;
|
|
72
77
|
this.clinicGroupService = clinicGroupService;
|
|
78
|
+
this.mediaService = mediaService;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Process media resource (string URL or File object)
|
|
83
|
+
* @param media String URL or File object
|
|
84
|
+
* @param ownerId Owner ID for the media (usually clinicId)
|
|
85
|
+
* @param collectionName Collection name for organizing files
|
|
86
|
+
* @returns URL string after processing
|
|
87
|
+
*/
|
|
88
|
+
private async processMedia(
|
|
89
|
+
media: string | File | Blob | null | undefined,
|
|
90
|
+
ownerId: string,
|
|
91
|
+
collectionName: string
|
|
92
|
+
): Promise<string | null> {
|
|
93
|
+
if (!media) return null;
|
|
94
|
+
|
|
95
|
+
// If already a string URL, return it directly
|
|
96
|
+
if (typeof media === "string") {
|
|
97
|
+
return media;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If it's a File, upload it using MediaService
|
|
101
|
+
if (media instanceof File || media instanceof Blob) {
|
|
102
|
+
console.log(
|
|
103
|
+
`[ClinicService] Uploading ${collectionName} media for ${ownerId}`
|
|
104
|
+
);
|
|
105
|
+
const metadata = await this.mediaService.uploadMedia(
|
|
106
|
+
media,
|
|
107
|
+
ownerId,
|
|
108
|
+
MediaAccessLevel.PUBLIC,
|
|
109
|
+
collectionName
|
|
110
|
+
);
|
|
111
|
+
return metadata.url;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Process array of media resources (strings or Files)
|
|
119
|
+
* @param mediaArray Array of string URLs or File objects
|
|
120
|
+
* @param ownerId Owner ID for the media
|
|
121
|
+
* @param collectionName Collection name for organizing files
|
|
122
|
+
* @returns Array of URL strings after processing
|
|
123
|
+
*/
|
|
124
|
+
private async processMediaArray(
|
|
125
|
+
mediaArray: (string | File | Blob)[] | undefined,
|
|
126
|
+
ownerId: string,
|
|
127
|
+
collectionName: string
|
|
128
|
+
): Promise<string[]> {
|
|
129
|
+
if (!mediaArray || mediaArray.length === 0) return [];
|
|
130
|
+
|
|
131
|
+
const result: string[] = [];
|
|
132
|
+
|
|
133
|
+
for (const media of mediaArray) {
|
|
134
|
+
const processedUrl = await this.processMedia(
|
|
135
|
+
media,
|
|
136
|
+
ownerId,
|
|
137
|
+
collectionName
|
|
138
|
+
);
|
|
139
|
+
if (processedUrl) {
|
|
140
|
+
result.push(processedUrl);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Process photos with tags array
|
|
149
|
+
* @param photosWithTags Array of objects containing media and tag
|
|
150
|
+
* @param ownerId Owner ID for the media
|
|
151
|
+
* @param collectionName Collection name for organizing files
|
|
152
|
+
* @returns Processed array with URL strings
|
|
153
|
+
*/
|
|
154
|
+
private async processPhotosWithTags(
|
|
155
|
+
photosWithTags: { url: string | File | Blob; tag: string }[] | undefined,
|
|
156
|
+
ownerId: string,
|
|
157
|
+
collectionName: string
|
|
158
|
+
): Promise<{ url: string; tag: string }[]> {
|
|
159
|
+
if (!photosWithTags || photosWithTags.length === 0) return [];
|
|
160
|
+
|
|
161
|
+
const result: { url: string; tag: string }[] = [];
|
|
162
|
+
|
|
163
|
+
for (const item of photosWithTags) {
|
|
164
|
+
const processedUrl = await this.processMedia(
|
|
165
|
+
item.url,
|
|
166
|
+
ownerId,
|
|
167
|
+
collectionName
|
|
168
|
+
);
|
|
169
|
+
if (processedUrl) {
|
|
170
|
+
result.push({
|
|
171
|
+
url: processedUrl,
|
|
172
|
+
tag: item.tag,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
73
178
|
}
|
|
74
179
|
|
|
75
180
|
/**
|
|
76
181
|
* Creates a new clinic.
|
|
182
|
+
* Handles both URL strings and File uploads for media fields.
|
|
77
183
|
*/
|
|
78
184
|
async createClinic(
|
|
79
185
|
data: CreateClinicData,
|
|
80
186
|
creatorAdminId: string
|
|
81
187
|
): Promise<Clinic> {
|
|
82
188
|
try {
|
|
189
|
+
// Generate ID first so we can use it for media uploads
|
|
190
|
+
const clinicId = this.generateId();
|
|
191
|
+
|
|
192
|
+
// Validate data - this now works because mediaResourceSchema has been updated to support Files/Blobs
|
|
83
193
|
const validatedData = createClinicSchema.parse(data);
|
|
194
|
+
|
|
84
195
|
const group = await this.clinicGroupService.getClinicGroup(
|
|
85
196
|
validatedData.clinicGroupId
|
|
86
197
|
);
|
|
87
|
-
if (!group)
|
|
198
|
+
if (!group) {
|
|
88
199
|
throw new Error(
|
|
89
200
|
`Clinic group ${validatedData.clinicGroupId} not found`
|
|
90
201
|
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Process media fields - convert File/Blob objects to URLs
|
|
205
|
+
const logoUrl = await this.processMedia(
|
|
206
|
+
validatedData.logo,
|
|
207
|
+
clinicId,
|
|
208
|
+
"clinic-logos"
|
|
209
|
+
);
|
|
210
|
+
const coverPhotoUrl = await this.processMedia(
|
|
211
|
+
validatedData.coverPhoto,
|
|
212
|
+
clinicId,
|
|
213
|
+
"clinic-cover-photos"
|
|
214
|
+
);
|
|
215
|
+
const featuredPhotos = await this.processMediaArray(
|
|
216
|
+
validatedData.featuredPhotos,
|
|
217
|
+
clinicId,
|
|
218
|
+
"clinic-featured-photos"
|
|
219
|
+
);
|
|
220
|
+
const photosWithTags = await this.processPhotosWithTags(
|
|
221
|
+
validatedData.photosWithTags,
|
|
222
|
+
clinicId,
|
|
223
|
+
"clinic-gallery"
|
|
224
|
+
);
|
|
225
|
+
|
|
91
226
|
const location = validatedData.location;
|
|
92
227
|
const hash = geohashForLocation([location.latitude, location.longitude]);
|
|
228
|
+
|
|
93
229
|
const defaultReviewInfo: ClinicReviewInfo = {
|
|
94
230
|
totalReviews: 0,
|
|
95
231
|
averageRating: 0,
|
|
@@ -100,7 +236,6 @@ export class ClinicService extends BaseService {
|
|
|
100
236
|
accessibility: 0,
|
|
101
237
|
recommendationPercentage: 0,
|
|
102
238
|
};
|
|
103
|
-
const clinicId = this.generateId();
|
|
104
239
|
|
|
105
240
|
const clinicData: Omit<Clinic, "createdAt" | "updatedAt"> & {
|
|
106
241
|
createdAt: FieldValue;
|
|
@@ -114,34 +249,41 @@ export class ClinicService extends BaseService {
|
|
|
114
249
|
contactInfo: validatedData.contactInfo,
|
|
115
250
|
workingHours: validatedData.workingHours,
|
|
116
251
|
tags: validatedData.tags,
|
|
117
|
-
featuredPhotos:
|
|
118
|
-
coverPhoto:
|
|
119
|
-
photosWithTags:
|
|
120
|
-
doctors: [],
|
|
121
|
-
procedures: [],
|
|
252
|
+
featuredPhotos: featuredPhotos,
|
|
253
|
+
coverPhoto: coverPhotoUrl,
|
|
254
|
+
photosWithTags: photosWithTags,
|
|
255
|
+
doctors: validatedData.doctors || [],
|
|
256
|
+
procedures: validatedData.procedures || [],
|
|
122
257
|
doctorsInfo: [],
|
|
123
|
-
proceduresInfo: [],
|
|
258
|
+
proceduresInfo: validatedData.proceduresInfo || [],
|
|
124
259
|
reviewInfo: defaultReviewInfo,
|
|
125
260
|
admins: [creatorAdminId],
|
|
126
|
-
isActive:
|
|
127
|
-
|
|
128
|
-
|
|
261
|
+
isActive:
|
|
262
|
+
validatedData.isActive !== undefined ? validatedData.isActive : true,
|
|
263
|
+
isVerified:
|
|
264
|
+
validatedData.isVerified !== undefined
|
|
265
|
+
? validatedData.isVerified
|
|
266
|
+
: false,
|
|
267
|
+
logo: logoUrl,
|
|
129
268
|
createdAt: serverTimestamp(),
|
|
130
269
|
updatedAt: serverTimestamp(),
|
|
131
270
|
};
|
|
132
271
|
|
|
133
|
-
//
|
|
272
|
+
// We can validate the final object with URLs using clinicSchema which also supports mediaResourceSchema
|
|
273
|
+
// However, we need to be careful with timestamps
|
|
274
|
+
// The validation below is optional and can be uncommented if needed
|
|
275
|
+
/*
|
|
134
276
|
clinicSchema.parse({
|
|
135
277
|
...clinicData,
|
|
136
278
|
createdAt: Timestamp.now(),
|
|
137
279
|
updatedAt: Timestamp.now(),
|
|
138
280
|
});
|
|
281
|
+
*/
|
|
139
282
|
|
|
140
283
|
const batch = writeBatch(this.db);
|
|
141
284
|
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
142
285
|
batch.set(clinicRef, clinicData);
|
|
143
286
|
|
|
144
|
-
// Update admin relationship - this part is still needed
|
|
145
287
|
const adminRef = doc(this.db, CLINIC_ADMINS_COLLECTION, creatorAdminId);
|
|
146
288
|
batch.update(adminRef, {
|
|
147
289
|
clinicsManaged: arrayUnion(clinicId),
|
|
@@ -149,6 +291,8 @@ export class ClinicService extends BaseService {
|
|
|
149
291
|
});
|
|
150
292
|
|
|
151
293
|
await batch.commit();
|
|
294
|
+
console.log(`[ClinicService] Clinic created successfully: ${clinicId}`);
|
|
295
|
+
|
|
152
296
|
const savedClinic = await this.getClinic(clinicId);
|
|
153
297
|
if (!savedClinic) throw new Error("Failed to retrieve created clinic");
|
|
154
298
|
return savedClinic;
|
|
@@ -163,66 +307,106 @@ export class ClinicService extends BaseService {
|
|
|
163
307
|
|
|
164
308
|
/**
|
|
165
309
|
* Updates a clinic.
|
|
310
|
+
* Handles both URL strings and File uploads for media fields.
|
|
166
311
|
*/
|
|
167
312
|
async updateClinic(
|
|
168
313
|
clinicId: string,
|
|
169
|
-
data: Partial<
|
|
314
|
+
data: Partial<CreateClinicData>,
|
|
170
315
|
adminId: string
|
|
171
316
|
): Promise<Clinic> {
|
|
172
|
-
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
173
|
-
const clinicDoc = await getDoc(clinicRef);
|
|
174
|
-
if (!clinicDoc.exists()) {
|
|
175
|
-
throw new Error(`Clinic ${clinicId} not found`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
317
|
try {
|
|
318
|
+
// First check if clinic exists
|
|
319
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
320
|
+
const clinicDoc = await getDoc(clinicRef);
|
|
321
|
+
|
|
322
|
+
if (!clinicDoc.exists()) {
|
|
323
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
324
|
+
}
|
|
325
|
+
|
|
179
326
|
const currentClinic = clinicDoc.data() as Clinic;
|
|
180
|
-
// Explicitly Omit fields managed by other services or internally
|
|
181
|
-
const { doctorsInfo, proceduresInfo, ...updatePayload } =
|
|
182
|
-
data as Partial<Clinic>;
|
|
183
327
|
|
|
184
|
-
|
|
185
|
-
|
|
328
|
+
// Validate update data - this works because updateClinicSchema supports Files/Blobs
|
|
329
|
+
const validatedData = updateClinicSchema.parse(data);
|
|
330
|
+
|
|
331
|
+
const updatePayload: Record<string, any> = {};
|
|
332
|
+
|
|
333
|
+
// Process media fields if provided
|
|
334
|
+
if (validatedData.logo !== undefined) {
|
|
335
|
+
updatePayload.logo = await this.processMedia(
|
|
336
|
+
validatedData.logo,
|
|
337
|
+
clinicId,
|
|
338
|
+
"clinic-logos"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (validatedData.coverPhoto !== undefined) {
|
|
343
|
+
updatePayload.coverPhoto = await this.processMedia(
|
|
344
|
+
validatedData.coverPhoto,
|
|
345
|
+
clinicId,
|
|
346
|
+
"clinic-cover-photos"
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (validatedData.featuredPhotos !== undefined) {
|
|
351
|
+
updatePayload.featuredPhotos = await this.processMediaArray(
|
|
352
|
+
validatedData.featuredPhotos,
|
|
353
|
+
clinicId,
|
|
354
|
+
"clinic-featured-photos"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (validatedData.photosWithTags !== undefined) {
|
|
359
|
+
updatePayload.photosWithTags = await this.processPhotosWithTags(
|
|
360
|
+
validatedData.photosWithTags,
|
|
361
|
+
clinicId,
|
|
362
|
+
"clinic-gallery"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Process non-media fields
|
|
367
|
+
const fieldsToUpdate = [
|
|
368
|
+
"name",
|
|
369
|
+
"description",
|
|
370
|
+
"contactInfo",
|
|
371
|
+
"workingHours",
|
|
372
|
+
"tags",
|
|
373
|
+
"doctors",
|
|
374
|
+
"procedures",
|
|
375
|
+
"proceduresInfo",
|
|
376
|
+
"isActive",
|
|
377
|
+
"isVerified",
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
for (const field of fieldsToUpdate) {
|
|
381
|
+
if (validatedData[field as keyof typeof validatedData] !== undefined) {
|
|
382
|
+
updatePayload[field] =
|
|
383
|
+
validatedData[field as keyof typeof validatedData];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Handle location update with geohash
|
|
388
|
+
if (validatedData.location) {
|
|
389
|
+
const loc = validatedData.location;
|
|
186
390
|
updatePayload.location = {
|
|
187
391
|
...loc,
|
|
188
392
|
geohash: geohashForLocation([loc.latitude, loc.longitude]),
|
|
189
393
|
};
|
|
190
394
|
}
|
|
191
395
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
...currentClinic,
|
|
195
|
-
...updatePayload, // Apply safe updates
|
|
196
|
-
// Explicitly keep arrays managed by other services from current state
|
|
197
|
-
doctorsInfo: currentClinic.doctorsInfo,
|
|
198
|
-
proceduresInfo: currentClinic.proceduresInfo,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// Ensure required fields for validation are present
|
|
202
|
-
clinicSchema.parse({
|
|
203
|
-
...finalStateForValidation,
|
|
204
|
-
updatedAt: Timestamp.now(), // Use current time for validation
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Prepare final update data for Firestore, including timestamp
|
|
208
|
-
const updateDataForFirestore = {
|
|
209
|
-
...updatePayload,
|
|
210
|
-
updatedAt: serverTimestamp(),
|
|
211
|
-
};
|
|
396
|
+
// Add timestamp
|
|
397
|
+
updatePayload.updatedAt = serverTimestamp();
|
|
212
398
|
|
|
213
|
-
|
|
399
|
+
// Update the clinic
|
|
400
|
+
await updateDoc(clinicRef, updatePayload);
|
|
401
|
+
console.log(`[ClinicService] Clinic ${clinicId} updated successfully`);
|
|
214
402
|
|
|
403
|
+
// Return the updated clinic
|
|
215
404
|
const updatedClinic = await this.getClinic(clinicId);
|
|
216
405
|
if (!updatedClinic) throw new Error("Failed to retrieve updated clinic");
|
|
217
406
|
return updatedClinic;
|
|
218
407
|
} catch (error) {
|
|
219
408
|
if (error instanceof z.ZodError) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
"Invalid clinic update data: " +
|
|
222
|
-
error.errors
|
|
223
|
-
.map((e) => `${e.path.join(".")} - ${e.message}`)
|
|
224
|
-
.join(", ")
|
|
225
|
-
);
|
|
409
|
+
throw new Error("Invalid clinic update data: " + error.message);
|
|
226
410
|
}
|
|
227
411
|
console.error(`Error updating clinic ${clinicId}:`, error);
|
|
228
412
|
throw error;
|
|
@@ -334,6 +518,10 @@ export class ClinicService extends BaseService {
|
|
|
334
518
|
);
|
|
335
519
|
}
|
|
336
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Creates a clinic branch from setup data.
|
|
523
|
+
* Handles both URL strings and File uploads for media fields.
|
|
524
|
+
*/
|
|
337
525
|
async createClinicBranch(
|
|
338
526
|
clinicGroupId: string,
|
|
339
527
|
setupData: ClinicBranchSetupData,
|
|
@@ -344,7 +532,7 @@ export class ClinicService extends BaseService {
|
|
|
344
532
|
adminId,
|
|
345
533
|
});
|
|
346
534
|
|
|
347
|
-
//
|
|
535
|
+
// Verify clinic group exists
|
|
348
536
|
const clinicGroup = await this.clinicGroupService.getClinicGroup(
|
|
349
537
|
clinicGroupId
|
|
350
538
|
);
|
|
@@ -356,47 +544,42 @@ export class ClinicService extends BaseService {
|
|
|
356
544
|
}
|
|
357
545
|
console.log("[CLINIC_SERVICE] Clinic group verified");
|
|
358
546
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
// Use path 'clinics/{clinicId}/{photoType}/{filename}' for storing the files
|
|
362
|
-
// Photo types: logo, coverPhoto, featuredPhotos, photosWithTags
|
|
363
|
-
// Storage can be accessed by using the storage service like app.getStorage()
|
|
547
|
+
// Validate branch setup data first
|
|
548
|
+
const validatedSetupData = clinicBranchSetupSchema.parse(setupData);
|
|
364
549
|
|
|
365
|
-
//
|
|
550
|
+
// Convert validated setup data to CreateClinicData
|
|
366
551
|
const createClinicData: CreateClinicData = {
|
|
367
552
|
clinicGroupId,
|
|
368
|
-
name:
|
|
369
|
-
description:
|
|
370
|
-
location:
|
|
371
|
-
contactInfo:
|
|
372
|
-
workingHours:
|
|
373
|
-
tags:
|
|
374
|
-
|
|
375
|
-
|
|
553
|
+
name: validatedSetupData.name,
|
|
554
|
+
description: validatedSetupData.description,
|
|
555
|
+
location: validatedSetupData.location,
|
|
556
|
+
contactInfo: validatedSetupData.contactInfo,
|
|
557
|
+
workingHours: validatedSetupData.workingHours,
|
|
558
|
+
tags: validatedSetupData.tags || [],
|
|
559
|
+
// Pass the media fields which can be string URLs or File objects
|
|
560
|
+
logo: validatedSetupData.logo,
|
|
561
|
+
coverPhoto: validatedSetupData.coverPhoto,
|
|
562
|
+
featuredPhotos: validatedSetupData.featuredPhotos,
|
|
563
|
+
photosWithTags: validatedSetupData.photosWithTags,
|
|
376
564
|
doctors: [],
|
|
377
565
|
procedures: [],
|
|
378
566
|
admins: [adminId],
|
|
379
567
|
isActive: true,
|
|
380
568
|
isVerified: false,
|
|
381
|
-
logo: setupData.logo,
|
|
382
|
-
featuredPhotos: setupData.featuredPhotos || [],
|
|
383
569
|
};
|
|
384
570
|
|
|
385
|
-
console.log("[CLINIC_SERVICE] Creating clinic branch
|
|
571
|
+
console.log("[CLINIC_SERVICE] Creating clinic branch", {
|
|
386
572
|
name: createClinicData.name,
|
|
387
573
|
hasLogo: !!createClinicData.logo,
|
|
388
574
|
hasCoverPhoto: !!createClinicData.coverPhoto,
|
|
389
|
-
featuredPhotosCount: createClinicData.featuredPhotos?.length || 0,
|
|
390
|
-
photosWithTagsCount: createClinicData.photosWithTags?.length || 0,
|
|
391
575
|
});
|
|
392
576
|
|
|
393
|
-
//
|
|
577
|
+
// Use createClinic which now handles validation and media uploads
|
|
394
578
|
const clinic = await this.createClinic(createClinicData, adminId);
|
|
579
|
+
|
|
395
580
|
console.log("[CLINIC_SERVICE] Clinic branch created successfully", {
|
|
396
581
|
clinicId: clinic.id,
|
|
397
582
|
});
|
|
398
|
-
|
|
399
|
-
// Note: The createClinic method already adds the clinic to the admin's managed clinics
|
|
400
583
|
return clinic;
|
|
401
584
|
}
|
|
402
585
|
|