@blackcode_sa/metaestetics-api 1.5.15 → 1.5.17
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/backoffice/index.d.mts +2 -2
- package/dist/backoffice/index.d.ts +2 -2
- package/dist/index.d.mts +232 -38
- package/dist/index.d.ts +232 -38
- package/dist/index.js +698 -342
- package/dist/index.mjs +942 -570
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/services/calendar/calendar-refactored.service.ts +147 -0
- package/src/services/calendar/utils/calendar-event.utils.ts +136 -0
- package/src/services/calendar/utils/index.ts +5 -5
- package/src/services/clinic/utils/clinic-group.utils.ts +3 -0
- package/src/services/patient/patient.service.ts +29 -0
- package/src/services/patient/utils/medical-stuff.utils.ts +69 -18
- package/src/services/patient/utils/profile.utils.ts +116 -0
- package/src/types/calendar/index.ts +41 -0
- package/src/types/clinic/index.ts +2 -0
- package/src/types/patient/index.ts +31 -1
- package/src/types/practitioner/index.ts +13 -14
- package/src/validations/clinic.schema.ts +2 -0
- package/src/validations/patient.schema.ts +43 -0
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
increment,
|
|
9
9
|
Firestore,
|
|
10
10
|
Timestamp,
|
|
11
|
+
collection,
|
|
12
|
+
query,
|
|
13
|
+
where,
|
|
14
|
+
getDocs,
|
|
15
|
+
QueryConstraint,
|
|
11
16
|
} from "firebase/firestore";
|
|
12
17
|
import {
|
|
13
18
|
ref,
|
|
@@ -21,10 +26,15 @@ import {
|
|
|
21
26
|
PatientProfile,
|
|
22
27
|
CreatePatientProfileData,
|
|
23
28
|
Gender,
|
|
29
|
+
SearchPatientsParams,
|
|
30
|
+
RequesterInfo,
|
|
31
|
+
PATIENTS_COLLECTION,
|
|
24
32
|
} from "../../../types/patient";
|
|
25
33
|
import {
|
|
26
34
|
patientProfileSchema,
|
|
27
35
|
createPatientProfileSchema,
|
|
36
|
+
searchPatientsSchema,
|
|
37
|
+
requesterInfoSchema,
|
|
28
38
|
} from "../../../validations/patient.schema";
|
|
29
39
|
import {
|
|
30
40
|
getPatientDocRef,
|
|
@@ -65,6 +75,8 @@ export const createPatientProfileUtil = async (
|
|
|
65
75
|
isVerified: validatedData.isVerified,
|
|
66
76
|
doctors: validatedData.doctors || [],
|
|
67
77
|
clinics: validatedData.clinics || [],
|
|
78
|
+
doctorIds: validatedData.doctors?.map((d) => d.userRef) || [],
|
|
79
|
+
clinicIds: validatedData.clinics?.map((c) => c.clinicId) || [],
|
|
68
80
|
createdAt: serverTimestamp(),
|
|
69
81
|
updatedAt: serverTimestamp(),
|
|
70
82
|
};
|
|
@@ -398,3 +410,107 @@ export const testCreateSubDocuments = async (
|
|
|
398
410
|
throw error;
|
|
399
411
|
}
|
|
400
412
|
};
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Searches for patient profiles based on clinic and/or practitioner association.
|
|
416
|
+
* Applies security checks based on the requester's role and associations.
|
|
417
|
+
*
|
|
418
|
+
* @param {Firestore} db - Firestore instance.
|
|
419
|
+
* @param {SearchPatientsParams} params - Search criteria (clinicId, practitionerId).
|
|
420
|
+
* @param {RequesterInfo} requester - Information about the user performing the search.
|
|
421
|
+
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of matching patient profiles.
|
|
422
|
+
*/
|
|
423
|
+
export const searchPatientsUtil = async (
|
|
424
|
+
db: Firestore,
|
|
425
|
+
params: SearchPatientsParams,
|
|
426
|
+
requester: RequesterInfo
|
|
427
|
+
): Promise<PatientProfile[]> => {
|
|
428
|
+
// Validate input
|
|
429
|
+
searchPatientsSchema.parse(params);
|
|
430
|
+
requesterInfoSchema.parse(requester);
|
|
431
|
+
|
|
432
|
+
const constraints: QueryConstraint[] = [];
|
|
433
|
+
const patientsCollectionRef = collection(db, PATIENTS_COLLECTION);
|
|
434
|
+
|
|
435
|
+
// --- Security Checks & Initial Filtering ---
|
|
436
|
+
|
|
437
|
+
if (requester.role === "clinic_admin") {
|
|
438
|
+
// Clinic admin can only search within their own clinic
|
|
439
|
+
if (!requester.associatedClinicId) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
"Associated clinic ID is required for clinic admin search."
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
// If the search params specify a different clinic, it's an invalid request for this admin.
|
|
445
|
+
if (params.clinicId && params.clinicId !== requester.associatedClinicId) {
|
|
446
|
+
console.warn(
|
|
447
|
+
`Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`
|
|
448
|
+
);
|
|
449
|
+
return []; // Or throw an error
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// **Mandatory filter**: Ensure patients belong to the admin's clinic.
|
|
453
|
+
constraints.push(
|
|
454
|
+
where("clinicIds", "array-contains", requester.associatedClinicId)
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Optional filter: If practitionerId is also provided, filter by that practitioner *within the admin's clinic*.
|
|
458
|
+
if (params.practitionerId) {
|
|
459
|
+
constraints.push(
|
|
460
|
+
where("doctorIds", "array-contains", params.practitionerId)
|
|
461
|
+
);
|
|
462
|
+
// We might need an additional check here if the practitioner MUST work at the admin's clinic.
|
|
463
|
+
// This would require fetching practitioner data or having denormalized clinic IDs on the practitioner.
|
|
464
|
+
}
|
|
465
|
+
} else if (requester.role === "practitioner") {
|
|
466
|
+
// Practitioner can only search for their own patients.
|
|
467
|
+
if (!requester.associatedPractitionerId) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
"Associated practitioner ID is required for practitioner search."
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
// If the search params specify a different practitioner, it's invalid.
|
|
473
|
+
if (
|
|
474
|
+
params.practitionerId &&
|
|
475
|
+
params.practitionerId !== requester.associatedPractitionerId
|
|
476
|
+
) {
|
|
477
|
+
console.warn(
|
|
478
|
+
`Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`
|
|
479
|
+
);
|
|
480
|
+
return []; // Or throw an error
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// **Mandatory filter**: Ensure patients are associated with this practitioner.
|
|
484
|
+
constraints.push(
|
|
485
|
+
where("doctorIds", "array-contains", requester.associatedPractitionerId)
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Optional filter: If clinicId is provided, filter by patients of this practitioner *at that specific clinic*.
|
|
489
|
+
if (params.clinicId) {
|
|
490
|
+
constraints.push(where("clinicIds", "array-contains", params.clinicId));
|
|
491
|
+
// Similar to above, we might need to check if the practitioner actually works at this clinic.
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
// Should not happen due to validation, but good practice to handle.
|
|
495
|
+
throw new Error("Invalid requester role.");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// --- Execute Query ---
|
|
499
|
+
try {
|
|
500
|
+
const finalQuery = query(patientsCollectionRef, ...constraints);
|
|
501
|
+
const querySnapshot = await getDocs(finalQuery);
|
|
502
|
+
|
|
503
|
+
const patients = querySnapshot.docs.map(
|
|
504
|
+
(doc) => doc.data() as PatientProfile
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
console.log(
|
|
508
|
+
`[searchPatientsUtil] Found ${patients.length} patients matching criteria.`
|
|
509
|
+
);
|
|
510
|
+
return patients;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error("[searchPatientsUtil] Error searching patients:", error);
|
|
513
|
+
// Consider logging more details or re-throwing a specific error type
|
|
514
|
+
return []; // Return empty array on error
|
|
515
|
+
}
|
|
516
|
+
};
|
|
@@ -185,3 +185,44 @@ export interface UpdateAppointmentParams {
|
|
|
185
185
|
* Collection names for calendar
|
|
186
186
|
*/
|
|
187
187
|
export const CALENDAR_COLLECTION = "calendar";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Enum for specifying the primary search location for calendar events
|
|
191
|
+
*/
|
|
192
|
+
export enum SearchLocationEnum {
|
|
193
|
+
PRACTITIONER = "practitioner",
|
|
194
|
+
PATIENT = "patient",
|
|
195
|
+
CLINIC = "clinic", // Represents searching events associated with a clinic
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Interface for defining a date range
|
|
200
|
+
*/
|
|
201
|
+
export interface DateRange {
|
|
202
|
+
start: Timestamp;
|
|
203
|
+
end: Timestamp;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Interface for general calendar event search parameters
|
|
208
|
+
*/
|
|
209
|
+
export interface SearchCalendarEventsParams {
|
|
210
|
+
/** The primary location to search within (practitioner, patient, or clinic calendar). */
|
|
211
|
+
searchLocation: SearchLocationEnum;
|
|
212
|
+
/** The ID of the entity (practitioner, patient, or clinic) whose calendar/events are being searched. */
|
|
213
|
+
entityId: string;
|
|
214
|
+
/** Optional filter for clinic ID. If searchLocation is CLINIC, this is implicitly applied using entityId. */
|
|
215
|
+
clinicId?: string;
|
|
216
|
+
/** Optional filter for practitioner ID. */
|
|
217
|
+
practitionerId?: string;
|
|
218
|
+
/** Optional filter for patient ID. */
|
|
219
|
+
patientId?: string;
|
|
220
|
+
/** Optional filter for procedure ID. */
|
|
221
|
+
procedureId?: string;
|
|
222
|
+
/** Optional filter for a specific date range (based on event start time). */
|
|
223
|
+
dateRange?: DateRange;
|
|
224
|
+
/** Optional filter for event status. */
|
|
225
|
+
eventStatus?: CalendarEventStatus;
|
|
226
|
+
/** Optional filter for event type. */
|
|
227
|
+
eventType?: CalendarEventType;
|
|
228
|
+
}
|
|
@@ -149,6 +149,7 @@ export enum AdminTokenStatus {
|
|
|
149
149
|
export interface AdminToken {
|
|
150
150
|
id: string;
|
|
151
151
|
token: string;
|
|
152
|
+
email?: string | null;
|
|
152
153
|
status: AdminTokenStatus;
|
|
153
154
|
usedByUserRef?: string;
|
|
154
155
|
createdAt: Timestamp;
|
|
@@ -234,6 +235,7 @@ export interface UpdateClinicGroupData extends Partial<CreateClinicGroupData> {
|
|
|
234
235
|
*/
|
|
235
236
|
export interface CreateAdminTokenData {
|
|
236
237
|
expiresInDays?: number; // How many days the token is valid, default 7
|
|
238
|
+
email?: string | null;
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
/**
|
|
@@ -172,6 +172,8 @@ export interface PatientProfile {
|
|
|
172
172
|
dateOfBirth?: Timestamp | null;
|
|
173
173
|
doctors: PatientDoctor[]; // Lista doktora pacijenta
|
|
174
174
|
clinics: PatientClinic[]; // Lista klinika pacijenta
|
|
175
|
+
doctorIds: string[]; // Denormalized array for querying
|
|
176
|
+
clinicIds: string[]; // Denormalized array for querying
|
|
175
177
|
createdAt: Timestamp;
|
|
176
178
|
updatedAt: Timestamp;
|
|
177
179
|
}
|
|
@@ -188,14 +190,42 @@ export interface CreatePatientProfileData {
|
|
|
188
190
|
isVerified: boolean;
|
|
189
191
|
doctors?: PatientDoctor[];
|
|
190
192
|
clinics?: PatientClinic[];
|
|
193
|
+
doctorIds?: string[]; // Initialize as empty or with initial doctors
|
|
194
|
+
clinicIds?: string[]; // Initialize as empty or with initial clinics
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
/**
|
|
194
198
|
* Tip za ažuriranje Patient profila
|
|
195
199
|
*/
|
|
196
200
|
export interface UpdatePatientProfileData
|
|
197
|
-
extends Partial<
|
|
201
|
+
extends Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">> {
|
|
202
|
+
// Use Omit to exclude base fields
|
|
198
203
|
updatedAt?: FieldValue;
|
|
204
|
+
// Note: doctors, clinics, doctorIds, clinicIds should ideally be updated via specific methods (add/removeDoctor/Clinic)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parameters for searching patient profiles.
|
|
209
|
+
*/
|
|
210
|
+
export interface SearchPatientsParams {
|
|
211
|
+
/** Optional: Filter patients associated with this clinic ID. */
|
|
212
|
+
clinicId?: string;
|
|
213
|
+
/** Optional: Filter patients associated with this practitioner ID. */
|
|
214
|
+
practitionerId?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Information about the entity requesting the patient search.
|
|
219
|
+
*/
|
|
220
|
+
export interface RequesterInfo {
|
|
221
|
+
/** ID of the clinic admin user or practitioner user making the request. */
|
|
222
|
+
id: string;
|
|
223
|
+
/** Role of the requester, determining the search context. */
|
|
224
|
+
role: "clinic_admin" | "practitioner";
|
|
225
|
+
/** If role is 'clinic_admin', this is the associated clinic ID. */
|
|
226
|
+
associatedClinicId?: string;
|
|
227
|
+
/** If role is 'practitioner', this is the associated practitioner profile ID. */
|
|
228
|
+
associatedPractitionerId?: string;
|
|
199
229
|
}
|
|
200
230
|
|
|
201
231
|
export * from "./medical-info.types";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Timestamp, FieldValue } from
|
|
1
|
+
import { Timestamp, FieldValue } from 'firebase/firestore';
|
|
2
2
|
import {
|
|
3
3
|
CertificationLevel,
|
|
4
4
|
CertificationSpecialty,
|
|
5
|
-
} from
|
|
5
|
+
} from '../../backoffice/types/static/certification.types';
|
|
6
6
|
|
|
7
|
-
export const PRACTITIONERS_COLLECTION =
|
|
8
|
-
export const REGISTER_TOKENS_COLLECTION =
|
|
7
|
+
export const PRACTITIONERS_COLLECTION = 'practitioners';
|
|
8
|
+
export const REGISTER_TOKENS_COLLECTION = 'register_tokens';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Osnovne informacije o zdravstvenom radniku
|
|
@@ -17,7 +17,7 @@ export interface PractitionerBasicInfo {
|
|
|
17
17
|
email: string;
|
|
18
18
|
phoneNumber: string;
|
|
19
19
|
dateOfBirth: Timestamp;
|
|
20
|
-
gender:
|
|
20
|
+
gender: 'male' | 'female' | 'other';
|
|
21
21
|
profileImageUrl?: string;
|
|
22
22
|
bio?: string;
|
|
23
23
|
languages: string[];
|
|
@@ -33,7 +33,7 @@ export interface PractitionerCertification {
|
|
|
33
33
|
issuingAuthority: string;
|
|
34
34
|
issueDate: Timestamp;
|
|
35
35
|
expiryDate?: Timestamp;
|
|
36
|
-
verificationStatus:
|
|
36
|
+
verificationStatus: 'pending' | 'verified' | 'rejected';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -59,18 +59,18 @@ export interface PractitionerClinicWorkingHours {
|
|
|
59
59
|
* Status of practitioner profile
|
|
60
60
|
*/
|
|
61
61
|
export enum PractitionerStatus {
|
|
62
|
-
DRAFT =
|
|
63
|
-
ACTIVE =
|
|
62
|
+
DRAFT = 'draft',
|
|
63
|
+
ACTIVE = 'active',
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Token status for practitioner invitations
|
|
68
68
|
*/
|
|
69
69
|
export enum PractitionerTokenStatus {
|
|
70
|
-
ACTIVE =
|
|
71
|
-
USED =
|
|
72
|
-
EXPIRED =
|
|
73
|
-
REVOKED =
|
|
70
|
+
ACTIVE = 'active',
|
|
71
|
+
USED = 'used',
|
|
72
|
+
EXPIRED = 'expired',
|
|
73
|
+
REVOKED = 'revoked',
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -119,8 +119,7 @@ export interface CreateDraftPractitionerData {
|
|
|
119
119
|
/**
|
|
120
120
|
* Tip za ažuriranje zdravstvenog radnika
|
|
121
121
|
*/
|
|
122
|
-
export interface UpdatePractitionerData
|
|
123
|
-
extends Partial<CreatePractitionerData> {
|
|
122
|
+
export interface UpdatePractitionerData extends Partial<CreatePractitionerData> {
|
|
124
123
|
updatedAt?: FieldValue;
|
|
125
124
|
}
|
|
126
125
|
|
|
@@ -172,6 +172,7 @@ export const clinicAdminSchema = z.object({
|
|
|
172
172
|
export const adminTokenSchema = z.object({
|
|
173
173
|
id: z.string(),
|
|
174
174
|
token: z.string(),
|
|
175
|
+
email: z.string().email().optional().nullable(),
|
|
175
176
|
status: z.nativeEnum(AdminTokenStatus),
|
|
176
177
|
usedByUserRef: z.string().optional(),
|
|
177
178
|
createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
|
|
@@ -183,6 +184,7 @@ export const adminTokenSchema = z.object({
|
|
|
183
184
|
*/
|
|
184
185
|
export const createAdminTokenSchema = z.object({
|
|
185
186
|
expiresInDays: z.number().min(1).max(30).optional(),
|
|
187
|
+
email: z.string().email().optional().nullable(),
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
/**
|
|
@@ -115,6 +115,8 @@ export const patientProfileSchema = z.object({
|
|
|
115
115
|
isVerified: z.boolean(),
|
|
116
116
|
doctors: z.array(patientDoctorSchema),
|
|
117
117
|
clinics: z.array(patientClinicSchema),
|
|
118
|
+
doctorIds: z.array(z.string()),
|
|
119
|
+
clinicIds: z.array(z.string()),
|
|
118
120
|
createdAt: z.instanceof(Timestamp),
|
|
119
121
|
updatedAt: z.instanceof(Timestamp),
|
|
120
122
|
});
|
|
@@ -132,6 +134,8 @@ export const createPatientProfileSchema = z.object({
|
|
|
132
134
|
isVerified: z.boolean(),
|
|
133
135
|
doctors: z.array(patientDoctorSchema).optional(),
|
|
134
136
|
clinics: z.array(patientClinicSchema).optional(),
|
|
137
|
+
doctorIds: z.array(z.string()).optional(),
|
|
138
|
+
clinicIds: z.array(z.string()).optional(),
|
|
135
139
|
});
|
|
136
140
|
|
|
137
141
|
/**
|
|
@@ -152,4 +156,43 @@ export const createPatientSensitiveInfoSchema = z.object({
|
|
|
152
156
|
emergencyContacts: z.array(emergencyContactSchema).optional(),
|
|
153
157
|
});
|
|
154
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Schema for validating patient search parameters.
|
|
161
|
+
*/
|
|
162
|
+
export const searchPatientsSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
clinicId: z.string().optional(),
|
|
165
|
+
practitionerId: z.string().optional(),
|
|
166
|
+
})
|
|
167
|
+
.refine((data) => data.clinicId || data.practitionerId, {
|
|
168
|
+
message: "At least one of clinicId or practitionerId must be provided",
|
|
169
|
+
path: [], // Optional: specify a path like ['clinicId'] or ['practitionerId']
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Schema for validating requester information during patient search.
|
|
174
|
+
*/
|
|
175
|
+
export const requesterInfoSchema = z
|
|
176
|
+
.object({
|
|
177
|
+
id: z.string(),
|
|
178
|
+
role: z.enum(["clinic_admin", "practitioner"]),
|
|
179
|
+
associatedClinicId: z.string().optional(),
|
|
180
|
+
associatedPractitionerId: z.string().optional(),
|
|
181
|
+
})
|
|
182
|
+
.refine(
|
|
183
|
+
(data) => {
|
|
184
|
+
if (data.role === "clinic_admin") {
|
|
185
|
+
return !!data.associatedClinicId;
|
|
186
|
+
} else if (data.role === "practitioner") {
|
|
187
|
+
return !!data.associatedPractitionerId;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
message:
|
|
193
|
+
"Associated ID (clinic or practitioner) is required based on role",
|
|
194
|
+
path: ["associatedClinicId", "associatedPractitionerId"],
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
155
198
|
export * from "./patient/medical-info.schema";
|