@blackcode_sa/metaestetics-api 1.12.67 → 1.13.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.
Files changed (47) hide show
  1. package/dist/admin/index.d.mts +801 -2
  2. package/dist/admin/index.d.ts +801 -2
  3. package/dist/admin/index.js +2332 -153
  4. package/dist/admin/index.mjs +2321 -153
  5. package/dist/backoffice/index.d.mts +40 -0
  6. package/dist/backoffice/index.d.ts +40 -0
  7. package/dist/backoffice/index.js +118 -18
  8. package/dist/backoffice/index.mjs +118 -20
  9. package/dist/index.d.mts +1097 -2
  10. package/dist/index.d.ts +1097 -2
  11. package/dist/index.js +4224 -2091
  12. package/dist/index.mjs +3941 -1821
  13. package/package.json +1 -1
  14. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +140 -0
  15. package/src/admin/analytics/analytics.admin.service.ts +278 -0
  16. package/src/admin/analytics/index.ts +2 -0
  17. package/src/admin/index.ts +6 -0
  18. package/src/backoffice/services/README.md +17 -0
  19. package/src/backoffice/services/analytics.service.proposal.md +863 -0
  20. package/src/backoffice/services/analytics.service.summary.md +143 -0
  21. package/src/backoffice/services/category.service.ts +49 -6
  22. package/src/backoffice/services/subcategory.service.ts +50 -6
  23. package/src/backoffice/services/technology.service.ts +53 -6
  24. package/src/services/analytics/ARCHITECTURE.md +199 -0
  25. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -0
  26. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -0
  27. package/src/services/analytics/QUICK_START.md +393 -0
  28. package/src/services/analytics/README.md +287 -0
  29. package/src/services/analytics/SUMMARY.md +141 -0
  30. package/src/services/analytics/USAGE_GUIDE.md +518 -0
  31. package/src/services/analytics/analytics-cloud.service.ts +222 -0
  32. package/src/services/analytics/analytics.service.ts +1632 -0
  33. package/src/services/analytics/index.ts +3 -0
  34. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -0
  35. package/src/services/analytics/utils/cost-calculation.utils.ts +154 -0
  36. package/src/services/analytics/utils/grouping.utils.ts +394 -0
  37. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -0
  38. package/src/services/analytics/utils/time-calculation.utils.ts +186 -0
  39. package/src/services/appointment/appointment.service.ts +50 -6
  40. package/src/services/index.ts +1 -0
  41. package/src/services/procedure/procedure.service.ts +3 -3
  42. package/src/types/analytics/analytics.types.ts +500 -0
  43. package/src/types/analytics/grouped-analytics.types.ts +148 -0
  44. package/src/types/analytics/index.ts +4 -0
  45. package/src/types/analytics/stored-analytics.types.ts +137 -0
  46. package/src/types/index.ts +3 -0
  47. package/src/types/notifications/index.ts +21 -0
@@ -0,0 +1,143 @@
1
+ # Analytics Service - Executive Summary
2
+
3
+ ## Overview
4
+
5
+ This document provides a high-level summary of the proposed Analytics Service for the Clinic Admin app. The full detailed proposal can be found in [analytics.service.proposal.md](./analytics.service.proposal.md).
6
+
7
+ ## Purpose
8
+
9
+ The Analytics Service will provide comprehensive financial and operational intelligence to clinic administrators, enabling data-driven decision making across:
10
+
11
+ - **Practitioner Performance**: Track doctor efficiency, revenue generation, and patient satisfaction
12
+ - **Procedure Analytics**: Understand procedure popularity, profitability, and product usage
13
+ - **Financial Intelligence**: Monitor revenue, costs, payment status, and trends
14
+ - **Operational Efficiency**: Analyze time utilization, cancellations, and no-shows
15
+ - **Patient Insights**: Track patient lifetime value, retention, and behavior patterns
16
+
17
+ ## Key Data Sources
18
+
19
+ The service will aggregate data from:
20
+
21
+ 1. **Appointments Collection**: Primary source for all appointment-related metrics
22
+ 2. **Procedures Collection**: Procedure metadata and categorization
23
+ 3. **Practitioners Collection**: Doctor information and associations
24
+ 4. **Patients Collection**: Patient profiles and history
25
+ 5. **Clinics Collection**: Clinic and branch information
26
+ 6. **Products Collection**: Product usage and pricing data
27
+
28
+ ## Core Analytics Categories
29
+
30
+ ### 1. Practitioner Analytics
31
+ - Total and completed appointments
32
+ - Cancellation and no-show rates
33
+ - Time efficiency (booked vs actual)
34
+ - Revenue generation
35
+ - Patient retention
36
+
37
+ ### 2. Procedure Analytics
38
+ - Appointment counts and popularity
39
+ - Revenue and profitability
40
+ - Cancellation rates
41
+ - Product usage patterns
42
+ - Performance by category/technology
43
+
44
+ ### 3. Time Analytics
45
+ - Booked vs actual time comparison
46
+ - Efficiency percentages
47
+ - Overrun/underutilization analysis
48
+ - Peak hours and trends
49
+
50
+ ### 4. Cancellation & No-Show Analytics
51
+ - Rates by clinic, practitioner, patient, procedure
52
+ - Cancellation reasons breakdown
53
+ - Patterns and trends
54
+ - Lead time analysis
55
+
56
+ ### 5. Financial Analytics
57
+ - Total and average revenue
58
+ - Cost per patient/appointment
59
+ - Payment status breakdown
60
+ - Revenue trends
61
+ - Product cost analysis
62
+
63
+ ### 6. Product Usage Analytics
64
+ - Products used per appointment
65
+ - Revenue contribution
66
+ - Usage by procedure/zone
67
+ - Quantity and pricing trends
68
+
69
+ ### 7. Patient Analytics
70
+ - Lifetime value
71
+ - Retention rates
72
+ - Appointment frequency
73
+ - Cancellation patterns
74
+
75
+ ### 8. Clinic Analytics
76
+ - Overall performance metrics
77
+ - Practitioner comparisons
78
+ - Procedure popularity
79
+ - Efficiency metrics
80
+
81
+ ## Proposed Service Structure
82
+
83
+ The `AnalyticsService` will extend `BaseService` and provide methods organized by analytics category:
84
+
85
+ - `getPractitionerAnalytics()` - Comprehensive practitioner metrics
86
+ - `getProcedureAnalytics()` - Procedure performance data
87
+ - `getTimeEfficiencyMetrics()` - Time utilization analysis
88
+ - `getCancellationMetrics()` - Cancellation insights
89
+ - `getRevenueMetrics()` - Financial intelligence
90
+ - `getProductUsageMetrics()` - Product analytics
91
+ - `getPatientAnalytics()` - Patient insights
92
+ - `getClinicAnalytics()` - Clinic performance
93
+ - `getDashboardData()` - Comprehensive dashboard aggregation
94
+
95
+ ## Key Calculations
96
+
97
+ ### Cost Calculation Priority
98
+ 1. `metadata.finalbilling.finalPrice` (if available)
99
+ 2. Sum of `metadata.zonesData` subtotals
100
+ 3. `appointment.cost` (fallback)
101
+
102
+ ### Time Efficiency
103
+ ```
104
+ efficiency = (actualDuration / bookedDuration) * 100
105
+ overrun = actualDuration > bookedDuration ? difference : 0
106
+ ```
107
+
108
+ ### Cancellation Rate
109
+ ```
110
+ cancellationRate = (canceledCount / totalAppointments) * 100
111
+ ```
112
+
113
+ ## Implementation Phases
114
+
115
+ 1. **Phase 1**: Core infrastructure and utilities
116
+ 2. **Phase 2**: Basic metrics (practitioner, procedure, financial)
117
+ 3. **Phase 3**: Advanced analytics (time, products, patients)
118
+ 4. **Phase 4**: Dashboard and trend analysis
119
+
120
+ ## Performance Considerations
121
+
122
+ - Firestore composite indexes for query optimization
123
+ - Pagination for large datasets
124
+ - Caching strategy for frequently accessed metrics
125
+ - Batch processing for complex aggregations
126
+ - Server-side calculations via Cloud Functions
127
+
128
+ ## Next Steps
129
+
130
+ 1. Review and approve the detailed proposal
131
+ 2. Create TypeScript type definitions
132
+ 3. Implement core service structure
133
+ 4. Build utility functions for calculations
134
+ 5. Implement metrics methods incrementally
135
+ 6. Add comprehensive tests
136
+ 7. Create API documentation
137
+ 8. Integrate with Clinic Admin app
138
+
139
+ ## Documentation
140
+
141
+ - **Full Proposal**: [analytics.service.proposal.md](./analytics.service.proposal.md)
142
+ - **Service README**: [README.md](./README.md)
143
+
@@ -17,6 +17,12 @@ import { Category, CATEGORIES_COLLECTION, ICategoryService } from '../types/cate
17
17
  import { BaseService } from '../../services/base.service';
18
18
  import { ProcedureFamily } from '../types/static/procedure-family.types';
19
19
 
20
+ /**
21
+ * ID of the consultation category that should be hidden from admin backoffice.
22
+ * This category is used internally for free consultation procedures.
23
+ */
24
+ const EXCLUDED_CATEGORY_ID = 'consultation';
25
+
20
26
  /**
21
27
  * Servis za upravljanje kategorijama procedura.
22
28
  * Kategorije su prvi nivo organizacije nakon procedure family (aesthetics/surgery).
@@ -31,6 +37,14 @@ import { ProcedureFamily } from '../types/static/procedure-family.types';
31
37
  * });
32
38
  */
33
39
  export class CategoryService extends BaseService implements ICategoryService {
40
+ /**
41
+ * Filters out excluded categories from a list.
42
+ * @param categories - List of categories to filter
43
+ * @returns Filtered list without excluded categories
44
+ */
45
+ private filterExcludedCategories(categories: Category[]): Category[] {
46
+ return categories.filter(cat => cat.id !== EXCLUDED_CATEGORY_ID);
47
+ }
34
48
  /**
35
49
  * Referenca na Firestore kolekciju kategorija
36
50
  */
@@ -71,8 +85,10 @@ export class CategoryService extends BaseService implements ICategoryService {
71
85
  where('family', '==', family),
72
86
  where('isActive', '==', active),
73
87
  );
74
- const snapshot = await getCountFromServer(q);
75
- counts[family] = snapshot.data().count;
88
+ const snapshot = await getDocs(q);
89
+ // Filter out excluded category and count
90
+ const filteredDocs = snapshot.docs.filter(doc => doc.id !== EXCLUDED_CATEGORY_ID);
91
+ counts[family] = filteredDocs.length;
76
92
  }
77
93
  return counts;
78
94
  }
@@ -84,13 +100,14 @@ export class CategoryService extends BaseService implements ICategoryService {
84
100
  async getAllForFilter() {
85
101
  const q = query(this.categoriesRef, where('isActive', '==', true));
86
102
  const snapshot = await getDocs(q);
87
- return snapshot.docs.map(
103
+ const categories = snapshot.docs.map(
88
104
  doc =>
89
105
  ({
90
106
  id: doc.id,
91
107
  ...doc.data(),
92
108
  } as Category),
93
109
  );
110
+ return this.filterExcludedCategories(categories);
94
111
  }
95
112
 
96
113
  /**
@@ -106,13 +123,14 @@ export class CategoryService extends BaseService implements ICategoryService {
106
123
  orderBy('name'),
107
124
  );
108
125
  const snapshot = await getDocs(q);
109
- return snapshot.docs.map(
126
+ const categories = snapshot.docs.map(
110
127
  doc =>
111
128
  ({
112
129
  id: doc.id,
113
130
  ...doc.data(),
114
131
  } as Category),
115
132
  );
133
+ return this.filterExcludedCategories(categories);
116
134
  }
117
135
 
118
136
  /**
@@ -144,8 +162,9 @@ export class CategoryService extends BaseService implements ICategoryService {
144
162
  ...doc.data(),
145
163
  } as Category),
146
164
  );
165
+ const filteredCategories = this.filterExcludedCategories(categories);
147
166
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
148
- return { categories, lastVisible: newLastVisible };
167
+ return { categories: filteredCategories, lastVisible: newLastVisible };
149
168
  }
150
169
 
151
170
  /**
@@ -180,8 +199,9 @@ export class CategoryService extends BaseService implements ICategoryService {
180
199
  ...doc.data(),
181
200
  } as Category),
182
201
  );
202
+ const filteredCategories = this.filterExcludedCategories(categories);
183
203
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
184
- return { categories, lastVisible: newLastVisible };
204
+ return { categories: filteredCategories, lastVisible: newLastVisible };
185
205
  }
186
206
 
187
207
  /**
@@ -223,6 +243,25 @@ export class CategoryService extends BaseService implements ICategoryService {
223
243
  * @returns Kategorija ili null ako ne postoji
224
244
  */
225
245
  async getById(id: string) {
246
+ // Prevent access to excluded category
247
+ if (id === EXCLUDED_CATEGORY_ID) return null;
248
+
249
+ const docRef = doc(this.categoriesRef, id);
250
+ const docSnap = await getDoc(docRef);
251
+ if (!docSnap.exists()) return null;
252
+ return {
253
+ id: docSnap.id,
254
+ ...docSnap.data(),
255
+ } as Category;
256
+ }
257
+
258
+ /**
259
+ * Internal method to get category by ID without filtering.
260
+ * Used internally for consultation procedures.
261
+ * @param id - ID of the category to get
262
+ * @returns Category or null if not found
263
+ */
264
+ async getByIdInternal(id: string): Promise<Category | null> {
226
265
  const docRef = doc(this.categoriesRef, id);
227
266
  const docSnap = await getDoc(docRef);
228
267
  if (!docSnap.exists()) return null;
@@ -249,6 +288,8 @@ export class CategoryService extends BaseService implements ICategoryService {
249
288
  const snapshot = await getDocs(q);
250
289
  if (snapshot.empty) return null;
251
290
  const doc = snapshot.docs[0];
291
+ // Exclude consultation category
292
+ if (doc.id === EXCLUDED_CATEGORY_ID) return null;
252
293
  return {
253
294
  id: doc.id,
254
295
  ...doc.data(),
@@ -299,6 +340,8 @@ export class CategoryService extends BaseService implements ICategoryService {
299
340
  if (snapshot.empty) break;
300
341
 
301
342
  for (const d of snapshot.docs) {
343
+ // Exclude consultation category from CSV export
344
+ if (d.id === EXCLUDED_CATEGORY_ID) continue;
302
345
  const category = ({ id: d.id, ...d.data() } as unknown) as Category;
303
346
  rows.push(this.categoryToCsvRow(category));
304
347
  }
@@ -23,6 +23,12 @@ import {
23
23
  import { BaseService } from "../../services/base.service";
24
24
  import { CATEGORIES_COLLECTION } from "../types/category.types";
25
25
 
26
+ /**
27
+ * ID of the free-consultation subcategory that should be hidden from admin backoffice.
28
+ * This subcategory is used internally for free consultation procedures.
29
+ */
30
+ const EXCLUDED_SUBCATEGORY_ID = 'free-consultation';
31
+
26
32
  /**
27
33
  * Servis za upravljanje podkategorijama procedura.
28
34
  * Podkategorije su drugi nivo organizacije i pripadaju određenoj kategoriji.
@@ -37,6 +43,14 @@ import { CATEGORIES_COLLECTION } from "../types/category.types";
37
43
  * });
38
44
  */
39
45
  export class SubcategoryService extends BaseService {
46
+ /**
47
+ * Filters out excluded subcategories from a list.
48
+ * @param subcategories - List of subcategories to filter
49
+ * @returns Filtered list without excluded subcategories
50
+ */
51
+ private filterExcludedSubcategories(subcategories: Subcategory[]): Subcategory[] {
52
+ return subcategories.filter(sub => sub.id !== EXCLUDED_SUBCATEGORY_ID);
53
+ }
40
54
  /**
41
55
  * Vraća referencu na Firestore kolekciju podkategorija za određenu kategoriju
42
56
  * @param categoryId - ID roditeljske kategorije
@@ -90,8 +104,10 @@ export class SubcategoryService extends BaseService {
90
104
  const categoryId = categoryDoc.id;
91
105
  const subcategoriesRef = this.getSubcategoriesRef(categoryId);
92
106
  const q = query(subcategoriesRef, where("isActive", "==", active));
93
- const snapshot = await getCountFromServer(q);
94
- counts[categoryId] = snapshot.data().count;
107
+ const snapshot = await getDocs(q);
108
+ // Filter out excluded subcategory and count
109
+ const filteredDocs = snapshot.docs.filter(doc => doc.id !== EXCLUDED_SUBCATEGORY_ID);
110
+ counts[categoryId] = filteredDocs.length;
95
111
  }
96
112
 
97
113
  return counts;
@@ -129,8 +145,9 @@ export class SubcategoryService extends BaseService {
129
145
  ...doc.data(),
130
146
  } as Subcategory)
131
147
  );
148
+ const filteredSubcategories = this.filterExcludedSubcategories(subcategories);
132
149
  const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
133
- return { subcategories, lastVisible: newLastVisible };
150
+ return { subcategories: filteredSubcategories, lastVisible: newLastVisible };
134
151
  }
135
152
 
136
153
  /**
@@ -169,8 +186,9 @@ export class SubcategoryService extends BaseService {
169
186
  ...doc.data(),
170
187
  } as Subcategory)
171
188
  );
189
+ const filteredSubcategories = this.filterExcludedSubcategories(subcategories);
172
190
  const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
173
- return { subcategories, lastVisible: newLastVisible };
191
+ return { subcategories: filteredSubcategories, lastVisible: newLastVisible };
174
192
  }
175
193
 
176
194
  /**
@@ -184,13 +202,14 @@ export class SubcategoryService extends BaseService {
184
202
  where("isActive", "==", true)
185
203
  );
186
204
  const querySnapshot = await getDocs(q);
187
- return querySnapshot.docs.map(
205
+ const subcategories = querySnapshot.docs.map(
188
206
  (doc) =>
189
207
  ({
190
208
  id: doc.id,
191
209
  ...doc.data(),
192
210
  } as Subcategory)
193
211
  );
212
+ return this.filterExcludedSubcategories(subcategories);
194
213
  }
195
214
 
196
215
  /**
@@ -203,13 +222,14 @@ export class SubcategoryService extends BaseService {
203
222
  where("isActive", "==", true)
204
223
  );
205
224
  const querySnapshot = await getDocs(q);
206
- return querySnapshot.docs.map(
225
+ const subcategories = querySnapshot.docs.map(
207
226
  (doc) =>
208
227
  ({
209
228
  id: doc.id,
210
229
  ...doc.data(),
211
230
  } as Subcategory)
212
231
  );
232
+ return this.filterExcludedSubcategories(subcategories);
213
233
  }
214
234
 
215
235
  /**
@@ -297,6 +317,26 @@ export class SubcategoryService extends BaseService {
297
317
  * @returns Podkategorija ili null ako ne postoji
298
318
  */
299
319
  async getById(categoryId: string, subcategoryId: string) {
320
+ // Prevent access to excluded subcategory
321
+ if (subcategoryId === EXCLUDED_SUBCATEGORY_ID) return null;
322
+
323
+ const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
324
+ const docSnap = await getDoc(docRef);
325
+ if (!docSnap.exists()) return null;
326
+ return {
327
+ id: docSnap.id,
328
+ ...docSnap.data(),
329
+ } as Subcategory;
330
+ }
331
+
332
+ /**
333
+ * Internal method to get subcategory by ID without filtering.
334
+ * Used internally for consultation procedures.
335
+ * @param categoryId - ID of the category
336
+ * @param subcategoryId - ID of the subcategory to get
337
+ * @returns Subcategory or null if not found
338
+ */
339
+ async getByIdInternal(categoryId: string, subcategoryId: string): Promise<Subcategory | null> {
300
340
  const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
301
341
  const docSnap = await getDoc(docRef);
302
342
  if (!docSnap.exists()) return null;
@@ -322,6 +362,8 @@ export class SubcategoryService extends BaseService {
322
362
  const querySnapshot = await getDocs(q);
323
363
  if (querySnapshot.empty) return null;
324
364
  const doc = querySnapshot.docs[0];
365
+ // Exclude free-consultation subcategory
366
+ if (doc.id === EXCLUDED_SUBCATEGORY_ID) return null;
325
367
  return {
326
368
  id: doc.id,
327
369
  ...doc.data(),
@@ -375,6 +417,8 @@ export class SubcategoryService extends BaseService {
375
417
  if (snapshot.empty) break;
376
418
 
377
419
  for (const d of snapshot.docs) {
420
+ // Exclude free-consultation subcategory from CSV export
421
+ if (d.id === EXCLUDED_SUBCATEGORY_ID) continue;
378
422
  const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
379
423
  rows.push(this.subcategoryToCsvRow(subcategory));
380
424
  }
@@ -33,6 +33,12 @@ import { ProcedureFamily } from '../types/static/procedure-family.types';
33
33
  import { Practitioner, PractitionerCertification } from '../../types/practitioner';
34
34
  import { Product, PRODUCTS_COLLECTION } from '../types/product.types';
35
35
 
36
+ /**
37
+ * ID of the free-consultation-tech technology that should be hidden from admin backoffice.
38
+ * This technology is used internally for free consultation procedures.
39
+ */
40
+ const EXCLUDED_TECHNOLOGY_ID = 'free-consultation-tech';
41
+
36
42
  /**
37
43
  * Default vrednosti za sertifikaciju
38
44
  */
@@ -45,6 +51,14 @@ const DEFAULT_CERTIFICATION_REQUIREMENT: CertificationRequirement = {
45
51
  * Service for managing technologies.
46
52
  */
47
53
  export class TechnologyService extends BaseService implements ITechnologyService {
54
+ /**
55
+ * Filters out excluded technologies from a list.
56
+ * @param technologies - List of technologies to filter
57
+ * @returns Filtered list without excluded technologies
58
+ */
59
+ private filterExcludedTechnologies(technologies: Technology[]): Technology[] {
60
+ return technologies.filter(tech => tech.id !== EXCLUDED_TECHNOLOGY_ID);
61
+ }
48
62
  /**
49
63
  * Reference to the Firestore collection of technologies.
50
64
  */
@@ -100,6 +114,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
100
114
  const snapshot = await getDocs(q);
101
115
  const counts: Record<string, number> = {};
102
116
  snapshot.docs.forEach(doc => {
117
+ // Exclude free-consultation-tech from counts
118
+ if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
103
119
  const tech = doc.data() as Technology;
104
120
  counts[tech.subcategoryId] = (counts[tech.subcategoryId] || 0) + 1;
105
121
  });
@@ -116,6 +132,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
116
132
  const snapshot = await getDocs(q);
117
133
  const counts: Record<string, number> = {};
118
134
  snapshot.docs.forEach(doc => {
135
+ // Exclude free-consultation-tech from counts
136
+ if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
119
137
  const tech = doc.data() as Technology;
120
138
  counts[tech.categoryId] = (counts[tech.categoryId] || 0) + 1;
121
139
  });
@@ -151,8 +169,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
151
169
  ...doc.data(),
152
170
  } as Technology),
153
171
  );
172
+ const filteredTechnologies = this.filterExcludedTechnologies(technologies);
154
173
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
155
- return { technologies, lastVisible: newLastVisible };
174
+ return { technologies: filteredTechnologies, lastVisible: newLastVisible };
156
175
  }
157
176
 
158
177
  /**
@@ -187,8 +206,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
187
206
  ...doc.data(),
188
207
  } as Technology),
189
208
  );
209
+ const filteredTechnologies = this.filterExcludedTechnologies(technologies);
190
210
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
191
- return { technologies, lastVisible: newLastVisible };
211
+ return { technologies: filteredTechnologies, lastVisible: newLastVisible };
192
212
  }
193
213
 
194
214
  /**
@@ -223,8 +243,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
223
243
  ...doc.data(),
224
244
  } as Technology),
225
245
  );
246
+ const filteredTechnologies = this.filterExcludedTechnologies(technologies);
226
247
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
227
- return { technologies, lastVisible: newLastVisible };
248
+ return { technologies: filteredTechnologies, lastVisible: newLastVisible };
228
249
  }
229
250
 
230
251
  /**
@@ -300,6 +321,25 @@ export class TechnologyService extends BaseService implements ITechnologyService
300
321
  * @returns The technology or null if it doesn't exist.
301
322
  */
302
323
  async getById(id: string): Promise<Technology | null> {
324
+ // Prevent access to excluded technology
325
+ if (id === EXCLUDED_TECHNOLOGY_ID) return null;
326
+
327
+ const docRef = doc(this.technologiesRef, id);
328
+ const docSnap = await getDoc(docRef);
329
+ if (!docSnap.exists()) return null;
330
+ return {
331
+ id: docSnap.id,
332
+ ...docSnap.data(),
333
+ } as Technology;
334
+ }
335
+
336
+ /**
337
+ * Internal method to get technology by ID without filtering.
338
+ * Used internally for consultation procedures.
339
+ * @param id - The ID of the requested technology
340
+ * @returns The technology or null if it doesn't exist
341
+ */
342
+ async getByIdInternal(id: string): Promise<Technology | null> {
303
343
  const docRef = doc(this.technologiesRef, id);
304
344
  const docSnap = await getDoc(docRef);
305
345
  if (!docSnap.exists()) return null;
@@ -324,6 +364,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
324
364
  const snapshot = await getDocs(q);
325
365
  if (snapshot.empty) return null;
326
366
  const doc = snapshot.docs[0];
367
+ // Exclude free-consultation-tech
368
+ if (doc.id === EXCLUDED_TECHNOLOGY_ID) return null;
327
369
  return {
328
370
  id: doc.id,
329
371
  ...doc.data(),
@@ -780,13 +822,14 @@ export class TechnologyService extends BaseService implements ITechnologyService
780
822
  orderBy('name'),
781
823
  );
782
824
  const snapshot = await getDocs(q);
783
- return snapshot.docs.map(
825
+ const technologies = snapshot.docs.map(
784
826
  doc =>
785
827
  ({
786
828
  id: doc.id,
787
829
  ...doc.data(),
788
830
  } as Technology),
789
831
  );
832
+ return this.filterExcludedTechnologies(technologies);
790
833
  }
791
834
 
792
835
  /**
@@ -806,13 +849,14 @@ export class TechnologyService extends BaseService implements ITechnologyService
806
849
  orderBy('name'),
807
850
  );
808
851
  const snapshot = await getDocs(q);
809
- return snapshot.docs.map(
852
+ const technologies = snapshot.docs.map(
810
853
  doc =>
811
854
  ({
812
855
  id: doc.id,
813
856
  ...doc.data(),
814
857
  } as Technology),
815
858
  );
859
+ return this.filterExcludedTechnologies(technologies);
816
860
  }
817
861
 
818
862
  /**
@@ -825,13 +869,14 @@ export class TechnologyService extends BaseService implements ITechnologyService
825
869
  orderBy('name'),
826
870
  );
827
871
  const snapshot = await getDocs(q);
828
- return snapshot.docs.map(
872
+ const technologies = snapshot.docs.map(
829
873
  doc =>
830
874
  ({
831
875
  id: doc.id,
832
876
  ...doc.data(),
833
877
  } as Technology),
834
878
  );
879
+ return this.filterExcludedTechnologies(technologies);
835
880
  }
836
881
 
837
882
  // ==========================================
@@ -1040,6 +1085,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
1040
1085
  if (snapshot.empty) break;
1041
1086
 
1042
1087
  for (const d of snapshot.docs) {
1088
+ // Exclude free-consultation-tech from CSV export
1089
+ if (d.id === EXCLUDED_TECHNOLOGY_ID) continue;
1043
1090
  const technology = ({ id: d.id, ...d.data() } as unknown) as Technology;
1044
1091
  // Fetch products for this technology
1045
1092
  const productNames = await this.getProductNamesForTechnology(technology.id!);