@blackcode_sa/metaestetics-api 1.14.56 → 1.14.57
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 -2
- package/dist/admin/index.d.ts +4 -2
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +101 -15
- package/dist/index.mjs +229 -141
- package/package.json +1 -1
- package/src/services/appointment/appointment.service.ts +41 -9
- package/src/services/appointment/utils/extended-procedure.utils.ts +1 -0
- package/src/services/appointment/utils/form-initialization.utils.ts +2 -0
- package/src/services/appointment/utils/zone-management.utils.ts +50 -3
- package/src/services/appointment/utils/zone-photo.utils.ts +39 -1
- package/src/types/appointment/index.ts +4 -2
- package/src/validations/appointment.schema.ts +2 -1
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getCountFromServer,
|
|
17
17
|
doc,
|
|
18
18
|
getDoc,
|
|
19
|
+
updateDoc,
|
|
19
20
|
} from 'firebase/firestore';
|
|
20
21
|
import { Auth } from 'firebase/auth';
|
|
21
22
|
import { FirebaseApp } from 'firebase/app';
|
|
@@ -2296,6 +2297,14 @@ export class AppointmentService extends BaseService {
|
|
|
2296
2297
|
finalizationNotes: currentMetadata.finalizationNotes,
|
|
2297
2298
|
});
|
|
2298
2299
|
|
|
2300
|
+
// Convert empty strings to null for proper Firestore storage
|
|
2301
|
+
const normalizedSharedNotes = sharedNotes !== undefined
|
|
2302
|
+
? (sharedNotes === '' || sharedNotes === null ? null : sharedNotes)
|
|
2303
|
+
: currentMetadata.finalizationNotesShared;
|
|
2304
|
+
const normalizedInternalNotes = internalNotes !== undefined
|
|
2305
|
+
? (internalNotes === '' || internalNotes === null ? null : internalNotes)
|
|
2306
|
+
: currentMetadata.finalizationNotesInternal;
|
|
2307
|
+
|
|
2299
2308
|
const metadataUpdate = {
|
|
2300
2309
|
selectedZones: currentMetadata.selectedZones,
|
|
2301
2310
|
zonePhotos: currentMetadata.zonePhotos,
|
|
@@ -2304,14 +2313,12 @@ export class AppointmentService extends BaseService {
|
|
|
2304
2313
|
extendedProcedures: currentMetadata.extendedProcedures || [],
|
|
2305
2314
|
recommendedProcedures: currentMetadata.recommendedProcedures || [],
|
|
2306
2315
|
finalbilling: currentMetadata.finalbilling,
|
|
2307
|
-
finalizationNotesShared:
|
|
2308
|
-
|
|
2309
|
-
finalizationNotesInternal:
|
|
2310
|
-
internalNotes !== undefined ? internalNotes : currentMetadata.finalizationNotesInternal,
|
|
2316
|
+
finalizationNotesShared: normalizedSharedNotes,
|
|
2317
|
+
finalizationNotesInternal: normalizedInternalNotes,
|
|
2311
2318
|
// Keep deprecated field for backward compatibility during migration
|
|
2312
2319
|
finalizationNotes:
|
|
2313
|
-
|
|
2314
|
-
?
|
|
2320
|
+
normalizedSharedNotes !== null && normalizedSharedNotes !== undefined
|
|
2321
|
+
? normalizedSharedNotes
|
|
2315
2322
|
: currentMetadata.finalizationNotes || currentMetadata.finalizationNotesShared,
|
|
2316
2323
|
};
|
|
2317
2324
|
|
|
@@ -2319,14 +2326,39 @@ export class AppointmentService extends BaseService {
|
|
|
2319
2326
|
finalizationNotesShared: metadataUpdate.finalizationNotesShared,
|
|
2320
2327
|
finalizationNotesInternal: metadataUpdate.finalizationNotesInternal,
|
|
2321
2328
|
finalizationNotes: metadataUpdate.finalizationNotes,
|
|
2329
|
+
normalizedSharedNotes,
|
|
2330
|
+
normalizedInternalNotes,
|
|
2322
2331
|
});
|
|
2323
2332
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2333
|
+
// Use direct Firestore update with dot notation to ensure fields are saved correctly
|
|
2334
|
+
const appointmentRef = doc(this.db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
2335
|
+
const updateFields: any = {
|
|
2336
|
+
'metadata.finalizationNotesShared': normalizedSharedNotes,
|
|
2337
|
+
'metadata.finalizationNotesInternal': normalizedInternalNotes,
|
|
2338
|
+
'metadata.finalizationNotes':
|
|
2339
|
+
normalizedSharedNotes !== null && normalizedSharedNotes !== undefined
|
|
2340
|
+
? normalizedSharedNotes
|
|
2341
|
+
: currentMetadata.finalizationNotes || currentMetadata.finalizationNotesShared,
|
|
2326
2342
|
updatedAt: serverTimestamp(),
|
|
2327
2343
|
};
|
|
2328
2344
|
|
|
2329
|
-
|
|
2345
|
+
console.log('🔍 [APPOINTMENT_SERVICE] Direct Firestore update with dot notation:', {
|
|
2346
|
+
appointmentId,
|
|
2347
|
+
updateFields: {
|
|
2348
|
+
'metadata.finalizationNotesShared': updateFields['metadata.finalizationNotesShared'],
|
|
2349
|
+
'metadata.finalizationNotesInternal': updateFields['metadata.finalizationNotesInternal'],
|
|
2350
|
+
'metadata.finalizationNotes': updateFields['metadata.finalizationNotes'],
|
|
2351
|
+
},
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
// Update using dot notation to ensure nested fields are saved correctly
|
|
2355
|
+
await updateDoc(appointmentRef, updateFields);
|
|
2356
|
+
|
|
2357
|
+
// Fetch and return the updated appointment
|
|
2358
|
+
const result = await this.getAppointmentById(appointmentId);
|
|
2359
|
+
if (!result) {
|
|
2360
|
+
throw new Error(`Failed to retrieve updated appointment ${appointmentId}`);
|
|
2361
|
+
}
|
|
2330
2362
|
|
|
2331
2363
|
console.log('🔍 [APPOINTMENT_SERVICE] After update, result metadata:', {
|
|
2332
2364
|
finalizationNotesShared: result.metadata?.finalizationNotesShared,
|
|
@@ -99,6 +99,7 @@ async function createExtendedProcedureInfo(
|
|
|
99
99
|
procedureId: procedureId,
|
|
100
100
|
procedureName: data.name,
|
|
101
101
|
procedureDescription: data.description || '',
|
|
102
|
+
procedurePrice: data.price || 0,
|
|
102
103
|
procedureFamily: data.family, // Use embedded family object
|
|
103
104
|
procedureCategoryId: data.category.id, // Access embedded category
|
|
104
105
|
procedureCategoryName: data.category.name, // Access embedded category
|
|
@@ -218,6 +218,7 @@ export async function initializeFormsForExtendedProcedure(
|
|
|
218
218
|
sortingOrder: templateRef.sortingOrder,
|
|
219
219
|
status: existingForm.status || FilledDocumentStatus.PENDING,
|
|
220
220
|
path: `${APPOINTMENTS_COLLECTION}/${appointmentId}/${formSubcollectionPath}/${existingForm.id}`,
|
|
221
|
+
procedureId: procedureId, // Track which procedure this form belongs to
|
|
221
222
|
};
|
|
222
223
|
initializedFormsInfo.push(linkedForm);
|
|
223
224
|
|
|
@@ -288,6 +289,7 @@ export async function initializeFormsForExtendedProcedure(
|
|
|
288
289
|
sortingOrder: templateRef.sortingOrder,
|
|
289
290
|
status: FilledDocumentStatus.PENDING,
|
|
290
291
|
path: docRef.path,
|
|
292
|
+
procedureId: procedureId, // Track which procedure this form belongs to
|
|
291
293
|
};
|
|
292
294
|
initializedFormsInfo.push(linkedForm);
|
|
293
295
|
|
|
@@ -278,19 +278,45 @@ export async function updateZoneItemUtil(
|
|
|
278
278
|
throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
// Filter out undefined values from updates (Firestore doesn't store undefined)
|
|
282
|
+
// Keep null values as they're used to explicitly delete/clear fields
|
|
283
|
+
// Convert empty strings to null for consistency
|
|
284
|
+
const cleanUpdates = Object.fromEntries(
|
|
285
|
+
Object.entries(updates)
|
|
286
|
+
.filter(([_, value]) => value !== undefined)
|
|
287
|
+
.map(([key, value]) => [
|
|
288
|
+
key,
|
|
289
|
+
value === '' ? null : value, // Convert empty strings to null
|
|
290
|
+
])
|
|
291
|
+
) as Partial<ZoneItemData>;
|
|
292
|
+
|
|
293
|
+
console.log(`[updateZoneItemUtil] Updates received:`, {
|
|
294
|
+
itemIndex,
|
|
295
|
+
zoneId,
|
|
296
|
+
rawUpdates: updates,
|
|
297
|
+
cleanUpdates,
|
|
298
|
+
hasIonNumber: 'ionNumber' in cleanUpdates,
|
|
299
|
+
hasExpiryDate: 'expiryDate' in cleanUpdates,
|
|
300
|
+
ionNumberValue: cleanUpdates.ionNumber,
|
|
301
|
+
expiryDateValue: cleanUpdates.expiryDate,
|
|
302
|
+
});
|
|
303
|
+
|
|
281
304
|
// Update item with updatedAt timestamp
|
|
282
305
|
items[itemIndex] = {
|
|
283
306
|
...items[itemIndex],
|
|
284
|
-
...
|
|
307
|
+
...cleanUpdates,
|
|
285
308
|
updatedAt: new Date().toISOString(),
|
|
286
309
|
};
|
|
287
310
|
|
|
288
|
-
console.log(`[updateZoneItemUtil]
|
|
311
|
+
console.log(`[updateZoneItemUtil] Item after update:`, {
|
|
289
312
|
itemIndex,
|
|
313
|
+
ionNumber: items[itemIndex].ionNumber,
|
|
314
|
+
expiryDate: items[itemIndex].expiryDate,
|
|
290
315
|
quantity: items[itemIndex].quantity,
|
|
291
316
|
priceOverrideAmount: items[itemIndex].priceOverrideAmount,
|
|
292
317
|
price: items[itemIndex].price,
|
|
293
318
|
oldSubtotal: items[itemIndex].subtotal,
|
|
319
|
+
allItemKeys: Object.keys(items[itemIndex]),
|
|
294
320
|
});
|
|
295
321
|
|
|
296
322
|
// Recalculate subtotal for this item
|
|
@@ -304,6 +330,18 @@ export async function updateZoneItemUtil(
|
|
|
304
330
|
// Recalculate final billing with Swiss tax rate (8.1%)
|
|
305
331
|
const finalbilling = calculateFinalBilling(metadata.zonesData, 0.081);
|
|
306
332
|
|
|
333
|
+
// Log what we're about to save to Firestore
|
|
334
|
+
console.log(`[updateZoneItemUtil] Saving to Firestore:`, {
|
|
335
|
+
appointmentId,
|
|
336
|
+
zoneId,
|
|
337
|
+
itemIndex,
|
|
338
|
+
itemToSave: items[itemIndex],
|
|
339
|
+
itemIonNumber: items[itemIndex].ionNumber,
|
|
340
|
+
itemExpiryDate: items[itemIndex].expiryDate,
|
|
341
|
+
zonesDataKeys: Object.keys(metadata.zonesData),
|
|
342
|
+
zoneItemsCount: metadata.zonesData[zoneId]?.length,
|
|
343
|
+
});
|
|
344
|
+
|
|
307
345
|
// Update appointment
|
|
308
346
|
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
309
347
|
await updateDoc(appointmentRef, {
|
|
@@ -312,7 +350,16 @@ export async function updateZoneItemUtil(
|
|
|
312
350
|
updatedAt: serverTimestamp(),
|
|
313
351
|
});
|
|
314
352
|
|
|
315
|
-
|
|
353
|
+
// Verify what was actually saved
|
|
354
|
+
const savedAppointment = await getAppointmentOrThrow(db, appointmentId);
|
|
355
|
+
const savedItem = savedAppointment.metadata?.zonesData?.[zoneId]?.[itemIndex];
|
|
356
|
+
console.log(`[updateZoneItemUtil] Verification after save:`, {
|
|
357
|
+
savedItemIonNumber: savedItem?.ionNumber,
|
|
358
|
+
savedItemExpiryDate: savedItem?.expiryDate,
|
|
359
|
+
savedItemKeys: savedItem ? Object.keys(savedItem) : [],
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return savedAppointment;
|
|
316
363
|
}
|
|
317
364
|
|
|
318
365
|
/**
|
|
@@ -206,6 +206,7 @@ export async function getZonePhotoEntryUtil(
|
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Updates visibility of a photo pair (before AND after together)
|
|
209
|
+
* Note: If photo pair visibility is disabled, note visibility is automatically disabled as well
|
|
209
210
|
*
|
|
210
211
|
* @param db Firestore instance
|
|
211
212
|
* @param appointmentId Appointment ID
|
|
@@ -223,18 +224,27 @@ export async function updateZonePhotoVisibilityUtil(
|
|
|
223
224
|
showToPatient: boolean,
|
|
224
225
|
doctorId: string,
|
|
225
226
|
): Promise<Appointment> {
|
|
227
|
+
const updates: Partial<BeforeAfterPerZone> = { showToPatient };
|
|
228
|
+
|
|
229
|
+
// If hiding photo pair from patient, also hide notes (notes can't be visible if photos aren't)
|
|
230
|
+
if (!showToPatient) {
|
|
231
|
+
updates.beforeNoteVisibleToPatient = false;
|
|
232
|
+
updates.afterNoteVisibleToPatient = false;
|
|
233
|
+
}
|
|
234
|
+
|
|
226
235
|
return updateZonePhotoEntryUtil(
|
|
227
236
|
db,
|
|
228
237
|
appointmentId,
|
|
229
238
|
zoneId,
|
|
230
239
|
photoIndex,
|
|
231
|
-
|
|
240
|
+
updates,
|
|
232
241
|
doctorId,
|
|
233
242
|
);
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
/**
|
|
237
246
|
* Updates visibility of a photo note (before or after)
|
|
247
|
+
* Note: Notes can only be visible to patient if the photo pair itself is visible to patient
|
|
238
248
|
*
|
|
239
249
|
* @param db Firestore instance
|
|
240
250
|
* @param appointmentId Appointment ID
|
|
@@ -244,6 +254,7 @@ export async function updateZonePhotoVisibilityUtil(
|
|
|
244
254
|
* @param visibleToPatient Whether the note should be visible to patient
|
|
245
255
|
* @param doctorId ID of the doctor making the change (for audit trail)
|
|
246
256
|
* @returns Updated appointment
|
|
257
|
+
* @throws Error if trying to make note visible when photo pair is not visible
|
|
247
258
|
*/
|
|
248
259
|
export async function updateZonePhotoNoteVisibilityUtil(
|
|
249
260
|
db: Firestore,
|
|
@@ -254,6 +265,33 @@ export async function updateZonePhotoNoteVisibilityUtil(
|
|
|
254
265
|
visibleToPatient: boolean,
|
|
255
266
|
doctorId: string,
|
|
256
267
|
): Promise<Appointment> {
|
|
268
|
+
// Get current appointment to check if photo pair is visible
|
|
269
|
+
const appointment = await getAppointmentOrThrow(db, appointmentId);
|
|
270
|
+
|
|
271
|
+
const zonePhotos = appointment.metadata?.zonePhotos as
|
|
272
|
+
| Record<string, BeforeAfterPerZone[]>
|
|
273
|
+
| undefined
|
|
274
|
+
| null;
|
|
275
|
+
|
|
276
|
+
if (!zonePhotos || !zonePhotos[zoneId] || !Array.isArray(zonePhotos[zoneId])) {
|
|
277
|
+
throw new Error(`No photos found for zone ${zoneId} in appointment ${appointmentId}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const zoneArray = zonePhotos[zoneId];
|
|
281
|
+
if (photoIndex < 0 || photoIndex >= zoneArray.length) {
|
|
282
|
+
throw new Error(`Invalid photo index ${photoIndex} for zone ${zoneId}. Must be between 0 and ${zoneArray.length - 1}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const photoEntry = zoneArray[photoIndex];
|
|
286
|
+
|
|
287
|
+
// Validate: Notes can only be visible if photo pair is visible
|
|
288
|
+
if (visibleToPatient && !photoEntry.showToPatient) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
'Cannot make note visible to patient: The photo pair must be visible to the patient first. ' +
|
|
291
|
+
'Please enable "Show to Patient" for the photo pair before making notes visible.'
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
257
295
|
const updates: Partial<BeforeAfterPerZone> =
|
|
258
296
|
noteType === 'before'
|
|
259
297
|
? { beforeNoteVisibleToPatient: visibleToPatient }
|
|
@@ -100,6 +100,7 @@ export interface LinkedFormInfo {
|
|
|
100
100
|
path: string; // Full Firestore path to the filled document (e.g., appointments/{aid}/user-forms/{fid})
|
|
101
101
|
submittedAt?: Timestamp;
|
|
102
102
|
completedAt?: Timestamp; // When the form reached a final state like 'completed' or 'signed'
|
|
103
|
+
procedureId?: string; // ID of the procedure this form belongs to (for extended procedure forms)
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/**
|
|
@@ -168,8 +169,8 @@ export interface ZoneItemData {
|
|
|
168
169
|
notes?: string;
|
|
169
170
|
notesVisibleToPatient?: boolean; // Whether notes are visible to patient (privacy-first, default false)
|
|
170
171
|
subtotal?: number;
|
|
171
|
-
ionNumber?: string; // Batch/Lot number
|
|
172
|
-
expiryDate?: string; // ISO date string (YYYY-MM-DD)
|
|
172
|
+
ionNumber?: string | null; // Batch/Lot number - can be null to clear/delete
|
|
173
|
+
expiryDate?: string | null; // ISO date string (YYYY-MM-DD) - can be null to clear/delete
|
|
173
174
|
createdAt?: string; // ISO timestamp
|
|
174
175
|
updatedAt?: string; // ISO timestamp
|
|
175
176
|
}
|
|
@@ -226,6 +227,7 @@ export interface ExtendedProcedureInfo {
|
|
|
226
227
|
procedureId: string;
|
|
227
228
|
procedureName: string;
|
|
228
229
|
procedureDescription?: string;
|
|
230
|
+
procedurePrice?: number;
|
|
229
231
|
procedureFamily?: ProcedureFamily;
|
|
230
232
|
procedureCategoryId: string;
|
|
231
233
|
procedureCategoryName: string;
|
|
@@ -227,7 +227,8 @@ export const zoneItemDataSchema = z
|
|
|
227
227
|
notes: z.string().max(MAX_STRING_LENGTH_LONG, 'Notes too long').optional(),
|
|
228
228
|
notesVisibleToPatient: z.boolean().optional().default(false),
|
|
229
229
|
subtotal: z.number().min(0, 'Subtotal must be non-negative').optional(),
|
|
230
|
-
ionNumber: z.string().optional(),
|
|
230
|
+
ionNumber: z.string().nullable().optional(), // Batch/Lot number - can be null to clear/delete
|
|
231
|
+
expiryDate: z.string().nullable().optional(), // ISO date string (YYYY-MM-DD) - can be null to clear/delete
|
|
231
232
|
createdAt: z.string().optional(),
|
|
232
233
|
updatedAt: z.string().optional(),
|
|
233
234
|
})
|