myio-js-library 0.1.157 → 0.1.159
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.cjs +458 -160
- package/dist/index.js +458 -160
- package/dist/myio-js-library.umd.js +458 -160
- package/dist/myio-js-library.umd.min.js +1 -1
- package/package.json +1 -1
|
@@ -5018,8 +5018,14 @@
|
|
|
5018
5018
|
}
|
|
5019
5019
|
function paint(root, state) {
|
|
5020
5020
|
const { entityObject, i18n, delayTimeConnectionInMins } = state;
|
|
5021
|
-
if (
|
|
5022
|
-
entityObject.
|
|
5021
|
+
if (entityObject.connectionStatus) {
|
|
5022
|
+
if (entityObject.connectionStatus === "offline") {
|
|
5023
|
+
entityObject.deviceStatus = DeviceStatusType.NO_INFO;
|
|
5024
|
+
}
|
|
5025
|
+
} else {
|
|
5026
|
+
if (verifyOfflineStatus(entityObject, delayTimeConnectionInMins) === false) {
|
|
5027
|
+
entityObject.deviceStatus = DeviceStatusType.NO_INFO;
|
|
5028
|
+
}
|
|
5023
5029
|
}
|
|
5024
5030
|
const stateClass = getCardStateClass(entityObject.deviceStatus);
|
|
5025
5031
|
root.className = `myio-ho-card ${stateClass}`;
|
|
@@ -10696,11 +10702,14 @@ ${rangeText}`;
|
|
|
10696
10702
|
isLoading = false;
|
|
10697
10703
|
currentTheme = "dark";
|
|
10698
10704
|
currentBarMode = "stacked";
|
|
10705
|
+
// RFC-0097: Granularity selector state (only 1h and 1d supported)
|
|
10706
|
+
currentGranularity = "1d";
|
|
10699
10707
|
constructor(modal, config) {
|
|
10700
10708
|
this.modal = modal;
|
|
10701
10709
|
this.config = config;
|
|
10702
10710
|
this.initializeTheme();
|
|
10703
10711
|
this.initializeBarMode();
|
|
10712
|
+
this.initializeGranularity();
|
|
10704
10713
|
this.validateConfiguration();
|
|
10705
10714
|
this.render();
|
|
10706
10715
|
}
|
|
@@ -10718,6 +10727,59 @@ ${rangeText}`;
|
|
|
10718
10727
|
const savedBarMode = localStorage.getItem("myio-modal-bar-mode");
|
|
10719
10728
|
this.currentBarMode = savedBarMode || "stacked";
|
|
10720
10729
|
}
|
|
10730
|
+
/**
|
|
10731
|
+
* RFC-0097: Initializes granularity from config or localStorage
|
|
10732
|
+
*/
|
|
10733
|
+
initializeGranularity() {
|
|
10734
|
+
const savedGranularity = localStorage.getItem("myio-modal-granularity");
|
|
10735
|
+
const configGranularity = this.config.params.granularity;
|
|
10736
|
+
const candidate = savedGranularity || configGranularity || "1d";
|
|
10737
|
+
this.currentGranularity = candidate === "1h" || candidate === "1d" ? candidate : "1d";
|
|
10738
|
+
}
|
|
10739
|
+
/**
|
|
10740
|
+
* RFC-0097: Sets granularity and re-renders chart
|
|
10741
|
+
*/
|
|
10742
|
+
setGranularity(granularity) {
|
|
10743
|
+
if (this.currentGranularity === granularity) return;
|
|
10744
|
+
this.currentGranularity = granularity;
|
|
10745
|
+
const buttons = document.querySelectorAll(".myio-btn-granularity");
|
|
10746
|
+
buttons.forEach((btn) => {
|
|
10747
|
+
const btnEl = btn;
|
|
10748
|
+
if (btnEl.dataset.granularity === granularity) {
|
|
10749
|
+
btnEl.classList.add("active");
|
|
10750
|
+
} else {
|
|
10751
|
+
btnEl.classList.remove("active");
|
|
10752
|
+
}
|
|
10753
|
+
});
|
|
10754
|
+
localStorage.setItem("myio-modal-granularity", granularity);
|
|
10755
|
+
this.reRenderChart();
|
|
10756
|
+
console.log("[EnergyModalView] [RFC-0097] Granularity changed to:", granularity);
|
|
10757
|
+
}
|
|
10758
|
+
/**
|
|
10759
|
+
* RFC-0097: Calculates suggested granularity based on date range
|
|
10760
|
+
* Only supports '1h' (hour) and '1d' (day)
|
|
10761
|
+
*/
|
|
10762
|
+
calculateSuggestedGranularity(startDate, endDate) {
|
|
10763
|
+
const start = new Date(startDate);
|
|
10764
|
+
const end = new Date(endDate);
|
|
10765
|
+
const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24));
|
|
10766
|
+
if (diffDays <= 1) return "1h";
|
|
10767
|
+
return "1d";
|
|
10768
|
+
}
|
|
10769
|
+
/**
|
|
10770
|
+
* RFC-0097: Applies granularity UI state (highlights correct button)
|
|
10771
|
+
*/
|
|
10772
|
+
applyGranularityUI() {
|
|
10773
|
+
const buttons = document.querySelectorAll(".myio-btn-granularity");
|
|
10774
|
+
buttons.forEach((btn) => {
|
|
10775
|
+
const btnEl = btn;
|
|
10776
|
+
if (btnEl.dataset.granularity === this.currentGranularity) {
|
|
10777
|
+
btnEl.classList.add("active");
|
|
10778
|
+
} else {
|
|
10779
|
+
btnEl.classList.remove("active");
|
|
10780
|
+
}
|
|
10781
|
+
});
|
|
10782
|
+
}
|
|
10721
10783
|
/**
|
|
10722
10784
|
* Toggles between dark and light theme
|
|
10723
10785
|
*/
|
|
@@ -10988,6 +11050,14 @@ ${rangeText}`;
|
|
|
10988
11050
|
</svg>
|
|
10989
11051
|
</button>
|
|
10990
11052
|
` : ""}
|
|
11053
|
+
${this.config.params.mode === "comparison" ? `
|
|
11054
|
+
<!-- RFC-0097: Granularity Selector (only 1h and 1d supported) -->
|
|
11055
|
+
<div class="myio-granularity-selector" style="display: flex; align-items: center; gap: 4px; margin-left: 8px; padding: 4px 8px; background: rgba(0,0,0,0.05); border-radius: 8px;">
|
|
11056
|
+
<span style="font-size: 11px; color: #666; margin-right: 4px; white-space: nowrap;">Granularidade:</span>
|
|
11057
|
+
<button class="myio-btn myio-btn-granularity ${this.currentGranularity === "1h" ? "active" : ""}" data-granularity="1h" title="Hora">1h</button>
|
|
11058
|
+
<button class="myio-btn myio-btn-granularity ${this.currentGranularity === "1d" ? "active" : ""}" data-granularity="1d" title="Dia">1d</button>
|
|
11059
|
+
</div>
|
|
11060
|
+
` : ""}
|
|
10991
11061
|
<button id="close-btn" class="myio-btn myio-btn-secondary">
|
|
10992
11062
|
Fechar
|
|
10993
11063
|
</button>
|
|
@@ -11210,8 +11280,8 @@ ${rangeText}`;
|
|
|
11210
11280
|
// ← NO TIME (YYYY-MM-DD)
|
|
11211
11281
|
endDate: endDateStr,
|
|
11212
11282
|
// ← NO TIME (YYYY-MM-DD)
|
|
11213
|
-
granularity: this.
|
|
11214
|
-
//
|
|
11283
|
+
granularity: this.currentGranularity,
|
|
11284
|
+
// RFC-0097: Use current granularity from selector
|
|
11215
11285
|
theme: this.currentTheme,
|
|
11216
11286
|
// ← Use current theme (dynamic)
|
|
11217
11287
|
bar_mode: this.currentBarMode,
|
|
@@ -11283,7 +11353,8 @@ ${rangeText}`;
|
|
|
11283
11353
|
readingType: "temperature",
|
|
11284
11354
|
startDate: startDateStr,
|
|
11285
11355
|
endDate: endDateStr,
|
|
11286
|
-
granularity: this.
|
|
11356
|
+
granularity: this.currentGranularity,
|
|
11357
|
+
// RFC-0097: Use current granularity from selector
|
|
11287
11358
|
theme: this.currentTheme,
|
|
11288
11359
|
timezone: tzIdentifier,
|
|
11289
11360
|
iframeBaseUrl: this.config.params.chartsBaseUrl || "https://graphs.apps.myio-bas.com",
|
|
@@ -11606,6 +11677,20 @@ ${rangeText}`;
|
|
|
11606
11677
|
this.toggleBarMode();
|
|
11607
11678
|
});
|
|
11608
11679
|
}
|
|
11680
|
+
const granularityButtons = document.querySelectorAll(".myio-btn-granularity");
|
|
11681
|
+
if (granularityButtons.length > 0) {
|
|
11682
|
+
this.applyGranularityUI();
|
|
11683
|
+
granularityButtons.forEach((btn) => {
|
|
11684
|
+
btn.addEventListener("click", (e) => {
|
|
11685
|
+
const target = e.currentTarget;
|
|
11686
|
+
const newGranularity = target.dataset.granularity;
|
|
11687
|
+
if (newGranularity) {
|
|
11688
|
+
this.setGranularity(newGranularity);
|
|
11689
|
+
}
|
|
11690
|
+
});
|
|
11691
|
+
});
|
|
11692
|
+
console.log("[EnergyModalView] [RFC-0097] Granularity selector initialized with:", this.currentGranularity);
|
|
11693
|
+
}
|
|
11609
11694
|
try {
|
|
11610
11695
|
this.dateRangePicker = await attach(dateRangeInput, {
|
|
11611
11696
|
presetStart: this.config.params.startDate instanceof Date ? this.config.params.startDate.toISOString().split("T")[0] : this.config.params.startDate,
|
|
@@ -11708,6 +11793,37 @@ ${rangeText}`;
|
|
|
11708
11793
|
background: #e5e7eb;
|
|
11709
11794
|
}
|
|
11710
11795
|
|
|
11796
|
+
/* RFC-0097: Granularity selector buttons */
|
|
11797
|
+
.myio-btn-granularity {
|
|
11798
|
+
padding: 4px 10px;
|
|
11799
|
+
font-size: 12px;
|
|
11800
|
+
font-weight: 600;
|
|
11801
|
+
border-radius: 6px;
|
|
11802
|
+
border: 1px solid var(--myio-energy-border);
|
|
11803
|
+
background: var(--myio-energy-bg);
|
|
11804
|
+
color: var(--myio-energy-text);
|
|
11805
|
+
cursor: pointer;
|
|
11806
|
+
transition: all 0.2s ease;
|
|
11807
|
+
min-width: 36px;
|
|
11808
|
+
}
|
|
11809
|
+
|
|
11810
|
+
.myio-btn-granularity:hover:not(.active) {
|
|
11811
|
+
background: #f3f4f6;
|
|
11812
|
+
border-color: var(--myio-energy-primary);
|
|
11813
|
+
color: var(--myio-energy-primary);
|
|
11814
|
+
}
|
|
11815
|
+
|
|
11816
|
+
.myio-btn-granularity.active {
|
|
11817
|
+
background: var(--myio-energy-primary);
|
|
11818
|
+
color: white;
|
|
11819
|
+
border-color: var(--myio-energy-primary);
|
|
11820
|
+
box-shadow: 0 2px 4px rgba(74, 20, 140, 0.25);
|
|
11821
|
+
}
|
|
11822
|
+
|
|
11823
|
+
.myio-granularity-selector {
|
|
11824
|
+
border: 1px solid var(--myio-energy-border);
|
|
11825
|
+
}
|
|
11826
|
+
|
|
11711
11827
|
.myio-modal-scope {
|
|
11712
11828
|
height: 100% !important;
|
|
11713
11829
|
display: flex !important;
|
|
@@ -12088,7 +12204,9 @@ ${rangeText}`;
|
|
|
12088
12204
|
return;
|
|
12089
12205
|
}
|
|
12090
12206
|
if (!this.context?.resolved.ingestionId) {
|
|
12091
|
-
const error = new Error(
|
|
12207
|
+
const error = new Error(
|
|
12208
|
+
"ingestionId not found in device attributes. Please configure the device properly."
|
|
12209
|
+
);
|
|
12092
12210
|
this.handleError(error);
|
|
12093
12211
|
return;
|
|
12094
12212
|
}
|
|
@@ -14311,7 +14429,7 @@ ${rangeText}`;
|
|
|
14311
14429
|
maxLevel: "Maximum Level",
|
|
14312
14430
|
dateRange: "Date Range",
|
|
14313
14431
|
deviceInfo: "Device Information",
|
|
14314
|
-
levelChart: "Level
|
|
14432
|
+
levelChart: "Water Level History (m.c.a)",
|
|
14315
14433
|
percentUnit: "%",
|
|
14316
14434
|
status: {
|
|
14317
14435
|
critical: "Critical",
|
|
@@ -14342,6 +14460,20 @@ ${rangeText}`;
|
|
|
14342
14460
|
return { status: "full", color: "#3498db", label: this.i18n.status.full };
|
|
14343
14461
|
}
|
|
14344
14462
|
}
|
|
14463
|
+
/**
|
|
14464
|
+
* Get tank image URL based on level percentage (same logic as device card)
|
|
14465
|
+
*/
|
|
14466
|
+
getTankImageUrl(percentage) {
|
|
14467
|
+
if (percentage >= 70) {
|
|
14468
|
+
return "https://dashboard.myio-bas.com/api/images/public/3t6WVhMQJFsrKA8bSZmrngDsNPkZV7fq";
|
|
14469
|
+
} else if (percentage >= 40) {
|
|
14470
|
+
return "https://dashboard.myio-bas.com/api/images/public/4UBbShfXCVWR9wcw6IzVMNran4x1EW5n";
|
|
14471
|
+
} else if (percentage >= 20) {
|
|
14472
|
+
return "https://dashboard.myio-bas.com/api/images/public/aB9nX28F54fBBQs1Ht8jKUdYAMcq9QSm";
|
|
14473
|
+
} else {
|
|
14474
|
+
return "https://dashboard.myio-bas.com/api/images/public/qLdwhV4qw295poSCa7HinpnmXoN7dAPO";
|
|
14475
|
+
}
|
|
14476
|
+
}
|
|
14345
14477
|
/**
|
|
14346
14478
|
* Format timestamp to readable date
|
|
14347
14479
|
*/
|
|
@@ -14354,13 +14486,18 @@ ${rangeText}`;
|
|
|
14354
14486
|
}
|
|
14355
14487
|
return dateStr;
|
|
14356
14488
|
}
|
|
14489
|
+
/**
|
|
14490
|
+
* Format timestamp to ISO date string for input
|
|
14491
|
+
*/
|
|
14492
|
+
formatDateForInput(ts) {
|
|
14493
|
+
const date = new Date(ts);
|
|
14494
|
+
return date.toISOString().split("T")[0];
|
|
14495
|
+
}
|
|
14357
14496
|
/**
|
|
14358
14497
|
* Render the modal HTML
|
|
14359
14498
|
*/
|
|
14360
14499
|
render() {
|
|
14361
|
-
const {
|
|
14362
|
-
const currentLevel = data.summary.currentLevel ?? context.device.currentLevel ?? 0;
|
|
14363
|
-
this.getLevelStatus(currentLevel);
|
|
14500
|
+
const { params } = this.config;
|
|
14364
14501
|
this.overlay = document.createElement("div");
|
|
14365
14502
|
this.overlay.className = "myio-water-tank-modal-overlay";
|
|
14366
14503
|
this.overlay.style.cssText = `
|
|
@@ -14381,9 +14518,9 @@ ${rangeText}`;
|
|
|
14381
14518
|
this.modal.className = "myio-water-tank-modal";
|
|
14382
14519
|
this.modal.style.cssText = `
|
|
14383
14520
|
background: white;
|
|
14384
|
-
border-radius:
|
|
14521
|
+
border-radius: 12px;
|
|
14385
14522
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
|
14386
|
-
width: ${params.ui?.width ||
|
|
14523
|
+
width: ${params.ui?.width || 700}px;
|
|
14387
14524
|
max-width: 95vw;
|
|
14388
14525
|
max-height: 90vh;
|
|
14389
14526
|
display: flex;
|
|
@@ -14449,97 +14586,150 @@ ${rangeText}`;
|
|
|
14449
14586
|
overflow-y: auto;
|
|
14450
14587
|
flex: 1;
|
|
14451
14588
|
">
|
|
14452
|
-
${this.
|
|
14589
|
+
${this.renderDateRangePicker()}
|
|
14590
|
+
${this.renderTankVisualization()}
|
|
14453
14591
|
${this.renderChart()}
|
|
14454
|
-
${this.renderDeviceInfo()}
|
|
14455
14592
|
</div>
|
|
14456
14593
|
${this.renderFooter()}
|
|
14457
14594
|
`;
|
|
14458
14595
|
}
|
|
14459
14596
|
/**
|
|
14460
|
-
* Render
|
|
14597
|
+
* Render date range picker
|
|
14461
14598
|
*/
|
|
14462
|
-
|
|
14463
|
-
const {
|
|
14464
|
-
const
|
|
14465
|
-
const
|
|
14599
|
+
renderDateRangePicker() {
|
|
14600
|
+
const { params } = this.config;
|
|
14601
|
+
const startDate = this.formatDateForInput(params.startTs);
|
|
14602
|
+
const endDate = this.formatDateForInput(params.endTs);
|
|
14466
14603
|
return `
|
|
14467
14604
|
<div style="
|
|
14468
|
-
|
|
14469
|
-
|
|
14605
|
+
background: #f8f9fa;
|
|
14606
|
+
border: 1px solid #e0e0e0;
|
|
14607
|
+
border-radius: 8px;
|
|
14608
|
+
padding: 16px;
|
|
14609
|
+
margin-bottom: 20px;
|
|
14610
|
+
display: flex;
|
|
14611
|
+
align-items: center;
|
|
14470
14612
|
gap: 16px;
|
|
14471
|
-
|
|
14613
|
+
flex-wrap: wrap;
|
|
14472
14614
|
">
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14615
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
14616
|
+
<label style="font-size: 14px; font-weight: 500; color: #2c3e50;">From:</label>
|
|
14617
|
+
<input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
|
|
14618
|
+
padding: 8px 12px;
|
|
14619
|
+
border: 1px solid #ddd;
|
|
14620
|
+
border-radius: 6px;
|
|
14621
|
+
font-size: 14px;
|
|
14622
|
+
color: #2c3e50;
|
|
14623
|
+
cursor: pointer;
|
|
14624
|
+
"/>
|
|
14625
|
+
</div>
|
|
14626
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
14627
|
+
<label style="font-size: 14px; font-weight: 500; color: #2c3e50;">To:</label>
|
|
14628
|
+
<input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
|
|
14629
|
+
padding: 8px 12px;
|
|
14630
|
+
border: 1px solid #ddd;
|
|
14631
|
+
border-radius: 6px;
|
|
14632
|
+
font-size: 14px;
|
|
14633
|
+
color: #2c3e50;
|
|
14634
|
+
cursor: pointer;
|
|
14635
|
+
"/>
|
|
14636
|
+
</div>
|
|
14637
|
+
<button id="myio-water-tank-apply-dates" style="
|
|
14638
|
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
|
14639
|
+
color: white;
|
|
14640
|
+
border: none;
|
|
14641
|
+
padding: 8px 20px;
|
|
14642
|
+
border-radius: 6px;
|
|
14643
|
+
font-size: 14px;
|
|
14644
|
+
font-weight: 500;
|
|
14645
|
+
cursor: pointer;
|
|
14646
|
+
transition: all 0.2s ease;
|
|
14647
|
+
">
|
|
14648
|
+
Apply
|
|
14649
|
+
</button>
|
|
14494
14650
|
</div>
|
|
14495
14651
|
`;
|
|
14496
14652
|
}
|
|
14497
14653
|
/**
|
|
14498
|
-
* Render
|
|
14654
|
+
* Render tank visualization with percentage
|
|
14499
14655
|
*/
|
|
14500
|
-
|
|
14656
|
+
renderTankVisualization() {
|
|
14657
|
+
const { data, context } = this.config;
|
|
14658
|
+
let percentage = 0;
|
|
14659
|
+
const percentagePoints = data.telemetry.filter((p) => p.key === "water_percentage");
|
|
14660
|
+
if (percentagePoints.length > 0) {
|
|
14661
|
+
const latestPercentage = percentagePoints[percentagePoints.length - 1].value;
|
|
14662
|
+
percentage = latestPercentage <= 1 ? latestPercentage * 100 : latestPercentage;
|
|
14663
|
+
} else if (context.device.currentLevel !== void 0) {
|
|
14664
|
+
const level = context.device.currentLevel;
|
|
14665
|
+
percentage = level <= 1 ? level * 100 : level;
|
|
14666
|
+
}
|
|
14667
|
+
const levelStatus = this.getLevelStatus(percentage);
|
|
14668
|
+
const tankImageUrl = this.getTankImageUrl(percentage);
|
|
14501
14669
|
return `
|
|
14502
14670
|
<div style="
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14671
|
+
display: flex;
|
|
14672
|
+
align-items: center;
|
|
14673
|
+
justify-content: center;
|
|
14674
|
+
gap: 32px;
|
|
14675
|
+
padding: 24px;
|
|
14676
|
+
background: linear-gradient(135deg, ${levelStatus.color}10 0%, ${levelStatus.color}05 100%);
|
|
14677
|
+
border: 1px solid ${levelStatus.color}30;
|
|
14678
|
+
border-radius: 12px;
|
|
14679
|
+
margin-bottom: 24px;
|
|
14508
14680
|
">
|
|
14509
|
-
|
|
14681
|
+
<div style="
|
|
14682
|
+
display: flex;
|
|
14683
|
+
flex-direction: column;
|
|
14684
|
+
align-items: center;
|
|
14685
|
+
gap: 12px;
|
|
14686
|
+
">
|
|
14687
|
+
<img src="${tankImageUrl}" alt="Water Tank" style="
|
|
14688
|
+
width: 120px;
|
|
14689
|
+
height: auto;
|
|
14690
|
+
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
|
|
14691
|
+
"/>
|
|
14510
14692
|
<div style="
|
|
14511
|
-
|
|
14512
|
-
top: 12px;
|
|
14513
|
-
right: 12px;
|
|
14514
|
-
background: ${color};
|
|
14693
|
+
background: ${levelStatus.color};
|
|
14515
14694
|
color: white;
|
|
14516
|
-
padding: 4px
|
|
14517
|
-
border-radius:
|
|
14518
|
-
font-size:
|
|
14695
|
+
padding: 4px 12px;
|
|
14696
|
+
border-radius: 20px;
|
|
14697
|
+
font-size: 12px;
|
|
14519
14698
|
font-weight: 600;
|
|
14520
14699
|
text-transform: uppercase;
|
|
14521
|
-
">${
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
font-size: 12px;
|
|
14525
|
-
color: #7f8c8d;
|
|
14526
|
-
margin-bottom: 8px;
|
|
14527
|
-
font-weight: 500;
|
|
14528
|
-
">${label}</div>
|
|
14700
|
+
">${levelStatus.label}</div>
|
|
14701
|
+
</div>
|
|
14702
|
+
|
|
14529
14703
|
<div style="
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14704
|
+
display: flex;
|
|
14705
|
+
flex-direction: column;
|
|
14706
|
+
align-items: center;
|
|
14707
|
+
gap: 8px;
|
|
14708
|
+
">
|
|
14709
|
+
<div style="
|
|
14710
|
+
font-size: 48px;
|
|
14711
|
+
font-weight: 700;
|
|
14712
|
+
color: ${levelStatus.color};
|
|
14713
|
+
line-height: 1;
|
|
14714
|
+
">${percentage.toFixed(1)}%</div>
|
|
14715
|
+
<div style="
|
|
14716
|
+
font-size: 14px;
|
|
14717
|
+
color: #7f8c8d;
|
|
14718
|
+
font-weight: 500;
|
|
14719
|
+
">${this.i18n.currentLevel}</div>
|
|
14720
|
+
</div>
|
|
14534
14721
|
</div>
|
|
14535
14722
|
`;
|
|
14536
14723
|
}
|
|
14537
14724
|
/**
|
|
14538
|
-
* Render chart section
|
|
14725
|
+
* Render chart section - shows water_level (m.c.a) over time
|
|
14539
14726
|
*/
|
|
14540
14727
|
renderChart() {
|
|
14541
14728
|
const { data } = this.config;
|
|
14542
|
-
|
|
14729
|
+
const waterLevelPoints = data.telemetry.filter(
|
|
14730
|
+
(p) => p.key === "water_level" || p.key === "waterLevel" || p.key === "nivel" || p.key === "level"
|
|
14731
|
+
);
|
|
14732
|
+
if (waterLevelPoints.length === 0) {
|
|
14543
14733
|
return `
|
|
14544
14734
|
<div style="
|
|
14545
14735
|
background: #f8f9fa;
|
|
@@ -14547,20 +14737,23 @@ ${rangeText}`;
|
|
|
14547
14737
|
border-radius: 8px;
|
|
14548
14738
|
padding: 48px;
|
|
14549
14739
|
text-align: center;
|
|
14550
|
-
margin-bottom: 24px;
|
|
14551
14740
|
">
|
|
14552
14741
|
<div style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;">\u{1F4CA}</div>
|
|
14553
14742
|
<div style="color: #7f8c8d; font-size: 16px;">${this.i18n.noData}</div>
|
|
14743
|
+
<div style="color: #bdc3c7; font-size: 13px; margin-top: 8px;">
|
|
14744
|
+
No water_level (m.c.a) data available for this period
|
|
14745
|
+
</div>
|
|
14554
14746
|
</div>
|
|
14555
14747
|
`;
|
|
14556
14748
|
}
|
|
14749
|
+
const firstTs = waterLevelPoints[0]?.ts;
|
|
14750
|
+
const lastTs = waterLevelPoints[waterLevelPoints.length - 1]?.ts;
|
|
14557
14751
|
return `
|
|
14558
14752
|
<div style="
|
|
14559
14753
|
background: white;
|
|
14560
14754
|
border: 1px solid #e0e0e0;
|
|
14561
14755
|
border-radius: 8px;
|
|
14562
14756
|
padding: 20px;
|
|
14563
|
-
margin-bottom: 24px;
|
|
14564
14757
|
">
|
|
14565
14758
|
<h3 style="
|
|
14566
14759
|
margin: 0 0 16px 0;
|
|
@@ -14568,56 +14761,18 @@ ${rangeText}`;
|
|
|
14568
14761
|
font-weight: 600;
|
|
14569
14762
|
color: #2c3e50;
|
|
14570
14763
|
">${this.i18n.levelChart}</h3>
|
|
14571
|
-
<canvas id="myio-water-tank-chart" style="width: 100%; height:
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
/**
|
|
14584
|
-
* Render device info section
|
|
14585
|
-
*/
|
|
14586
|
-
renderDeviceInfo() {
|
|
14587
|
-
const { context, data } = this.config;
|
|
14588
|
-
return `
|
|
14589
|
-
<div style="
|
|
14590
|
-
background: #f8f9fa;
|
|
14591
|
-
border: 1px solid #e0e0e0;
|
|
14592
|
-
border-radius: 8px;
|
|
14593
|
-
padding: 20px;
|
|
14594
|
-
">
|
|
14595
|
-
<h3 style="
|
|
14596
|
-
margin: 0 0 16px 0;
|
|
14597
|
-
font-size: 16px;
|
|
14598
|
-
font-weight: 600;
|
|
14599
|
-
color: #2c3e50;
|
|
14600
|
-
">${this.i18n.deviceInfo}</h3>
|
|
14601
|
-
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;">
|
|
14602
|
-
${this.renderInfoRow("Device ID", context.device.id)}
|
|
14603
|
-
${this.renderInfoRow("Label", context.device.label)}
|
|
14604
|
-
${context.device.type ? this.renderInfoRow("Type", context.device.type) : ""}
|
|
14605
|
-
${context.metadata.slaveId ? this.renderInfoRow("Slave ID", String(context.metadata.slaveId)) : ""}
|
|
14606
|
-
${context.metadata.centralId ? this.renderInfoRow("Central ID", context.metadata.centralId) : ""}
|
|
14607
|
-
${this.renderInfoRow("Total Readings", String(data.summary.totalReadings))}
|
|
14608
|
-
${this.renderInfoRow("Data Keys", data.metadata.keys.join(", "))}
|
|
14609
|
-
</div>
|
|
14610
|
-
</div>
|
|
14611
|
-
`;
|
|
14612
|
-
}
|
|
14613
|
-
/**
|
|
14614
|
-
* Render an info row
|
|
14615
|
-
*/
|
|
14616
|
-
renderInfoRow(label, value) {
|
|
14617
|
-
return `
|
|
14618
|
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
14619
|
-
<span style="color: #7f8c8d; font-size: 13px;">${label}:</span>
|
|
14620
|
-
<span style="color: #2c3e50; font-size: 13px; font-weight: 500; margin-left: 8px;">${value}</span>
|
|
14764
|
+
<canvas id="myio-water-tank-chart" style="width: 100%; height: 280px;"></canvas>
|
|
14765
|
+
${firstTs && lastTs ? `
|
|
14766
|
+
<div style="
|
|
14767
|
+
margin-top: 12px;
|
|
14768
|
+
font-size: 12px;
|
|
14769
|
+
color: #7f8c8d;
|
|
14770
|
+
text-align: center;
|
|
14771
|
+
">
|
|
14772
|
+
${this.formatDate(firstTs, false)} \u2014 ${this.formatDate(lastTs, false)}
|
|
14773
|
+
(${waterLevelPoints.length} readings)
|
|
14774
|
+
</div>
|
|
14775
|
+
` : ""}
|
|
14621
14776
|
</div>
|
|
14622
14777
|
`;
|
|
14623
14778
|
}
|
|
@@ -14666,6 +14821,10 @@ ${rangeText}`;
|
|
|
14666
14821
|
if (exportBtn) {
|
|
14667
14822
|
exportBtn.addEventListener("click", () => this.config.onExport());
|
|
14668
14823
|
}
|
|
14824
|
+
const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
|
|
14825
|
+
if (applyDatesBtn) {
|
|
14826
|
+
applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
|
|
14827
|
+
}
|
|
14669
14828
|
this.overlay.addEventListener("click", (e) => {
|
|
14670
14829
|
if (e.target === this.overlay) {
|
|
14671
14830
|
this.config.onClose();
|
|
@@ -14679,59 +14838,122 @@ ${rangeText}`;
|
|
|
14679
14838
|
this.renderCanvasChart();
|
|
14680
14839
|
});
|
|
14681
14840
|
}
|
|
14841
|
+
/**
|
|
14842
|
+
* Handle date range change
|
|
14843
|
+
*/
|
|
14844
|
+
handleDateRangeChange() {
|
|
14845
|
+
if (!this.modal) return;
|
|
14846
|
+
const startInput = this.modal.querySelector("#myio-water-tank-start-date");
|
|
14847
|
+
const endInput = this.modal.querySelector("#myio-water-tank-end-date");
|
|
14848
|
+
if (startInput && endInput) {
|
|
14849
|
+
const startTs = new Date(startInput.value).setHours(0, 0, 0, 0);
|
|
14850
|
+
const endTs = new Date(endInput.value).setHours(23, 59, 59, 999);
|
|
14851
|
+
if (startTs >= endTs) {
|
|
14852
|
+
alert("Start date must be before end date");
|
|
14853
|
+
return;
|
|
14854
|
+
}
|
|
14855
|
+
console.log("[WaterTankModalView] Date range changed:", {
|
|
14856
|
+
startTs,
|
|
14857
|
+
endTs,
|
|
14858
|
+
startDate: new Date(startTs).toISOString(),
|
|
14859
|
+
endDate: new Date(endTs).toISOString()
|
|
14860
|
+
});
|
|
14861
|
+
if (this.config.onDateRangeChange) {
|
|
14862
|
+
this.config.onDateRangeChange(startTs, endTs);
|
|
14863
|
+
}
|
|
14864
|
+
}
|
|
14865
|
+
}
|
|
14682
14866
|
handleEscapeKey(e) {
|
|
14683
14867
|
if (e.key === "Escape") {
|
|
14684
14868
|
this.config.onClose();
|
|
14685
14869
|
}
|
|
14686
14870
|
}
|
|
14687
14871
|
/**
|
|
14688
|
-
* Render chart using Canvas API
|
|
14872
|
+
* Render chart using Canvas API - shows water_level (m.c.a) over time
|
|
14689
14873
|
*/
|
|
14690
14874
|
renderCanvasChart() {
|
|
14691
14875
|
const canvas = document.getElementById("myio-water-tank-chart");
|
|
14692
14876
|
if (!canvas) return;
|
|
14693
14877
|
const { data } = this.config;
|
|
14694
|
-
|
|
14878
|
+
const points = data.telemetry.filter(
|
|
14879
|
+
(p) => p.key === "water_level" || p.key === "waterLevel" || p.key === "nivel" || p.key === "level"
|
|
14880
|
+
);
|
|
14881
|
+
if (points.length < 2) return;
|
|
14695
14882
|
const ctx = canvas.getContext("2d");
|
|
14696
14883
|
if (!ctx) return;
|
|
14697
14884
|
const rect = canvas.getBoundingClientRect();
|
|
14698
14885
|
canvas.width = rect.width * window.devicePixelRatio;
|
|
14699
|
-
canvas.height =
|
|
14886
|
+
canvas.height = 280 * window.devicePixelRatio;
|
|
14700
14887
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
14701
14888
|
const width = rect.width;
|
|
14702
|
-
const height =
|
|
14703
|
-
const padding = 40;
|
|
14889
|
+
const height = 280;
|
|
14890
|
+
const padding = { top: 20, right: 20, bottom: 40, left: 60 };
|
|
14704
14891
|
ctx.clearRect(0, 0, width, height);
|
|
14705
|
-
|
|
14892
|
+
const values = points.map((p) => p.value);
|
|
14893
|
+
const minValue = Math.min(...values);
|
|
14894
|
+
const maxValue = Math.max(...values);
|
|
14895
|
+
const valueRange = maxValue - minValue || 1;
|
|
14896
|
+
const valuePadding = valueRange * 0.1;
|
|
14897
|
+
const chartMinY = minValue - valuePadding;
|
|
14898
|
+
const chartMaxY = maxValue + valuePadding;
|
|
14899
|
+
const chartRangeY = chartMaxY - chartMinY;
|
|
14900
|
+
ctx.fillStyle = "#fafafa";
|
|
14901
|
+
ctx.fillRect(padding.left, padding.top, width - padding.left - padding.right, height - padding.top - padding.bottom);
|
|
14902
|
+
ctx.strokeStyle = "#e8e8e8";
|
|
14706
14903
|
ctx.lineWidth = 1;
|
|
14707
|
-
ctx.
|
|
14708
|
-
ctx.
|
|
14709
|
-
ctx.
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
for (let i = 0; i <= 5; i++) {
|
|
14715
|
-
const y = padding + (height - 2 * padding) * (i / 5);
|
|
14904
|
+
ctx.fillStyle = "#666";
|
|
14905
|
+
ctx.font = "11px Arial";
|
|
14906
|
+
ctx.textAlign = "right";
|
|
14907
|
+
const ySteps = 5;
|
|
14908
|
+
for (let i = 0; i <= ySteps; i++) {
|
|
14909
|
+
const y = padding.top + (height - padding.top - padding.bottom) * (i / ySteps);
|
|
14910
|
+
const value = chartMaxY - chartRangeY * i / ySteps;
|
|
14716
14911
|
ctx.beginPath();
|
|
14717
|
-
ctx.moveTo(padding, y);
|
|
14718
|
-
ctx.lineTo(width - padding, y);
|
|
14912
|
+
ctx.moveTo(padding.left, y);
|
|
14913
|
+
ctx.lineTo(width - padding.right, y);
|
|
14719
14914
|
ctx.stroke();
|
|
14720
|
-
ctx.
|
|
14721
|
-
ctx.font = "12px Arial";
|
|
14722
|
-
ctx.textAlign = "right";
|
|
14723
|
-
ctx.fillText(`${100 - i * 20}%`, padding - 10, y + 4);
|
|
14915
|
+
ctx.fillText(`${value.toFixed(2)}`, padding.left - 8, y + 4);
|
|
14724
14916
|
}
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14917
|
+
ctx.save();
|
|
14918
|
+
ctx.translate(15, height / 2);
|
|
14919
|
+
ctx.rotate(-Math.PI / 2);
|
|
14920
|
+
ctx.textAlign = "center";
|
|
14921
|
+
ctx.fillStyle = "#666";
|
|
14922
|
+
ctx.font = "12px Arial";
|
|
14923
|
+
ctx.fillText("m.c.a", 0, 0);
|
|
14924
|
+
ctx.restore();
|
|
14925
|
+
ctx.strokeStyle = "#ccc";
|
|
14926
|
+
ctx.lineWidth = 1;
|
|
14927
|
+
ctx.beginPath();
|
|
14928
|
+
ctx.moveTo(padding.left, padding.top);
|
|
14929
|
+
ctx.lineTo(padding.left, height - padding.bottom);
|
|
14930
|
+
ctx.lineTo(width - padding.right, height - padding.bottom);
|
|
14931
|
+
ctx.stroke();
|
|
14932
|
+
const chartWidth = width - padding.left - padding.right;
|
|
14933
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
14934
|
+
const xScale = chartWidth / (points.length - 1);
|
|
14935
|
+
ctx.beginPath();
|
|
14936
|
+
ctx.moveTo(padding.left, height - padding.bottom);
|
|
14937
|
+
points.forEach((point, index) => {
|
|
14938
|
+
const x = padding.left + index * xScale;
|
|
14939
|
+
const y = padding.top + chartHeight - (point.value - chartMinY) / chartRangeY * chartHeight;
|
|
14940
|
+
ctx.lineTo(x, y);
|
|
14941
|
+
});
|
|
14942
|
+
ctx.lineTo(padding.left + (points.length - 1) * xScale, height - padding.bottom);
|
|
14943
|
+
ctx.closePath();
|
|
14944
|
+
const gradient = ctx.createLinearGradient(0, padding.top, 0, height - padding.bottom);
|
|
14945
|
+
gradient.addColorStop(0, "rgba(52, 152, 219, 0.3)");
|
|
14946
|
+
gradient.addColorStop(1, "rgba(52, 152, 219, 0.05)");
|
|
14947
|
+
ctx.fillStyle = gradient;
|
|
14948
|
+
ctx.fill();
|
|
14729
14949
|
ctx.strokeStyle = "#3498db";
|
|
14730
14950
|
ctx.lineWidth = 2;
|
|
14951
|
+
ctx.lineJoin = "round";
|
|
14952
|
+
ctx.lineCap = "round";
|
|
14731
14953
|
ctx.beginPath();
|
|
14732
14954
|
points.forEach((point, index) => {
|
|
14733
|
-
const x = padding + index * xScale;
|
|
14734
|
-
const y =
|
|
14955
|
+
const x = padding.left + index * xScale;
|
|
14956
|
+
const y = padding.top + chartHeight - (point.value - chartMinY) / chartRangeY * chartHeight;
|
|
14735
14957
|
if (index === 0) {
|
|
14736
14958
|
ctx.moveTo(x, y);
|
|
14737
14959
|
} else {
|
|
@@ -14739,14 +14961,56 @@ ${rangeText}`;
|
|
|
14739
14961
|
}
|
|
14740
14962
|
});
|
|
14741
14963
|
ctx.stroke();
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14964
|
+
if (points.length <= 50) {
|
|
14965
|
+
ctx.fillStyle = "#3498db";
|
|
14966
|
+
points.forEach((point, index) => {
|
|
14967
|
+
const x = padding.left + index * xScale;
|
|
14968
|
+
const y = padding.top + chartHeight - (point.value - chartMinY) / chartRangeY * chartHeight;
|
|
14969
|
+
ctx.beginPath();
|
|
14970
|
+
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
|
14971
|
+
ctx.fill();
|
|
14972
|
+
});
|
|
14973
|
+
}
|
|
14974
|
+
ctx.fillStyle = "#888";
|
|
14975
|
+
ctx.font = "10px Arial";
|
|
14976
|
+
ctx.textAlign = "center";
|
|
14977
|
+
const xLabelCount = Math.min(6, points.length);
|
|
14978
|
+
const xLabelStep = Math.floor(points.length / xLabelCount);
|
|
14979
|
+
for (let i = 0; i < points.length; i += xLabelStep) {
|
|
14980
|
+
const x = padding.left + i * xScale;
|
|
14981
|
+
const date = new Date(points[i].ts);
|
|
14982
|
+
const label = `${date.getDate()}/${date.getMonth() + 1}`;
|
|
14983
|
+
ctx.fillText(label, x, height - padding.bottom + 16);
|
|
14984
|
+
}
|
|
14985
|
+
if (points.length > 1) {
|
|
14986
|
+
const lastX = padding.left + (points.length - 1) * xScale;
|
|
14987
|
+
const lastDate = new Date(points[points.length - 1].ts);
|
|
14988
|
+
const lastLabel = `${lastDate.getDate()}/${lastDate.getMonth() + 1}`;
|
|
14989
|
+
ctx.fillText(lastLabel, lastX, height - padding.bottom + 16);
|
|
14990
|
+
}
|
|
14991
|
+
}
|
|
14992
|
+
/**
|
|
14993
|
+
* Update data and re-render chart
|
|
14994
|
+
*/
|
|
14995
|
+
updateData(data) {
|
|
14996
|
+
this.config.data = data;
|
|
14997
|
+
if (this.modal) {
|
|
14998
|
+
const bodyEl = this.modal.querySelector(".myio-water-tank-modal-body");
|
|
14999
|
+
if (bodyEl) {
|
|
15000
|
+
bodyEl.innerHTML = `
|
|
15001
|
+
${this.renderDateRangePicker()}
|
|
15002
|
+
${this.renderTankVisualization()}
|
|
15003
|
+
${this.renderChart()}
|
|
15004
|
+
`;
|
|
15005
|
+
const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
|
|
15006
|
+
if (applyDatesBtn) {
|
|
15007
|
+
applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
|
|
15008
|
+
}
|
|
15009
|
+
requestAnimationFrame(() => {
|
|
15010
|
+
this.renderCanvasChart();
|
|
15011
|
+
});
|
|
15012
|
+
}
|
|
15013
|
+
}
|
|
14750
15014
|
}
|
|
14751
15015
|
/**
|
|
14752
15016
|
* Show the modal with animation
|
|
@@ -15011,8 +15275,9 @@ ${rangeText}`;
|
|
|
15011
15275
|
data: this.data,
|
|
15012
15276
|
onExport: () => this.handleExport(),
|
|
15013
15277
|
onError: (error) => this.handleError(error),
|
|
15014
|
-
onClose: () => this.close()
|
|
15278
|
+
onClose: () => this.close(),
|
|
15015
15279
|
// Call close() to destroy view and trigger user callback
|
|
15280
|
+
onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs)
|
|
15016
15281
|
});
|
|
15017
15282
|
this.view.render();
|
|
15018
15283
|
this.view.show();
|
|
@@ -15044,6 +15309,39 @@ ${rangeText}`;
|
|
|
15044
15309
|
}
|
|
15045
15310
|
this.handleClose();
|
|
15046
15311
|
}
|
|
15312
|
+
/**
|
|
15313
|
+
* Handle date range change from view
|
|
15314
|
+
*/
|
|
15315
|
+
async handleDateRangeChange(startTs, endTs) {
|
|
15316
|
+
console.log("[WaterTankModal] Date range changed:", {
|
|
15317
|
+
startTs,
|
|
15318
|
+
endTs,
|
|
15319
|
+
startDate: new Date(startTs).toISOString(),
|
|
15320
|
+
endDate: new Date(endTs).toISOString()
|
|
15321
|
+
});
|
|
15322
|
+
this.options.startTs = startTs;
|
|
15323
|
+
this.options.endTs = endTs;
|
|
15324
|
+
this.context.timeRange.startTs = startTs;
|
|
15325
|
+
this.context.timeRange.endTs = endTs;
|
|
15326
|
+
try {
|
|
15327
|
+
console.log("[WaterTankModal] Fetching new data for date range...");
|
|
15328
|
+
this.data = await this.fetchTelemetryData();
|
|
15329
|
+
if (this.view) {
|
|
15330
|
+
this.view.updateData(this.data);
|
|
15331
|
+
}
|
|
15332
|
+
if (this.options.onDataLoaded) {
|
|
15333
|
+
try {
|
|
15334
|
+
this.options.onDataLoaded(this.data);
|
|
15335
|
+
} catch (callbackError) {
|
|
15336
|
+
console.warn("[WaterTankModal] onDataLoaded callback error:", callbackError);
|
|
15337
|
+
}
|
|
15338
|
+
}
|
|
15339
|
+
console.log("[WaterTankModal] Data refreshed successfully");
|
|
15340
|
+
} catch (error) {
|
|
15341
|
+
console.error("[WaterTankModal] Failed to fetch data for new date range:", error);
|
|
15342
|
+
this.handleError(error);
|
|
15343
|
+
}
|
|
15344
|
+
}
|
|
15047
15345
|
/**
|
|
15048
15346
|
* Handle export functionality
|
|
15049
15347
|
*/
|