myio-js-library 0.1.212 → 0.1.214

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.
@@ -21113,26 +21113,26 @@
21113
21113
  */
21114
21114
  getI18n() {
21115
21115
  const defaults = {
21116
- title: "Water Tank",
21117
- loading: "Loading...",
21118
- error: "Error loading data",
21119
- noData: "No data available",
21120
- exportCsv: "Export CSV",
21121
- close: "Close",
21122
- currentLevel: "Current Level",
21123
- averageLevel: "Average Level",
21124
- minLevel: "Minimum Level",
21125
- maxLevel: "Maximum Level",
21126
- dateRange: "Date Range",
21127
- deviceInfo: "Device Information",
21128
- levelChart: "Water Level History (m.c.a)",
21116
+ title: "Caixa d'\xC1gua",
21117
+ loading: "Carregando...",
21118
+ error: "Erro ao carregar dados",
21119
+ noData: "Nenhum dado dispon\xEDvel",
21120
+ exportCsv: "Exportar CSV",
21121
+ close: "Fechar",
21122
+ currentLevel: "N\xEDvel Atual",
21123
+ averageLevel: "N\xEDvel M\xE9dio",
21124
+ minLevel: "N\xEDvel M\xEDnimo",
21125
+ maxLevel: "N\xEDvel M\xE1ximo",
21126
+ dateRange: "Per\xEDodo",
21127
+ deviceInfo: "Informa\xE7\xF5es do Dispositivo",
21128
+ levelChart: "Hist\xF3rico de N\xEDvel (m.c.a)",
21129
21129
  percentUnit: "%",
21130
21130
  status: {
21131
- critical: "Critical",
21132
- low: "Low",
21133
- medium: "Medium",
21134
- good: "Good",
21135
- full: "Full"
21131
+ critical: "Cr\xEDtico",
21132
+ low: "Baixo",
21133
+ medium: "M\xE9dio",
21134
+ good: "Bom",
21135
+ full: "Cheio"
21136
21136
  }
21137
21137
  };
21138
21138
  return {
@@ -21233,62 +21233,327 @@
21233
21233
  this.attachEventListeners();
21234
21234
  }
21235
21235
  /**
21236
- * Render modal header
21236
+ * Render modal header - MyIO Premium Style
21237
21237
  */
21238
21238
  renderHeader() {
21239
21239
  const { context, params } = this.config;
21240
21240
  const title = params.ui?.title || `${this.i18n.title} - ${context.device.label}`;
21241
21241
  return `
21242
21242
  <div class="myio-water-tank-modal-header" style="
21243
- padding: 20px 24px;
21244
- border-bottom: 1px solid #e0e0e0;
21243
+ padding: 4px 8px;
21245
21244
  display: flex;
21246
21245
  align-items: center;
21247
21246
  justify-content: space-between;
21247
+ background: #3e1a7d;
21248
+ color: white;
21249
+ border-radius: 12px 12px 0 0;
21250
+ min-height: 20px;
21248
21251
  ">
21249
21252
  <h2 style="
21250
- margin: 0;
21251
- font-size: 20px;
21253
+ margin: 6px;
21254
+ font-size: 18px;
21252
21255
  font-weight: 600;
21253
- color: #2c3e50;
21254
- ">${title}</h2>
21255
- <button class="myio-water-tank-modal-close" style="
21256
- background: none;
21257
- border: none;
21258
- font-size: 24px;
21259
- color: #7f8c8d;
21260
- cursor: pointer;
21261
- padding: 0;
21262
- width: 32px;
21263
- height: 32px;
21264
- display: flex;
21265
- align-items: center;
21266
- justify-content: center;
21267
- border-radius: 4px;
21268
- transition: background 0.2s ease;
21269
- " title="${this.i18n.close}">
21270
- \xD7
21271
- </button>
21256
+ color: white;
21257
+ line-height: 2;
21258
+ ">\u{1F4A7} ${title}</h2>
21259
+ <div style="display: flex; gap: 4px; align-items: center;">
21260
+ <button class="myio-water-tank-modal-close" title="${this.i18n.close}" style="
21261
+ background: none;
21262
+ border: none;
21263
+ font-size: 20px;
21264
+ cursor: pointer;
21265
+ padding: 4px 8px;
21266
+ border-radius: 6px;
21267
+ color: rgba(255,255,255,0.8);
21268
+ transition: background-color 0.2s;
21269
+ ">\xD7</button>
21270
+ </div>
21272
21271
  </div>
21273
21272
  `;
21274
21273
  }
21275
21274
  /**
21276
- * Render modal body
21275
+ * RFC-0107: Render modal body with new layout
21276
+ * Left side: Tank visualization with percentage
21277
+ * Right side: Chart with controls (larger area)
21277
21278
  */
21278
21279
  renderBody() {
21279
21280
  return `
21280
21281
  <div class="myio-water-tank-modal-body" style="
21281
- padding: 24px;
21282
+ padding: 20px;
21282
21283
  overflow-y: auto;
21283
21284
  flex: 1;
21285
+ display: flex;
21286
+ flex-direction: column;
21287
+ gap: 16px;
21284
21288
  ">
21285
- ${this.renderDateRangePicker()}
21286
- ${this.renderTankVisualization()}
21287
- ${this.renderChart()}
21289
+ ${this.renderControlsBar()}
21290
+ <div style="
21291
+ display: flex;
21292
+ gap: 20px;
21293
+ flex: 1;
21294
+ min-height: 400px;
21295
+ ">
21296
+ ${this.renderTankPanel()}
21297
+ ${this.renderChartPanel()}
21298
+ </div>
21288
21299
  </div>
21289
21300
  ${this.renderFooter()}
21290
21301
  `;
21291
21302
  }
21303
+ /**
21304
+ * RFC-0107: Render controls bar with date range, aggregation, and limit
21305
+ */
21306
+ renderControlsBar() {
21307
+ const { params } = this.config;
21308
+ const startDate = this.formatDateForInput(params.startTs);
21309
+ const endDate = this.formatDateForInput(params.endTs);
21310
+ const currentAggregation = params.aggregation || "NONE";
21311
+ const currentLimit = params.limit || 1e3;
21312
+ return `
21313
+ <div style="
21314
+ background: #f8f9fa;
21315
+ border: 1px solid #e0e0e0;
21316
+ border-radius: 8px;
21317
+ padding: 12px 16px;
21318
+ display: flex;
21319
+ align-items: center;
21320
+ gap: 16px;
21321
+ flex-wrap: wrap;
21322
+ ">
21323
+ <div style="display: flex; align-items: center; gap: 8px;">
21324
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">De:</label>
21325
+ <input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
21326
+ padding: 6px 10px;
21327
+ border: 1px solid #ddd;
21328
+ border-radius: 6px;
21329
+ font-size: 13px;
21330
+ color: #2c3e50;
21331
+ cursor: pointer;
21332
+ "/>
21333
+ </div>
21334
+ <div style="display: flex; align-items: center; gap: 8px;">
21335
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">At\xE9:</label>
21336
+ <input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
21337
+ padding: 6px 10px;
21338
+ border: 1px solid #ddd;
21339
+ border-radius: 6px;
21340
+ font-size: 13px;
21341
+ color: #2c3e50;
21342
+ cursor: pointer;
21343
+ "/>
21344
+ </div>
21345
+ <div style="display: flex; align-items: center; gap: 8px;">
21346
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">Agrega\xE7\xE3o:</label>
21347
+ <select id="myio-water-tank-aggregation" style="
21348
+ padding: 6px 10px;
21349
+ border: 1px solid #ddd;
21350
+ border-radius: 6px;
21351
+ font-size: 13px;
21352
+ color: #2c3e50;
21353
+ background: white;
21354
+ cursor: pointer;
21355
+ ">
21356
+ <option value="NONE" ${currentAggregation === "NONE" ? "selected" : ""}>Nenhuma</option>
21357
+ <option value="AVG" ${currentAggregation === "AVG" ? "selected" : ""}>M\xE9dia</option>
21358
+ <option value="MIN" ${currentAggregation === "MIN" ? "selected" : ""}>M\xEDnimo</option>
21359
+ <option value="MAX" ${currentAggregation === "MAX" ? "selected" : ""}>M\xE1ximo</option>
21360
+ <option value="SUM" ${currentAggregation === "SUM" ? "selected" : ""}>Soma</option>
21361
+ <option value="COUNT" ${currentAggregation === "COUNT" ? "selected" : ""}>Contagem</option>
21362
+ </select>
21363
+ </div>
21364
+ <div style="display: flex; align-items: center; gap: 8px;">
21365
+ <label style="font-size: 13px; font-weight: 500; color: #2c3e50;">Limite:</label>
21366
+ <select id="myio-water-tank-limit" style="
21367
+ padding: 6px 10px;
21368
+ border: 1px solid #ddd;
21369
+ border-radius: 6px;
21370
+ font-size: 13px;
21371
+ color: #2c3e50;
21372
+ background: white;
21373
+ cursor: pointer;
21374
+ ">
21375
+ <option value="100" ${currentLimit === 100 ? "selected" : ""}>100</option>
21376
+ <option value="500" ${currentLimit === 500 ? "selected" : ""}>500</option>
21377
+ <option value="1000" ${currentLimit === 1e3 ? "selected" : ""}>1000</option>
21378
+ <option value="2000" ${currentLimit === 2e3 ? "selected" : ""}>2000</option>
21379
+ <option value="5000" ${currentLimit === 5e3 ? "selected" : ""}>5000</option>
21380
+ </select>
21381
+ </div>
21382
+ <button id="myio-water-tank-apply-dates" style="
21383
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
21384
+ color: white;
21385
+ border: none;
21386
+ padding: 6px 16px;
21387
+ border-radius: 6px;
21388
+ font-size: 13px;
21389
+ font-weight: 500;
21390
+ cursor: pointer;
21391
+ transition: all 0.2s ease;
21392
+ ">
21393
+ Aplicar
21394
+ </button>
21395
+ </div>
21396
+ `;
21397
+ }
21398
+ /**
21399
+ * RFC-0107: Render tank panel (left side)
21400
+ */
21401
+ renderTankPanel() {
21402
+ const { data, context } = this.config;
21403
+ let percentage = 0;
21404
+ const percentagePoints = data.telemetry.filter((p) => p.key === "water_percentage");
21405
+ if (percentagePoints.length > 0) {
21406
+ const latestPercentage = percentagePoints[percentagePoints.length - 1].value;
21407
+ percentage = latestPercentage <= 1.5 ? latestPercentage * 100 : latestPercentage;
21408
+ } else if (context.device.currentLevel !== void 0) {
21409
+ const level = context.device.currentLevel;
21410
+ percentage = level <= 1.5 ? level * 100 : level;
21411
+ }
21412
+ const levelStatus = this.getLevelStatus(Math.min(percentage, 100));
21413
+ const tankImageUrl = this.getTankImageUrl(Math.min(percentage, 100));
21414
+ const displayPercentage = percentage.toFixed(1);
21415
+ return `
21416
+ <div style="
21417
+ width: 200px;
21418
+ min-width: 200px;
21419
+ background: linear-gradient(135deg, ${levelStatus.color}10 0%, ${levelStatus.color}05 100%);
21420
+ border: 1px solid ${levelStatus.color}30;
21421
+ border-radius: 12px;
21422
+ padding: 24px 16px;
21423
+ display: flex;
21424
+ flex-direction: column;
21425
+ align-items: center;
21426
+ justify-content: center;
21427
+ gap: 16px;
21428
+ ">
21429
+ <img src="${tankImageUrl}" alt="Water Tank" style="
21430
+ width: 100px;
21431
+ height: auto;
21432
+ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
21433
+ "/>
21434
+ <div style="
21435
+ font-size: 42px;
21436
+ font-weight: 700;
21437
+ color: ${levelStatus.color};
21438
+ line-height: 1;
21439
+ ">${displayPercentage}%</div>
21440
+ <div style="
21441
+ background: ${levelStatus.color};
21442
+ color: white;
21443
+ padding: 4px 12px;
21444
+ border-radius: 20px;
21445
+ font-size: 11px;
21446
+ font-weight: 600;
21447
+ text-transform: uppercase;
21448
+ ">${levelStatus.label}</div>
21449
+ <div style="
21450
+ font-size: 12px;
21451
+ color: #7f8c8d;
21452
+ text-align: center;
21453
+ ">${this.i18n.currentLevel}</div>
21454
+ </div>
21455
+ `;
21456
+ }
21457
+ /**
21458
+ * RFC-0107: Render chart panel (right side) with maximize button
21459
+ */
21460
+ renderChartPanel() {
21461
+ const chartPoints = this.getChartDataPoints();
21462
+ const chartTitle = this.chartDisplayMode === "water_percentage" ? "Hist\xF3rico de N\xEDvel (%)" : this.i18n.levelChart;
21463
+ if (chartPoints.length === 0) {
21464
+ const displayLabel = this.chartDisplayMode === "water_percentage" ? "%" : "m.c.a";
21465
+ return `
21466
+ <div style="
21467
+ flex: 1;
21468
+ background: #f8f9fa;
21469
+ border: 1px solid #e0e0e0;
21470
+ border-radius: 12px;
21471
+ display: flex;
21472
+ flex-direction: column;
21473
+ align-items: center;
21474
+ justify-content: center;
21475
+ padding: 24px;
21476
+ ">
21477
+ <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;">\u{1F4CA}</div>
21478
+ <div style="color: #7f8c8d; font-size: 16px;">${this.i18n.noData}</div>
21479
+ <div style="color: #bdc3c7; font-size: 13px; margin-top: 8px;">
21480
+ Sem dados de ${this.chartDisplayMode === "water_percentage" ? "percentual" : "n\xEDvel"} (${displayLabel}) dispon\xEDveis
21481
+ </div>
21482
+ </div>
21483
+ `;
21484
+ }
21485
+ const firstTs = chartPoints[0]?.ts;
21486
+ const lastTs = chartPoints[chartPoints.length - 1]?.ts;
21487
+ return `
21488
+ <div id="myio-water-tank-chart-panel" style="
21489
+ flex: 1;
21490
+ background: white;
21491
+ border: 1px solid #e0e0e0;
21492
+ border-radius: 12px;
21493
+ padding: 16px;
21494
+ display: flex;
21495
+ flex-direction: column;
21496
+ position: relative;
21497
+ ">
21498
+ <div style="
21499
+ display: flex;
21500
+ align-items: center;
21501
+ justify-content: space-between;
21502
+ margin-bottom: 12px;
21503
+ ">
21504
+ <h3 style="
21505
+ margin: 0;
21506
+ font-size: 15px;
21507
+ font-weight: 600;
21508
+ color: #2c3e50;
21509
+ ">${chartTitle}</h3>
21510
+ <div style="
21511
+ display: flex;
21512
+ align-items: center;
21513
+ gap: 8px;
21514
+ ">
21515
+ <select id="myio-water-tank-display-mode" style="
21516
+ padding: 4px 8px;
21517
+ border: 1px solid #ddd;
21518
+ border-radius: 4px;
21519
+ font-size: 12px;
21520
+ color: #2c3e50;
21521
+ background: white;
21522
+ cursor: pointer;
21523
+ ">
21524
+ <option value="water_level" ${this.chartDisplayMode === "water_level" ? "selected" : ""}>N\xEDvel (m.c.a)</option>
21525
+ <option value="water_percentage" ${this.chartDisplayMode === "water_percentage" ? "selected" : ""}>Percentual (%)</option>
21526
+ </select>
21527
+ <button id="myio-water-tank-maximize" title="Maximizar gr\xE1fico" style="
21528
+ background: #f0f0f0;
21529
+ border: 1px solid #ddd;
21530
+ border-radius: 4px;
21531
+ padding: 4px 8px;
21532
+ cursor: pointer;
21533
+ font-size: 14px;
21534
+ display: flex;
21535
+ align-items: center;
21536
+ justify-content: center;
21537
+ ">\u26F6</button>
21538
+ </div>
21539
+ </div>
21540
+ <div style="flex: 1; min-height: 300px;">
21541
+ <canvas id="myio-water-tank-chart" style="width: 100%; height: 100%;"></canvas>
21542
+ </div>
21543
+ ${firstTs && lastTs ? `
21544
+ <div style="
21545
+ margin-top: 8px;
21546
+ font-size: 11px;
21547
+ color: #7f8c8d;
21548
+ text-align: center;
21549
+ ">
21550
+ ${this.formatDate(firstTs, false)} \u2014 ${this.formatDate(lastTs, false)}
21551
+ (${chartPoints.length} leituras)
21552
+ </div>
21553
+ ` : ""}
21554
+ </div>
21555
+ `;
21556
+ }
21292
21557
  /**
21293
21558
  * Render date range picker
21294
21559
  */
@@ -21309,7 +21574,7 @@
21309
21574
  flex-wrap: wrap;
21310
21575
  ">
21311
21576
  <div style="display: flex; align-items: center; gap: 8px;">
21312
- <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">From:</label>
21577
+ <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">De:</label>
21313
21578
  <input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
21314
21579
  padding: 8px 12px;
21315
21580
  border: 1px solid #ddd;
@@ -21320,7 +21585,7 @@
21320
21585
  "/>
21321
21586
  </div>
21322
21587
  <div style="display: flex; align-items: center; gap: 8px;">
21323
- <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">To:</label>
21588
+ <label style="font-size: 14px; font-weight: 500; color: #2c3e50;">At\xE9:</label>
21324
21589
  <input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
21325
21590
  padding: 8px 12px;
21326
21591
  border: 1px solid #ddd;
@@ -21341,7 +21606,7 @@
21341
21606
  cursor: pointer;
21342
21607
  transition: all 0.2s ease;
21343
21608
  ">
21344
- Apply
21609
+ Aplicar
21345
21610
  </button>
21346
21611
  </div>
21347
21612
  `;
@@ -21563,7 +21828,7 @@
21563
21828
  }
21564
21829
  const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
21565
21830
  if (applyDatesBtn) {
21566
- applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
21831
+ applyDatesBtn.addEventListener("click", () => this.handleApplyParams());
21567
21832
  }
21568
21833
  const displayModeSelect = this.modal.querySelector("#myio-water-tank-display-mode");
21569
21834
  if (displayModeSelect) {
@@ -21572,6 +21837,10 @@
21572
21837
  this.refreshChart();
21573
21838
  });
21574
21839
  }
21840
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
21841
+ if (maximizeBtn) {
21842
+ maximizeBtn.addEventListener("click", () => this.handleMaximize());
21843
+ }
21575
21844
  this.overlay.addEventListener("click", (e) => {
21576
21845
  if (e.target === this.overlay) {
21577
21846
  this.config.onClose();
@@ -21586,12 +21855,20 @@
21586
21855
  });
21587
21856
  }
21588
21857
  /**
21589
- * Handle date range change
21858
+ * Handle date range change (legacy - kept for compatibility)
21590
21859
  */
21591
21860
  handleDateRangeChange() {
21861
+ this.handleApplyParams();
21862
+ }
21863
+ /**
21864
+ * RFC-0107: Handle apply params (date range, aggregation, limit)
21865
+ */
21866
+ handleApplyParams() {
21592
21867
  if (!this.modal) return;
21593
21868
  const startInput = this.modal.querySelector("#myio-water-tank-start-date");
21594
21869
  const endInput = this.modal.querySelector("#myio-water-tank-end-date");
21870
+ const aggregationSelect = this.modal.querySelector("#myio-water-tank-aggregation");
21871
+ const limitSelect = this.modal.querySelector("#myio-water-tank-limit");
21595
21872
  if (startInput && endInput) {
21596
21873
  const startTs = new Date(startInput.value).setHours(0, 0, 0, 0);
21597
21874
  const endTs = new Date(endInput.value).setHours(23, 59, 59, 999);
@@ -21599,17 +21876,77 @@
21599
21876
  alert("Start date must be before end date");
21600
21877
  return;
21601
21878
  }
21602
- console.log("[WaterTankModalView] Date range changed:", {
21879
+ const aggregation = aggregationSelect?.value || "NONE";
21880
+ const limit = parseInt(limitSelect?.value || "1000", 10);
21881
+ console.log("[WaterTankModalView] Params changed:", {
21603
21882
  startTs,
21604
21883
  endTs,
21884
+ aggregation,
21885
+ limit,
21605
21886
  startDate: new Date(startTs).toISOString(),
21606
21887
  endDate: new Date(endTs).toISOString()
21607
21888
  });
21608
- if (this.config.onDateRangeChange) {
21889
+ this.config.params.startTs = startTs;
21890
+ this.config.params.endTs = endTs;
21891
+ this.config.params.aggregation = aggregation;
21892
+ this.config.params.limit = limit;
21893
+ if (this.config.onParamsChange) {
21894
+ this.config.onParamsChange({ startTs, endTs, aggregation, limit });
21895
+ } else if (this.config.onDateRangeChange) {
21609
21896
  this.config.onDateRangeChange(startTs, endTs);
21610
21897
  }
21611
21898
  }
21612
21899
  }
21900
+ /**
21901
+ * RFC-0107: Handle maximize/restore chart
21902
+ */
21903
+ isMaximized = false;
21904
+ originalModalStyle = "";
21905
+ handleMaximize() {
21906
+ if (!this.modal) return;
21907
+ const chartPanel = this.modal.querySelector("#myio-water-tank-chart-panel");
21908
+ const tankPanel = chartPanel?.previousElementSibling;
21909
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
21910
+ if (!chartPanel) return;
21911
+ if (this.isMaximized) {
21912
+ this.modal.style.cssText = this.originalModalStyle;
21913
+ if (tankPanel) tankPanel.style.display = "";
21914
+ chartPanel.style.cssText = `
21915
+ flex: 1;
21916
+ background: white;
21917
+ border: 1px solid #e0e0e0;
21918
+ border-radius: 12px;
21919
+ padding: 16px;
21920
+ display: flex;
21921
+ flex-direction: column;
21922
+ position: relative;
21923
+ `;
21924
+ if (maximizeBtn) maximizeBtn.textContent = "\u26F6";
21925
+ this.isMaximized = false;
21926
+ } else {
21927
+ this.originalModalStyle = this.modal.style.cssText;
21928
+ this.modal.style.width = "95vw";
21929
+ this.modal.style.height = "90vh";
21930
+ this.modal.style.maxWidth = "95vw";
21931
+ if (tankPanel) tankPanel.style.display = "none";
21932
+ chartPanel.style.cssText = `
21933
+ flex: 1;
21934
+ background: white;
21935
+ border: 1px solid #e0e0e0;
21936
+ border-radius: 12px;
21937
+ padding: 16px;
21938
+ display: flex;
21939
+ flex-direction: column;
21940
+ position: relative;
21941
+ min-height: 100%;
21942
+ `;
21943
+ if (maximizeBtn) maximizeBtn.textContent = "\u26F6";
21944
+ this.isMaximized = true;
21945
+ }
21946
+ requestAnimationFrame(() => {
21947
+ this.renderCanvasChart();
21948
+ });
21949
+ }
21613
21950
  handleEscapeKey(e) {
21614
21951
  if (e.key === "Escape") {
21615
21952
  this.config.onClose();
@@ -21748,7 +22085,7 @@
21748
22085
  }
21749
22086
  }
21750
22087
  /**
21751
- * Update data and re-render chart
22088
+ * Update data and re-render chart with new layout
21752
22089
  */
21753
22090
  updateData(data) {
21754
22091
  this.config.data = data;
@@ -21756,13 +22093,20 @@
21756
22093
  const bodyEl = this.modal.querySelector(".myio-water-tank-modal-body");
21757
22094
  if (bodyEl) {
21758
22095
  bodyEl.innerHTML = `
21759
- ${this.renderDateRangePicker()}
21760
- ${this.renderTankVisualization()}
21761
- ${this.renderChart()}
22096
+ ${this.renderControlsBar()}
22097
+ <div style="
22098
+ display: flex;
22099
+ gap: 20px;
22100
+ flex: 1;
22101
+ min-height: 400px;
22102
+ ">
22103
+ ${this.renderTankPanel()}
22104
+ ${this.renderChartPanel()}
22105
+ </div>
21762
22106
  `;
21763
22107
  const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
21764
22108
  if (applyDatesBtn) {
21765
- applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
22109
+ applyDatesBtn.addEventListener("click", () => this.handleApplyParams());
21766
22110
  }
21767
22111
  const displayModeSelect = this.modal.querySelector("#myio-water-tank-display-mode");
21768
22112
  if (displayModeSelect) {
@@ -21771,6 +22115,10 @@
21771
22115
  this.refreshChart();
21772
22116
  });
21773
22117
  }
22118
+ const maximizeBtn = this.modal.querySelector("#myio-water-tank-maximize");
22119
+ if (maximizeBtn) {
22120
+ maximizeBtn.addEventListener("click", () => this.handleMaximize());
22121
+ }
21774
22122
  requestAnimationFrame(() => {
21775
22123
  this.renderCanvasChart();
21776
22124
  });
@@ -21943,13 +22291,12 @@
21943
22291
  }
21944
22292
  /**
21945
22293
  * Transform raw ThingsBoard response to our data points
21946
- * RFC-0107: Support displayKey option to choose between water_level or water_percentage
22294
+ * RFC-0107: Keep ALL data points with their original keys (no deduplication)
22295
+ * This allows the chart to switch between water_level and water_percentage dynamically
21947
22296
  */
21948
22297
  transformTelemetryData(rawData, keys) {
21949
22298
  const allPoints = [];
21950
- const displayKey = this.options.displayKey || "water_percentage";
21951
- const prioritizedKeys = rawData[displayKey] ? [displayKey, ...keys.filter((k) => k !== displayKey)] : keys;
21952
- for (const key of prioritizedKeys) {
22299
+ for (const key of keys) {
21953
22300
  if (rawData[key] && Array.isArray(rawData[key])) {
21954
22301
  const keyPoints = rawData[key].map((point) => {
21955
22302
  let value = typeof point.value === "string" ? parseFloat(point.value) : point.value;
@@ -21966,16 +22313,12 @@
21966
22313
  }
21967
22314
  }
21968
22315
  allPoints.sort((a, b) => a.ts - b.ts);
21969
- const uniquePoints = [];
21970
- const seenTimestamps = /* @__PURE__ */ new Set();
21971
- for (const point of allPoints) {
21972
- if (!seenTimestamps.has(point.ts)) {
21973
- seenTimestamps.add(point.ts);
21974
- uniquePoints.push(point);
21975
- }
22316
+ const keyCounts = {};
22317
+ for (const p of allPoints) {
22318
+ keyCounts[p.key || "unknown"] = (keyCounts[p.key || "unknown"] || 0) + 1;
21976
22319
  }
21977
- console.log(`[WaterTankModal] Transformed ${uniquePoints.length} points, displayKey: ${displayKey}`);
21978
- return uniquePoints;
22320
+ console.log(`[WaterTankModal] Transformed ${allPoints.length} total points:`, keyCounts);
22321
+ return allPoints;
21979
22322
  }
21980
22323
  /**
21981
22324
  * Calculate summary statistics from telemetry data
@@ -22055,7 +22398,8 @@
22055
22398
  onError: (error) => this.handleError(error),
22056
22399
  onClose: () => this.close(),
22057
22400
  // Call close() to destroy view and trigger user callback
22058
- onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs)
22401
+ onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs),
22402
+ onParamsChange: (params) => this.handleParamsChange(params)
22059
22403
  });
22060
22404
  this.view.render();
22061
22405
  this.view.show();
@@ -22120,6 +22464,43 @@
22120
22464
  this.handleError(error);
22121
22465
  }
22122
22466
  }
22467
+ /**
22468
+ * RFC-0107: Handle params change (date range, aggregation, limit)
22469
+ */
22470
+ async handleParamsChange(params) {
22471
+ console.log("[WaterTankModal] Params changed:", {
22472
+ startTs: params.startTs,
22473
+ endTs: params.endTs,
22474
+ aggregation: params.aggregation,
22475
+ limit: params.limit,
22476
+ startDate: new Date(params.startTs).toISOString(),
22477
+ endDate: new Date(params.endTs).toISOString()
22478
+ });
22479
+ this.options.startTs = params.startTs;
22480
+ this.options.endTs = params.endTs;
22481
+ this.options.aggregation = params.aggregation;
22482
+ this.options.limit = params.limit;
22483
+ this.context.timeRange.startTs = params.startTs;
22484
+ this.context.timeRange.endTs = params.endTs;
22485
+ try {
22486
+ console.log("[WaterTankModal] Fetching data with new params...");
22487
+ this.data = await this.fetchTelemetryData();
22488
+ if (this.view) {
22489
+ this.view.updateData(this.data);
22490
+ }
22491
+ if (this.options.onDataLoaded) {
22492
+ try {
22493
+ this.options.onDataLoaded(this.data);
22494
+ } catch (callbackError) {
22495
+ console.warn("[WaterTankModal] onDataLoaded callback error:", callbackError);
22496
+ }
22497
+ }
22498
+ console.log("[WaterTankModal] Data refreshed with new params successfully");
22499
+ } catch (error) {
22500
+ console.error("[WaterTankModal] Failed to fetch data with new params:", error);
22501
+ this.handleError(error);
22502
+ }
22503
+ }
22123
22504
  /**
22124
22505
  * Handle export functionality
22125
22506
  */
@@ -22241,8 +22622,8 @@
22241
22622
  }
22242
22623
  if (options.currentLevel !== void 0) {
22243
22624
  const level = Number(options.currentLevel);
22244
- if (isNaN(level) || level < 0 || level > 100) {
22245
- errors.push("currentLevel must be a number between 0 and 100");
22625
+ if (isNaN(level) || level < 0) {
22626
+ errors.push("currentLevel must be a non-negative number");
22246
22627
  }
22247
22628
  }
22248
22629
  if (options.limit !== void 0) {