@blackcode_sa/metaestetics-api 1.10.0 → 1.11.0
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 +9 -0
- package/dist/admin/index.d.ts +9 -0
- package/dist/admin/index.js +98 -79
- package/dist/admin/index.mjs +98 -79
- package/dist/backoffice/index.d.mts +1 -0
- package/dist/backoffice/index.d.ts +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +91 -18
- package/dist/index.mjs +94 -19
- package/package.json +3 -1
- package/src/admin/booking/booking.admin.ts +2 -0
- package/src/admin/booking/booking.calculator.ts +121 -117
- package/src/admin/booking/booking.types.ts +3 -0
- package/src/services/clinic/clinic.service.ts +163 -82
- package/src/services/procedure/procedure.service.ts +3 -2
- package/src/types/appointment/index.ts +4 -0
- package/src/types/clinic/index.ts +2 -0
- package/src/validations/appointment.schema.ts +2 -0
- package/src/validations/clinic.schema.ts +2 -0
- package/src/validations/procedure.schema.ts +8 -10
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
writeBatch,
|
|
17
17
|
arrayUnion,
|
|
18
18
|
arrayRemove,
|
|
19
|
-
} from
|
|
20
|
-
import {
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
21
|
+
import { BaseService } from "../base.service";
|
|
21
22
|
import {
|
|
22
23
|
Clinic,
|
|
23
24
|
CreateClinicData,
|
|
@@ -29,36 +30,41 @@ import {
|
|
|
29
30
|
ClinicBranchSetupData,
|
|
30
31
|
CLINIC_ADMINS_COLLECTION,
|
|
31
32
|
DoctorInfo,
|
|
32
|
-
} from
|
|
33
|
+
} from "../../types/clinic";
|
|
33
34
|
// Correct imports
|
|
34
|
-
import { ProcedureSummaryInfo } from
|
|
35
|
-
import { ClinicInfo } from
|
|
36
|
-
import { ClinicGroupService } from
|
|
37
|
-
import { ClinicAdminService } from
|
|
38
|
-
import {
|
|
35
|
+
import { ProcedureSummaryInfo } from "../../types/procedure";
|
|
36
|
+
import { ClinicInfo } from "../../types/profile";
|
|
37
|
+
import { ClinicGroupService } from "./clinic-group.service";
|
|
38
|
+
import { ClinicAdminService } from "./clinic-admin.service";
|
|
39
|
+
import {
|
|
40
|
+
geohashForLocation,
|
|
41
|
+
geohashQueryBounds,
|
|
42
|
+
distanceBetween,
|
|
43
|
+
} from "geofire-common";
|
|
39
44
|
import {
|
|
40
45
|
clinicSchema,
|
|
41
46
|
createClinicSchema,
|
|
42
47
|
updateClinicSchema,
|
|
43
48
|
clinicBranchSetupSchema,
|
|
44
|
-
} from
|
|
45
|
-
import { z } from
|
|
46
|
-
import { Auth } from
|
|
47
|
-
import { Firestore } from
|
|
48
|
-
import { FirebaseApp } from
|
|
49
|
-
import * as ClinicUtils from
|
|
50
|
-
import * as TagUtils from
|
|
51
|
-
import * as SearchUtils from
|
|
52
|
-
import * as AdminUtils from
|
|
53
|
-
import * as FilterUtils from
|
|
54
|
-
import { ClinicReviewInfo } from
|
|
55
|
-
import { PRACTITIONERS_COLLECTION } from
|
|
56
|
-
import { MediaService, MediaAccessLevel } from
|
|
49
|
+
} from "../../validations/clinic.schema";
|
|
50
|
+
import { z } from "zod";
|
|
51
|
+
import { Auth } from "firebase/auth";
|
|
52
|
+
import { Firestore } from "firebase/firestore";
|
|
53
|
+
import { FirebaseApp } from "firebase/app";
|
|
54
|
+
import * as ClinicUtils from "./utils/clinic.utils";
|
|
55
|
+
import * as TagUtils from "./utils/tag.utils";
|
|
56
|
+
import * as SearchUtils from "./utils/search.utils";
|
|
57
|
+
import * as AdminUtils from "./utils/admin.utils";
|
|
58
|
+
import * as FilterUtils from "./utils/filter.utils";
|
|
59
|
+
import { ClinicReviewInfo } from "../../types/reviews";
|
|
60
|
+
import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
|
|
61
|
+
import { MediaService, MediaAccessLevel } from "../media/media.service";
|
|
57
62
|
|
|
58
63
|
export class ClinicService extends BaseService {
|
|
59
64
|
private clinicGroupService: ClinicGroupService;
|
|
60
65
|
private clinicAdminService: ClinicAdminService;
|
|
61
66
|
private mediaService: MediaService;
|
|
67
|
+
private functions: any;
|
|
62
68
|
|
|
63
69
|
constructor(
|
|
64
70
|
db: Firestore,
|
|
@@ -66,12 +72,34 @@ export class ClinicService extends BaseService {
|
|
|
66
72
|
app: FirebaseApp,
|
|
67
73
|
clinicGroupService: ClinicGroupService,
|
|
68
74
|
clinicAdminService: ClinicAdminService,
|
|
69
|
-
mediaService: MediaService
|
|
75
|
+
mediaService: MediaService
|
|
70
76
|
) {
|
|
71
77
|
super(db, auth, app);
|
|
72
78
|
this.clinicAdminService = clinicAdminService;
|
|
73
79
|
this.clinicGroupService = clinicGroupService;
|
|
74
80
|
this.mediaService = mediaService;
|
|
81
|
+
this.functions = getFunctions(app);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get timezone from coordinates using the callable function
|
|
86
|
+
* @param lat Latitude
|
|
87
|
+
* @param lng Longitude
|
|
88
|
+
* @returns IANA timezone string
|
|
89
|
+
*/
|
|
90
|
+
private async getTimezone(lat: number, lng: number): Promise<string | null> {
|
|
91
|
+
try {
|
|
92
|
+
const getTimezoneFromCoordinates = httpsCallable(
|
|
93
|
+
this.functions,
|
|
94
|
+
"getTimezoneFromCoordinates"
|
|
95
|
+
);
|
|
96
|
+
const result = await getTimezoneFromCoordinates({ lat, lng });
|
|
97
|
+
const data = result.data as { timezone: string };
|
|
98
|
+
return data.timezone;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error("Error getting timezone:", error);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
/**
|
|
@@ -84,23 +112,25 @@ export class ClinicService extends BaseService {
|
|
|
84
112
|
private async processMedia(
|
|
85
113
|
media: string | File | Blob | null | undefined,
|
|
86
114
|
ownerId: string,
|
|
87
|
-
collectionName: string
|
|
115
|
+
collectionName: string
|
|
88
116
|
): Promise<string | null> {
|
|
89
117
|
if (!media) return null;
|
|
90
118
|
|
|
91
119
|
// If already a string URL, return it directly
|
|
92
|
-
if (typeof media ===
|
|
120
|
+
if (typeof media === "string") {
|
|
93
121
|
return media;
|
|
94
122
|
}
|
|
95
123
|
|
|
96
124
|
// If it's a File, upload it using MediaService
|
|
97
125
|
if (media instanceof File || media instanceof Blob) {
|
|
98
|
-
console.log(
|
|
126
|
+
console.log(
|
|
127
|
+
`[ClinicService] Uploading ${collectionName} media for ${ownerId}`
|
|
128
|
+
);
|
|
99
129
|
const metadata = await this.mediaService.uploadMedia(
|
|
100
130
|
media,
|
|
101
131
|
ownerId,
|
|
102
132
|
MediaAccessLevel.PUBLIC,
|
|
103
|
-
collectionName
|
|
133
|
+
collectionName
|
|
104
134
|
);
|
|
105
135
|
return metadata.url;
|
|
106
136
|
}
|
|
@@ -118,14 +148,18 @@ export class ClinicService extends BaseService {
|
|
|
118
148
|
private async processMediaArray(
|
|
119
149
|
mediaArray: (string | File | Blob)[] | undefined,
|
|
120
150
|
ownerId: string,
|
|
121
|
-
collectionName: string
|
|
151
|
+
collectionName: string
|
|
122
152
|
): Promise<string[]> {
|
|
123
153
|
if (!mediaArray || mediaArray.length === 0) return [];
|
|
124
154
|
|
|
125
155
|
const result: string[] = [];
|
|
126
156
|
|
|
127
157
|
for (const media of mediaArray) {
|
|
128
|
-
const processedUrl = await this.processMedia(
|
|
158
|
+
const processedUrl = await this.processMedia(
|
|
159
|
+
media,
|
|
160
|
+
ownerId,
|
|
161
|
+
collectionName
|
|
162
|
+
);
|
|
129
163
|
if (processedUrl) {
|
|
130
164
|
result.push(processedUrl);
|
|
131
165
|
}
|
|
@@ -144,14 +178,18 @@ export class ClinicService extends BaseService {
|
|
|
144
178
|
private async processPhotosWithTags(
|
|
145
179
|
photosWithTags: { url: string | File | Blob; tag: string }[] | undefined,
|
|
146
180
|
ownerId: string,
|
|
147
|
-
collectionName: string
|
|
181
|
+
collectionName: string
|
|
148
182
|
): Promise<{ url: string; tag: string }[]> {
|
|
149
183
|
if (!photosWithTags || photosWithTags.length === 0) return [];
|
|
150
184
|
|
|
151
185
|
const result: { url: string; tag: string }[] = [];
|
|
152
186
|
|
|
153
187
|
for (const item of photosWithTags) {
|
|
154
|
-
const processedUrl = await this.processMedia(
|
|
188
|
+
const processedUrl = await this.processMedia(
|
|
189
|
+
item.url,
|
|
190
|
+
ownerId,
|
|
191
|
+
collectionName
|
|
192
|
+
);
|
|
155
193
|
if (processedUrl) {
|
|
156
194
|
result.push({
|
|
157
195
|
url: processedUrl,
|
|
@@ -167,7 +205,10 @@ export class ClinicService extends BaseService {
|
|
|
167
205
|
* Creates a new clinic.
|
|
168
206
|
* Handles both URL strings and File uploads for media fields.
|
|
169
207
|
*/
|
|
170
|
-
async createClinic(
|
|
208
|
+
async createClinic(
|
|
209
|
+
data: CreateClinicData,
|
|
210
|
+
creatorAdminId: string
|
|
211
|
+
): Promise<Clinic> {
|
|
171
212
|
try {
|
|
172
213
|
// Generate ID first so we can use it for media uploads
|
|
173
214
|
const clinicId = this.generateId();
|
|
@@ -175,31 +216,40 @@ export class ClinicService extends BaseService {
|
|
|
175
216
|
// Validate data - this now works because mediaResourceSchema has been updated to support Files/Blobs
|
|
176
217
|
const validatedData = createClinicSchema.parse(data);
|
|
177
218
|
|
|
178
|
-
const group = await this.clinicGroupService.getClinicGroup(
|
|
219
|
+
const group = await this.clinicGroupService.getClinicGroup(
|
|
220
|
+
validatedData.clinicGroupId
|
|
221
|
+
);
|
|
179
222
|
if (!group) {
|
|
180
|
-
throw new Error(
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Clinic group ${validatedData.clinicGroupId} not found`
|
|
225
|
+
);
|
|
181
226
|
}
|
|
182
227
|
|
|
183
228
|
// Process media fields - convert File/Blob objects to URLs
|
|
184
|
-
const logoUrl = await this.processMedia(
|
|
229
|
+
const logoUrl = await this.processMedia(
|
|
230
|
+
validatedData.logo,
|
|
231
|
+
clinicId,
|
|
232
|
+
"clinic-logos"
|
|
233
|
+
);
|
|
185
234
|
const coverPhotoUrl = await this.processMedia(
|
|
186
235
|
validatedData.coverPhoto,
|
|
187
236
|
clinicId,
|
|
188
|
-
|
|
237
|
+
"clinic-cover-photos"
|
|
189
238
|
);
|
|
190
239
|
const featuredPhotos = await this.processMediaArray(
|
|
191
240
|
validatedData.featuredPhotos,
|
|
192
241
|
clinicId,
|
|
193
|
-
|
|
242
|
+
"clinic-featured-photos"
|
|
194
243
|
);
|
|
195
244
|
const photosWithTags = await this.processPhotosWithTags(
|
|
196
245
|
validatedData.photosWithTags,
|
|
197
246
|
clinicId,
|
|
198
|
-
|
|
247
|
+
"clinic-gallery"
|
|
199
248
|
);
|
|
200
249
|
|
|
201
250
|
const location = validatedData.location;
|
|
202
251
|
const hash = geohashForLocation([location.latitude, location.longitude]);
|
|
252
|
+
const tz = await this.getTimezone(location.latitude, location.longitude);
|
|
203
253
|
|
|
204
254
|
const defaultReviewInfo: ClinicReviewInfo = {
|
|
205
255
|
totalReviews: 0,
|
|
@@ -212,7 +262,7 @@ export class ClinicService extends BaseService {
|
|
|
212
262
|
recommendationPercentage: 0,
|
|
213
263
|
};
|
|
214
264
|
|
|
215
|
-
const clinicData: Omit<Clinic,
|
|
265
|
+
const clinicData: Omit<Clinic, "createdAt" | "updatedAt"> & {
|
|
216
266
|
createdAt: FieldValue;
|
|
217
267
|
updatedAt: FieldValue;
|
|
218
268
|
} = {
|
|
@@ -221,7 +271,7 @@ export class ClinicService extends BaseService {
|
|
|
221
271
|
name: validatedData.name,
|
|
222
272
|
nameLower: validatedData.name.toLowerCase(), // Add this line
|
|
223
273
|
description: validatedData.description,
|
|
224
|
-
location: { ...location, geohash: hash },
|
|
274
|
+
location: { ...location, geohash: hash, tz },
|
|
225
275
|
contactInfo: validatedData.contactInfo,
|
|
226
276
|
workingHours: validatedData.workingHours,
|
|
227
277
|
tags: validatedData.tags,
|
|
@@ -234,8 +284,12 @@ export class ClinicService extends BaseService {
|
|
|
234
284
|
proceduresInfo: validatedData.proceduresInfo || [],
|
|
235
285
|
reviewInfo: defaultReviewInfo,
|
|
236
286
|
admins: [creatorAdminId],
|
|
237
|
-
isActive:
|
|
238
|
-
|
|
287
|
+
isActive:
|
|
288
|
+
validatedData.isActive !== undefined ? validatedData.isActive : true,
|
|
289
|
+
isVerified:
|
|
290
|
+
validatedData.isVerified !== undefined
|
|
291
|
+
? validatedData.isVerified
|
|
292
|
+
: false,
|
|
239
293
|
logo: logoUrl,
|
|
240
294
|
createdAt: serverTimestamp(),
|
|
241
295
|
updatedAt: serverTimestamp(),
|
|
@@ -266,13 +320,13 @@ export class ClinicService extends BaseService {
|
|
|
266
320
|
console.log(`[ClinicService] Clinic created successfully: ${clinicId}`);
|
|
267
321
|
|
|
268
322
|
const savedClinic = await this.getClinic(clinicId);
|
|
269
|
-
if (!savedClinic) throw new Error(
|
|
323
|
+
if (!savedClinic) throw new Error("Failed to retrieve created clinic");
|
|
270
324
|
return savedClinic;
|
|
271
325
|
} catch (error) {
|
|
272
326
|
if (error instanceof z.ZodError) {
|
|
273
|
-
throw new Error(
|
|
327
|
+
throw new Error("Invalid clinic data: " + error.message);
|
|
274
328
|
}
|
|
275
|
-
console.error(
|
|
329
|
+
console.error("Error creating clinic:", error);
|
|
276
330
|
throw error;
|
|
277
331
|
}
|
|
278
332
|
}
|
|
@@ -284,7 +338,7 @@ export class ClinicService extends BaseService {
|
|
|
284
338
|
async updateClinic(
|
|
285
339
|
clinicId: string,
|
|
286
340
|
data: Partial<CreateClinicData>,
|
|
287
|
-
adminId: string
|
|
341
|
+
adminId: string
|
|
288
342
|
): Promise<Clinic> {
|
|
289
343
|
try {
|
|
290
344
|
// First check if clinic exists
|
|
@@ -304,14 +358,18 @@ export class ClinicService extends BaseService {
|
|
|
304
358
|
|
|
305
359
|
// Process media fields if provided
|
|
306
360
|
if (validatedData.logo !== undefined) {
|
|
307
|
-
updatePayload.logo = await this.processMedia(
|
|
361
|
+
updatePayload.logo = await this.processMedia(
|
|
362
|
+
validatedData.logo,
|
|
363
|
+
clinicId,
|
|
364
|
+
"clinic-logos"
|
|
365
|
+
);
|
|
308
366
|
}
|
|
309
367
|
|
|
310
368
|
if (validatedData.coverPhoto !== undefined) {
|
|
311
369
|
updatePayload.coverPhoto = await this.processMedia(
|
|
312
370
|
validatedData.coverPhoto,
|
|
313
371
|
clinicId,
|
|
314
|
-
|
|
372
|
+
"clinic-cover-photos"
|
|
315
373
|
);
|
|
316
374
|
}
|
|
317
375
|
|
|
@@ -319,7 +377,7 @@ export class ClinicService extends BaseService {
|
|
|
319
377
|
updatePayload.featuredPhotos = await this.processMediaArray(
|
|
320
378
|
validatedData.featuredPhotos,
|
|
321
379
|
clinicId,
|
|
322
|
-
|
|
380
|
+
"clinic-featured-photos"
|
|
323
381
|
);
|
|
324
382
|
}
|
|
325
383
|
|
|
@@ -327,27 +385,28 @@ export class ClinicService extends BaseService {
|
|
|
327
385
|
updatePayload.photosWithTags = await this.processPhotosWithTags(
|
|
328
386
|
validatedData.photosWithTags,
|
|
329
387
|
clinicId,
|
|
330
|
-
|
|
388
|
+
"clinic-gallery"
|
|
331
389
|
);
|
|
332
390
|
}
|
|
333
391
|
|
|
334
392
|
// Process non-media fields
|
|
335
393
|
const fieldsToUpdate = [
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
394
|
+
"name",
|
|
395
|
+
"description",
|
|
396
|
+
"contactInfo",
|
|
397
|
+
"workingHours",
|
|
398
|
+
"tags",
|
|
399
|
+
"doctors",
|
|
400
|
+
"procedures",
|
|
401
|
+
"proceduresInfo",
|
|
402
|
+
"isActive",
|
|
403
|
+
"isVerified",
|
|
346
404
|
];
|
|
347
405
|
|
|
348
406
|
for (const field of fieldsToUpdate) {
|
|
349
407
|
if (validatedData[field as keyof typeof validatedData] !== undefined) {
|
|
350
|
-
updatePayload[field] =
|
|
408
|
+
updatePayload[field] =
|
|
409
|
+
validatedData[field as keyof typeof validatedData];
|
|
351
410
|
}
|
|
352
411
|
}
|
|
353
412
|
|
|
@@ -359,9 +418,11 @@ export class ClinicService extends BaseService {
|
|
|
359
418
|
// Handle location update with geohash
|
|
360
419
|
if (validatedData.location) {
|
|
361
420
|
const loc = validatedData.location;
|
|
421
|
+
const tz = await this.getTimezone(loc.latitude, loc.longitude);
|
|
362
422
|
updatePayload.location = {
|
|
363
423
|
...loc,
|
|
364
424
|
geohash: geohashForLocation([loc.latitude, loc.longitude]),
|
|
425
|
+
tz,
|
|
365
426
|
};
|
|
366
427
|
}
|
|
367
428
|
|
|
@@ -374,11 +435,11 @@ export class ClinicService extends BaseService {
|
|
|
374
435
|
|
|
375
436
|
// Return the updated clinic
|
|
376
437
|
const updatedClinic = await this.getClinic(clinicId);
|
|
377
|
-
if (!updatedClinic) throw new Error(
|
|
438
|
+
if (!updatedClinic) throw new Error("Failed to retrieve updated clinic");
|
|
378
439
|
return updatedClinic;
|
|
379
440
|
} catch (error) {
|
|
380
441
|
if (error instanceof z.ZodError) {
|
|
381
|
-
throw new Error(
|
|
442
|
+
throw new Error("Invalid clinic update data: " + error.message);
|
|
382
443
|
}
|
|
383
444
|
console.error(`Error updating clinic ${clinicId}:`, error);
|
|
384
445
|
throw error;
|
|
@@ -433,9 +494,14 @@ export class ClinicService extends BaseService {
|
|
|
433
494
|
procedures?: string[];
|
|
434
495
|
tags?: ClinicTag[];
|
|
435
496
|
// Add other relevant filters based on Clinic/ProcedureSummaryInfo fields
|
|
436
|
-
}
|
|
497
|
+
}
|
|
437
498
|
): Promise<Clinic[]> {
|
|
438
|
-
return SearchUtils.findClinicsInRadius(
|
|
499
|
+
return SearchUtils.findClinicsInRadius(
|
|
500
|
+
this.db,
|
|
501
|
+
center,
|
|
502
|
+
radiusInKm,
|
|
503
|
+
filters
|
|
504
|
+
);
|
|
439
505
|
}
|
|
440
506
|
|
|
441
507
|
async addTags(
|
|
@@ -443,9 +509,16 @@ export class ClinicService extends BaseService {
|
|
|
443
509
|
adminId: string,
|
|
444
510
|
newTags: {
|
|
445
511
|
tags?: ClinicTag[];
|
|
446
|
-
}
|
|
512
|
+
}
|
|
447
513
|
): Promise<Clinic> {
|
|
448
|
-
return TagUtils.addTags(
|
|
514
|
+
return TagUtils.addTags(
|
|
515
|
+
this.db,
|
|
516
|
+
clinicId,
|
|
517
|
+
adminId,
|
|
518
|
+
newTags,
|
|
519
|
+
this.clinicAdminService,
|
|
520
|
+
this.app
|
|
521
|
+
);
|
|
449
522
|
}
|
|
450
523
|
|
|
451
524
|
async removeTags(
|
|
@@ -453,7 +526,7 @@ export class ClinicService extends BaseService {
|
|
|
453
526
|
adminId: string,
|
|
454
527
|
tagsToRemove: {
|
|
455
528
|
tags?: ClinicTag[];
|
|
456
|
-
}
|
|
529
|
+
}
|
|
457
530
|
): Promise<Clinic> {
|
|
458
531
|
return TagUtils.removeTags(
|
|
459
532
|
this.db,
|
|
@@ -461,7 +534,7 @@ export class ClinicService extends BaseService {
|
|
|
461
534
|
adminId,
|
|
462
535
|
tagsToRemove,
|
|
463
536
|
this.clinicAdminService,
|
|
464
|
-
this.app
|
|
537
|
+
this.app
|
|
465
538
|
);
|
|
466
539
|
}
|
|
467
540
|
|
|
@@ -470,14 +543,14 @@ export class ClinicService extends BaseService {
|
|
|
470
543
|
options?: {
|
|
471
544
|
isActive?: boolean;
|
|
472
545
|
includeGroupClinics?: boolean; // ako je true i admin je owner, uključuje sve klinike grupe
|
|
473
|
-
}
|
|
546
|
+
}
|
|
474
547
|
): Promise<Clinic[]> {
|
|
475
548
|
return ClinicUtils.getClinicsByAdmin(
|
|
476
549
|
this.db,
|
|
477
550
|
adminId,
|
|
478
551
|
options,
|
|
479
552
|
this.clinicAdminService,
|
|
480
|
-
this.clinicGroupService
|
|
553
|
+
this.clinicGroupService
|
|
481
554
|
);
|
|
482
555
|
}
|
|
483
556
|
|
|
@@ -486,7 +559,7 @@ export class ClinicService extends BaseService {
|
|
|
486
559
|
this.db,
|
|
487
560
|
adminId,
|
|
488
561
|
this.clinicAdminService,
|
|
489
|
-
this.clinicGroupService
|
|
562
|
+
this.clinicGroupService
|
|
490
563
|
);
|
|
491
564
|
}
|
|
492
565
|
|
|
@@ -497,22 +570,24 @@ export class ClinicService extends BaseService {
|
|
|
497
570
|
async createClinicBranch(
|
|
498
571
|
clinicGroupId: string,
|
|
499
572
|
setupData: ClinicBranchSetupData,
|
|
500
|
-
adminId: string
|
|
573
|
+
adminId: string
|
|
501
574
|
): Promise<Clinic> {
|
|
502
|
-
console.log(
|
|
575
|
+
console.log("[CLINIC_SERVICE] Starting clinic branch creation", {
|
|
503
576
|
clinicGroupId,
|
|
504
577
|
adminId,
|
|
505
578
|
});
|
|
506
579
|
|
|
507
580
|
// Verify clinic group exists
|
|
508
|
-
const clinicGroup = await this.clinicGroupService.getClinicGroup(
|
|
581
|
+
const clinicGroup = await this.clinicGroupService.getClinicGroup(
|
|
582
|
+
clinicGroupId
|
|
583
|
+
);
|
|
509
584
|
if (!clinicGroup) {
|
|
510
|
-
console.error(
|
|
585
|
+
console.error("[CLINIC_SERVICE] Clinic group not found", {
|
|
511
586
|
clinicGroupId,
|
|
512
587
|
});
|
|
513
588
|
throw new Error(`Clinic group with ID ${clinicGroupId} not found`);
|
|
514
589
|
}
|
|
515
|
-
console.log(
|
|
590
|
+
console.log("[CLINIC_SERVICE] Clinic group verified");
|
|
516
591
|
|
|
517
592
|
// Validate branch setup data first
|
|
518
593
|
const validatedSetupData = clinicBranchSetupSchema.parse(setupData);
|
|
@@ -538,7 +613,7 @@ export class ClinicService extends BaseService {
|
|
|
538
613
|
isVerified: false,
|
|
539
614
|
};
|
|
540
615
|
|
|
541
|
-
console.log(
|
|
616
|
+
console.log("[CLINIC_SERVICE] Creating clinic branch", {
|
|
542
617
|
name: createClinicData.name,
|
|
543
618
|
hasLogo: !!createClinicData.logo,
|
|
544
619
|
hasCoverPhoto: !!createClinicData.coverPhoto,
|
|
@@ -547,7 +622,7 @@ export class ClinicService extends BaseService {
|
|
|
547
622
|
// Use createClinic which now handles validation and media uploads
|
|
548
623
|
const clinic = await this.createClinic(createClinicData, adminId);
|
|
549
624
|
|
|
550
|
-
console.log(
|
|
625
|
+
console.log("[CLINIC_SERVICE] Clinic branch created successfully", {
|
|
551
626
|
clinicId: clinic.id,
|
|
552
627
|
});
|
|
553
628
|
return clinic;
|
|
@@ -559,7 +634,7 @@ export class ClinicService extends BaseService {
|
|
|
559
634
|
|
|
560
635
|
async getAllClinics(
|
|
561
636
|
pagination?: number,
|
|
562
|
-
lastDoc?: any
|
|
637
|
+
lastDoc?: any
|
|
563
638
|
): Promise<{ clinics: Clinic[]; lastDoc: any }> {
|
|
564
639
|
return ClinicUtils.getAllClinics(this.db, pagination, lastDoc);
|
|
565
640
|
}
|
|
@@ -568,9 +643,15 @@ export class ClinicService extends BaseService {
|
|
|
568
643
|
center: { latitude: number; longitude: number },
|
|
569
644
|
rangeInKm: number,
|
|
570
645
|
pagination?: number,
|
|
571
|
-
lastDoc?: any
|
|
646
|
+
lastDoc?: any
|
|
572
647
|
): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
|
|
573
|
-
return ClinicUtils.getAllClinicsInRange(
|
|
648
|
+
return ClinicUtils.getAllClinicsInRange(
|
|
649
|
+
this.db,
|
|
650
|
+
center,
|
|
651
|
+
rangeInKm,
|
|
652
|
+
pagination,
|
|
653
|
+
lastDoc
|
|
654
|
+
);
|
|
574
655
|
}
|
|
575
656
|
|
|
576
657
|
/**
|
|
@@ -621,7 +702,7 @@ export class ClinicService extends BaseService {
|
|
|
621
702
|
return {
|
|
622
703
|
id: doc.id,
|
|
623
704
|
name: data.name,
|
|
624
|
-
address: data.location?.address ||
|
|
705
|
+
address: data.location?.address || "",
|
|
625
706
|
latitude: data.location?.latitude,
|
|
626
707
|
longitude: data.location?.longitude,
|
|
627
708
|
};
|
|
@@ -221,7 +221,8 @@ export class ProcedureService extends BaseService {
|
|
|
221
221
|
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
222
222
|
id: procedureId,
|
|
223
223
|
...validatedData,
|
|
224
|
-
|
|
224
|
+
// Ensure nameLower is always set even if omitted by client
|
|
225
|
+
nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
225
226
|
photos: processedPhotos,
|
|
226
227
|
category, // Embed full objects
|
|
227
228
|
subcategory,
|
|
@@ -376,7 +377,7 @@ export class ProcedureService extends BaseService {
|
|
|
376
377
|
const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
|
|
377
378
|
id: procedureId,
|
|
378
379
|
...validatedData,
|
|
379
|
-
nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
|
|
380
|
+
nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
|
|
380
381
|
practitionerId: practitionerId, // Override practitionerId with the correct one
|
|
381
382
|
photos: processedPhotos,
|
|
382
383
|
category,
|
|
@@ -196,6 +196,8 @@ export interface Appointment {
|
|
|
196
196
|
clinicBranchId: string;
|
|
197
197
|
/** Aggregated clinic information (snapshot) */
|
|
198
198
|
clinicInfo: ClinicInfo;
|
|
199
|
+
/** IANA timezone of the clinic */
|
|
200
|
+
clinic_tz: string;
|
|
199
201
|
|
|
200
202
|
/** ID of the practitioner */
|
|
201
203
|
practitionerId: string;
|
|
@@ -299,6 +301,7 @@ export interface CreateAppointmentData {
|
|
|
299
301
|
patientNotes?: string | null;
|
|
300
302
|
initialStatus: AppointmentStatus;
|
|
301
303
|
initialPaymentStatus?: PaymentStatus; // Defaults to UNPAID if not provided
|
|
304
|
+
clinic_tz: string;
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
/**
|
|
@@ -336,6 +339,7 @@ export interface UpdateAppointmentData {
|
|
|
336
339
|
cost?: number; // If cost is adjusted
|
|
337
340
|
clinicBranchId?: string; // If appointment is moved to another branch (complex scenario)
|
|
338
341
|
practitionerId?: string; // If practitioner is changed
|
|
342
|
+
clinic_tz?: string;
|
|
339
343
|
|
|
340
344
|
/** NEW: For updating linked forms - typically managed by dedicated methods */
|
|
341
345
|
linkedFormIds?: string[] | FieldValue;
|
|
@@ -44,6 +44,7 @@ export interface ClinicLocation {
|
|
|
44
44
|
latitude: number;
|
|
45
45
|
longitude: number;
|
|
46
46
|
geohash?: string | null;
|
|
47
|
+
tz?: string | null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -228,6 +229,7 @@ export interface CreateClinicGroupData {
|
|
|
228
229
|
calendarSyncEnabled?: boolean;
|
|
229
230
|
autoConfirmAppointments?: boolean;
|
|
230
231
|
businessIdentificationNumber?: string | null;
|
|
232
|
+
tz?: string | null;
|
|
231
233
|
onboarding?: {
|
|
232
234
|
completed?: boolean;
|
|
233
235
|
step?: number;
|
|
@@ -248,6 +248,7 @@ export const createAppointmentSchema = z
|
|
|
248
248
|
initialPaymentStatus: paymentStatusSchema
|
|
249
249
|
.optional()
|
|
250
250
|
.default(PaymentStatus.UNPAID),
|
|
251
|
+
clinic_tz: z.string().min(1, "Timezone is required"),
|
|
251
252
|
})
|
|
252
253
|
.refine((data) => data.appointmentEndTime > data.appointmentStartTime, {
|
|
253
254
|
message: "Appointment end time must be after start time",
|
|
@@ -325,6 +326,7 @@ export const updateAppointmentSchema = z
|
|
|
325
326
|
cost: z.number().min(0).optional(),
|
|
326
327
|
clinicBranchId: z.string().min(MIN_STRING_LENGTH).optional(),
|
|
327
328
|
practitionerId: z.string().min(MIN_STRING_LENGTH).optional(),
|
|
329
|
+
clinic_tz: z.string().min(MIN_STRING_LENGTH).optional(),
|
|
328
330
|
linkedForms: z
|
|
329
331
|
.union([z.array(linkedFormInfoSchema).max(MAX_ARRAY_LENGTH), z.any()])
|
|
330
332
|
.optional(),
|
|
@@ -42,6 +42,7 @@ export const clinicLocationSchema = z.object({
|
|
|
42
42
|
latitude: z.number().min(-90).max(90),
|
|
43
43
|
longitude: z.number().min(-180).max(180),
|
|
44
44
|
geohash: z.string().nullable().optional(),
|
|
45
|
+
tz: z.string().nullable().optional(),
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -261,6 +262,7 @@ export const createClinicGroupSchema = z.object({
|
|
|
261
262
|
calendarSyncEnabled: z.boolean().optional(),
|
|
262
263
|
autoConfirmAppointments: z.boolean().optional(),
|
|
263
264
|
businessIdentificationNumber: z.string().optional().nullable(),
|
|
265
|
+
tz: z.string().nullable().optional(),
|
|
264
266
|
onboarding: z
|
|
265
267
|
.object({
|
|
266
268
|
completed: z.boolean().optional().default(false),
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { ProcedureFamily } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from
|
|
7
|
-
import { clinicInfoSchema, doctorInfoSchema } from "./shared.schema";
|
|
8
|
-
import { procedureReviewInfoSchema } from "./reviews.schema";
|
|
9
|
-
import { mediaResourceSchema } from "./media.schema";
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ProcedureFamily } from '../backoffice/types/static/procedure-family.types';
|
|
3
|
+
import { Currency, PricingMeasure } from '../backoffice/types/static/pricing.types';
|
|
4
|
+
import { clinicInfoSchema, doctorInfoSchema } from './shared.schema';
|
|
5
|
+
import { procedureReviewInfoSchema } from './reviews.schema';
|
|
6
|
+
import { mediaResourceSchema } from './media.schema';
|
|
10
7
|
/**
|
|
11
8
|
* Schema for creating a new procedure
|
|
12
9
|
*/
|
|
13
10
|
export const createProcedureSchema = z.object({
|
|
14
11
|
name: z.string().min(1).max(200),
|
|
15
|
-
|
|
12
|
+
// Optional: service will derive from name if not provided by client
|
|
13
|
+
nameLower: z.string().min(1).max(200).optional(),
|
|
16
14
|
description: z.string().min(1).max(2000),
|
|
17
15
|
family: z.nativeEnum(ProcedureFamily),
|
|
18
16
|
categoryId: z.string().min(1),
|