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.
@@ -5018,8 +5018,14 @@
5018
5018
  }
5019
5019
  function paint(root, state) {
5020
5020
  const { entityObject, i18n, delayTimeConnectionInMins } = state;
5021
- if (verifyOfflineStatus(entityObject, delayTimeConnectionInMins) === false) {
5022
- entityObject.deviceStatus = DeviceStatusType.NO_INFO;
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.config.params.granularity,
11214
- // REQUIRED
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.config.params.granularity,
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("ingestionId not found in device attributes. Please configure the device properly.");
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 Chart",
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 { context, params, data } = this.config;
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: 8px;
14521
+ border-radius: 12px;
14385
14522
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
14386
- width: ${params.ui?.width || 900}px;
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.renderSummaryCards()}
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 summary cards
14597
+ * Render date range picker
14461
14598
  */
14462
- renderSummaryCards() {
14463
- const { data } = this.config;
14464
- const currentLevel = data.summary.currentLevel ?? 0;
14465
- const levelStatus = this.getLevelStatus(currentLevel);
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
- display: grid;
14469
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
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
- margin-bottom: 24px;
14613
+ flex-wrap: wrap;
14472
14614
  ">
14473
- ${this.renderSummaryCard(
14474
- this.i18n.currentLevel,
14475
- `${currentLevel.toFixed(1)}${this.i18n.percentUnit}`,
14476
- levelStatus.color,
14477
- levelStatus.label
14478
- )}
14479
- ${this.renderSummaryCard(
14480
- this.i18n.averageLevel,
14481
- `${data.summary.avgLevel.toFixed(1)}${this.i18n.percentUnit}`,
14482
- "#3498db"
14483
- )}
14484
- ${this.renderSummaryCard(
14485
- this.i18n.minLevel,
14486
- `${data.summary.minLevel.toFixed(1)}${this.i18n.percentUnit}`,
14487
- "#e74c3c"
14488
- )}
14489
- ${this.renderSummaryCard(
14490
- this.i18n.maxLevel,
14491
- `${data.summary.maxLevel.toFixed(1)}${this.i18n.percentUnit}`,
14492
- "#27ae60"
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 a single summary card
14654
+ * Render tank visualization with percentage
14499
14655
  */
14500
- renderSummaryCard(label, value, color, badge) {
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
- background: linear-gradient(135deg, ${color}15 0%, ${color}05 100%);
14504
- border: 1px solid ${color}30;
14505
- border-radius: 8px;
14506
- padding: 16px;
14507
- position: relative;
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
- ${badge ? `
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
- position: absolute;
14512
- top: 12px;
14513
- right: 12px;
14514
- background: ${color};
14693
+ background: ${levelStatus.color};
14515
14694
  color: white;
14516
- padding: 4px 8px;
14517
- border-radius: 4px;
14518
- font-size: 10px;
14695
+ padding: 4px 12px;
14696
+ border-radius: 20px;
14697
+ font-size: 12px;
14519
14698
  font-weight: 600;
14520
14699
  text-transform: uppercase;
14521
- ">${badge}</div>
14522
- ` : ""}
14523
- <div style="
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
- font-size: 28px;
14531
- font-weight: 700;
14532
- color: ${color};
14533
- ">${value}</div>
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
- if (data.telemetry.length === 0) {
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: 300px;"></canvas>
14572
- <div style="
14573
- margin-top: 16px;
14574
- font-size: 12px;
14575
- color: #7f8c8d;
14576
- text-align: center;
14577
- ">
14578
- ${this.i18n.dateRange}: ${this.formatDate(data.summary.firstReadingTs, false)} - ${this.formatDate(data.summary.lastReadingTs, false)}
14579
- </div>
14580
- </div>
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
- if (data.telemetry.length === 0) return;
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 = 300 * window.devicePixelRatio;
14886
+ canvas.height = 280 * window.devicePixelRatio;
14700
14887
  ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
14701
14888
  const width = rect.width;
14702
- const height = 300;
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
- ctx.strokeStyle = "#e0e0e0";
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.beginPath();
14708
- ctx.moveTo(padding, padding);
14709
- ctx.lineTo(padding, height - padding);
14710
- ctx.lineTo(width - padding, height - padding);
14711
- ctx.stroke();
14712
- ctx.strokeStyle = "#f0f0f0";
14713
- ctx.lineWidth = 0.5;
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.fillStyle = "#7f8c8d";
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
- const points = data.telemetry;
14726
- if (points.length < 2) return;
14727
- const xScale = (width - 2 * padding) / (points.length - 1);
14728
- const yScale = (height - 2 * padding) / 100;
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 = height - padding - point.value * yScale;
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
- ctx.fillStyle = "#3498db";
14743
- points.forEach((point, index) => {
14744
- const x = padding + index * xScale;
14745
- const y = height - padding - point.value * yScale;
14746
- ctx.beginPath();
14747
- ctx.arc(x, y, 3, 0, 2 * Math.PI);
14748
- ctx.fill();
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
  */