@blackcode_sa/metaestetics-api 1.14.32 → 1.14.36

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/index.mjs CHANGED
@@ -2138,7 +2138,8 @@ var AnalyticsService = class extends BaseService {
2138
2138
  return metrics;
2139
2139
  }
2140
2140
  }
2141
- const appointments = await this.fetchAppointments(void 0, dateRange);
2141
+ const filters = (options == null ? void 0 : options.clinicBranchId) ? { clinicBranchId: options.clinicBranchId } : void 0;
2142
+ const appointments = await this.fetchAppointments(filters, dateRange);
2142
2143
  const canceled = getCanceledAppointments(appointments);
2143
2144
  if (groupBy === "clinic") {
2144
2145
  return this.groupCancellationsByClinic(canceled, appointments);
@@ -2367,7 +2368,8 @@ var AnalyticsService = class extends BaseService {
2367
2368
  return metrics;
2368
2369
  }
2369
2370
  }
2370
- const appointments = await this.fetchAppointments(void 0, dateRange);
2371
+ const filters = (options == null ? void 0 : options.clinicBranchId) ? { clinicBranchId: options.clinicBranchId } : void 0;
2372
+ const appointments = await this.fetchAppointments(filters, dateRange);
2371
2373
  const noShow = getNoShowAppointments(appointments);
2372
2374
  if (groupBy === "clinic") {
2373
2375
  return this.groupNoShowsByClinic(noShow, appointments);
@@ -23616,13 +23618,12 @@ var BrandService = class extends BaseService {
23616
23618
  return { id: docRef.id, ...newBrand };
23617
23619
  }
23618
23620
  /**
23619
- * Gets a paginated list of active brands, optionally filtered by name and category.
23621
+ * Gets a paginated list of active brands, optionally filtered by name.
23620
23622
  * @param rowsPerPage - The number of brands to fetch.
23621
23623
  * @param searchTerm - An optional string to filter brand names by (starts-with search).
23622
23624
  * @param lastVisible - An optional document snapshot to use as a cursor for pagination.
23623
- * @param category - An optional category to filter brands by.
23624
23625
  */
23625
- async getAll(rowsPerPage, searchTerm, lastVisible, category) {
23626
+ async getAll(rowsPerPage, searchTerm, lastVisible) {
23626
23627
  const constraints = [
23627
23628
  where35("isActive", "==", true),
23628
23629
  orderBy19("name_lowercase")
@@ -23634,9 +23635,6 @@ var BrandService = class extends BaseService {
23634
23635
  where35("name_lowercase", "<=", lowercasedSearchTerm + "\uF8FF")
23635
23636
  );
23636
23637
  }
23637
- if (category) {
23638
- constraints.push(where35("category", "==", category));
23639
- }
23640
23638
  if (lastVisible) {
23641
23639
  constraints.push(startAfter15(lastVisible));
23642
23640
  }
@@ -23653,11 +23651,10 @@ var BrandService = class extends BaseService {
23653
23651
  return { brands, lastVisible: newLastVisible };
23654
23652
  }
23655
23653
  /**
23656
- * Gets the total count of active brands, optionally filtered by name and category.
23654
+ * Gets the total count of active brands, optionally filtered by name.
23657
23655
  * @param searchTerm - An optional string to filter brand names by (starts-with search).
23658
- * @param category - An optional category to filter brands by.
23659
23656
  */
23660
- async getBrandsCount(searchTerm, category) {
23657
+ async getBrandsCount(searchTerm) {
23661
23658
  const constraints = [where35("isActive", "==", true)];
23662
23659
  if (searchTerm) {
23663
23660
  const lowercasedSearchTerm = searchTerm.toLowerCase();
@@ -23666,9 +23663,6 @@ var BrandService = class extends BaseService {
23666
23663
  where35("name_lowercase", "<=", lowercasedSearchTerm + "\uF8FF")
23667
23664
  );
23668
23665
  }
23669
- if (category) {
23670
- constraints.push(where35("category", "==", category));
23671
- }
23672
23666
  const q = query35(this.getBrandsRef(), ...constraints);
23673
23667
  const snapshot = await getCountFromServer3(q);
23674
23668
  return snapshot.data().count;
@@ -23738,7 +23732,6 @@ var BrandService = class extends BaseService {
23738
23732
  "id",
23739
23733
  "name",
23740
23734
  "manufacturer",
23741
- "category",
23742
23735
  "website",
23743
23736
  "description",
23744
23737
  "isActive"
@@ -23769,15 +23762,14 @@ var BrandService = class extends BaseService {
23769
23762
  return includeBom ? "\uFEFF" + csvBody : csvBody;
23770
23763
  }
23771
23764
  brandToCsvRow(brand) {
23772
- var _a, _b, _c, _d, _e, _f, _g;
23765
+ var _a, _b, _c, _d, _e, _f;
23773
23766
  const values = [
23774
23767
  (_a = brand.id) != null ? _a : "",
23775
23768
  (_b = brand.name) != null ? _b : "",
23776
23769
  (_c = brand.manufacturer) != null ? _c : "",
23777
- (_d = brand.category) != null ? _d : "",
23778
- (_e = brand.website) != null ? _e : "",
23779
- (_f = brand.description) != null ? _f : "",
23780
- String((_g = brand.isActive) != null ? _g : "")
23770
+ (_d = brand.website) != null ? _d : "",
23771
+ (_e = brand.description) != null ? _e : "",
23772
+ String((_f = brand.isActive) != null ? _f : "")
23781
23773
  ];
23782
23774
  return values.map((v) => this.formatCsvValue(v)).join(",");
23783
23775
  }
@@ -25245,7 +25237,8 @@ var TechnologyService = class extends BaseService {
25245
25237
  const products = await this.getAssignedProducts(technologyId);
25246
25238
  const byBrand = {};
25247
25239
  products.forEach((product) => {
25248
- byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
25240
+ const brandName = product.brandName || "Unknown";
25241
+ byBrand[brandName] = (byBrand[brandName] || 0) + 1;
25249
25242
  });
25250
25243
  return {
25251
25244
  totalAssigned: products.length,
@@ -25592,11 +25585,10 @@ var ProductService = class extends BaseService {
25592
25585
  /**
25593
25586
  * Creates a new product in the top-level collection
25594
25587
  */
25595
- async createTopLevel(brandId, product, technologyIds = []) {
25588
+ async createTopLevel(product, technologyIds = []) {
25596
25589
  const now = /* @__PURE__ */ new Date();
25597
25590
  const newProduct = {
25598
25591
  ...product,
25599
- brandId,
25600
25592
  assignedTechnologyIds: technologyIds,
25601
25593
  createdAt: now,
25602
25594
  updatedAt: now,
@@ -25609,11 +25601,14 @@ var ProductService = class extends BaseService {
25609
25601
  * Gets all products from the top-level collection
25610
25602
  */
25611
25603
  async getAllTopLevel(options) {
25612
- const { rowsPerPage, lastVisible, brandId } = options;
25604
+ const { rowsPerPage, lastVisible, brandId, category } = options;
25613
25605
  const constraints = [where39("isActive", "==", true), orderBy23("name")];
25614
25606
  if (brandId) {
25615
25607
  constraints.push(where39("brandId", "==", brandId));
25616
25608
  }
25609
+ if (category) {
25610
+ constraints.push(where39("category", "==", category));
25611
+ }
25617
25612
  if (lastVisible) {
25618
25613
  constraints.push(startAfter19(lastVisible));
25619
25614
  }
@@ -25754,14 +25749,10 @@ var ProductService = class extends BaseService {
25754
25749
  "name",
25755
25750
  "brandId",
25756
25751
  "brandName",
25752
+ "category",
25757
25753
  "assignedTechnologyIds",
25758
25754
  "description",
25759
- "technicalDetails",
25760
- "dosage",
25761
- "composition",
25762
- "indications",
25763
- "contraindications",
25764
- "warnings",
25755
+ "metadata",
25765
25756
  "isActive"
25766
25757
  ];
25767
25758
  const rows = [];
@@ -25790,21 +25781,17 @@ var ProductService = class extends BaseService {
25790
25781
  return includeBom ? "\uFEFF" + csvBody : csvBody;
25791
25782
  }
25792
25783
  productToCsvRow(product) {
25793
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
25784
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
25794
25785
  const values = [
25795
25786
  (_a = product.id) != null ? _a : "",
25796
25787
  (_b = product.name) != null ? _b : "",
25797
25788
  (_c = product.brandId) != null ? _c : "",
25798
25789
  (_d = product.brandName) != null ? _d : "",
25799
- (_f = (_e = product.assignedTechnologyIds) == null ? void 0 : _e.join(";")) != null ? _f : "",
25800
- (_g = product.description) != null ? _g : "",
25801
- (_h = product.technicalDetails) != null ? _h : "",
25802
- (_i = product.dosage) != null ? _i : "",
25803
- (_j = product.composition) != null ? _j : "",
25804
- (_l = (_k = product.indications) == null ? void 0 : _k.join(";")) != null ? _l : "",
25805
- (_n = (_m = product.contraindications) == null ? void 0 : _m.map((c) => c.name).join(";")) != null ? _n : "",
25806
- (_p = (_o = product.warnings) == null ? void 0 : _o.join(";")) != null ? _p : "",
25807
- String((_q = product.isActive) != null ? _q : "")
25790
+ (_e = product.category) != null ? _e : "",
25791
+ (_g = (_f = product.assignedTechnologyIds) == null ? void 0 : _f.join(";")) != null ? _g : "",
25792
+ (_h = product.description) != null ? _h : "",
25793
+ product.metadata ? JSON.stringify(product.metadata) : "",
25794
+ String((_i = product.isActive) != null ? _i : "")
25808
25795
  ];
25809
25796
  return values.map((v) => this.formatCsvValue(v)).join(",");
25810
25797
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.14.32",
4
+ "version": "1.14.36",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -69,7 +69,7 @@ export class PatientInviteMailingService extends BaseMailingService {
69
69
  private readonly DEFAULT_REGISTRATION_URL =
70
70
  "https://metaesthetics.net/patient/register";
71
71
  private readonly DEFAULT_SUBJECT =
72
- "Claim Your Patient Profile - MetaEstetics";
72
+ "Claim Your Patient Profile - MetaEsthetics";
73
73
  private readonly DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
74
74
 
75
75
  /**
@@ -120,7 +120,7 @@ export class PatientInviteMailingService extends BaseMailingService {
120
120
  // Determine 'from' address
121
121
  const fromAddress =
122
122
  data.options?.fromAddress ||
123
- `MetaEstetics <no-reply@${
123
+ `MetaEsthetics <no-reply@${
124
124
  data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN
125
125
  }>`;
126
126
 
@@ -358,7 +358,7 @@ export class PatientInviteMailingService extends BaseMailingService {
358
358
  Logger.warn(
359
359
  "[PatientInviteMailingService] No fromAddress provided, using default"
360
360
  );
361
- mailgunConfig.fromAddress = `MetaEstetics <no-reply@${this.DEFAULT_MAILGUN_DOMAIN}>`;
361
+ mailgunConfig.fromAddress = `MetaEsthetics <no-reply@${this.DEFAULT_MAILGUN_DOMAIN}>`;
362
362
  }
363
363
 
364
364
  // Prepare email data
@@ -22,7 +22,7 @@ export const patientInvitationTemplate = `
22
22
  padding: 20px;
23
23
  }
24
24
  .header {
25
- background-color: #2C8E99;
25
+ background-color: #4A90E2;
26
26
  padding: 20px;
27
27
  text-align: center;
28
28
  color: white;
@@ -37,31 +37,20 @@ export const patientInvitationTemplate = `
37
37
  font-size: 12px;
38
38
  color: #888;
39
39
  }
40
- .button {
41
- display: inline-block;
42
- background-color: #2C8E99;
43
- color: white;
44
- text-decoration: none;
45
- padding: 12px 24px;
46
- border-radius: 4px;
47
- margin: 20px 0;
48
- font-weight: bold;
49
- }
50
40
  .token {
51
- font-size: 28px;
41
+ font-size: 24px;
52
42
  font-weight: bold;
53
- color: #2C8E99;
54
- padding: 15px 25px;
55
- background-color: #e0f4f6;
56
- border-radius: 8px;
43
+ color: #4A90E2;
44
+ padding: 10px;
45
+ background-color: #e9f0f9;
46
+ border-radius: 4px;
57
47
  display: inline-block;
58
- letter-spacing: 4px;
59
- margin: 15px 0;
60
- font-family: monospace;
48
+ letter-spacing: 2px;
49
+ margin: 10px 0;
61
50
  }
62
51
  .info-box {
63
52
  background-color: #fff;
64
- border-left: 4px solid #2C8E99;
53
+ border-left: 4px solid #4A90E2;
65
54
  padding: 15px;
66
55
  margin: 20px 0;
67
56
  }
@@ -70,7 +59,7 @@ export const patientInvitationTemplate = `
70
59
  <body>
71
60
  <div class="container">
72
61
  <div class="header">
73
- <h1>Welcome to MetaEstetics</h1>
62
+ <h1>Welcome to MetaEsthetics</h1>
74
63
  </div>
75
64
  <div class="content">
76
65
  <p>Hello {{patientName}},</p>
@@ -97,21 +86,17 @@ export const patientInvitationTemplate = `
97
86
 
98
87
  <p>To create your account:</p>
99
88
  <ol>
100
- <li>Download the MetaEstetics Patient app or visit {{registrationUrl}}</li>
101
- <li>Create an account using your email address</li>
89
+ <li>Download the <strong>MetaEsthetics</strong> app from the App Store (iOS) or Google Play Store (Android)</li>
90
+ <li>Open the app and create an account using your email address</li>
102
91
  <li>When prompted, enter the token shown above</li>
103
92
  <li>Your profile will be automatically linked to your new account</li>
104
93
  </ol>
105
94
 
106
- <div style="text-align: center;">
107
- <a href="{{registrationUrl}}" class="button">Create Your Account</a>
108
- </div>
109
-
110
95
  <p>If you have any questions or didn't expect this email, please contact {{contactName}} at {{contactEmail}}.</p>
111
96
  </div>
112
97
  <div class="footer">
113
98
  <p>This is an automated message from {{clinicName}}. Please do not reply to this email.</p>
114
- <p>&copy; {{currentYear}} MetaEstetics. All rights reserved.</p>
99
+ <p>&copy; {{currentYear}} MetaEsthetics. All rights reserved.</p>
115
100
  </div>
116
101
  </div>
117
102
  </body>
@@ -46,17 +46,15 @@ export class BrandService extends BaseService {
46
46
  }
47
47
 
48
48
  /**
49
- * Gets a paginated list of active brands, optionally filtered by name and category.
49
+ * Gets a paginated list of active brands, optionally filtered by name.
50
50
  * @param rowsPerPage - The number of brands to fetch.
51
51
  * @param searchTerm - An optional string to filter brand names by (starts-with search).
52
52
  * @param lastVisible - An optional document snapshot to use as a cursor for pagination.
53
- * @param category - An optional category to filter brands by.
54
53
  */
55
54
  async getAll(
56
55
  rowsPerPage: number,
57
56
  searchTerm?: string,
58
- lastVisible?: any,
59
- category?: string
57
+ lastVisible?: any
60
58
  ) {
61
59
  const constraints: QueryConstraint[] = [
62
60
  where("isActive", "==", true),
@@ -71,10 +69,6 @@ export class BrandService extends BaseService {
71
69
  );
72
70
  }
73
71
 
74
- if (category) {
75
- constraints.push(where("category", "==", category));
76
- }
77
-
78
72
  if (lastVisible) {
79
73
  constraints.push(startAfter(lastVisible));
80
74
  }
@@ -97,11 +91,10 @@ export class BrandService extends BaseService {
97
91
  }
98
92
 
99
93
  /**
100
- * Gets the total count of active brands, optionally filtered by name and category.
94
+ * Gets the total count of active brands, optionally filtered by name.
101
95
  * @param searchTerm - An optional string to filter brand names by (starts-with search).
102
- * @param category - An optional category to filter brands by.
103
96
  */
104
- async getBrandsCount(searchTerm?: string, category?: string) {
97
+ async getBrandsCount(searchTerm?: string) {
105
98
  const constraints: QueryConstraint[] = [where("isActive", "==", true)];
106
99
 
107
100
  if (searchTerm) {
@@ -112,10 +105,6 @@ export class BrandService extends BaseService {
112
105
  );
113
106
  }
114
107
 
115
- if (category) {
116
- constraints.push(where("category", "==", category));
117
- }
118
-
119
108
  const q = query(this.getBrandsRef(), ...constraints);
120
109
  const snapshot = await getCountFromServer(q);
121
110
  return snapshot.data().count;
@@ -199,7 +188,6 @@ export class BrandService extends BaseService {
199
188
  "id",
200
189
  "name",
201
190
  "manufacturer",
202
- "category",
203
191
  "website",
204
192
  "description",
205
193
  "isActive",
@@ -246,7 +234,6 @@ export class BrandService extends BaseService {
246
234
  brand.id ?? "",
247
235
  brand.name ?? "",
248
236
  brand.manufacturer ?? "",
249
- brand.category ?? "",
250
237
  brand.website ?? "",
251
238
  brand.description ?? "",
252
239
  String(brand.isActive ?? ""),
@@ -268,14 +268,12 @@ export class ProductService extends BaseService implements IProductService {
268
268
  * Creates a new product in the top-level collection
269
269
  */
270
270
  async createTopLevel(
271
- brandId: string,
272
- product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'assignedTechnologyIds'>,
271
+ product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'assignedTechnologyIds'>,
273
272
  technologyIds: string[] = [],
274
273
  ): Promise<Product> {
275
274
  const now = new Date();
276
275
  const newProduct: Omit<Product, 'id'> = {
277
276
  ...product,
278
- brandId,
279
277
  assignedTechnologyIds: technologyIds,
280
278
  createdAt: now,
281
279
  updatedAt: now,
@@ -293,8 +291,9 @@ export class ProductService extends BaseService implements IProductService {
293
291
  rowsPerPage: number;
294
292
  lastVisible?: any;
295
293
  brandId?: string;
294
+ category?: string;
296
295
  }): Promise<{ products: Product[]; lastVisible: any }> {
297
- const { rowsPerPage, lastVisible, brandId } = options;
296
+ const { rowsPerPage, lastVisible, brandId, category } = options;
298
297
 
299
298
  const constraints: QueryConstraint[] = [where('isActive', '==', true), orderBy('name')];
300
299
 
@@ -302,6 +301,10 @@ export class ProductService extends BaseService implements IProductService {
302
301
  constraints.push(where('brandId', '==', brandId));
303
302
  }
304
303
 
304
+ if (category) {
305
+ constraints.push(where('category', '==', category));
306
+ }
307
+
305
308
  if (lastVisible) {
306
309
  constraints.push(startAfter(lastVisible));
307
310
  }
@@ -340,7 +343,7 @@ export class ProductService extends BaseService implements IProductService {
340
343
  */
341
344
  async updateTopLevel(
342
345
  productId: string,
343
- product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId'>>,
346
+ product: Partial<Omit<Product, 'id' | 'createdAt'>>,
344
347
  ): Promise<Product | null> {
345
348
  const updateData = {
346
349
  ...product,
@@ -468,14 +471,10 @@ export class ProductService extends BaseService implements IProductService {
468
471
  "name",
469
472
  "brandId",
470
473
  "brandName",
474
+ "category",
471
475
  "assignedTechnologyIds",
472
476
  "description",
473
- "technicalDetails",
474
- "dosage",
475
- "composition",
476
- "indications",
477
- "contraindications",
478
- "warnings",
477
+ "metadata",
479
478
  "isActive",
480
479
  ];
481
480
 
@@ -521,14 +520,10 @@ export class ProductService extends BaseService implements IProductService {
521
520
  product.name ?? "",
522
521
  product.brandId ?? "",
523
522
  product.brandName ?? "",
523
+ product.category ?? "",
524
524
  product.assignedTechnologyIds?.join(";") ?? "",
525
525
  product.description ?? "",
526
- product.technicalDetails ?? "",
527
- product.dosage ?? "",
528
- product.composition ?? "",
529
- product.indications?.join(";") ?? "",
530
- product.contraindications?.map(c => c.name).join(";") ?? "",
531
- product.warnings?.join(";") ?? "",
526
+ product.metadata ? JSON.stringify(product.metadata) : "",
532
527
  String(product.isActive ?? ""),
533
528
  ];
534
529
  return values.map((v) => this.formatCsvValue(v)).join(",");
@@ -979,7 +979,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
979
979
 
980
980
  const byBrand: Record<string, number> = {};
981
981
  products.forEach(product => {
982
- byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
982
+ // Use brandName for grouping stats
983
+ const brandName = product.brandName || 'Unknown';
984
+ byBrand[brandName] = (byBrand[brandName] || 0) + 1;
983
985
  });
984
986
 
985
987
  return {
@@ -7,7 +7,6 @@
7
7
  * @property manufacturer - Naziv proizvođača
8
8
  * @property description - Detaljan opis brenda i njegovih proizvoda
9
9
  * @property website - Web stranica brenda
10
- * @property category - Kategorija brenda (npr. "laser", "peeling", "injectables") - za filtriranje
11
10
  * @property isActive - Da li je brend aktivan u sistemu
12
11
  * @property createdAt - Datum kreiranja
13
12
  * @property updatedAt - Datum poslednjeg ažuriranja
@@ -22,7 +21,6 @@ export interface Brand {
22
21
  isActive: boolean;
23
22
  website?: string;
24
23
  description?: string;
25
- category?: string;
26
24
  }
27
25
 
28
26
  /**
@@ -1,21 +1,14 @@
1
- import type { ContraindicationDynamic } from './admin-constants.types';
2
-
3
1
  /**
4
2
  * Product used in procedures
5
- * Can be consumables, equipment, or any other product needed for performing procedures
3
+ * Simplified structure with only essential fields and flexible metadata
6
4
  *
7
5
  * @property id - Unique identifier of the product
8
6
  * @property name - Name of the product
9
- * @property brandId - ID of the brand that manufactures this product
10
- * @property brandName - Name of the brand (denormalized for display)
7
+ * @property brandId - Reference to the Brand document ID
8
+ * @property brandName - Display name of the brand (denormalized for performance)
9
+ * @property description - Detailed description of the product
11
10
  * @property assignedTechnologyIds - Array of technology IDs this product is assigned to
12
- * @property description - Detailed description of the product and its purpose
13
- * @property technicalDetails - Technical details and specifications
14
- * @property warnings - List of warnings related to product use
15
- * @property dosage - Dosage information (if applicable)
16
- * @property composition - Product composition
17
- * @property indications - List of indications for use
18
- * @property contraindications - List of contraindications
11
+ * @property metadata - Flexible key-value pairs for additional product information
19
12
  * @property isActive - Whether the product is active in the system
20
13
  * @property createdAt - Creation date
21
14
  * @property updatedAt - Last update date
@@ -23,23 +16,21 @@ import type { ContraindicationDynamic } from './admin-constants.types';
23
16
  export interface Product {
24
17
  id?: string;
25
18
  name: string;
26
- brandId: string;
27
- brandName: string;
19
+ brandId: string; // Reference to Brand document
20
+ brandName: string; // Denormalized brand name for display
21
+ description: string;
22
+ category?: string; // "Injectables", "Laser Devices", "Skincare", etc.
28
23
 
29
- // NEW: Technology assignment tracking
24
+ // Technology assignment tracking
30
25
  assignedTechnologyIds?: string[];
31
26
 
32
- // Product details
27
+ // Flexible metadata for any additional information
28
+ metadata?: Record<string, string | number | boolean>;
29
+
30
+ // System fields
31
+ isActive: boolean;
33
32
  createdAt: Date;
34
33
  updatedAt: Date;
35
- isActive: boolean;
36
- description?: string;
37
- technicalDetails?: string;
38
- warnings?: string[];
39
- dosage?: string;
40
- composition?: string;
41
- indications?: string[];
42
- contraindications?: ContraindicationDynamic[];
43
34
 
44
35
  // LEGACY FIELDS: Only present in technology subcollections (/technologies/{id}/products/)
45
36
  // These fields are synced by Cloud Functions for backward compatibility
@@ -72,13 +63,11 @@ export interface IProductService {
72
63
 
73
64
  /**
74
65
  * Creates a new product in the top-level collection
75
- * @param brandId - ID of the brand that manufactures this product
76
66
  * @param product - Product data
77
67
  * @param technologyIds - Optional array of technology IDs to assign this product to
78
68
  */
79
69
  createTopLevel(
80
- brandId: string,
81
- product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'assignedTechnologyIds'>,
70
+ product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'assignedTechnologyIds'>,
82
71
  technologyIds?: string[],
83
72
  ): Promise<Product>;
84
73
 
@@ -90,6 +79,7 @@ export interface IProductService {
90
79
  rowsPerPage: number;
91
80
  lastVisible?: any;
92
81
  brandId?: string;
82
+ category?: string;
93
83
  }): Promise<{ products: Product[]; lastVisible: any }>;
94
84
 
95
85
  /**
@@ -105,7 +95,7 @@ export interface IProductService {
105
95
  */
106
96
  updateTopLevel(
107
97
  productId: string,
108
- product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId'>>,
98
+ product: Partial<Omit<Product, 'id' | 'createdAt'>>,
109
99
  ): Promise<Product | null>;
110
100
 
111
101
  /**
@@ -644,8 +644,11 @@ export class AnalyticsService extends BaseService {
644
644
  }
645
645
  }
646
646
 
647
- // Fall back to calculation
648
- const appointments = await this.fetchAppointments(undefined, dateRange);
647
+ // Fall back to calculation - filter by clinic if specified
648
+ const filters: AnalyticsFilters | undefined = options?.clinicBranchId
649
+ ? { clinicBranchId: options.clinicBranchId }
650
+ : undefined;
651
+ const appointments = await this.fetchAppointments(filters, dateRange);
649
652
  const canceled = getCanceledAppointments(appointments);
650
653
 
651
654
  if (groupBy === 'clinic') {
@@ -941,8 +944,11 @@ export class AnalyticsService extends BaseService {
941
944
  }
942
945
  }
943
946
 
944
- // Fall back to calculation
945
- const appointments = await this.fetchAppointments(undefined, dateRange);
947
+ // Fall back to calculation - filter by clinic if specified
948
+ const filters: AnalyticsFilters | undefined = options?.clinicBranchId
949
+ ? { clinicBranchId: options.clinicBranchId }
950
+ : undefined;
951
+ const appointments = await this.fetchAppointments(filters, dateRange);
946
952
  const noShow = getNoShowAppointments(appointments);
947
953
 
948
954
  if (groupBy === 'clinic') {