@blackcode_sa/metaestetics-api 1.12.61 → 1.12.63
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 +4 -0
- package/dist/admin/index.d.ts +4 -0
- package/dist/backoffice/index.d.mts +9 -0
- package/dist/backoffice/index.d.ts +9 -0
- package/dist/backoffice/index.js +11 -0
- package/dist/backoffice/index.mjs +11 -0
- package/dist/index.d.mts +99 -1
- package/dist/index.d.ts +99 -1
- package/dist/index.js +534 -228
- package/dist/index.mjs +856 -550
- package/package.json +1 -1
- package/src/backoffice/services/technology.service.ts +13 -0
- package/src/backoffice/types/technology.types.ts +2 -0
- package/src/backoffice/validations/schemas.ts +1 -0
- package/src/services/appointment/appointment.service.ts +423 -0
- package/src/types/appointment/index.ts +28 -0
- package/src/types/patient/index.ts +2 -0
- package/src/validations/patient.schema.ts +1 -0
package/package.json
CHANGED
|
@@ -82,6 +82,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
82
82
|
if (technology.technicalDetails) {
|
|
83
83
|
newTechnology.technicalDetails = technology.technicalDetails;
|
|
84
84
|
}
|
|
85
|
+
if (technology.photoTemplate) {
|
|
86
|
+
newTechnology.photoTemplate = technology.photoTemplate;
|
|
87
|
+
}
|
|
85
88
|
|
|
86
89
|
const docRef = await addDoc(this.technologiesRef, newTechnology as any);
|
|
87
90
|
return { id: docRef.id, ...newTechnology };
|
|
@@ -240,6 +243,16 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
240
243
|
}
|
|
241
244
|
});
|
|
242
245
|
|
|
246
|
+
// Handle photoTemplate: if explicitly set to null or empty string, allow it to be cleared
|
|
247
|
+
// If undefined, don't include it in the update (field won't change)
|
|
248
|
+
if ('photoTemplate' in technology) {
|
|
249
|
+
if (technology.photoTemplate === null || technology.photoTemplate === '') {
|
|
250
|
+
updateData.photoTemplate = null;
|
|
251
|
+
} else if (technology.photoTemplate !== undefined) {
|
|
252
|
+
updateData.photoTemplate = technology.photoTemplate;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
243
256
|
updateData.updatedAt = new Date();
|
|
244
257
|
|
|
245
258
|
const docRef = doc(this.technologiesRef, id);
|
|
@@ -69,6 +69,8 @@ export interface Technology {
|
|
|
69
69
|
benefits: TreatmentBenefitDynamic[];
|
|
70
70
|
certificationRequirement: CertificationRequirement;
|
|
71
71
|
documentationTemplates?: TechnologyDocumentationTemplate[];
|
|
72
|
+
/** Media ID of the default photo template image for this technology */
|
|
73
|
+
photoTemplate?: string;
|
|
72
74
|
isActive: boolean;
|
|
73
75
|
createdAt: Date;
|
|
74
76
|
updatedAt: Date;
|
|
@@ -120,6 +120,7 @@ export const technologySchema = z.object({
|
|
|
120
120
|
documentationTemplates: z.array(documentTemplateSchema),
|
|
121
121
|
benefits: z.array(treatmentBenefitSchemaBackoffice),
|
|
122
122
|
certificationRequirement: certificationRequirementSchema,
|
|
123
|
+
photoTemplate: z.string().url("Photo template must be a valid URL").optional(),
|
|
123
124
|
isActive: z.boolean().default(true),
|
|
124
125
|
});
|
|
125
126
|
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
startAfter,
|
|
15
15
|
getDocs,
|
|
16
16
|
getCountFromServer,
|
|
17
|
+
doc,
|
|
18
|
+
getDoc,
|
|
17
19
|
} from 'firebase/firestore';
|
|
18
20
|
import { Auth } from 'firebase/auth';
|
|
19
21
|
import { FirebaseApp } from 'firebase/app';
|
|
@@ -34,8 +36,10 @@ import {
|
|
|
34
36
|
ExtendedProcedureInfo,
|
|
35
37
|
AppointmentProductMetadata,
|
|
36
38
|
RecommendedProcedure,
|
|
39
|
+
NextStepsRecommendation,
|
|
37
40
|
APPOINTMENTS_COLLECTION,
|
|
38
41
|
} from '../../types/appointment';
|
|
42
|
+
import { PROCEDURES_COLLECTION } from '../../types/procedure';
|
|
39
43
|
import {
|
|
40
44
|
updateAppointmentSchema,
|
|
41
45
|
searchAppointmentsSchema,
|
|
@@ -2079,4 +2083,423 @@ export class AppointmentService extends BaseService {
|
|
|
2079
2083
|
throw error;
|
|
2080
2084
|
}
|
|
2081
2085
|
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Gets all next steps recommendations for a patient from their past appointments.
|
|
2089
|
+
* Returns recommendations with context about which appointment, practitioner, and clinic suggested them.
|
|
2090
|
+
*
|
|
2091
|
+
* @param patientId ID of the patient
|
|
2092
|
+
* @param options Optional parameters for filtering
|
|
2093
|
+
* @returns Array of next steps recommendations with context
|
|
2094
|
+
*/
|
|
2095
|
+
async getPatientNextStepsRecommendations(
|
|
2096
|
+
patientId: string,
|
|
2097
|
+
options?: {
|
|
2098
|
+
/** Include dismissed recommendations (default: false) */
|
|
2099
|
+
includeDismissed?: boolean;
|
|
2100
|
+
/** Filter by clinic branch ID */
|
|
2101
|
+
clinicBranchId?: string;
|
|
2102
|
+
/** Filter by practitioner ID */
|
|
2103
|
+
practitionerId?: string;
|
|
2104
|
+
/** Limit the number of results */
|
|
2105
|
+
limit?: number;
|
|
2106
|
+
},
|
|
2107
|
+
): Promise<NextStepsRecommendation[]> {
|
|
2108
|
+
try {
|
|
2109
|
+
console.log(
|
|
2110
|
+
`[APPOINTMENT_SERVICE] Getting next steps recommendations for patient: ${patientId}`,
|
|
2111
|
+
options,
|
|
2112
|
+
);
|
|
2113
|
+
|
|
2114
|
+
// Get patient profile to check dismissed recommendations
|
|
2115
|
+
const patientProfile = await this.patientService.getPatientProfile(patientId);
|
|
2116
|
+
const dismissedIds = new Set(
|
|
2117
|
+
patientProfile?.dismissedNextStepsRecommendations || [],
|
|
2118
|
+
);
|
|
2119
|
+
|
|
2120
|
+
// Get past appointments (completed appointments)
|
|
2121
|
+
const pastAppointments = await this.getPastPatientAppointments(patientId, {
|
|
2122
|
+
showCanceled: false,
|
|
2123
|
+
showNoShow: false,
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
const recommendations: NextStepsRecommendation[] = [];
|
|
2127
|
+
|
|
2128
|
+
// Iterate through past appointments and extract recommendations
|
|
2129
|
+
for (const appointment of pastAppointments.appointments) {
|
|
2130
|
+
// Filter by clinic if specified
|
|
2131
|
+
if (options?.clinicBranchId && appointment.clinicBranchId !== options.clinicBranchId) {
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// Filter by practitioner if specified
|
|
2136
|
+
if (options?.practitionerId && appointment.practitionerId !== options.practitionerId) {
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// Get recommended procedures from appointment metadata
|
|
2141
|
+
const recommendedProcedures =
|
|
2142
|
+
appointment.metadata?.recommendedProcedures || [];
|
|
2143
|
+
|
|
2144
|
+
// Create NextStepsRecommendation for each recommended procedure
|
|
2145
|
+
for (let index = 0; index < recommendedProcedures.length; index++) {
|
|
2146
|
+
const recommendedProcedure = recommendedProcedures[index];
|
|
2147
|
+
const recommendationId = `${appointment.id}:${index}`;
|
|
2148
|
+
|
|
2149
|
+
// Skip if dismissed and not including dismissed
|
|
2150
|
+
if (!options?.includeDismissed && dismissedIds.has(recommendationId)) {
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const nextStepsRecommendation: NextStepsRecommendation = {
|
|
2155
|
+
id: recommendationId,
|
|
2156
|
+
recommendedProcedure,
|
|
2157
|
+
appointmentId: appointment.id,
|
|
2158
|
+
appointmentDate: appointment.appointmentStartTime,
|
|
2159
|
+
practitionerId: appointment.practitionerId,
|
|
2160
|
+
practitionerName: appointment.practitionerInfo?.name || 'Unknown Practitioner',
|
|
2161
|
+
clinicBranchId: appointment.clinicBranchId,
|
|
2162
|
+
clinicName: appointment.clinicInfo?.name || 'Unknown Clinic',
|
|
2163
|
+
appointmentStatus: appointment.status,
|
|
2164
|
+
isDismissed: dismissedIds.has(recommendationId),
|
|
2165
|
+
dismissedAt: null, // We don't track when it was dismissed, just that it was
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
recommendations.push(nextStepsRecommendation);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// Sort by appointment date (most recent first)
|
|
2173
|
+
recommendations.sort((a, b) => {
|
|
2174
|
+
const dateA = a.appointmentDate.toMillis();
|
|
2175
|
+
const dateB = b.appointmentDate.toMillis();
|
|
2176
|
+
return dateB - dateA;
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
// Apply limit if specified
|
|
2180
|
+
const limitedRecommendations = options?.limit
|
|
2181
|
+
? recommendations.slice(0, options.limit)
|
|
2182
|
+
: recommendations;
|
|
2183
|
+
|
|
2184
|
+
console.log(
|
|
2185
|
+
`[APPOINTMENT_SERVICE] Found ${limitedRecommendations.length} next steps recommendations for patient ${patientId}`,
|
|
2186
|
+
);
|
|
2187
|
+
|
|
2188
|
+
return limitedRecommendations;
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
console.error(
|
|
2191
|
+
`[APPOINTMENT_SERVICE] Error getting next steps recommendations for patient ${patientId}:`,
|
|
2192
|
+
error,
|
|
2193
|
+
);
|
|
2194
|
+
throw error;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Dismisses a next steps recommendation for a patient.
|
|
2200
|
+
* This prevents the recommendation from showing up in the default view.
|
|
2201
|
+
*
|
|
2202
|
+
* @param patientId ID of the patient
|
|
2203
|
+
* @param recommendationId ID of the recommendation to dismiss (format: appointmentId:recommendationIndex)
|
|
2204
|
+
* @returns Updated patient profile
|
|
2205
|
+
*/
|
|
2206
|
+
async dismissNextStepsRecommendation(
|
|
2207
|
+
patientId: string,
|
|
2208
|
+
recommendationId: string,
|
|
2209
|
+
): Promise<void> {
|
|
2210
|
+
try {
|
|
2211
|
+
console.log(
|
|
2212
|
+
`[APPOINTMENT_SERVICE] Dismissing recommendation ${recommendationId} for patient ${patientId}`,
|
|
2213
|
+
);
|
|
2214
|
+
|
|
2215
|
+
// Get patient profile
|
|
2216
|
+
const patientProfile = await this.patientService.getPatientProfile(patientId);
|
|
2217
|
+
if (!patientProfile) {
|
|
2218
|
+
throw new Error(`Patient profile not found for patient ${patientId}`);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// Get current dismissed recommendations
|
|
2222
|
+
const dismissedRecommendations =
|
|
2223
|
+
patientProfile.dismissedNextStepsRecommendations || [];
|
|
2224
|
+
|
|
2225
|
+
// Check if already dismissed
|
|
2226
|
+
if (dismissedRecommendations.includes(recommendationId)) {
|
|
2227
|
+
console.log(
|
|
2228
|
+
`[APPOINTMENT_SERVICE] Recommendation ${recommendationId} already dismissed`,
|
|
2229
|
+
);
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Add to dismissed list
|
|
2234
|
+
const updatedDismissed = [...dismissedRecommendations, recommendationId];
|
|
2235
|
+
|
|
2236
|
+
// Update patient profile
|
|
2237
|
+
await this.patientService.updatePatientProfile(patientId, {
|
|
2238
|
+
dismissedNextStepsRecommendations: updatedDismissed,
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
console.log(
|
|
2242
|
+
`[APPOINTMENT_SERVICE] Successfully dismissed recommendation ${recommendationId} for patient ${patientId}`,
|
|
2243
|
+
);
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
console.error(
|
|
2246
|
+
`[APPOINTMENT_SERVICE] Error dismissing recommendation for patient ${patientId}:`,
|
|
2247
|
+
error,
|
|
2248
|
+
);
|
|
2249
|
+
throw error;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* Undismisses a next steps recommendation for a patient.
|
|
2255
|
+
* This makes the recommendation visible again in the default view.
|
|
2256
|
+
*
|
|
2257
|
+
* @param patientId ID of the patient
|
|
2258
|
+
* @param recommendationId ID of the recommendation to undismiss (format: appointmentId:recommendationIndex)
|
|
2259
|
+
* @returns Updated patient profile
|
|
2260
|
+
*/
|
|
2261
|
+
async undismissNextStepsRecommendation(
|
|
2262
|
+
patientId: string,
|
|
2263
|
+
recommendationId: string,
|
|
2264
|
+
): Promise<void> {
|
|
2265
|
+
try {
|
|
2266
|
+
console.log(
|
|
2267
|
+
`[APPOINTMENT_SERVICE] Undismissing recommendation ${recommendationId} for patient ${patientId}`,
|
|
2268
|
+
);
|
|
2269
|
+
|
|
2270
|
+
// Get patient profile
|
|
2271
|
+
const patientProfile = await this.patientService.getPatientProfile(patientId);
|
|
2272
|
+
if (!patientProfile) {
|
|
2273
|
+
throw new Error(`Patient profile not found for patient ${patientId}`);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// Get current dismissed recommendations
|
|
2277
|
+
const dismissedRecommendations =
|
|
2278
|
+
patientProfile.dismissedNextStepsRecommendations || [];
|
|
2279
|
+
|
|
2280
|
+
// Check if not dismissed
|
|
2281
|
+
if (!dismissedRecommendations.includes(recommendationId)) {
|
|
2282
|
+
console.log(
|
|
2283
|
+
`[APPOINTMENT_SERVICE] Recommendation ${recommendationId} is not dismissed`,
|
|
2284
|
+
);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// Remove from dismissed list
|
|
2289
|
+
const updatedDismissed = dismissedRecommendations.filter(
|
|
2290
|
+
id => id !== recommendationId,
|
|
2291
|
+
);
|
|
2292
|
+
|
|
2293
|
+
// Update patient profile
|
|
2294
|
+
await this.patientService.updatePatientProfile(patientId, {
|
|
2295
|
+
dismissedNextStepsRecommendations: updatedDismissed,
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
console.log(
|
|
2299
|
+
`[APPOINTMENT_SERVICE] Successfully undismissed recommendation ${recommendationId} for patient ${patientId}`,
|
|
2300
|
+
);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
console.error(
|
|
2303
|
+
`[APPOINTMENT_SERVICE] Error undismissing recommendation for patient ${patientId}:`,
|
|
2304
|
+
error,
|
|
2305
|
+
);
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/**
|
|
2311
|
+
* Gets next steps recommendations for a clinic.
|
|
2312
|
+
* Returns all recommendations from appointments at the specified clinic.
|
|
2313
|
+
* This is useful for clinic admins to see what treatments have been recommended to their patients.
|
|
2314
|
+
*
|
|
2315
|
+
* @param clinicBranchId ID of the clinic branch
|
|
2316
|
+
* @param options Optional parameters for filtering
|
|
2317
|
+
* @returns Array of next steps recommendations with context
|
|
2318
|
+
*/
|
|
2319
|
+
async getClinicNextStepsRecommendations(
|
|
2320
|
+
clinicBranchId: string,
|
|
2321
|
+
options?: {
|
|
2322
|
+
/** Filter by patient ID */
|
|
2323
|
+
patientId?: string;
|
|
2324
|
+
/** Filter by practitioner ID */
|
|
2325
|
+
practitionerId?: string;
|
|
2326
|
+
/** Limit the number of results */
|
|
2327
|
+
limit?: number;
|
|
2328
|
+
},
|
|
2329
|
+
): Promise<NextStepsRecommendation[]> {
|
|
2330
|
+
try {
|
|
2331
|
+
console.log(
|
|
2332
|
+
`[APPOINTMENT_SERVICE] Getting next steps recommendations for clinic: ${clinicBranchId}`,
|
|
2333
|
+
options,
|
|
2334
|
+
);
|
|
2335
|
+
|
|
2336
|
+
// Get past appointments for the clinic
|
|
2337
|
+
const searchParams: SearchAppointmentsParams = {
|
|
2338
|
+
clinicBranchId,
|
|
2339
|
+
patientId: options?.patientId,
|
|
2340
|
+
practitionerId: options?.practitionerId,
|
|
2341
|
+
status: AppointmentStatus.COMPLETED,
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
const { appointments } = await this.searchAppointments(searchParams);
|
|
2345
|
+
|
|
2346
|
+
const recommendations: NextStepsRecommendation[] = [];
|
|
2347
|
+
|
|
2348
|
+
// Iterate through appointments and extract recommendations
|
|
2349
|
+
for (const appointment of appointments) {
|
|
2350
|
+
// Get recommended procedures from appointment metadata
|
|
2351
|
+
const recommendedProcedures =
|
|
2352
|
+
appointment.metadata?.recommendedProcedures || [];
|
|
2353
|
+
|
|
2354
|
+
// Create NextStepsRecommendation for each recommended procedure
|
|
2355
|
+
for (let index = 0; index < recommendedProcedures.length; index++) {
|
|
2356
|
+
const recommendedProcedure = recommendedProcedures[index];
|
|
2357
|
+
const recommendationId = `${appointment.id}:${index}`;
|
|
2358
|
+
|
|
2359
|
+
const nextStepsRecommendation: NextStepsRecommendation = {
|
|
2360
|
+
id: recommendationId,
|
|
2361
|
+
recommendedProcedure,
|
|
2362
|
+
appointmentId: appointment.id,
|
|
2363
|
+
appointmentDate: appointment.appointmentStartTime,
|
|
2364
|
+
practitionerId: appointment.practitionerId,
|
|
2365
|
+
practitionerName: appointment.practitionerInfo?.name || 'Unknown Practitioner',
|
|
2366
|
+
clinicBranchId: appointment.clinicBranchId,
|
|
2367
|
+
clinicName: appointment.clinicInfo?.name || 'Unknown Clinic',
|
|
2368
|
+
appointmentStatus: appointment.status,
|
|
2369
|
+
isDismissed: false, // Clinic view doesn't track dismissals
|
|
2370
|
+
dismissedAt: null,
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
recommendations.push(nextStepsRecommendation);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// Sort by appointment date (most recent first)
|
|
2378
|
+
recommendations.sort((a, b) => {
|
|
2379
|
+
const dateA = a.appointmentDate.toMillis();
|
|
2380
|
+
const dateB = b.appointmentDate.toMillis();
|
|
2381
|
+
return dateB - dateA;
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
// Apply limit if specified
|
|
2385
|
+
const limitedRecommendations = options?.limit
|
|
2386
|
+
? recommendations.slice(0, options.limit)
|
|
2387
|
+
: recommendations;
|
|
2388
|
+
|
|
2389
|
+
console.log(
|
|
2390
|
+
`[APPOINTMENT_SERVICE] Found ${limitedRecommendations.length} next steps recommendations for clinic ${clinicBranchId}`,
|
|
2391
|
+
);
|
|
2392
|
+
|
|
2393
|
+
return limitedRecommendations;
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
console.error(
|
|
2396
|
+
`[APPOINTMENT_SERVICE] Error getting next steps recommendations for clinic ${clinicBranchId}:`,
|
|
2397
|
+
error,
|
|
2398
|
+
);
|
|
2399
|
+
throw error;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* Gets next steps recommendations from a specific appointment.
|
|
2405
|
+
* This is useful when viewing an appointment detail page in the clinic app
|
|
2406
|
+
* to see what procedures were recommended during that appointment.
|
|
2407
|
+
*
|
|
2408
|
+
* @param appointmentId ID of the appointment
|
|
2409
|
+
* @param options Optional parameters for filtering
|
|
2410
|
+
* @returns Array of next steps recommendations from that appointment
|
|
2411
|
+
*/
|
|
2412
|
+
async getAppointmentNextStepsRecommendations(
|
|
2413
|
+
appointmentId: string,
|
|
2414
|
+
options?: {
|
|
2415
|
+
/** Filter by clinic branch ID - only show recommendations for procedures available at this clinic */
|
|
2416
|
+
clinicBranchId?: string;
|
|
2417
|
+
},
|
|
2418
|
+
): Promise<NextStepsRecommendation[]> {
|
|
2419
|
+
try {
|
|
2420
|
+
console.log(
|
|
2421
|
+
`[APPOINTMENT_SERVICE] Getting next steps recommendations for appointment: ${appointmentId}`,
|
|
2422
|
+
options,
|
|
2423
|
+
);
|
|
2424
|
+
|
|
2425
|
+
// Get the appointment
|
|
2426
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
2427
|
+
if (!appointment) {
|
|
2428
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// Get recommended procedures from appointment metadata
|
|
2432
|
+
const recommendedProcedures =
|
|
2433
|
+
appointment.metadata?.recommendedProcedures || [];
|
|
2434
|
+
|
|
2435
|
+
const recommendations: NextStepsRecommendation[] = [];
|
|
2436
|
+
|
|
2437
|
+
// If clinicBranchId is provided, we need to check which procedures are available at that clinic
|
|
2438
|
+
let availableProcedureIds: Set<string> | null = null;
|
|
2439
|
+
if (options?.clinicBranchId) {
|
|
2440
|
+
// Query procedures collection to get all procedure IDs available at this clinic
|
|
2441
|
+
const proceduresQuery = query(
|
|
2442
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
2443
|
+
where('clinicBranchId', '==', options.clinicBranchId),
|
|
2444
|
+
where('isActive', '==', true),
|
|
2445
|
+
);
|
|
2446
|
+
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
2447
|
+
availableProcedureIds = new Set(
|
|
2448
|
+
proceduresSnapshot.docs.map(doc => doc.id),
|
|
2449
|
+
);
|
|
2450
|
+
console.log(
|
|
2451
|
+
`[APPOINTMENT_SERVICE] Found ${availableProcedureIds.size} procedures available at clinic ${options.clinicBranchId}`,
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// Create NextStepsRecommendation for each recommended procedure
|
|
2456
|
+
for (let index = 0; index < recommendedProcedures.length; index++) {
|
|
2457
|
+
const recommendedProcedure = recommendedProcedures[index];
|
|
2458
|
+
const procedureId = recommendedProcedure.procedure.procedureId;
|
|
2459
|
+
|
|
2460
|
+
// If clinicBranchId is provided, filter to only include procedures available at that clinic
|
|
2461
|
+
if (options?.clinicBranchId && availableProcedureIds) {
|
|
2462
|
+
if (!availableProcedureIds.has(procedureId)) {
|
|
2463
|
+
console.log(
|
|
2464
|
+
`[APPOINTMENT_SERVICE] Skipping recommendation for procedure ${procedureId} - not available at clinic ${options.clinicBranchId}`,
|
|
2465
|
+
);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
const recommendationId = `${appointment.id}:${index}`;
|
|
2471
|
+
|
|
2472
|
+
const nextStepsRecommendation: NextStepsRecommendation = {
|
|
2473
|
+
id: recommendationId,
|
|
2474
|
+
recommendedProcedure,
|
|
2475
|
+
appointmentId: appointment.id,
|
|
2476
|
+
appointmentDate: appointment.appointmentStartTime,
|
|
2477
|
+
practitionerId: appointment.practitionerId,
|
|
2478
|
+
practitionerName: appointment.practitionerInfo?.name || 'Unknown Practitioner',
|
|
2479
|
+
clinicBranchId: appointment.clinicBranchId,
|
|
2480
|
+
clinicName: appointment.clinicInfo?.name || 'Unknown Clinic',
|
|
2481
|
+
appointmentStatus: appointment.status,
|
|
2482
|
+
isDismissed: false, // Clinic view doesn't track dismissals
|
|
2483
|
+
dismissedAt: null,
|
|
2484
|
+
};
|
|
2485
|
+
|
|
2486
|
+
recommendations.push(nextStepsRecommendation);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
console.log(
|
|
2490
|
+
`[APPOINTMENT_SERVICE] Found ${recommendations.length} next steps recommendations for appointment ${appointmentId}`,
|
|
2491
|
+
options?.clinicBranchId
|
|
2492
|
+
? `(filtered to procedures available at clinic ${options.clinicBranchId})`
|
|
2493
|
+
: '',
|
|
2494
|
+
);
|
|
2495
|
+
|
|
2496
|
+
return recommendations;
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
console.error(
|
|
2499
|
+
`[APPOINTMENT_SERVICE] Error getting next steps recommendations for appointment ${appointmentId}:`,
|
|
2500
|
+
error,
|
|
2501
|
+
);
|
|
2502
|
+
throw error;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2082
2505
|
}
|
|
@@ -451,3 +451,31 @@ export interface SearchAppointmentsParams {
|
|
|
451
451
|
|
|
452
452
|
/** Firestore collection name */
|
|
453
453
|
export const APPOINTMENTS_COLLECTION = 'appointments';
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Interface for next steps recommendation with context about when and who suggested it
|
|
457
|
+
*/
|
|
458
|
+
export interface NextStepsRecommendation {
|
|
459
|
+
/** Unique identifier for this recommendation (appointmentId + recommendationIndex) */
|
|
460
|
+
id: string;
|
|
461
|
+
/** The recommended procedure details */
|
|
462
|
+
recommendedProcedure: RecommendedProcedure;
|
|
463
|
+
/** ID of the appointment where this was recommended */
|
|
464
|
+
appointmentId: string;
|
|
465
|
+
/** Date of the appointment when this was recommended */
|
|
466
|
+
appointmentDate: Timestamp;
|
|
467
|
+
/** ID of the practitioner who made the recommendation */
|
|
468
|
+
practitionerId: string;
|
|
469
|
+
/** Name of the practitioner who made the recommendation */
|
|
470
|
+
practitionerName: string;
|
|
471
|
+
/** ID of the clinic where the appointment took place */
|
|
472
|
+
clinicBranchId: string;
|
|
473
|
+
/** Name of the clinic where the appointment took place */
|
|
474
|
+
clinicName: string;
|
|
475
|
+
/** Status of the appointment when recommendation was made */
|
|
476
|
+
appointmentStatus: AppointmentStatus;
|
|
477
|
+
/** Whether this recommendation has been dismissed by the patient */
|
|
478
|
+
isDismissed?: boolean;
|
|
479
|
+
/** When the recommendation was dismissed (if dismissed) */
|
|
480
|
+
dismissedAt?: Timestamp | null;
|
|
481
|
+
}
|
|
@@ -175,6 +175,8 @@ export interface PatientProfile {
|
|
|
175
175
|
clinics: PatientClinic[]; // Lista klinika pacijenta
|
|
176
176
|
doctorIds: string[]; // Denormalized array for querying
|
|
177
177
|
clinicIds: string[]; // Denormalized array for querying
|
|
178
|
+
/** IDs of dismissed next steps recommendations (format: appointmentId:recommendationIndex) */
|
|
179
|
+
dismissedNextStepsRecommendations?: string[];
|
|
178
180
|
createdAt: Timestamp;
|
|
179
181
|
updatedAt: Timestamp;
|
|
180
182
|
}
|
|
@@ -120,6 +120,7 @@ export const patientProfileSchema = z.object({
|
|
|
120
120
|
clinics: z.array(patientClinicSchema),
|
|
121
121
|
doctorIds: z.array(z.string()),
|
|
122
122
|
clinicIds: z.array(z.string()),
|
|
123
|
+
dismissedNextStepsRecommendations: z.array(z.string()).optional(),
|
|
123
124
|
createdAt: z.instanceof(Timestamp),
|
|
124
125
|
updatedAt: z.instanceof(Timestamp),
|
|
125
126
|
});
|