@blackcode_sa/metaestetics-api 1.12.32 → 1.12.34

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.
@@ -0,0 +1,225 @@
1
+ import { Firestore, collection, doc, addDoc, deleteDoc, getDocs, query, where, serverTimestamp, getDoc } from 'firebase/firestore';
2
+ import {
3
+ DocumentTemplate,
4
+ FilledDocumentStatus,
5
+ DOCTOR_FORMS_SUBCOLLECTION,
6
+ DOCUMENTATION_TEMPLATES_COLLECTION,
7
+ } from '../../../types/documentation-templates';
8
+ import {
9
+ APPOINTMENTS_COLLECTION,
10
+ LinkedFormInfo,
11
+ } from '../../../types/appointment';
12
+ import { TechnologyDocumentationTemplate } from '../../../backoffice/types/technology.types';
13
+
14
+ export interface InitializeExtendedProcedureFormsResult {
15
+ initializedFormsInfo: LinkedFormInfo[];
16
+ pendingUserFormsIds: string[];
17
+ allLinkedFormIds: string[];
18
+ }
19
+
20
+ /**
21
+ * Initializes forms for an extended procedure using client-side Firestore
22
+ * Similar to DocumentManagerAdminService but for client-side usage
23
+ * @param db Firestore instance
24
+ * @param appointmentId Appointment ID
25
+ * @param procedureId Procedure ID for forms
26
+ * @param technologyTemplates Technology documentation templates
27
+ * @param patientId Patient ID
28
+ * @param practitionerId Practitioner ID
29
+ * @param clinicId Clinic ID
30
+ * @returns Form initialization result
31
+ */
32
+ export async function initializeFormsForExtendedProcedure(
33
+ db: Firestore,
34
+ appointmentId: string,
35
+ procedureId: string,
36
+ technologyTemplates: TechnologyDocumentationTemplate[],
37
+ patientId: string,
38
+ practitionerId: string,
39
+ clinicId: string
40
+ ): Promise<InitializeExtendedProcedureFormsResult> {
41
+ const initializedFormsInfo: LinkedFormInfo[] = [];
42
+ const pendingUserFormsIds: string[] = [];
43
+ const allLinkedFormIds: string[] = [];
44
+
45
+ if (!technologyTemplates || technologyTemplates.length === 0) {
46
+ console.log(
47
+ `[FormInit] No document templates to initialize for extended procedure ${procedureId} in appointment ${appointmentId}.`
48
+ );
49
+ return {
50
+ initializedFormsInfo,
51
+ pendingUserFormsIds,
52
+ allLinkedFormIds,
53
+ };
54
+ }
55
+
56
+ // Fetch all template documents
57
+ const templateIds = technologyTemplates.map((t) => t.templateId);
58
+
59
+ // Note: Client-side Firestore doesn't support 'in' queries with more than 10 items
60
+ // We'll fetch templates one by one for now (can be optimized later)
61
+ const templatesMap = new Map<string, DocumentTemplate>();
62
+
63
+ for (const templateId of templateIds) {
64
+ try {
65
+ const templateDoc = doc(db, DOCUMENTATION_TEMPLATES_COLLECTION, templateId);
66
+ const templateSnap = await getDoc(templateDoc);
67
+
68
+ if (templateSnap.exists()) {
69
+ templatesMap.set(templateId, templateSnap.data() as DocumentTemplate);
70
+ } else {
71
+ console.warn(
72
+ `[FormInit] Template ${templateId} not found in Firestore.`
73
+ );
74
+ }
75
+ } catch (error) {
76
+ console.error(`[FormInit] Error fetching template ${templateId}:`, error);
77
+ }
78
+ }
79
+
80
+ // Initialize forms for each template (only doctor forms for extended procedures)
81
+ for (const templateRef of technologyTemplates) {
82
+ const template = templatesMap.get(templateRef.templateId);
83
+ if (!template) {
84
+ console.warn(
85
+ `[FormInit] Template ${templateRef.templateId} not found in Firestore.`
86
+ );
87
+ continue;
88
+ }
89
+
90
+ // Skip user forms - only create doctor forms for extended procedures
91
+ if (templateRef.isUserForm) {
92
+ console.log(
93
+ `[FormInit] Skipping user form ${templateRef.templateId} for extended procedure ${procedureId}.`
94
+ );
95
+ continue;
96
+ }
97
+
98
+ const isRequired = templateRef.isRequired;
99
+ const formSubcollectionPath = DOCTOR_FORMS_SUBCOLLECTION;
100
+
101
+ // Create form document in subcollection
102
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
103
+ const formsCollectionRef = collection(appointmentRef, formSubcollectionPath);
104
+
105
+ const filledDocumentData = {
106
+ templateId: templateRef.templateId,
107
+ templateVersion: template.version,
108
+ isUserForm: false, // Always false for extended procedures
109
+ isRequired: isRequired,
110
+ appointmentId: appointmentId,
111
+ procedureId: procedureId,
112
+ patientId: patientId,
113
+ practitionerId: practitionerId,
114
+ clinicId: clinicId,
115
+ createdAt: serverTimestamp(),
116
+ updatedAt: serverTimestamp(),
117
+ values: {},
118
+ status: FilledDocumentStatus.PENDING,
119
+ };
120
+
121
+ try {
122
+ const docRef = await addDoc(formsCollectionRef, filledDocumentData);
123
+ const filledDocumentId = docRef.id;
124
+
125
+ // No user forms for extended procedures, so no pending user forms
126
+
127
+ allLinkedFormIds.push(filledDocumentId);
128
+
129
+ const linkedForm: LinkedFormInfo = {
130
+ formId: filledDocumentId,
131
+ templateId: template.id,
132
+ templateVersion: template.version,
133
+ title: template.title,
134
+ isUserForm: false, // Always false for extended procedures
135
+ isRequired: isRequired,
136
+ sortingOrder: templateRef.sortingOrder,
137
+ status: FilledDocumentStatus.PENDING,
138
+ path: docRef.path,
139
+ };
140
+ initializedFormsInfo.push(linkedForm);
141
+
142
+ console.log(
143
+ `[FormInit] Created FilledDocument ${filledDocumentId} (template: ${template.id}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
144
+ );
145
+ } catch (error) {
146
+ console.error(
147
+ `[FormInit] Error creating form for template ${templateRef.templateId}:`,
148
+ error
149
+ );
150
+ }
151
+ }
152
+
153
+ return { initializedFormsInfo, pendingUserFormsIds, allLinkedFormIds };
154
+ }
155
+
156
+ /**
157
+ * Removes all forms associated with a specific procedure from an appointment
158
+ * @param db Firestore instance
159
+ * @param appointmentId Appointment ID
160
+ * @param procedureId Procedure ID to remove forms for
161
+ * @returns Array of removed form IDs
162
+ */
163
+ export async function removeFormsForExtendedProcedure(
164
+ db: Firestore,
165
+ appointmentId: string,
166
+ procedureId: string
167
+ ): Promise<string[]> {
168
+ const removedFormIds: string[] = [];
169
+
170
+ // Only remove from doctor forms subcollection (no user forms for extended procedures)
171
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
172
+ const doctorFormsRef = collection(appointmentRef, DOCTOR_FORMS_SUBCOLLECTION);
173
+
174
+ // Query doctor forms for this procedure
175
+ const doctorFormsQuery = query(
176
+ doctorFormsRef,
177
+ where('procedureId', '==', procedureId)
178
+ );
179
+ const doctorFormsSnap = await getDocs(doctorFormsQuery);
180
+
181
+ for (const formDoc of doctorFormsSnap.docs) {
182
+ try {
183
+ await deleteDoc(formDoc.ref);
184
+ removedFormIds.push(formDoc.id);
185
+ console.log(
186
+ `[FormInit] Removed doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
187
+ );
188
+ } catch (error) {
189
+ console.error(
190
+ `[FormInit] Error removing doctor form ${formDoc.id}:`,
191
+ error
192
+ );
193
+ }
194
+ }
195
+
196
+ return removedFormIds;
197
+ }
198
+
199
+ /**
200
+ * Gets all forms associated with a specific procedure in an appointment
201
+ * @param db Firestore instance
202
+ * @param appointmentId Appointment ID
203
+ * @param procedureId Procedure ID
204
+ * @returns Array of form IDs
205
+ */
206
+ export async function getFormsForExtendedProcedure(
207
+ db: Firestore,
208
+ appointmentId: string,
209
+ procedureId: string
210
+ ): Promise<string[]> {
211
+ const formIds: string[] = [];
212
+
213
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
214
+ const doctorFormsRef = collection(appointmentRef, DOCTOR_FORMS_SUBCOLLECTION);
215
+
216
+ // Query doctor forms for this procedure
217
+ const doctorFormsQuery = query(
218
+ doctorFormsRef,
219
+ where('procedureId', '==', procedureId)
220
+ );
221
+ const doctorFormsSnap = await getDocs(doctorFormsQuery);
222
+ doctorFormsSnap.docs.forEach(doc => formIds.push(doc.id));
223
+
224
+ return formIds;
225
+ }
@@ -0,0 +1,335 @@
1
+ import { Firestore, getDoc, updateDoc, serverTimestamp } from 'firebase/firestore';
2
+ import {
3
+ ZoneItemData,
4
+ AppointmentMetadata,
5
+ FinalBilling,
6
+ Appointment,
7
+ APPOINTMENTS_COLLECTION,
8
+ } from '../../../types/appointment';
9
+ import { getAppointmentByIdUtil } from './appointment.utils';
10
+ import { doc } from 'firebase/firestore';
11
+
12
+ /**
13
+ * Validates that a zone key follows the category.zone format
14
+ * @param zoneKey Zone key to validate
15
+ * @throws Error if format is invalid
16
+ */
17
+ export function validateZoneKeyFormat(zoneKey: string): void {
18
+ const parts = zoneKey.split('.');
19
+ if (parts.length !== 2) {
20
+ throw new Error(
21
+ `Invalid zone key format: "${zoneKey}". Must be "category.zone" (e.g., "face.forehead")`
22
+ );
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Calculates subtotal for a zone item
28
+ * @param item Zone item data
29
+ * @returns Calculated subtotal
30
+ */
31
+ export function calculateItemSubtotal(item: Partial<ZoneItemData>): number {
32
+ if (item.type === 'note') {
33
+ return 0;
34
+ }
35
+
36
+ // If price override amount is set, use it
37
+ if (item.priceOverrideAmount !== undefined && item.priceOverrideAmount !== null) {
38
+ return item.priceOverrideAmount;
39
+ }
40
+
41
+ // Calculate normally: price * quantity
42
+ const price = item.price || 0;
43
+ const quantity = item.quantity || 0;
44
+ return price * quantity;
45
+ }
46
+
47
+ /**
48
+ * Recalculates final billing based on all zone items
49
+ * @param zonesData Zone items data
50
+ * @param taxRate Tax rate (e.g., 0.20 for 20%)
51
+ * @returns Calculated final billing
52
+ */
53
+ export function calculateFinalBilling(
54
+ zonesData: Record<string, ZoneItemData[]>,
55
+ taxRate: number = 0.20
56
+ ): FinalBilling {
57
+ let subtotalAll = 0;
58
+
59
+ // Sum up all zone items
60
+ Object.values(zonesData).forEach(items => {
61
+ items.forEach(item => {
62
+ if (item.type === 'item' && item.subtotal) {
63
+ subtotalAll += item.subtotal;
64
+ }
65
+ });
66
+ });
67
+
68
+ const taxPrice = subtotalAll * taxRate;
69
+ const finalPrice = subtotalAll + taxPrice;
70
+
71
+ // Get currency from first item (assuming all same currency)
72
+ let currency: any = 'CHF'; // Default
73
+
74
+ for (const items of Object.values(zonesData)) {
75
+ const firstItem = items.find(i => i.type === 'item');
76
+ if (firstItem) {
77
+ currency = firstItem.currency || currency;
78
+ break;
79
+ }
80
+ }
81
+
82
+ return {
83
+ subtotalAll,
84
+ taxRate,
85
+ taxPrice,
86
+ finalPrice,
87
+ currency,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Gets appointment and validates it exists
93
+ * @param db Firestore instance
94
+ * @param appointmentId Appointment ID
95
+ * @returns Appointment document
96
+ */
97
+ export async function getAppointmentOrThrow(
98
+ db: Firestore,
99
+ appointmentId: string
100
+ ): Promise<Appointment> {
101
+ const appointment = await getAppointmentByIdUtil(db, appointmentId);
102
+ if (!appointment) {
103
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
104
+ }
105
+ return appointment;
106
+ }
107
+
108
+ /**
109
+ * Initializes appointment metadata if it doesn't exist
110
+ * @param appointment Appointment document
111
+ * @returns Initialized metadata
112
+ */
113
+ export function initializeMetadata(
114
+ appointment: Appointment
115
+ ): AppointmentMetadata {
116
+ return appointment.metadata || {
117
+ selectedZones: null,
118
+ zonePhotos: null,
119
+ zonesData: null,
120
+ appointmentProducts: [],
121
+ extendedProcedures: [],
122
+ finalbilling: null,
123
+ finalizationNotes: null,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Adds an item to a specific zone
129
+ * @param db Firestore instance
130
+ * @param appointmentId Appointment ID
131
+ * @param zoneId Zone ID (must be category.zone format)
132
+ * @param item Zone item data to add (without parentZone)
133
+ * @returns Updated appointment
134
+ */
135
+ export async function addItemToZoneUtil(
136
+ db: Firestore,
137
+ appointmentId: string,
138
+ zoneId: string,
139
+ item: Omit<ZoneItemData, 'subtotal' | 'parentZone'>
140
+ ): Promise<Appointment> {
141
+ // Validate zone key format
142
+ validateZoneKeyFormat(zoneId);
143
+
144
+ // Get appointment
145
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
146
+ const metadata = initializeMetadata(appointment);
147
+
148
+ // Initialize zonesData if needed
149
+ const zonesData = metadata.zonesData || {};
150
+
151
+ // Initialize zone array if needed
152
+ if (!zonesData[zoneId]) {
153
+ zonesData[zoneId] = [];
154
+ }
155
+
156
+ // Calculate subtotal for the item
157
+ const itemWithSubtotal: ZoneItemData = {
158
+ ...item,
159
+ parentZone: zoneId, // Set parentZone to the zone key
160
+ subtotal: calculateItemSubtotal(item),
161
+ };
162
+
163
+ // Add item to zone
164
+ zonesData[zoneId].push(itemWithSubtotal);
165
+
166
+ // Recalculate final billing
167
+ const finalbilling = calculateFinalBilling(zonesData); //TODO: add correct amount of tax
168
+
169
+ // Update appointment
170
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
171
+ await updateDoc(appointmentRef, {
172
+ 'metadata.zonesData': zonesData,
173
+ 'metadata.finalbilling': finalbilling,
174
+ updatedAt: serverTimestamp(),
175
+ });
176
+
177
+ // Return updated appointment
178
+ return getAppointmentOrThrow(db, appointmentId);
179
+ }
180
+
181
+ /**
182
+ * Removes an item from a specific zone
183
+ * @param db Firestore instance
184
+ * @param appointmentId Appointment ID
185
+ * @param zoneId Zone ID
186
+ * @param itemIndex Index of item to remove
187
+ * @returns Updated appointment
188
+ */
189
+ export async function removeItemFromZoneUtil(
190
+ db: Firestore,
191
+ appointmentId: string,
192
+ zoneId: string,
193
+ itemIndex: number
194
+ ): Promise<Appointment> {
195
+ validateZoneKeyFormat(zoneId);
196
+
197
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
198
+ const metadata = initializeMetadata(appointment);
199
+
200
+ if (!metadata.zonesData || !metadata.zonesData[zoneId]) {
201
+ throw new Error(`No items found for zone ${zoneId}`);
202
+ }
203
+
204
+ const items = metadata.zonesData[zoneId];
205
+ if (itemIndex < 0 || itemIndex >= items.length) {
206
+ throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
207
+ }
208
+
209
+ // Remove item
210
+ items.splice(itemIndex, 1);
211
+
212
+ // If zone is now empty, remove it
213
+ if (items.length === 0) {
214
+ delete metadata.zonesData[zoneId];
215
+ }
216
+
217
+ // Recalculate final billing
218
+ const finalbilling = calculateFinalBilling(metadata.zonesData); //TODO: add correct amount of tax
219
+
220
+ // Update appointment
221
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
222
+ await updateDoc(appointmentRef, {
223
+ 'metadata.zonesData': metadata.zonesData,
224
+ 'metadata.finalbilling': finalbilling,
225
+ updatedAt: serverTimestamp(),
226
+ });
227
+
228
+ return getAppointmentOrThrow(db, appointmentId);
229
+ }
230
+
231
+ /**
232
+ * Updates a specific item in a zone
233
+ * @param db Firestore instance
234
+ * @param appointmentId Appointment ID
235
+ * @param zoneId Zone ID
236
+ * @param itemIndex Index of item to update
237
+ * @param updates Partial updates to apply
238
+ * @returns Updated appointment
239
+ */
240
+ export async function updateZoneItemUtil(
241
+ db: Firestore,
242
+ appointmentId: string,
243
+ zoneId: string,
244
+ itemIndex: number,
245
+ updates: Partial<ZoneItemData>
246
+ ): Promise<Appointment> {
247
+ validateZoneKeyFormat(zoneId);
248
+
249
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
250
+ const metadata = initializeMetadata(appointment);
251
+
252
+ if (!metadata.zonesData || !metadata.zonesData[zoneId]) {
253
+ throw new Error(`No items found for zone ${zoneId}`);
254
+ }
255
+
256
+ const items = metadata.zonesData[zoneId];
257
+ if (itemIndex < 0 || itemIndex >= items.length) {
258
+ throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
259
+ }
260
+
261
+ // Update item
262
+ items[itemIndex] = {
263
+ ...items[itemIndex],
264
+ ...updates,
265
+ };
266
+
267
+ // Recalculate subtotal for this item
268
+ items[itemIndex].subtotal = calculateItemSubtotal(items[itemIndex]);
269
+
270
+ // Recalculate final billing
271
+ const finalbilling = calculateFinalBilling(metadata.zonesData); //TODO: add correct amount of tax
272
+
273
+ // Update appointment
274
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
275
+ await updateDoc(appointmentRef, {
276
+ 'metadata.zonesData': metadata.zonesData,
277
+ 'metadata.finalbilling': finalbilling,
278
+ updatedAt: serverTimestamp(),
279
+ });
280
+
281
+ return getAppointmentOrThrow(db, appointmentId);
282
+ }
283
+
284
+ /**
285
+ * Overrides price for a specific zone item
286
+ * @param db Firestore instance
287
+ * @param appointmentId Appointment ID
288
+ * @param zoneId Zone ID
289
+ * @param itemIndex Index of item
290
+ * @param newPrice New price amount
291
+ * @returns Updated appointment
292
+ */
293
+ export async function overridePriceForZoneItemUtil(
294
+ db: Firestore,
295
+ appointmentId: string,
296
+ zoneId: string,
297
+ itemIndex: number,
298
+ newPrice: number
299
+ ): Promise<Appointment> {
300
+ return updateZoneItemUtil(db, appointmentId, zoneId, itemIndex, {
301
+ priceOverrideAmount: newPrice,
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Updates subzones for a specific zone item
307
+ * @param db Firestore instance
308
+ * @param appointmentId Appointment ID
309
+ * @param zoneId Zone ID
310
+ * @param itemIndex Index of item
311
+ * @param subzones Array of subzone keys (empty array = entire zone)
312
+ * @returns Updated appointment
313
+ */
314
+ export async function updateSubzonesUtil(
315
+ db: Firestore,
316
+ appointmentId: string,
317
+ zoneId: string,
318
+ itemIndex: number,
319
+ subzones: string[]
320
+ ): Promise<Appointment> {
321
+ // Validate subzone format if provided
322
+ subzones.forEach(subzone => {
323
+ const parts = subzone.split('.');
324
+ if (parts.length !== 3) {
325
+ throw new Error(
326
+ `Invalid subzone format: "${subzone}". Must be "category.zone.subzone" (e.g., "face.forehead.left")`
327
+ );
328
+ }
329
+ });
330
+
331
+ return updateZoneItemUtil(db, appointmentId, zoneId, itemIndex, {
332
+ subzones,
333
+ });
334
+ }
335
+
@@ -93,6 +93,17 @@ import {
93
93
  getActiveInviteTokensByPatientUtil,
94
94
  } from './utils';
95
95
 
96
+ import {
97
+ getAestheticAnalysisUtil,
98
+ createOrUpdateAestheticAnalysisUtil,
99
+ } from './utils/aesthetic-analysis.utils';
100
+
101
+ import {
102
+ AestheticAnalysis,
103
+ CreateAestheticAnalysisData,
104
+ UpdateAestheticAnalysisData,
105
+ } from '../../types/patient';
106
+
96
107
  import { CreatePatientTokenData, PatientToken } from '../../types/patient/token.types';
97
108
 
98
109
  export class PatientService extends BaseService {
@@ -834,4 +845,39 @@ export class PatientService extends BaseService {
834
845
  // the admin has permission to view this patient's tokens.
835
846
  return getActiveInviteTokensByPatientUtil(this.db, patientId);
836
847
  }
848
+
849
+ async getAestheticAnalysis(patientId: string): Promise<AestheticAnalysis | null> {
850
+ const currentUser = await this.getCurrentUser();
851
+ return getAestheticAnalysisUtil(this.db, patientId, currentUser.uid, currentUser.roles);
852
+ }
853
+
854
+ async createAestheticAnalysis(
855
+ patientId: string,
856
+ data: CreateAestheticAnalysisData
857
+ ): Promise<void> {
858
+ const currentUser = await this.getCurrentUser();
859
+ return createOrUpdateAestheticAnalysisUtil(
860
+ this.db,
861
+ patientId,
862
+ data,
863
+ currentUser.uid,
864
+ currentUser.roles,
865
+ false
866
+ );
867
+ }
868
+
869
+ async updateAestheticAnalysis(
870
+ patientId: string,
871
+ data: UpdateAestheticAnalysisData
872
+ ): Promise<void> {
873
+ const currentUser = await this.getCurrentUser();
874
+ return createOrUpdateAestheticAnalysisUtil(
875
+ this.db,
876
+ patientId,
877
+ data,
878
+ currentUser.uid,
879
+ currentUser.roles,
880
+ true
881
+ );
882
+ }
837
883
  }
@@ -1508,4 +1508,58 @@ export class ProcedureService extends BaseService {
1508
1508
  });
1509
1509
  return proceduresForMap;
1510
1510
  }
1511
+
1512
+ /**
1513
+ * Gets procedures filtered by clinic and practitioner with optional family filter
1514
+ * @param clinicBranchId Clinic branch ID to filter by
1515
+ * @param practitionerId Practitioner ID to filter by
1516
+ * @param filterByFamily If true, shows only procedures of the same family as the default procedure
1517
+ * @param defaultProcedureId Optional default procedure ID to determine the family
1518
+ * @returns Array of procedures
1519
+ */
1520
+ async getProceduresForConsultation(
1521
+ clinicBranchId: string,
1522
+ practitionerId: string,
1523
+ filterByFamily: boolean = true,
1524
+ defaultProcedureId?: string
1525
+ ): Promise<Procedure[]> {
1526
+ let familyToFilter: ProcedureFamily | null = null;
1527
+
1528
+ // If family filtering is enabled and we have a default procedure, get its family
1529
+ if (filterByFamily && defaultProcedureId) {
1530
+ const defaultProcedureRef = doc(this.db, PROCEDURES_COLLECTION, defaultProcedureId);
1531
+ const defaultProcedureSnap = await getDoc(defaultProcedureRef);
1532
+
1533
+ if (defaultProcedureSnap.exists()) {
1534
+ const defaultProcedure = defaultProcedureSnap.data() as Procedure;
1535
+ familyToFilter = defaultProcedure.family;
1536
+ }
1537
+ }
1538
+
1539
+ // Build query constraints
1540
+ const constraints: QueryConstraint[] = [
1541
+ where('clinicBranchId', '==', clinicBranchId),
1542
+ where('practitionerId', '==', practitionerId),
1543
+ where('isActive', '==', true),
1544
+ ];
1545
+
1546
+ // Add family filter if applicable
1547
+ if (filterByFamily && familyToFilter) {
1548
+ constraints.push(where('family', '==', familyToFilter));
1549
+ }
1550
+
1551
+ // Execute query
1552
+ const proceduresQuery = query(
1553
+ collection(this.db, PROCEDURES_COLLECTION),
1554
+ ...constraints,
1555
+ orderBy('name', 'asc')
1556
+ );
1557
+
1558
+ const querySnapshot = await getDocs(proceduresQuery);
1559
+
1560
+ return querySnapshot.docs.map(doc => ({
1561
+ id: doc.id,
1562
+ ...doc.data(),
1563
+ } as Procedure));
1564
+ }
1511
1565
  }