@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/admin/index.d.mts +9 -18
- package/dist/admin/index.d.ts +9 -18
- package/dist/admin/index.js +20 -33
- package/dist/admin/index.mjs +20 -33
- package/dist/backoffice/index.d.mts +41 -54
- package/dist/backoffice/index.d.ts +41 -54
- package/dist/backoffice/index.js +23 -38
- package/dist/backoffice/index.mjs +23 -38
- package/dist/index.d.mts +21 -34
- package/dist/index.d.ts +21 -34
- package/dist/index.js +27 -40
- package/dist/index.mjs +27 -40
- package/package.json +1 -1
- package/src/admin/mailing/patientInvite/patientInvite.mailing.ts +3 -3
- package/src/admin/mailing/patientInvite/templates/invitation.template.ts +13 -28
- package/src/backoffice/services/brand.service.ts +4 -17
- package/src/backoffice/services/product.service.ts +12 -17
- package/src/backoffice/services/technology.service.ts +3 -1
- package/src/backoffice/types/brand.types.ts +0 -2
- package/src/backoffice/types/product.types.ts +18 -28
- package/src/services/analytics/analytics.service.ts +10 -4
package/dist/index.mjs
CHANGED
|
@@ -2138,7 +2138,8 @@ var AnalyticsService = class extends BaseService {
|
|
|
2138
2138
|
return metrics;
|
|
2139
2139
|
}
|
|
2140
2140
|
}
|
|
2141
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
23778
|
-
(_e = brand.
|
|
23779
|
-
(_f = brand.
|
|
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
|
-
|
|
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(
|
|
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
|
-
"
|
|
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
|
|
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
|
-
(
|
|
25800
|
-
(_g = product.
|
|
25801
|
-
(_h = product.
|
|
25802
|
-
|
|
25803
|
-
(
|
|
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
|
@@ -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 -
|
|
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
|
-
`
|
|
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 = `
|
|
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: #
|
|
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:
|
|
41
|
+
font-size: 24px;
|
|
52
42
|
font-weight: bold;
|
|
53
|
-
color: #
|
|
54
|
-
padding:
|
|
55
|
-
background-color: #
|
|
56
|
-
border-radius:
|
|
43
|
+
color: #4A90E2;
|
|
44
|
+
padding: 10px;
|
|
45
|
+
background-color: #e9f0f9;
|
|
46
|
+
border-radius: 4px;
|
|
57
47
|
display: inline-block;
|
|
58
|
-
letter-spacing:
|
|
59
|
-
margin:
|
|
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 #
|
|
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
|
|
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
|
|
101
|
-
<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>© {{currentYear}}
|
|
99
|
+
<p>© {{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
|
|
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
|
|
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
|
|
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
|
-
|
|
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'
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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 -
|
|
10
|
-
* @property brandName -
|
|
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
|
|
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
|
-
//
|
|
24
|
+
// Technology assignment tracking
|
|
30
25
|
assignedTechnologyIds?: string[];
|
|
31
26
|
|
|
32
|
-
//
|
|
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
|
-
|
|
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'
|
|
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
|
|
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
|
|
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') {
|