myio-js-library 0.1.140 → 0.1.142

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.
@@ -17972,13 +17972,13 @@ ${rangeText}`;
17972
17972
  function initializeModal() {
17973
17973
  if (data) {
17974
17974
  modalState.goalsData = data;
17975
- renderModal();
17975
+ renderModal4();
17976
17976
  } else {
17977
- renderModal();
17977
+ renderModal4();
17978
17978
  loadGoalsData();
17979
17979
  }
17980
17980
  }
17981
- function renderModal() {
17981
+ function renderModal4() {
17982
17982
  const existing = document.getElementById("myio-goals-panel-modal");
17983
17983
  if (existing) {
17984
17984
  existing.remove();
@@ -19367,7 +19367,2331 @@ ${rangeText}`;
19367
19367
  }
19368
19368
  }
19369
19369
 
19370
+ // src/components/temperature/utils.ts
19371
+ var DAY_PERIODS = [
19372
+ { id: "madrugada", label: "Madrugada (00h-06h)", startHour: 0, endHour: 6 },
19373
+ { id: "manha", label: "Manh\xE3 (06h-12h)", startHour: 6, endHour: 12 },
19374
+ { id: "tarde", label: "Tarde (12h-18h)", startHour: 12, endHour: 18 },
19375
+ { id: "noite", label: "Noite (18h-24h)", startHour: 18, endHour: 24 }
19376
+ ];
19377
+ var DEFAULT_CLAMP_RANGE = { min: 15, max: 40 };
19378
+ function getTodaySoFar() {
19379
+ const now = /* @__PURE__ */ new Date();
19380
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
19381
+ return {
19382
+ startTs: startOfDay.getTime(),
19383
+ endTs: now.getTime()
19384
+ };
19385
+ }
19386
+ var CHART_COLORS = [
19387
+ "#1976d2",
19388
+ // Blue
19389
+ "#FF6B6B",
19390
+ // Red
19391
+ "#4CAF50",
19392
+ // Green
19393
+ "#FF9800",
19394
+ // Orange
19395
+ "#9C27B0",
19396
+ // Purple
19397
+ "#00BCD4",
19398
+ // Cyan
19399
+ "#E91E63",
19400
+ // Pink
19401
+ "#795548"
19402
+ // Brown
19403
+ ];
19404
+ async function fetchTemperatureData(token, deviceId, startTs, endTs) {
19405
+ const url = `/api/plugins/telemetry/DEVICE/${deviceId}/values/timeseries?keys=temperature&startTs=${encodeURIComponent(startTs)}&endTs=${encodeURIComponent(endTs)}&limit=50000&agg=NONE`;
19406
+ const response = await fetch(url, {
19407
+ headers: {
19408
+ "X-Authorization": `Bearer ${token}`,
19409
+ "Content-Type": "application/json"
19410
+ }
19411
+ });
19412
+ if (!response.ok) {
19413
+ throw new Error(`Failed to fetch temperature data: ${response.status}`);
19414
+ }
19415
+ const data = await response.json();
19416
+ return data?.temperature || [];
19417
+ }
19418
+ function clampTemperature(value, range = DEFAULT_CLAMP_RANGE) {
19419
+ const num = Number(value || 0);
19420
+ if (num < range.min) return range.min;
19421
+ if (num > range.max) return range.max;
19422
+ return num;
19423
+ }
19424
+ function calculateStats(data, clampRange = DEFAULT_CLAMP_RANGE) {
19425
+ if (data.length === 0) {
19426
+ return { avg: 0, min: 0, max: 0, count: 0 };
19427
+ }
19428
+ const values = data.map((item) => clampTemperature(item.value, clampRange));
19429
+ const sum = values.reduce((acc, v) => acc + v, 0);
19430
+ return {
19431
+ avg: sum / values.length,
19432
+ min: Math.min(...values),
19433
+ max: Math.max(...values),
19434
+ count: values.length
19435
+ };
19436
+ }
19437
+ function interpolateTemperature(data, options) {
19438
+ const { intervalMinutes, startTs, endTs, clampRange = DEFAULT_CLAMP_RANGE } = options;
19439
+ const intervalMs = intervalMinutes * 60 * 1e3;
19440
+ if (data.length === 0) {
19441
+ return [];
19442
+ }
19443
+ const sortedData = [...data].sort((a, b) => a.ts - b.ts);
19444
+ const result = [];
19445
+ let lastKnownValue = clampTemperature(sortedData[0].value, clampRange);
19446
+ let dataIndex = 0;
19447
+ for (let ts = startTs; ts <= endTs; ts += intervalMs) {
19448
+ while (dataIndex < sortedData.length - 1 && sortedData[dataIndex + 1].ts <= ts) {
19449
+ dataIndex++;
19450
+ }
19451
+ const currentData = sortedData[dataIndex];
19452
+ if (currentData && Math.abs(currentData.ts - ts) < intervalMs) {
19453
+ lastKnownValue = clampTemperature(currentData.value, clampRange);
19454
+ }
19455
+ result.push({
19456
+ ts,
19457
+ value: lastKnownValue
19458
+ });
19459
+ }
19460
+ return result;
19461
+ }
19462
+ function aggregateByDay(data, clampRange = DEFAULT_CLAMP_RANGE) {
19463
+ if (data.length === 0) {
19464
+ return [];
19465
+ }
19466
+ const dayMap = /* @__PURE__ */ new Map();
19467
+ data.forEach((item) => {
19468
+ const date = new Date(item.ts);
19469
+ const dateKey = date.toISOString().split("T")[0];
19470
+ if (!dayMap.has(dateKey)) {
19471
+ dayMap.set(dateKey, []);
19472
+ }
19473
+ dayMap.get(dateKey).push(item);
19474
+ });
19475
+ const result = [];
19476
+ dayMap.forEach((dayData, dateKey) => {
19477
+ const values = dayData.map((item) => clampTemperature(item.value, clampRange));
19478
+ const sum = values.reduce((acc, v) => acc + v, 0);
19479
+ result.push({
19480
+ date: dateKey,
19481
+ dateTs: new Date(dateKey).getTime(),
19482
+ avg: sum / values.length,
19483
+ min: Math.min(...values),
19484
+ max: Math.max(...values),
19485
+ count: values.length
19486
+ });
19487
+ });
19488
+ return result.sort((a, b) => a.dateTs - b.dateTs);
19489
+ }
19490
+ function filterByDayPeriods(data, selectedPeriods) {
19491
+ if (selectedPeriods.length === 0 || selectedPeriods.length === DAY_PERIODS.length) {
19492
+ return data;
19493
+ }
19494
+ return data.filter((item) => {
19495
+ const date = new Date(item.ts);
19496
+ const hour = date.getHours();
19497
+ return selectedPeriods.some((periodId) => {
19498
+ const period = DAY_PERIODS.find((p) => p.id === periodId);
19499
+ if (!period) return false;
19500
+ return hour >= period.startHour && hour < period.endHour;
19501
+ });
19502
+ });
19503
+ }
19504
+ function getSelectedPeriodsLabel(selectedPeriods) {
19505
+ if (selectedPeriods.length === 0 || selectedPeriods.length === DAY_PERIODS.length) {
19506
+ return "Todos os per\xEDodos";
19507
+ }
19508
+ if (selectedPeriods.length === 1) {
19509
+ const period = DAY_PERIODS.find((p) => p.id === selectedPeriods[0]);
19510
+ return period?.label || "";
19511
+ }
19512
+ return `${selectedPeriods.length} per\xEDodos selecionados`;
19513
+ }
19514
+ function formatTemperature(value, decimals = 1) {
19515
+ return `${value.toFixed(decimals)}\xB0C`;
19516
+ }
19517
+ function exportTemperatureCSV(data, deviceLabel, stats, startDate, endDate) {
19518
+ if (data.length === 0) {
19519
+ console.warn("No data to export");
19520
+ return;
19521
+ }
19522
+ const BOM = "\uFEFF";
19523
+ let csvContent = BOM;
19524
+ csvContent += `Relat\xF3rio de Temperatura - ${deviceLabel}
19525
+ `;
19526
+ csvContent += `Per\xEDodo: ${startDate} at\xE9 ${endDate}
19527
+ `;
19528
+ csvContent += `M\xE9dia: ${formatTemperature(stats.avg)}
19529
+ `;
19530
+ csvContent += `M\xEDnima: ${formatTemperature(stats.min)}
19531
+ `;
19532
+ csvContent += `M\xE1xima: ${formatTemperature(stats.max)}
19533
+ `;
19534
+ csvContent += `Total de leituras: ${stats.count}
19535
+ `;
19536
+ csvContent += "\n";
19537
+ csvContent += "Data/Hora,Temperatura (\xB0C)\n";
19538
+ data.forEach((item) => {
19539
+ const date = new Date(item.ts).toLocaleString("pt-BR");
19540
+ const temp = Number(item.value).toFixed(2);
19541
+ csvContent += `"${date}",${temp}
19542
+ `;
19543
+ });
19544
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
19545
+ const url = URL.createObjectURL(blob);
19546
+ const link = document.createElement("a");
19547
+ link.href = url;
19548
+ link.download = `temperatura_${deviceLabel.replace(/\s+/g, "_")}_${startDate}_${endDate}.csv`;
19549
+ document.body.appendChild(link);
19550
+ link.click();
19551
+ document.body.removeChild(link);
19552
+ URL.revokeObjectURL(url);
19553
+ }
19554
+ var DARK_THEME = {
19555
+ background: "rgba(0, 0, 0, 0.85)",
19556
+ surface: "#1a1f28",
19557
+ text: "#ffffff",
19558
+ textMuted: "rgba(255, 255, 255, 0.7)",
19559
+ border: "rgba(255, 255, 255, 0.1)",
19560
+ primary: "#1976d2",
19561
+ success: "#4CAF50",
19562
+ warning: "#FF9800",
19563
+ danger: "#f44336",
19564
+ chartLine: "#1976d2",
19565
+ chartGrid: "rgba(255, 255, 255, 0.1)"
19566
+ };
19567
+ var LIGHT_THEME = {
19568
+ background: "rgba(0, 0, 0, 0.6)",
19569
+ surface: "#ffffff",
19570
+ text: "#333333",
19571
+ textMuted: "#666666",
19572
+ border: "#e0e0e0",
19573
+ primary: "#1976d2",
19574
+ success: "#4CAF50",
19575
+ warning: "#FF9800",
19576
+ danger: "#f44336",
19577
+ chartLine: "#1976d2",
19578
+ chartGrid: "#e0e0e0"
19579
+ };
19580
+ function getThemeColors(theme) {
19581
+ return theme === "dark" ? DARK_THEME : LIGHT_THEME;
19582
+ }
19583
+
19584
+ // src/components/temperature/TemperatureModal.ts
19585
+ async function openTemperatureModal(params) {
19586
+ const modalId = `myio-temp-modal-${Date.now()}`;
19587
+ const defaultDateRange = getTodaySoFar();
19588
+ const startTs = params.startDate ? new Date(params.startDate).getTime() : defaultDateRange.startTs;
19589
+ const endTs = params.endDate ? new Date(params.endDate).getTime() : defaultDateRange.endTs;
19590
+ const state = {
19591
+ token: params.token,
19592
+ deviceId: params.deviceId,
19593
+ label: params.label || "Sensor de Temperatura",
19594
+ currentTemperature: params.currentTemperature ?? null,
19595
+ temperatureMin: params.temperatureMin ?? null,
19596
+ temperatureMax: params.temperatureMax ?? null,
19597
+ temperatureStatus: params.temperatureStatus ?? null,
19598
+ startTs,
19599
+ endTs,
19600
+ granularity: params.granularity || "hour",
19601
+ theme: params.theme || "light",
19602
+ clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
19603
+ locale: params.locale || "pt-BR",
19604
+ data: [],
19605
+ stats: { avg: 0, min: 0, max: 0, count: 0 },
19606
+ isLoading: true,
19607
+ dateRangePicker: null,
19608
+ selectedPeriods: ["madrugada", "manha", "tarde", "noite"]
19609
+ // All periods selected by default
19610
+ };
19611
+ const savedGranularity = localStorage.getItem("myio-temp-modal-granularity");
19612
+ const savedTheme = localStorage.getItem("myio-temp-modal-theme");
19613
+ if (savedGranularity) state.granularity = savedGranularity;
19614
+ if (savedTheme) state.theme = savedTheme;
19615
+ const modalContainer = document.createElement("div");
19616
+ modalContainer.id = modalId;
19617
+ document.body.appendChild(modalContainer);
19618
+ renderModal(modalContainer, state, modalId);
19619
+ try {
19620
+ state.data = await fetchTemperatureData(state.token, state.deviceId, state.startTs, state.endTs);
19621
+ state.stats = calculateStats(state.data, state.clampRange);
19622
+ state.isLoading = false;
19623
+ renderModal(modalContainer, state, modalId);
19624
+ drawChart(modalId, state);
19625
+ } catch (error) {
19626
+ console.error("[TemperatureModal] Error fetching data:", error);
19627
+ state.isLoading = false;
19628
+ renderModal(modalContainer, state, modalId, error);
19629
+ }
19630
+ await setupEventListeners(modalContainer, state, modalId, params.onClose);
19631
+ return {
19632
+ destroy: () => {
19633
+ modalContainer.remove();
19634
+ params.onClose?.();
19635
+ },
19636
+ updateData: async (startDate, endDate, granularity) => {
19637
+ state.startTs = new Date(startDate).getTime();
19638
+ state.endTs = new Date(endDate).getTime();
19639
+ if (granularity) state.granularity = granularity;
19640
+ state.isLoading = true;
19641
+ renderModal(modalContainer, state, modalId);
19642
+ try {
19643
+ state.data = await fetchTemperatureData(state.token, state.deviceId, state.startTs, state.endTs);
19644
+ state.stats = calculateStats(state.data, state.clampRange);
19645
+ state.isLoading = false;
19646
+ renderModal(modalContainer, state, modalId);
19647
+ drawChart(modalId, state);
19648
+ } catch (error) {
19649
+ console.error("[TemperatureModal] Error updating data:", error);
19650
+ state.isLoading = false;
19651
+ renderModal(modalContainer, state, modalId, error);
19652
+ }
19653
+ }
19654
+ };
19655
+ }
19656
+ function renderModal(container, state, modalId, error) {
19657
+ const colors = getThemeColors(state.theme);
19658
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale);
19659
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale);
19660
+ const statusText = state.temperatureStatus === "ok" ? "Dentro da faixa" : state.temperatureStatus === "above" ? "Acima do limite" : state.temperatureStatus === "below" ? "Abaixo do limite" : "N/A";
19661
+ const statusColor = state.temperatureStatus === "ok" ? colors.success : state.temperatureStatus === "above" ? colors.danger : state.temperatureStatus === "below" ? colors.primary : colors.textMuted;
19662
+ const rangeText = state.temperatureMin !== null && state.temperatureMax !== null ? `${state.temperatureMin}\xB0C - ${state.temperatureMax}\xB0C` : "N\xE3o definida";
19663
+ new Date(state.startTs).toISOString().slice(0, 16);
19664
+ new Date(state.endTs).toISOString().slice(0, 16);
19665
+ const isMaximized = container.__isMaximized || false;
19666
+ const contentMaxWidth = isMaximized ? "100%" : "900px";
19667
+ const contentMaxHeight = isMaximized ? "100vh" : "95vh";
19668
+ const contentBorderRadius = isMaximized ? "0" : "10px";
19669
+ container.innerHTML = `
19670
+ <div class="myio-temp-modal-overlay" style="
19671
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
19672
+ background: rgba(0, 0, 0, 0.5); z-index: 9998;
19673
+ display: flex; justify-content: center; align-items: center;
19674
+ backdrop-filter: blur(2px);
19675
+ ">
19676
+ <div class="myio-temp-modal-content" style="
19677
+ background: ${colors.surface}; border-radius: ${contentBorderRadius};
19678
+ max-width: ${contentMaxWidth}; width: ${isMaximized ? "100%" : "95%"};
19679
+ max-height: ${contentMaxHeight}; height: ${isMaximized ? "100%" : "auto"};
19680
+ overflow: hidden; display: flex; flex-direction: column;
19681
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
19682
+ font-family: 'Roboto', Arial, sans-serif;
19683
+ ">
19684
+ <!-- Header - MyIO Premium Style -->
19685
+ <div style="
19686
+ padding: 4px 8px; display: flex; align-items: center; justify-content: space-between;
19687
+ background: #3e1a7d; color: white; border-radius: ${isMaximized ? "0" : "10px 10px 0 0"};
19688
+ min-height: 20px;
19689
+ ">
19690
+ <h2 style="margin: 6px; font-size: 18px; font-weight: 600; color: white; line-height: 2;">
19691
+ \u{1F321}\uFE0F ${state.label} - Hist\xF3rico de Temperatura
19692
+ </h2>
19693
+ <div style="display: flex; gap: 4px; align-items: center;">
19694
+ <!-- Theme Toggle -->
19695
+ <button id="${modalId}-theme-toggle" title="Alternar tema" style="
19696
+ background: none; border: none; font-size: 16px; cursor: pointer;
19697
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
19698
+ transition: background-color 0.2s;
19699
+ ">${state.theme === "dark" ? "\u2600\uFE0F" : "\u{1F319}"}</button>
19700
+ <!-- Maximize Button -->
19701
+ <button id="${modalId}-maximize" title="${isMaximized ? "Restaurar" : "Maximizar"}" style="
19702
+ background: none; border: none; font-size: 16px; cursor: pointer;
19703
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
19704
+ transition: background-color 0.2s;
19705
+ ">${isMaximized ? "\u{1F5D7}" : "\u{1F5D6}"}</button>
19706
+ <!-- Close Button -->
19707
+ <button id="${modalId}-close" title="Fechar" style="
19708
+ background: none; border: none; font-size: 20px; cursor: pointer;
19709
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
19710
+ transition: background-color 0.2s;
19711
+ ">\xD7</button>
19712
+ </div>
19713
+ </div>
19714
+
19715
+ <!-- Body -->
19716
+ <div style="flex: 1; overflow-y: auto; padding: 16px;">
19717
+
19718
+ <!-- Controls Row -->
19719
+ <div style="
19720
+ display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end;
19721
+ margin-bottom: 16px; padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#f7f7f7"};
19722
+ border-radius: 6px; border: 1px solid ${colors.border};
19723
+ ">
19724
+ <!-- Granularity Select -->
19725
+ <div>
19726
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
19727
+ Granularidade
19728
+ </label>
19729
+ <select id="${modalId}-granularity" style="
19730
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
19731
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
19732
+ cursor: pointer; min-width: 130px;
19733
+ ">
19734
+ <option value="hour" ${state.granularity === "hour" ? "selected" : ""}>Hora (30 min)</option>
19735
+ <option value="day" ${state.granularity === "day" ? "selected" : ""}>Dia (m\xE9dia)</option>
19736
+ </select>
19737
+ </div>
19738
+ <!-- Day Period Filter (Multiselect) -->
19739
+ <div style="position: relative;">
19740
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
19741
+ Per\xEDodos do Dia
19742
+ </label>
19743
+ <button id="${modalId}-period-btn" type="button" style="
19744
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
19745
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
19746
+ cursor: pointer; min-width: 180px; text-align: left;
19747
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
19748
+ ">
19749
+ <span>${getSelectedPeriodsLabel(state.selectedPeriods)}</span>
19750
+ <span style="font-size: 10px;">\u25BC</span>
19751
+ </button>
19752
+ <div id="${modalId}-period-dropdown" style="
19753
+ display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
19754
+ background: ${colors.surface}; border: 1px solid ${colors.border};
19755
+ border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
19756
+ min-width: 200px; margin-top: 4px; padding: 8px 0;
19757
+ ">
19758
+ ${DAY_PERIODS.map((period) => `
19759
+ <label style="
19760
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
19761
+ cursor: pointer; font-size: 13px; color: ${colors.text};
19762
+ " onmouseover="this.style.background='${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"}'"
19763
+ onmouseout="this.style.background='transparent'">
19764
+ <input type="checkbox"
19765
+ name="${modalId}-period"
19766
+ value="${period.id}"
19767
+ ${state.selectedPeriods.includes(period.id) ? "checked" : ""}
19768
+ style="width: 16px; height: 16px; cursor: pointer; accent-color: #3e1a7d;">
19769
+ ${period.label}
19770
+ </label>
19771
+ `).join("")}
19772
+ <div style="border-top: 1px solid ${colors.border}; margin-top: 8px; padding-top: 8px;">
19773
+ <button id="${modalId}-period-select-all" type="button" style="
19774
+ width: calc(100% - 16px); margin: 0 8px 4px; padding: 6px;
19775
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
19776
+ border: none; border-radius: 4px; cursor: pointer;
19777
+ font-size: 12px; color: ${colors.text};
19778
+ ">Selecionar Todos</button>
19779
+ <button id="${modalId}-period-clear" type="button" style="
19780
+ width: calc(100% - 16px); margin: 0 8px; padding: 6px;
19781
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
19782
+ border: none; border-radius: 4px; cursor: pointer;
19783
+ font-size: 12px; color: ${colors.text};
19784
+ ">Limpar Sele\xE7\xE3o</button>
19785
+ </div>
19786
+ </div>
19787
+ </div>
19788
+ <!-- Date Range Picker -->
19789
+ <div style="flex: 1; min-width: 220px;">
19790
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
19791
+ Per\xEDodo
19792
+ </label>
19793
+ <input type="text" id="${modalId}-date-range" readonly placeholder="Selecione o per\xEDodo..." style="
19794
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
19795
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
19796
+ width: 100%; cursor: pointer; box-sizing: border-box;
19797
+ "/>
19798
+ </div>
19799
+ <!-- Query Button -->
19800
+ <button id="${modalId}-query" style="
19801
+ background: #3e1a7d; color: white; border: none;
19802
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
19803
+ font-size: 14px; font-weight: 500; height: 38px;
19804
+ display: flex; align-items: center; gap: 8px;
19805
+ font-family: 'Roboto', Arial, sans-serif;
19806
+ " ${state.isLoading ? "disabled" : ""}>
19807
+ ${state.isLoading ? '<span style="animation: spin 1s linear infinite; display: inline-block;">\u21BB</span> Carregando...' : "Carregar"}
19808
+ </button>
19809
+ </div>
19810
+
19811
+ <!-- Stats Cards -->
19812
+ <div style="
19813
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
19814
+ gap: 12px; margin-bottom: 16px;
19815
+ ">
19816
+ <!-- Current Temperature -->
19817
+ <div style="
19818
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
19819
+ border-radius: 12px; border: 1px solid ${colors.border};
19820
+ ">
19821
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">Temperatura Atual</span>
19822
+ <div style="font-weight: 700; font-size: 24px; color: ${statusColor}; margin-top: 4px;">
19823
+ ${state.currentTemperature !== null ? formatTemperature(state.currentTemperature) : "N/A"}
19824
+ </div>
19825
+ <div style="font-size: 11px; color: ${statusColor}; margin-top: 2px;">${statusText}</div>
19826
+ </div>
19827
+ <!-- Average -->
19828
+ <div style="
19829
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
19830
+ border-radius: 12px; border: 1px solid ${colors.border};
19831
+ ">
19832
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">M\xE9dia do Per\xEDodo</span>
19833
+ <div id="${modalId}-avg" style="font-weight: 600; font-size: 20px; color: ${colors.text}; margin-top: 4px;">
19834
+ ${state.stats.count > 0 ? formatTemperature(state.stats.avg) : "N/A"}
19835
+ </div>
19836
+ <div style="font-size: 11px; color: ${colors.textMuted}; margin-top: 2px;">${startDateStr} - ${endDateStr}</div>
19837
+ </div>
19838
+ <!-- Min/Max -->
19839
+ <div style="
19840
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
19841
+ border-radius: 12px; border: 1px solid ${colors.border};
19842
+ ">
19843
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">Min / Max</span>
19844
+ <div id="${modalId}-minmax" style="font-weight: 600; font-size: 20px; color: ${colors.text}; margin-top: 4px;">
19845
+ ${state.stats.count > 0 ? `${formatTemperature(state.stats.min)} / ${formatTemperature(state.stats.max)}` : "N/A"}
19846
+ </div>
19847
+ <div style="font-size: 11px; color: ${colors.textMuted}; margin-top: 2px;">${state.stats.count} leituras</div>
19848
+ </div>
19849
+ <!-- Ideal Range -->
19850
+ <div style="
19851
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
19852
+ border-radius: 12px; border: 1px solid ${colors.border};
19853
+ ">
19854
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">Faixa Ideal</span>
19855
+ <div style="font-weight: 600; font-size: 20px; color: ${colors.success}; margin-top: 4px;">
19856
+ ${rangeText}
19857
+ </div>
19858
+ </div>
19859
+ </div>
19860
+
19861
+ <!-- Chart Container -->
19862
+ <div style="margin-bottom: 20px;">
19863
+ <h3 style="margin: 0 0 12px 0; font-size: 14px; color: ${colors.textMuted}; font-weight: 500;">
19864
+ Hist\xF3rico de Temperatura
19865
+ </h3>
19866
+ <div id="${modalId}-chart" style="
19867
+ height: 320px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.03)" : "#fafafa"};
19868
+ border-radius: 12px; display: flex; justify-content: center; align-items: center;
19869
+ border: 1px solid ${colors.border}; position: relative;
19870
+ ">
19871
+ ${state.isLoading ? `<div style="text-align: center; color: ${colors.textMuted};">
19872
+ <div style="animation: spin 1s linear infinite; font-size: 32px; margin-bottom: 8px;">\u21BB</div>
19873
+ <div>Carregando dados...</div>
19874
+ </div>` : error ? `<div style="text-align: center; color: ${colors.danger};">
19875
+ <div style="font-size: 32px; margin-bottom: 8px;">\u26A0\uFE0F</div>
19876
+ <div>Erro ao carregar dados</div>
19877
+ <div style="font-size: 12px; margin-top: 4px;">${error.message}</div>
19878
+ </div>` : state.data.length === 0 ? `<div style="text-align: center; color: ${colors.textMuted};">
19879
+ <div style="font-size: 32px; margin-bottom: 8px;">\u{1F4ED}</div>
19880
+ <div>Sem dados para o per\xEDodo selecionado</div>
19881
+ </div>` : `<canvas id="${modalId}-canvas" style="width: 100%; height: 100%;"></canvas>`}
19882
+ </div>
19883
+ </div>
19884
+
19885
+ <!-- Actions -->
19886
+ <div style="display: flex; justify-content: flex-end; gap: 12px;">
19887
+ <button id="${modalId}-export" style="
19888
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f7f7f7"};
19889
+ color: ${colors.text}; border: 1px solid ${colors.border};
19890
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
19891
+ font-size: 14px; display: flex; align-items: center; gap: 8px;
19892
+ font-family: 'Roboto', Arial, sans-serif;
19893
+ " ${state.data.length === 0 ? "disabled" : ""}>
19894
+ \u{1F4E5} Exportar CSV
19895
+ </button>
19896
+ <button id="${modalId}-close-btn" style="
19897
+ background: #3e1a7d; color: white; border: none;
19898
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
19899
+ font-size: 14px; font-weight: 500;
19900
+ font-family: 'Roboto', Arial, sans-serif;
19901
+ ">
19902
+ Fechar
19903
+ </button>
19904
+ </div>
19905
+ </div><!-- End Body -->
19906
+ </div>
19907
+ </div>
19908
+ <style>
19909
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
19910
+ #${modalId} select:focus, #${modalId} input:focus {
19911
+ outline: 2px solid #3e1a7d;
19912
+ outline-offset: 2px;
19913
+ }
19914
+ #${modalId} button:hover:not(:disabled) {
19915
+ opacity: 0.9;
19916
+ }
19917
+ #${modalId} button:disabled {
19918
+ opacity: 0.5;
19919
+ cursor: not-allowed;
19920
+ }
19921
+ #${modalId} .myio-temp-modal-content > div:first-child button:hover {
19922
+ background: rgba(255, 255, 255, 0.1) !important;
19923
+ color: white !important;
19924
+ }
19925
+ </style>
19926
+ `;
19927
+ }
19928
+ function drawChart(modalId, state) {
19929
+ const chartContainer = document.getElementById(`${modalId}-chart`);
19930
+ const canvas = document.getElementById(`${modalId}-canvas`);
19931
+ if (!chartContainer || !canvas || state.data.length === 0) return;
19932
+ const ctx = canvas.getContext("2d");
19933
+ if (!ctx) return;
19934
+ const colors = getThemeColors(state.theme);
19935
+ const filteredData = filterByDayPeriods(state.data, state.selectedPeriods);
19936
+ if (filteredData.length === 0) {
19937
+ canvas.width = chartContainer.clientWidth;
19938
+ canvas.height = chartContainer.clientHeight;
19939
+ ctx.fillStyle = colors.textMuted;
19940
+ ctx.font = "14px Roboto, Arial, sans-serif";
19941
+ ctx.textAlign = "center";
19942
+ ctx.fillText("Nenhum dado para os per\xEDodos selecionados", canvas.width / 2, canvas.height / 2);
19943
+ return;
19944
+ }
19945
+ let chartData;
19946
+ if (state.granularity === "hour") {
19947
+ const interpolated = interpolateTemperature(filteredData, {
19948
+ intervalMinutes: 30,
19949
+ startTs: state.startTs,
19950
+ endTs: state.endTs,
19951
+ clampRange: state.clampRange
19952
+ });
19953
+ const filteredInterpolated = filterByDayPeriods(interpolated, state.selectedPeriods);
19954
+ chartData = filteredInterpolated.map((item) => ({
19955
+ x: item.ts,
19956
+ y: Number(item.value),
19957
+ screenX: 0,
19958
+ screenY: 0
19959
+ }));
19960
+ } else {
19961
+ const daily = aggregateByDay(filteredData, state.clampRange);
19962
+ chartData = daily.map((item) => ({
19963
+ x: item.dateTs,
19964
+ y: item.avg,
19965
+ screenX: 0,
19966
+ screenY: 0,
19967
+ label: item.date
19968
+ }));
19969
+ }
19970
+ if (chartData.length === 0) return;
19971
+ const width = chartContainer.clientWidth - 2;
19972
+ const height = 320;
19973
+ canvas.width = width;
19974
+ canvas.height = height;
19975
+ const paddingLeft = 60;
19976
+ const paddingRight = 20;
19977
+ const paddingTop = 20;
19978
+ const paddingBottom = 55;
19979
+ const isPeriodsFiltered = state.selectedPeriods.length < 4 && state.selectedPeriods.length > 0;
19980
+ const values = chartData.map((d) => d.y);
19981
+ const dataMin = Math.min(...values);
19982
+ const dataMax = Math.max(...values);
19983
+ const thresholdMin = state.temperatureMin !== null ? state.temperatureMin : dataMin;
19984
+ const thresholdMax = state.temperatureMax !== null ? state.temperatureMax : dataMax;
19985
+ const minY = Math.min(dataMin, thresholdMin) - 1;
19986
+ const maxY = Math.max(dataMax, thresholdMax) + 1;
19987
+ const chartWidth = width - paddingLeft - paddingRight;
19988
+ const chartHeight = height - paddingTop - paddingBottom;
19989
+ const scaleY = chartHeight / (maxY - minY || 1);
19990
+ if (isPeriodsFiltered) {
19991
+ const pointSpacing = chartWidth / Math.max(1, chartData.length - 1);
19992
+ chartData.forEach((point, index) => {
19993
+ point.screenX = paddingLeft + index * pointSpacing;
19994
+ point.screenY = height - paddingBottom - (point.y - minY) * scaleY;
19995
+ });
19996
+ } else {
19997
+ const minX = chartData[0].x;
19998
+ const maxX = chartData[chartData.length - 1].x;
19999
+ const timeRange = maxX - minX || 1;
20000
+ const scaleX = chartWidth / timeRange;
20001
+ chartData.forEach((point) => {
20002
+ point.screenX = paddingLeft + (point.x - minX) * scaleX;
20003
+ point.screenY = height - paddingBottom - (point.y - minY) * scaleY;
20004
+ });
20005
+ }
20006
+ ctx.clearRect(0, 0, width, height);
20007
+ ctx.strokeStyle = colors.chartGrid;
20008
+ ctx.lineWidth = 1;
20009
+ for (let i = 0; i <= 4; i++) {
20010
+ const y = paddingTop + chartHeight * i / 4;
20011
+ ctx.beginPath();
20012
+ ctx.moveTo(paddingLeft, y);
20013
+ ctx.lineTo(width - paddingRight, y);
20014
+ ctx.stroke();
20015
+ }
20016
+ if (state.temperatureMin !== null && state.temperatureMax !== null) {
20017
+ const rangeMinY = height - paddingBottom - (state.temperatureMin - minY) * scaleY;
20018
+ const rangeMaxY = height - paddingBottom - (state.temperatureMax - minY) * scaleY;
20019
+ ctx.fillStyle = "rgba(76, 175, 80, 0.1)";
20020
+ ctx.fillRect(paddingLeft, rangeMaxY, chartWidth, rangeMinY - rangeMaxY);
20021
+ ctx.strokeStyle = colors.success;
20022
+ ctx.setLineDash([5, 5]);
20023
+ ctx.beginPath();
20024
+ ctx.moveTo(paddingLeft, rangeMinY);
20025
+ ctx.lineTo(width - paddingRight, rangeMinY);
20026
+ ctx.moveTo(paddingLeft, rangeMaxY);
20027
+ ctx.lineTo(width - paddingRight, rangeMaxY);
20028
+ ctx.stroke();
20029
+ ctx.setLineDash([]);
20030
+ }
20031
+ ctx.strokeStyle = colors.chartLine;
20032
+ ctx.lineWidth = 2;
20033
+ ctx.beginPath();
20034
+ chartData.forEach((point, i) => {
20035
+ if (i === 0) ctx.moveTo(point.screenX, point.screenY);
20036
+ else ctx.lineTo(point.screenX, point.screenY);
20037
+ });
20038
+ ctx.stroke();
20039
+ ctx.fillStyle = colors.chartLine;
20040
+ chartData.forEach((point) => {
20041
+ ctx.beginPath();
20042
+ ctx.arc(point.screenX, point.screenY, 4, 0, Math.PI * 2);
20043
+ ctx.fill();
20044
+ });
20045
+ ctx.fillStyle = colors.textMuted;
20046
+ ctx.font = "11px system-ui, sans-serif";
20047
+ ctx.textAlign = "right";
20048
+ for (let i = 0; i <= 4; i++) {
20049
+ const val = minY + (maxY - minY) * (4 - i) / 4;
20050
+ const y = paddingTop + chartHeight * i / 4;
20051
+ ctx.fillText(val.toFixed(1) + "\xB0C", paddingLeft - 8, y + 4);
20052
+ }
20053
+ ctx.textAlign = "center";
20054
+ const numLabels = Math.min(8, chartData.length);
20055
+ const labelInterval = Math.max(1, Math.floor(chartData.length / numLabels));
20056
+ for (let i = 0; i < chartData.length; i += labelInterval) {
20057
+ const point = chartData[i];
20058
+ const date = new Date(point.x);
20059
+ let label;
20060
+ if (state.granularity === "hour") {
20061
+ label = date.toLocaleTimeString(state.locale, { hour: "2-digit", minute: "2-digit" });
20062
+ } else {
20063
+ label = date.toLocaleDateString(state.locale, { day: "2-digit", month: "2-digit" });
20064
+ }
20065
+ ctx.strokeStyle = colors.chartGrid;
20066
+ ctx.lineWidth = 1;
20067
+ ctx.beginPath();
20068
+ ctx.moveTo(point.screenX, paddingTop);
20069
+ ctx.lineTo(point.screenX, height - paddingBottom);
20070
+ ctx.stroke();
20071
+ ctx.fillStyle = colors.textMuted;
20072
+ ctx.fillText(label, point.screenX, height - paddingBottom + 18);
20073
+ }
20074
+ ctx.strokeStyle = colors.border;
20075
+ ctx.lineWidth = 1;
20076
+ ctx.beginPath();
20077
+ ctx.moveTo(paddingLeft, paddingTop);
20078
+ ctx.lineTo(paddingLeft, height - paddingBottom);
20079
+ ctx.lineTo(width - paddingRight, height - paddingBottom);
20080
+ ctx.stroke();
20081
+ setupChartTooltip(canvas, chartContainer, chartData, state, colors);
20082
+ }
20083
+ function setupChartTooltip(canvas, container, chartData, state, colors) {
20084
+ const existingTooltip = container.querySelector(".myio-chart-tooltip");
20085
+ if (existingTooltip) existingTooltip.remove();
20086
+ const tooltip = document.createElement("div");
20087
+ tooltip.className = "myio-chart-tooltip";
20088
+ tooltip.style.cssText = `
20089
+ position: absolute;
20090
+ background: ${state.theme === "dark" ? "rgba(30, 30, 40, 0.95)" : "rgba(255, 255, 255, 0.98)"};
20091
+ border: 1px solid ${colors.border};
20092
+ border-radius: 8px;
20093
+ padding: 10px 14px;
20094
+ font-size: 13px;
20095
+ color: ${colors.text};
20096
+ pointer-events: none;
20097
+ opacity: 0;
20098
+ transition: opacity 0.15s;
20099
+ z-index: 1000;
20100
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20101
+ min-width: 140px;
20102
+ `;
20103
+ container.appendChild(tooltip);
20104
+ const findNearestPoint = (mouseX, mouseY) => {
20105
+ const threshold = 20;
20106
+ let nearest = null;
20107
+ let minDist = Infinity;
20108
+ for (const point of chartData) {
20109
+ const dist = Math.sqrt(
20110
+ Math.pow(mouseX - point.screenX, 2) + Math.pow(mouseY - point.screenY, 2)
20111
+ );
20112
+ if (dist < minDist && dist < threshold) {
20113
+ minDist = dist;
20114
+ nearest = point;
20115
+ }
20116
+ }
20117
+ return nearest;
20118
+ };
20119
+ canvas.addEventListener("mousemove", (e) => {
20120
+ const rect = canvas.getBoundingClientRect();
20121
+ const mouseX = e.clientX - rect.left;
20122
+ const mouseY = e.clientY - rect.top;
20123
+ const point = findNearestPoint(mouseX, mouseY);
20124
+ if (point) {
20125
+ const date = new Date(point.x);
20126
+ let dateStr;
20127
+ if (state.granularity === "hour") {
20128
+ dateStr = date.toLocaleDateString(state.locale, {
20129
+ day: "2-digit",
20130
+ month: "2-digit",
20131
+ year: "numeric"
20132
+ }) + " " + date.toLocaleTimeString(state.locale, {
20133
+ hour: "2-digit",
20134
+ minute: "2-digit"
20135
+ });
20136
+ } else {
20137
+ dateStr = date.toLocaleDateString(state.locale, {
20138
+ day: "2-digit",
20139
+ month: "2-digit",
20140
+ year: "numeric"
20141
+ });
20142
+ }
20143
+ tooltip.innerHTML = `
20144
+ <div style="font-weight: 600; margin-bottom: 6px; color: ${colors.primary};">
20145
+ ${formatTemperature(point.y)}
20146
+ </div>
20147
+ <div style="font-size: 11px; color: ${colors.textMuted};">
20148
+ \u{1F4C5} ${dateStr}
20149
+ </div>
20150
+ `;
20151
+ let tooltipX = point.screenX + 15;
20152
+ let tooltipY = point.screenY - 15;
20153
+ const tooltipRect = tooltip.getBoundingClientRect();
20154
+ const containerRect = container.getBoundingClientRect();
20155
+ if (tooltipX + tooltipRect.width > containerRect.width - 10) {
20156
+ tooltipX = point.screenX - tooltipRect.width - 15;
20157
+ }
20158
+ if (tooltipY < 10) {
20159
+ tooltipY = point.screenY + 15;
20160
+ }
20161
+ tooltip.style.left = `${tooltipX}px`;
20162
+ tooltip.style.top = `${tooltipY}px`;
20163
+ tooltip.style.opacity = "1";
20164
+ canvas.style.cursor = "pointer";
20165
+ } else {
20166
+ tooltip.style.opacity = "0";
20167
+ canvas.style.cursor = "default";
20168
+ }
20169
+ });
20170
+ canvas.addEventListener("mouseleave", () => {
20171
+ tooltip.style.opacity = "0";
20172
+ canvas.style.cursor = "default";
20173
+ });
20174
+ }
20175
+ async function setupEventListeners(container, state, modalId, onClose) {
20176
+ const closeModal = () => {
20177
+ container.remove();
20178
+ onClose?.();
20179
+ };
20180
+ container.querySelector(".myio-temp-modal-overlay")?.addEventListener("click", (e) => {
20181
+ if (e.target === e.currentTarget) closeModal();
20182
+ });
20183
+ document.getElementById(`${modalId}-close`)?.addEventListener("click", closeModal);
20184
+ document.getElementById(`${modalId}-close-btn`)?.addEventListener("click", closeModal);
20185
+ const dateRangeInput = document.getElementById(`${modalId}-date-range`);
20186
+ if (dateRangeInput && !state.dateRangePicker) {
20187
+ try {
20188
+ state.dateRangePicker = await createDateRangePicker2(dateRangeInput, {
20189
+ presetStart: new Date(state.startTs).toISOString(),
20190
+ presetEnd: new Date(state.endTs).toISOString(),
20191
+ includeTime: true,
20192
+ timePrecision: "minute",
20193
+ maxRangeDays: 90,
20194
+ locale: state.locale,
20195
+ parentEl: container.querySelector(".myio-temp-modal-content"),
20196
+ onApply: (result) => {
20197
+ state.startTs = new Date(result.startISO).getTime();
20198
+ state.endTs = new Date(result.endISO).getTime();
20199
+ console.log("[TemperatureModal] Date range applied:", result);
20200
+ }
20201
+ });
20202
+ } catch (error) {
20203
+ console.warn("[TemperatureModal] DateRangePicker initialization failed:", error);
20204
+ }
20205
+ }
20206
+ document.getElementById(`${modalId}-theme-toggle`)?.addEventListener("click", async () => {
20207
+ state.theme = state.theme === "dark" ? "light" : "dark";
20208
+ localStorage.setItem("myio-temp-modal-theme", state.theme);
20209
+ state.dateRangePicker = null;
20210
+ renderModal(container, state, modalId);
20211
+ if (state.data.length > 0) drawChart(modalId, state);
20212
+ await setupEventListeners(container, state, modalId, onClose);
20213
+ });
20214
+ document.getElementById(`${modalId}-maximize`)?.addEventListener("click", async () => {
20215
+ container.__isMaximized = !container.__isMaximized;
20216
+ state.dateRangePicker = null;
20217
+ renderModal(container, state, modalId);
20218
+ if (state.data.length > 0) drawChart(modalId, state);
20219
+ await setupEventListeners(container, state, modalId, onClose);
20220
+ });
20221
+ const periodBtn = document.getElementById(`${modalId}-period-btn`);
20222
+ const periodDropdown = document.getElementById(`${modalId}-period-dropdown`);
20223
+ periodBtn?.addEventListener("click", (e) => {
20224
+ e.stopPropagation();
20225
+ if (periodDropdown) {
20226
+ periodDropdown.style.display = periodDropdown.style.display === "none" ? "block" : "none";
20227
+ }
20228
+ });
20229
+ document.addEventListener("click", (e) => {
20230
+ if (periodDropdown && !periodDropdown.contains(e.target) && e.target !== periodBtn) {
20231
+ periodDropdown.style.display = "none";
20232
+ }
20233
+ });
20234
+ const periodCheckboxes = document.querySelectorAll(`input[name="${modalId}-period"]`);
20235
+ periodCheckboxes.forEach((checkbox) => {
20236
+ checkbox.addEventListener("change", () => {
20237
+ const checked = Array.from(periodCheckboxes).filter((cb) => cb.checked).map((cb) => cb.value);
20238
+ state.selectedPeriods = checked;
20239
+ const btnLabel = periodBtn?.querySelector("span:first-child");
20240
+ if (btnLabel) {
20241
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
20242
+ }
20243
+ if (state.data.length > 0) drawChart(modalId, state);
20244
+ });
20245
+ });
20246
+ document.getElementById(`${modalId}-period-select-all`)?.addEventListener("click", () => {
20247
+ periodCheckboxes.forEach((cb) => {
20248
+ cb.checked = true;
20249
+ });
20250
+ state.selectedPeriods = ["madrugada", "manha", "tarde", "noite"];
20251
+ const btnLabel = periodBtn?.querySelector("span:first-child");
20252
+ if (btnLabel) {
20253
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
20254
+ }
20255
+ if (state.data.length > 0) drawChart(modalId, state);
20256
+ });
20257
+ document.getElementById(`${modalId}-period-clear`)?.addEventListener("click", () => {
20258
+ periodCheckboxes.forEach((cb) => {
20259
+ cb.checked = false;
20260
+ });
20261
+ state.selectedPeriods = [];
20262
+ const btnLabel = periodBtn?.querySelector("span:first-child");
20263
+ if (btnLabel) {
20264
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
20265
+ }
20266
+ if (state.data.length > 0) drawChart(modalId, state);
20267
+ });
20268
+ document.getElementById(`${modalId}-granularity`)?.addEventListener("change", (e) => {
20269
+ state.granularity = e.target.value;
20270
+ localStorage.setItem("myio-temp-modal-granularity", state.granularity);
20271
+ if (state.data.length > 0) drawChart(modalId, state);
20272
+ });
20273
+ document.getElementById(`${modalId}-query`)?.addEventListener("click", async () => {
20274
+ if (state.startTs >= state.endTs) {
20275
+ alert("Por favor, selecione um per\xEDodo v\xE1lido");
20276
+ return;
20277
+ }
20278
+ state.isLoading = true;
20279
+ state.dateRangePicker = null;
20280
+ renderModal(container, state, modalId);
20281
+ try {
20282
+ state.data = await fetchTemperatureData(state.token, state.deviceId, state.startTs, state.endTs);
20283
+ state.stats = calculateStats(state.data, state.clampRange);
20284
+ state.isLoading = false;
20285
+ renderModal(container, state, modalId);
20286
+ drawChart(modalId, state);
20287
+ await setupEventListeners(container, state, modalId, onClose);
20288
+ } catch (error) {
20289
+ console.error("[TemperatureModal] Error fetching data:", error);
20290
+ state.isLoading = false;
20291
+ renderModal(container, state, modalId, error);
20292
+ await setupEventListeners(container, state, modalId, onClose);
20293
+ }
20294
+ });
20295
+ document.getElementById(`${modalId}-export`)?.addEventListener("click", () => {
20296
+ if (state.data.length === 0) return;
20297
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale).replace(/\//g, "-");
20298
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale).replace(/\//g, "-");
20299
+ exportTemperatureCSV(
20300
+ state.data,
20301
+ state.label,
20302
+ state.stats,
20303
+ startDateStr,
20304
+ endDateStr
20305
+ );
20306
+ });
20307
+ }
20308
+
20309
+ // src/components/temperature/TemperatureComparisonModal.ts
20310
+ async function openTemperatureComparisonModal(params) {
20311
+ const modalId = `myio-temp-comparison-modal-${Date.now()}`;
20312
+ const defaultDateRange = getTodaySoFar();
20313
+ const startTs = params.startDate ? new Date(params.startDate).getTime() : defaultDateRange.startTs;
20314
+ const endTs = params.endDate ? new Date(params.endDate).getTime() : defaultDateRange.endTs;
20315
+ const state = {
20316
+ token: params.token,
20317
+ devices: params.devices,
20318
+ startTs,
20319
+ endTs,
20320
+ granularity: params.granularity || "hour",
20321
+ theme: params.theme || "dark",
20322
+ clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
20323
+ locale: params.locale || "pt-BR",
20324
+ deviceData: [],
20325
+ isLoading: true,
20326
+ dateRangePicker: null,
20327
+ selectedPeriods: ["madrugada", "manha", "tarde", "noite"],
20328
+ // All periods selected by default
20329
+ temperatureMin: params.temperatureMin ?? null,
20330
+ temperatureMax: params.temperatureMax ?? null
20331
+ };
20332
+ const savedGranularity = localStorage.getItem("myio-temp-comparison-granularity");
20333
+ const savedTheme = localStorage.getItem("myio-temp-comparison-theme");
20334
+ if (savedGranularity) state.granularity = savedGranularity;
20335
+ if (savedTheme) state.theme = savedTheme;
20336
+ const modalContainer = document.createElement("div");
20337
+ modalContainer.id = modalId;
20338
+ document.body.appendChild(modalContainer);
20339
+ renderModal2(modalContainer, state, modalId);
20340
+ await fetchAllDevicesData(state);
20341
+ renderModal2(modalContainer, state, modalId);
20342
+ drawComparisonChart(modalId, state);
20343
+ await setupEventListeners2(modalContainer, state, modalId, params.onClose);
20344
+ return {
20345
+ destroy: () => {
20346
+ modalContainer.remove();
20347
+ params.onClose?.();
20348
+ },
20349
+ updateData: async (startDate, endDate, granularity) => {
20350
+ state.startTs = new Date(startDate).getTime();
20351
+ state.endTs = new Date(endDate).getTime();
20352
+ if (granularity) state.granularity = granularity;
20353
+ state.isLoading = true;
20354
+ renderModal2(modalContainer, state, modalId);
20355
+ await fetchAllDevicesData(state);
20356
+ renderModal2(modalContainer, state, modalId);
20357
+ drawComparisonChart(modalId, state);
20358
+ setupEventListeners2(modalContainer, state, modalId, params.onClose);
20359
+ }
20360
+ };
20361
+ }
20362
+ async function fetchAllDevicesData(state) {
20363
+ state.isLoading = true;
20364
+ state.deviceData = [];
20365
+ try {
20366
+ const results = await Promise.all(
20367
+ state.devices.map(async (device, index) => {
20368
+ const deviceId = device.tbId || device.id;
20369
+ try {
20370
+ const data = await fetchTemperatureData(state.token, deviceId, state.startTs, state.endTs);
20371
+ const stats = calculateStats(data, state.clampRange);
20372
+ return {
20373
+ device,
20374
+ data,
20375
+ stats,
20376
+ color: CHART_COLORS[index % CHART_COLORS.length]
20377
+ };
20378
+ } catch (error) {
20379
+ console.error(`[TemperatureComparisonModal] Error fetching data for ${device.label}:`, error);
20380
+ return {
20381
+ device,
20382
+ data: [],
20383
+ stats: { avg: 0, min: 0, max: 0, count: 0 },
20384
+ color: CHART_COLORS[index % CHART_COLORS.length]
20385
+ };
20386
+ }
20387
+ })
20388
+ );
20389
+ state.deviceData = results;
20390
+ } catch (error) {
20391
+ console.error("[TemperatureComparisonModal] Error fetching data:", error);
20392
+ }
20393
+ state.isLoading = false;
20394
+ }
20395
+ function renderModal2(container, state, modalId) {
20396
+ const colors = getThemeColors(state.theme);
20397
+ new Date(state.startTs).toLocaleDateString(state.locale);
20398
+ new Date(state.endTs).toLocaleDateString(state.locale);
20399
+ new Date(state.startTs).toISOString().slice(0, 16);
20400
+ new Date(state.endTs).toISOString().slice(0, 16);
20401
+ const legendHTML = state.deviceData.map((dd) => `
20402
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px;
20403
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)"};
20404
+ border-radius: 8px;">
20405
+ <span style="width: 12px; height: 12px; border-radius: 50%; background: ${dd.color};"></span>
20406
+ <span style="color: ${colors.text}; font-size: 13px;">${dd.device.label}</span>
20407
+ <span style="color: ${colors.textMuted}; font-size: 11px; margin-left: auto;">
20408
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.avg) : "N/A"}
20409
+ </span>
20410
+ </div>
20411
+ `).join("");
20412
+ const statsHTML = state.deviceData.map((dd) => `
20413
+ <div style="
20414
+ padding: 12px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
20415
+ border-radius: 10px; border-left: 4px solid ${dd.color};
20416
+ min-width: 150px;
20417
+ ">
20418
+ <div style="font-weight: 600; color: ${colors.text}; font-size: 13px; margin-bottom: 8px;">
20419
+ ${dd.device.label}
20420
+ </div>
20421
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; font-size: 11px;">
20422
+ <span style="color: ${colors.textMuted};">M\xE9dia:</span>
20423
+ <span style="color: ${colors.text}; font-weight: 500;">
20424
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.avg) : "N/A"}
20425
+ </span>
20426
+ <span style="color: ${colors.textMuted};">Min:</span>
20427
+ <span style="color: ${colors.text};">
20428
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.min) : "N/A"}
20429
+ </span>
20430
+ <span style="color: ${colors.textMuted};">Max:</span>
20431
+ <span style="color: ${colors.text};">
20432
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.max) : "N/A"}
20433
+ </span>
20434
+ <span style="color: ${colors.textMuted};">Leituras:</span>
20435
+ <span style="color: ${colors.text};">${dd.stats.count}</span>
20436
+ </div>
20437
+ </div>
20438
+ `).join("");
20439
+ const isMaximized = container.__isMaximized || false;
20440
+ const contentMaxWidth = isMaximized ? "100%" : "1100px";
20441
+ const contentMaxHeight = isMaximized ? "100vh" : "95vh";
20442
+ const contentBorderRadius = isMaximized ? "0" : "10px";
20443
+ container.innerHTML = `
20444
+ <div class="myio-temp-comparison-overlay" style="
20445
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
20446
+ background: rgba(0, 0, 0, 0.5); z-index: 9998;
20447
+ display: flex; justify-content: center; align-items: center;
20448
+ backdrop-filter: blur(2px);
20449
+ ">
20450
+ <div class="myio-temp-comparison-content" style="
20451
+ background: ${colors.surface}; border-radius: ${contentBorderRadius};
20452
+ max-width: ${contentMaxWidth}; width: ${isMaximized ? "100%" : "95%"};
20453
+ max-height: ${contentMaxHeight}; height: ${isMaximized ? "100%" : "auto"};
20454
+ overflow: hidden; display: flex; flex-direction: column;
20455
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20456
+ font-family: 'Roboto', Arial, sans-serif;
20457
+ ">
20458
+ <!-- Header - MyIO Premium Style -->
20459
+ <div style="
20460
+ padding: 4px 8px; display: flex; align-items: center; justify-content: space-between;
20461
+ background: #3e1a7d; color: white; border-radius: ${isMaximized ? "0" : "10px 10px 0 0"};
20462
+ min-height: 20px;
20463
+ ">
20464
+ <h2 style="margin: 6px; font-size: 18px; font-weight: 600; color: white; line-height: 2;">
20465
+ \u{1F321}\uFE0F Compara\xE7\xE3o de Temperatura - ${state.devices.length} sensores
20466
+ </h2>
20467
+ <div style="display: flex; gap: 4px; align-items: center;">
20468
+ <!-- Theme Toggle -->
20469
+ <button id="${modalId}-theme-toggle" title="Alternar tema" style="
20470
+ background: none; border: none; font-size: 16px; cursor: pointer;
20471
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
20472
+ transition: background-color 0.2s;
20473
+ ">${state.theme === "dark" ? "\u2600\uFE0F" : "\u{1F319}"}</button>
20474
+ <!-- Maximize Button -->
20475
+ <button id="${modalId}-maximize" title="${isMaximized ? "Restaurar" : "Maximizar"}" style="
20476
+ background: none; border: none; font-size: 16px; cursor: pointer;
20477
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
20478
+ transition: background-color 0.2s;
20479
+ ">${isMaximized ? "\u{1F5D7}" : "\u{1F5D6}"}</button>
20480
+ <!-- Close Button -->
20481
+ <button id="${modalId}-close" title="Fechar" style="
20482
+ background: none; border: none; font-size: 20px; cursor: pointer;
20483
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
20484
+ transition: background-color 0.2s;
20485
+ ">\xD7</button>
20486
+ </div>
20487
+ </div>
20488
+
20489
+ <!-- Body -->
20490
+ <div style="flex: 1; overflow-y: auto; padding: 16px;">
20491
+
20492
+ <!-- Controls Row -->
20493
+ <div style="
20494
+ display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end;
20495
+ margin-bottom: 16px; padding: 16px;
20496
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#f7f7f7"};
20497
+ border-radius: 6px; border: 1px solid ${colors.border};
20498
+ ">
20499
+ <!-- Granularity Select -->
20500
+ <div>
20501
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20502
+ Granularidade
20503
+ </label>
20504
+ <select id="${modalId}-granularity" style="
20505
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20506
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20507
+ cursor: pointer; min-width: 130px;
20508
+ ">
20509
+ <option value="hour" ${state.granularity === "hour" ? "selected" : ""}>Hora (30 min)</option>
20510
+ <option value="day" ${state.granularity === "day" ? "selected" : ""}>Dia (m\xE9dia)</option>
20511
+ </select>
20512
+ </div>
20513
+ <!-- Day Period Filter (Multiselect) -->
20514
+ <div style="position: relative;">
20515
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20516
+ Per\xEDodos do Dia
20517
+ </label>
20518
+ <button id="${modalId}-period-btn" type="button" style="
20519
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20520
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20521
+ cursor: pointer; min-width: 180px; text-align: left;
20522
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
20523
+ ">
20524
+ <span>${getSelectedPeriodsLabel(state.selectedPeriods)}</span>
20525
+ <span style="font-size: 10px;">\u25BC</span>
20526
+ </button>
20527
+ <div id="${modalId}-period-dropdown" style="
20528
+ display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
20529
+ background: ${colors.surface}; border: 1px solid ${colors.border};
20530
+ border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20531
+ min-width: 200px; margin-top: 4px; padding: 8px 0;
20532
+ ">
20533
+ ${DAY_PERIODS.map((period) => `
20534
+ <label style="
20535
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
20536
+ cursor: pointer; font-size: 13px; color: ${colors.text};
20537
+ " onmouseover="this.style.background='${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"}'"
20538
+ onmouseout="this.style.background='transparent'">
20539
+ <input type="checkbox"
20540
+ name="${modalId}-period"
20541
+ value="${period.id}"
20542
+ ${state.selectedPeriods.includes(period.id) ? "checked" : ""}
20543
+ style="width: 16px; height: 16px; cursor: pointer; accent-color: #3e1a7d;">
20544
+ ${period.label}
20545
+ </label>
20546
+ `).join("")}
20547
+ <div style="border-top: 1px solid ${colors.border}; margin-top: 8px; padding-top: 8px;">
20548
+ <button id="${modalId}-period-select-all" type="button" style="
20549
+ width: calc(100% - 16px); margin: 0 8px 4px; padding: 6px;
20550
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
20551
+ border: none; border-radius: 4px; cursor: pointer;
20552
+ font-size: 12px; color: ${colors.text};
20553
+ ">Selecionar Todos</button>
20554
+ <button id="${modalId}-period-clear" type="button" style="
20555
+ width: calc(100% - 16px); margin: 0 8px; padding: 6px;
20556
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
20557
+ border: none; border-radius: 4px; cursor: pointer;
20558
+ font-size: 12px; color: ${colors.text};
20559
+ ">Limpar Sele\xE7\xE3o</button>
20560
+ </div>
20561
+ </div>
20562
+ </div>
20563
+ <!-- Date Range Picker -->
20564
+ <div style="flex: 1; min-width: 220px;">
20565
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20566
+ Per\xEDodo
20567
+ </label>
20568
+ <input type="text" id="${modalId}-date-range" readonly placeholder="Selecione o per\xEDodo..." style="
20569
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20570
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20571
+ width: 100%; cursor: pointer; box-sizing: border-box;
20572
+ "/>
20573
+ </div>
20574
+ <!-- Query Button -->
20575
+ <button id="${modalId}-query" style="
20576
+ background: #3e1a7d; color: white; border: none;
20577
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20578
+ font-size: 14px; font-weight: 500; height: 38px;
20579
+ display: flex; align-items: center; gap: 8px;
20580
+ font-family: 'Roboto', Arial, sans-serif;
20581
+ " ${state.isLoading ? "disabled" : ""}>
20582
+ ${state.isLoading ? '<span style="animation: spin 1s linear infinite; display: inline-block;">\u21BB</span> Carregando...' : "Carregar"}
20583
+ </button>
20584
+ </div>
20585
+
20586
+ <!-- Legend -->
20587
+ <div style="
20588
+ display: flex; flex-wrap: wrap; gap: 10px;
20589
+ margin-bottom: 20px;
20590
+ ">
20591
+ ${legendHTML}
20592
+ </div>
20593
+
20594
+ <!-- Chart Container -->
20595
+ <div style="margin-bottom: 24px;">
20596
+ <div id="${modalId}-chart" style="
20597
+ height: 380px;
20598
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.03)" : "#fafafa"};
20599
+ border-radius: 14px; display: flex; justify-content: center; align-items: center;
20600
+ border: 1px solid ${colors.border}; position: relative;
20601
+ ">
20602
+ ${state.isLoading ? `<div style="text-align: center; color: ${colors.textMuted};">
20603
+ <div style="animation: spin 1s linear infinite; font-size: 36px; margin-bottom: 12px;">\u21BB</div>
20604
+ <div style="font-size: 15px;">Carregando dados de ${state.devices.length} sensores...</div>
20605
+ </div>` : state.deviceData.every((dd) => dd.data.length === 0) ? `<div style="text-align: center; color: ${colors.textMuted};">
20606
+ <div style="font-size: 48px; margin-bottom: 12px;">\u{1F4ED}</div>
20607
+ <div style="font-size: 16px;">Sem dados para o per\xEDodo selecionado</div>
20608
+ </div>` : `<canvas id="${modalId}-canvas" style="width: 100%; height: 100%;"></canvas>`}
20609
+ </div>
20610
+ </div>
20611
+
20612
+ <!-- Stats Cards -->
20613
+ <div style="
20614
+ display: flex; flex-wrap: wrap; gap: 12px;
20615
+ margin-bottom: 24px;
20616
+ ">
20617
+ ${statsHTML}
20618
+ </div>
20619
+
20620
+ <!-- Actions -->
20621
+ <div style="display: flex; justify-content: flex-end; gap: 12px;">
20622
+ <button id="${modalId}-export" style="
20623
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f7f7f7"};
20624
+ color: ${colors.text}; border: 1px solid ${colors.border};
20625
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20626
+ font-size: 14px; display: flex; align-items: center; gap: 8px;
20627
+ font-family: 'Roboto', Arial, sans-serif;
20628
+ " ${state.deviceData.every((dd) => dd.data.length === 0) ? "disabled" : ""}>
20629
+ \u{1F4E5} Exportar CSV
20630
+ </button>
20631
+ <button id="${modalId}-close-btn" style="
20632
+ background: #3e1a7d; color: white; border: none;
20633
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20634
+ font-size: 14px; font-weight: 500;
20635
+ font-family: 'Roboto', Arial, sans-serif;
20636
+ ">
20637
+ Fechar
20638
+ </button>
20639
+ </div>
20640
+ </div><!-- End Body -->
20641
+ </div>
20642
+ </div>
20643
+ <style>
20644
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
20645
+ #${modalId} select:focus, #${modalId} input:focus {
20646
+ outline: 2px solid #3e1a7d;
20647
+ outline-offset: 2px;
20648
+ }
20649
+ #${modalId} button:hover:not(:disabled) {
20650
+ opacity: 0.9;
20651
+ }
20652
+ #${modalId} button:disabled {
20653
+ opacity: 0.5;
20654
+ cursor: not-allowed;
20655
+ }
20656
+ #${modalId} .myio-temp-comparison-content > div:first-child button:hover {
20657
+ background: rgba(255, 255, 255, 0.1) !important;
20658
+ color: white !important;
20659
+ }
20660
+ </style>
20661
+ `;
20662
+ }
20663
+ function drawComparisonChart(modalId, state) {
20664
+ const chartContainer = document.getElementById(`${modalId}-chart`);
20665
+ const canvas = document.getElementById(`${modalId}-canvas`);
20666
+ if (!chartContainer || !canvas) return;
20667
+ const hasData = state.deviceData.some((dd) => dd.data.length > 0);
20668
+ if (!hasData) return;
20669
+ const ctx = canvas.getContext("2d");
20670
+ if (!ctx) return;
20671
+ const colors = getThemeColors(state.theme);
20672
+ const width = chartContainer.clientWidth - 2;
20673
+ const height = 380;
20674
+ canvas.width = width;
20675
+ canvas.height = height;
20676
+ const paddingLeft = 65;
20677
+ const paddingRight = 25;
20678
+ const paddingTop = 25;
20679
+ const paddingBottom = 55;
20680
+ ctx.clearRect(0, 0, width, height);
20681
+ const processedData = [];
20682
+ state.deviceData.forEach((dd) => {
20683
+ if (dd.data.length === 0) return;
20684
+ const filteredData = filterByDayPeriods(dd.data, state.selectedPeriods);
20685
+ if (filteredData.length === 0) return;
20686
+ let points;
20687
+ if (state.granularity === "hour") {
20688
+ const interpolated = interpolateTemperature(filteredData, {
20689
+ intervalMinutes: 30,
20690
+ startTs: state.startTs,
20691
+ endTs: state.endTs,
20692
+ clampRange: state.clampRange
20693
+ });
20694
+ const filteredInterpolated = filterByDayPeriods(interpolated, state.selectedPeriods);
20695
+ points = filteredInterpolated.map((item) => ({
20696
+ x: item.ts,
20697
+ y: Number(item.value),
20698
+ screenX: 0,
20699
+ screenY: 0,
20700
+ deviceLabel: dd.device.label,
20701
+ deviceColor: dd.color
20702
+ }));
20703
+ } else {
20704
+ const daily = aggregateByDay(filteredData, state.clampRange);
20705
+ points = daily.map((item) => ({
20706
+ x: item.dateTs,
20707
+ y: item.avg,
20708
+ screenX: 0,
20709
+ screenY: 0,
20710
+ deviceLabel: dd.device.label,
20711
+ deviceColor: dd.color
20712
+ }));
20713
+ }
20714
+ if (points.length > 0) {
20715
+ processedData.push({ device: dd, points });
20716
+ }
20717
+ });
20718
+ if (processedData.length === 0) {
20719
+ ctx.fillStyle = colors.textMuted;
20720
+ ctx.font = "14px Roboto, Arial, sans-serif";
20721
+ ctx.textAlign = "center";
20722
+ ctx.fillText("Nenhum dado para os per\xEDodos selecionados", width / 2, height / 2);
20723
+ return;
20724
+ }
20725
+ const isPeriodsFiltered = state.selectedPeriods.length < 4 && state.selectedPeriods.length > 0;
20726
+ let dataMinY = Infinity;
20727
+ let dataMaxY = -Infinity;
20728
+ processedData.forEach(({ points }) => {
20729
+ points.forEach((point) => {
20730
+ if (point.y < dataMinY) dataMinY = point.y;
20731
+ if (point.y > dataMaxY) dataMaxY = point.y;
20732
+ });
20733
+ });
20734
+ const rangeMap = /* @__PURE__ */ new Map();
20735
+ state.deviceData.forEach((dd, index) => {
20736
+ const device = dd.device;
20737
+ const min = device.temperatureMin;
20738
+ const max = device.temperatureMax;
20739
+ if (min !== void 0 && min !== null && max !== void 0 && max !== null) {
20740
+ const key = `${min}-${max}`;
20741
+ if (!rangeMap.has(key)) {
20742
+ rangeMap.set(key, {
20743
+ min,
20744
+ max,
20745
+ customerName: device.customerName || "",
20746
+ color: CHART_COLORS[index % CHART_COLORS.length],
20747
+ deviceLabels: [device.label]
20748
+ });
20749
+ } else {
20750
+ rangeMap.get(key).deviceLabels.push(device.label);
20751
+ }
20752
+ }
20753
+ });
20754
+ if (rangeMap.size === 0 && state.temperatureMin !== null && state.temperatureMax !== null) {
20755
+ rangeMap.set("global", {
20756
+ min: state.temperatureMin,
20757
+ max: state.temperatureMax,
20758
+ customerName: "Global",
20759
+ color: colors.success,
20760
+ deviceLabels: []
20761
+ });
20762
+ }
20763
+ const temperatureRanges = Array.from(rangeMap.values());
20764
+ let thresholdMinY = dataMinY;
20765
+ let thresholdMaxY = dataMaxY;
20766
+ temperatureRanges.forEach((range) => {
20767
+ if (range.min < thresholdMinY) thresholdMinY = range.min;
20768
+ if (range.max > thresholdMaxY) thresholdMaxY = range.max;
20769
+ });
20770
+ const globalMinY = Math.floor(Math.min(dataMinY, thresholdMinY)) - 1;
20771
+ const globalMaxY = Math.ceil(Math.max(dataMaxY, thresholdMaxY)) + 1;
20772
+ const chartWidth = width - paddingLeft - paddingRight;
20773
+ const chartHeight = height - paddingTop - paddingBottom;
20774
+ const scaleY = chartHeight / (globalMaxY - globalMinY || 1);
20775
+ if (isPeriodsFiltered) {
20776
+ const maxPoints = Math.max(...processedData.map(({ points }) => points.length));
20777
+ const pointSpacing = chartWidth / Math.max(1, maxPoints - 1);
20778
+ processedData.forEach(({ points }) => {
20779
+ points.forEach((point, index) => {
20780
+ point.screenX = paddingLeft + index * pointSpacing;
20781
+ point.screenY = height - paddingBottom - (point.y - globalMinY) * scaleY;
20782
+ });
20783
+ });
20784
+ } else {
20785
+ let globalMinX = Infinity;
20786
+ let globalMaxX = -Infinity;
20787
+ processedData.forEach(({ points }) => {
20788
+ points.forEach((point) => {
20789
+ if (point.x < globalMinX) globalMinX = point.x;
20790
+ if (point.x > globalMaxX) globalMaxX = point.x;
20791
+ });
20792
+ });
20793
+ const timeRange = globalMaxX - globalMinX || 1;
20794
+ const scaleX = chartWidth / timeRange;
20795
+ processedData.forEach(({ points }) => {
20796
+ points.forEach((point) => {
20797
+ point.screenX = paddingLeft + (point.x - globalMinX) * scaleX;
20798
+ point.screenY = height - paddingBottom - (point.y - globalMinY) * scaleY;
20799
+ });
20800
+ });
20801
+ }
20802
+ ctx.strokeStyle = colors.chartGrid;
20803
+ ctx.lineWidth = 1;
20804
+ for (let i = 0; i <= 5; i++) {
20805
+ const y = paddingTop + chartHeight * i / 5;
20806
+ ctx.beginPath();
20807
+ ctx.moveTo(paddingLeft, y);
20808
+ ctx.lineTo(width - paddingRight, y);
20809
+ ctx.stroke();
20810
+ }
20811
+ const rangeColors = [
20812
+ { fill: "rgba(76, 175, 80, 0.12)", stroke: "#4CAF50" },
20813
+ // Green
20814
+ { fill: "rgba(33, 150, 243, 0.12)", stroke: "#2196F3" },
20815
+ // Blue
20816
+ { fill: "rgba(255, 152, 0, 0.12)", stroke: "#FF9800" },
20817
+ // Orange
20818
+ { fill: "rgba(156, 39, 176, 0.12)", stroke: "#9C27B0" }
20819
+ // Purple
20820
+ ];
20821
+ temperatureRanges.forEach((range, index) => {
20822
+ const rangeMinY = height - paddingBottom - (range.min - globalMinY) * scaleY;
20823
+ const rangeMaxY = height - paddingBottom - (range.max - globalMinY) * scaleY;
20824
+ const colorSet = rangeColors[index % rangeColors.length];
20825
+ ctx.fillStyle = colorSet.fill;
20826
+ ctx.fillRect(paddingLeft, rangeMaxY, chartWidth, rangeMinY - rangeMaxY);
20827
+ ctx.strokeStyle = colorSet.stroke;
20828
+ ctx.lineWidth = 1.5;
20829
+ ctx.setLineDash([6, 4]);
20830
+ ctx.beginPath();
20831
+ ctx.moveTo(paddingLeft, rangeMinY);
20832
+ ctx.lineTo(width - paddingRight, rangeMinY);
20833
+ ctx.moveTo(paddingLeft, rangeMaxY);
20834
+ ctx.lineTo(width - paddingRight, rangeMaxY);
20835
+ ctx.stroke();
20836
+ ctx.setLineDash([]);
20837
+ if (temperatureRanges.length > 1 || range.customerName) {
20838
+ ctx.fillStyle = colorSet.stroke;
20839
+ ctx.font = "10px system-ui, sans-serif";
20840
+ ctx.textAlign = "left";
20841
+ const labelY = (rangeMinY + rangeMaxY) / 2;
20842
+ const labelText = range.customerName || `${range.min}\xB0-${range.max}\xB0C`;
20843
+ ctx.fillText(labelText, width - paddingRight + 5, labelY + 3);
20844
+ }
20845
+ });
20846
+ processedData.forEach(({ device, points }) => {
20847
+ ctx.strokeStyle = device.color;
20848
+ ctx.lineWidth = 2.5;
20849
+ ctx.beginPath();
20850
+ points.forEach((point, i) => {
20851
+ if (i === 0) ctx.moveTo(point.screenX, point.screenY);
20852
+ else ctx.lineTo(point.screenX, point.screenY);
20853
+ });
20854
+ ctx.stroke();
20855
+ ctx.fillStyle = device.color;
20856
+ points.forEach((point) => {
20857
+ ctx.beginPath();
20858
+ ctx.arc(point.screenX, point.screenY, 4, 0, Math.PI * 2);
20859
+ ctx.fill();
20860
+ });
20861
+ });
20862
+ ctx.fillStyle = colors.textMuted;
20863
+ ctx.font = "12px system-ui, sans-serif";
20864
+ ctx.textAlign = "right";
20865
+ for (let i = 0; i <= 5; i++) {
20866
+ const val = globalMinY + (globalMaxY - globalMinY) * (5 - i) / 5;
20867
+ const y = paddingTop + chartHeight * i / 5;
20868
+ ctx.fillText(val.toFixed(1) + "\xB0C", paddingLeft - 10, y + 4);
20869
+ }
20870
+ ctx.textAlign = "center";
20871
+ const xAxisPoints = processedData[0]?.points || [];
20872
+ const numLabels = Math.min(8, xAxisPoints.length);
20873
+ const labelInterval = Math.max(1, Math.floor(xAxisPoints.length / numLabels));
20874
+ for (let i = 0; i < xAxisPoints.length; i += labelInterval) {
20875
+ const point = xAxisPoints[i];
20876
+ const date = new Date(point.x);
20877
+ let label;
20878
+ if (state.granularity === "hour") {
20879
+ label = date.toLocaleTimeString(state.locale, { hour: "2-digit", minute: "2-digit" });
20880
+ } else {
20881
+ label = date.toLocaleDateString(state.locale, { day: "2-digit", month: "2-digit" });
20882
+ }
20883
+ ctx.strokeStyle = colors.chartGrid;
20884
+ ctx.lineWidth = 1;
20885
+ ctx.beginPath();
20886
+ ctx.moveTo(point.screenX, paddingTop);
20887
+ ctx.lineTo(point.screenX, height - paddingBottom);
20888
+ ctx.stroke();
20889
+ ctx.fillStyle = colors.textMuted;
20890
+ ctx.fillText(label, point.screenX, height - paddingBottom + 18);
20891
+ }
20892
+ ctx.strokeStyle = colors.border;
20893
+ ctx.lineWidth = 1;
20894
+ ctx.beginPath();
20895
+ ctx.moveTo(paddingLeft, paddingTop);
20896
+ ctx.lineTo(paddingLeft, height - paddingBottom);
20897
+ ctx.lineTo(width - paddingRight, height - paddingBottom);
20898
+ ctx.stroke();
20899
+ const allChartPoints = processedData.flatMap((pd) => pd.points);
20900
+ setupComparisonChartTooltip(canvas, chartContainer, allChartPoints, state, colors);
20901
+ }
20902
+ function setupComparisonChartTooltip(canvas, container, chartData, state, colors) {
20903
+ const existingTooltip = container.querySelector(".myio-chart-tooltip");
20904
+ if (existingTooltip) existingTooltip.remove();
20905
+ const tooltip = document.createElement("div");
20906
+ tooltip.className = "myio-chart-tooltip";
20907
+ tooltip.style.cssText = `
20908
+ position: absolute;
20909
+ background: ${state.theme === "dark" ? "rgba(30, 30, 40, 0.95)" : "rgba(255, 255, 255, 0.98)"};
20910
+ border: 1px solid ${colors.border};
20911
+ border-radius: 8px;
20912
+ padding: 10px 14px;
20913
+ font-size: 13px;
20914
+ color: ${colors.text};
20915
+ pointer-events: none;
20916
+ opacity: 0;
20917
+ transition: opacity 0.15s;
20918
+ z-index: 1000;
20919
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20920
+ min-width: 160px;
20921
+ `;
20922
+ container.appendChild(tooltip);
20923
+ const findNearestPoint = (mouseX, mouseY) => {
20924
+ const threshold = 20;
20925
+ let nearest = null;
20926
+ let minDist = Infinity;
20927
+ for (const point of chartData) {
20928
+ const dist = Math.sqrt(
20929
+ Math.pow(mouseX - point.screenX, 2) + Math.pow(mouseY - point.screenY, 2)
20930
+ );
20931
+ if (dist < minDist && dist < threshold) {
20932
+ minDist = dist;
20933
+ nearest = point;
20934
+ }
20935
+ }
20936
+ return nearest;
20937
+ };
20938
+ canvas.addEventListener("mousemove", (e) => {
20939
+ const rect = canvas.getBoundingClientRect();
20940
+ const mouseX = e.clientX - rect.left;
20941
+ const mouseY = e.clientY - rect.top;
20942
+ const point = findNearestPoint(mouseX, mouseY);
20943
+ if (point) {
20944
+ const date = new Date(point.x);
20945
+ let dateStr;
20946
+ if (state.granularity === "hour") {
20947
+ dateStr = date.toLocaleDateString(state.locale, {
20948
+ day: "2-digit",
20949
+ month: "2-digit",
20950
+ year: "numeric"
20951
+ }) + " " + date.toLocaleTimeString(state.locale, {
20952
+ hour: "2-digit",
20953
+ minute: "2-digit"
20954
+ });
20955
+ } else {
20956
+ dateStr = date.toLocaleDateString(state.locale, {
20957
+ day: "2-digit",
20958
+ month: "2-digit",
20959
+ year: "numeric"
20960
+ });
20961
+ }
20962
+ tooltip.innerHTML = `
20963
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
20964
+ <span style="width: 10px; height: 10px; border-radius: 50%; background: ${point.deviceColor};"></span>
20965
+ <span style="font-weight: 600;">${point.deviceLabel}</span>
20966
+ </div>
20967
+ <div style="font-weight: 600; font-size: 16px; color: ${point.deviceColor}; margin-bottom: 4px;">
20968
+ ${formatTemperature(point.y)}
20969
+ </div>
20970
+ <div style="font-size: 11px; color: ${colors.textMuted};">
20971
+ \u{1F4C5} ${dateStr}
20972
+ </div>
20973
+ `;
20974
+ let tooltipX = point.screenX + 15;
20975
+ let tooltipY = point.screenY - 15;
20976
+ const tooltipRect = tooltip.getBoundingClientRect();
20977
+ const containerRect = container.getBoundingClientRect();
20978
+ if (tooltipX + tooltipRect.width > containerRect.width - 10) {
20979
+ tooltipX = point.screenX - tooltipRect.width - 15;
20980
+ }
20981
+ if (tooltipY < 10) {
20982
+ tooltipY = point.screenY + 15;
20983
+ }
20984
+ tooltip.style.left = `${tooltipX}px`;
20985
+ tooltip.style.top = `${tooltipY}px`;
20986
+ tooltip.style.opacity = "1";
20987
+ canvas.style.cursor = "pointer";
20988
+ } else {
20989
+ tooltip.style.opacity = "0";
20990
+ canvas.style.cursor = "default";
20991
+ }
20992
+ });
20993
+ canvas.addEventListener("mouseleave", () => {
20994
+ tooltip.style.opacity = "0";
20995
+ canvas.style.cursor = "default";
20996
+ });
20997
+ }
20998
+ async function setupEventListeners2(container, state, modalId, onClose) {
20999
+ const closeModal = () => {
21000
+ container.remove();
21001
+ onClose?.();
21002
+ };
21003
+ container.querySelector(".myio-temp-comparison-overlay")?.addEventListener("click", (e) => {
21004
+ if (e.target === e.currentTarget) closeModal();
21005
+ });
21006
+ document.getElementById(`${modalId}-close`)?.addEventListener("click", closeModal);
21007
+ document.getElementById(`${modalId}-close-btn`)?.addEventListener("click", closeModal);
21008
+ const dateRangeInput = document.getElementById(`${modalId}-date-range`);
21009
+ if (dateRangeInput && !state.dateRangePicker) {
21010
+ try {
21011
+ state.dateRangePicker = await createDateRangePicker2(dateRangeInput, {
21012
+ presetStart: new Date(state.startTs).toISOString(),
21013
+ presetEnd: new Date(state.endTs).toISOString(),
21014
+ includeTime: true,
21015
+ timePrecision: "minute",
21016
+ maxRangeDays: 90,
21017
+ locale: state.locale,
21018
+ parentEl: container.querySelector(".myio-temp-comparison-content"),
21019
+ onApply: (result) => {
21020
+ state.startTs = new Date(result.startISO).getTime();
21021
+ state.endTs = new Date(result.endISO).getTime();
21022
+ console.log("[TemperatureComparisonModal] Date range applied:", result);
21023
+ }
21024
+ });
21025
+ } catch (error) {
21026
+ console.warn("[TemperatureComparisonModal] DateRangePicker initialization failed:", error);
21027
+ }
21028
+ }
21029
+ document.getElementById(`${modalId}-theme-toggle`)?.addEventListener("click", async () => {
21030
+ state.theme = state.theme === "dark" ? "light" : "dark";
21031
+ localStorage.setItem("myio-temp-comparison-theme", state.theme);
21032
+ state.dateRangePicker = null;
21033
+ renderModal2(container, state, modalId);
21034
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21035
+ drawComparisonChart(modalId, state);
21036
+ }
21037
+ await setupEventListeners2(container, state, modalId, onClose);
21038
+ });
21039
+ document.getElementById(`${modalId}-maximize`)?.addEventListener("click", async () => {
21040
+ container.__isMaximized = !container.__isMaximized;
21041
+ state.dateRangePicker = null;
21042
+ renderModal2(container, state, modalId);
21043
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21044
+ drawComparisonChart(modalId, state);
21045
+ }
21046
+ await setupEventListeners2(container, state, modalId, onClose);
21047
+ });
21048
+ const periodBtn = document.getElementById(`${modalId}-period-btn`);
21049
+ const periodDropdown = document.getElementById(`${modalId}-period-dropdown`);
21050
+ periodBtn?.addEventListener("click", (e) => {
21051
+ e.stopPropagation();
21052
+ if (periodDropdown) {
21053
+ periodDropdown.style.display = periodDropdown.style.display === "none" ? "block" : "none";
21054
+ }
21055
+ });
21056
+ document.addEventListener("click", (e) => {
21057
+ if (periodDropdown && !periodDropdown.contains(e.target) && e.target !== periodBtn) {
21058
+ periodDropdown.style.display = "none";
21059
+ }
21060
+ });
21061
+ const periodCheckboxes = document.querySelectorAll(`input[name="${modalId}-period"]`);
21062
+ periodCheckboxes.forEach((checkbox) => {
21063
+ checkbox.addEventListener("change", () => {
21064
+ const checked = Array.from(periodCheckboxes).filter((cb) => cb.checked).map((cb) => cb.value);
21065
+ state.selectedPeriods = checked;
21066
+ const btnLabel = periodBtn?.querySelector("span:first-child");
21067
+ if (btnLabel) {
21068
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
21069
+ }
21070
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21071
+ drawComparisonChart(modalId, state);
21072
+ }
21073
+ });
21074
+ });
21075
+ document.getElementById(`${modalId}-period-select-all`)?.addEventListener("click", () => {
21076
+ periodCheckboxes.forEach((cb) => {
21077
+ cb.checked = true;
21078
+ });
21079
+ state.selectedPeriods = ["madrugada", "manha", "tarde", "noite"];
21080
+ const btnLabel = periodBtn?.querySelector("span:first-child");
21081
+ if (btnLabel) {
21082
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
21083
+ }
21084
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21085
+ drawComparisonChart(modalId, state);
21086
+ }
21087
+ });
21088
+ document.getElementById(`${modalId}-period-clear`)?.addEventListener("click", () => {
21089
+ periodCheckboxes.forEach((cb) => {
21090
+ cb.checked = false;
21091
+ });
21092
+ state.selectedPeriods = [];
21093
+ const btnLabel = periodBtn?.querySelector("span:first-child");
21094
+ if (btnLabel) {
21095
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
21096
+ }
21097
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21098
+ drawComparisonChart(modalId, state);
21099
+ }
21100
+ });
21101
+ document.getElementById(`${modalId}-granularity`)?.addEventListener("change", (e) => {
21102
+ state.granularity = e.target.value;
21103
+ localStorage.setItem("myio-temp-comparison-granularity", state.granularity);
21104
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21105
+ drawComparisonChart(modalId, state);
21106
+ }
21107
+ });
21108
+ document.getElementById(`${modalId}-query`)?.addEventListener("click", async () => {
21109
+ if (state.startTs >= state.endTs) {
21110
+ alert("Por favor, selecione um per\xEDodo v\xE1lido");
21111
+ return;
21112
+ }
21113
+ state.isLoading = true;
21114
+ state.dateRangePicker = null;
21115
+ renderModal2(container, state, modalId);
21116
+ await fetchAllDevicesData(state);
21117
+ renderModal2(container, state, modalId);
21118
+ drawComparisonChart(modalId, state);
21119
+ await setupEventListeners2(container, state, modalId, onClose);
21120
+ });
21121
+ document.getElementById(`${modalId}-export`)?.addEventListener("click", () => {
21122
+ if (state.deviceData.every((dd) => dd.data.length === 0)) return;
21123
+ exportComparisonCSV(state);
21124
+ });
21125
+ }
21126
+ function exportComparisonCSV(state) {
21127
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale).replace(/\//g, "-");
21128
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale).replace(/\//g, "-");
21129
+ const BOM = "\uFEFF";
21130
+ let csvContent = BOM;
21131
+ csvContent += `Compara\xE7\xE3o de Temperatura
21132
+ `;
21133
+ csvContent += `Per\xEDodo: ${startDateStr} at\xE9 ${endDateStr}
21134
+ `;
21135
+ csvContent += `Sensores: ${state.devices.map((d) => d.label).join(", ")}
21136
+ `;
21137
+ csvContent += "\n";
21138
+ csvContent += "Estat\xEDsticas por Sensor:\n";
21139
+ csvContent += "Sensor,M\xE9dia (\xB0C),Min (\xB0C),Max (\xB0C),Leituras\n";
21140
+ state.deviceData.forEach((dd) => {
21141
+ csvContent += `"${dd.device.label}",${dd.stats.avg.toFixed(2)},${dd.stats.min.toFixed(2)},${dd.stats.max.toFixed(2)},${dd.stats.count}
21142
+ `;
21143
+ });
21144
+ csvContent += "\n";
21145
+ csvContent += "Dados Detalhados:\n";
21146
+ csvContent += "Data/Hora,Sensor,Temperatura (\xB0C)\n";
21147
+ state.deviceData.forEach((dd) => {
21148
+ dd.data.forEach((item) => {
21149
+ const date = new Date(item.ts).toLocaleString(state.locale);
21150
+ const temp = Number(item.value).toFixed(2);
21151
+ csvContent += `"${date}","${dd.device.label}",${temp}
21152
+ `;
21153
+ });
21154
+ });
21155
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
21156
+ const url = URL.createObjectURL(blob);
21157
+ const link = document.createElement("a");
21158
+ link.href = url;
21159
+ link.download = `comparacao_temperatura_${startDateStr}_${endDateStr}.csv`;
21160
+ document.body.appendChild(link);
21161
+ link.click();
21162
+ document.body.removeChild(link);
21163
+ URL.revokeObjectURL(url);
21164
+ }
21165
+
21166
+ // src/components/temperature/TemperatureSettingsModal.ts
21167
+ var DARK_THEME2 = {
21168
+ modalBg: "linear-gradient(180deg, #1e1e2e 0%, #151521 100%)",
21169
+ headerBg: "#3e1a7d",
21170
+ textPrimary: "#ffffff",
21171
+ textSecondary: "rgba(255, 255, 255, 0.7)",
21172
+ textMuted: "rgba(255, 255, 255, 0.5)",
21173
+ inputBg: "rgba(255, 255, 255, 0.08)",
21174
+ inputBorder: "rgba(255, 255, 255, 0.2)",
21175
+ inputText: "#ffffff",
21176
+ buttonPrimary: "#3e1a7d",
21177
+ buttonPrimaryHover: "#5a2da8",
21178
+ buttonSecondary: "rgba(255, 255, 255, 0.1)",
21179
+ success: "#4CAF50",
21180
+ error: "#f44336",
21181
+ overlay: "rgba(0, 0, 0, 0.85)"
21182
+ };
21183
+ var LIGHT_THEME2 = {
21184
+ modalBg: "#ffffff",
21185
+ headerBg: "#3e1a7d",
21186
+ textPrimary: "#1a1a2e",
21187
+ textSecondary: "rgba(0, 0, 0, 0.7)",
21188
+ textMuted: "rgba(0, 0, 0, 0.5)",
21189
+ inputBg: "#f5f5f5",
21190
+ inputBorder: "rgba(0, 0, 0, 0.2)",
21191
+ inputText: "#1a1a2e",
21192
+ buttonPrimary: "#3e1a7d",
21193
+ buttonPrimaryHover: "#5a2da8",
21194
+ buttonSecondary: "rgba(0, 0, 0, 0.05)",
21195
+ success: "#4CAF50",
21196
+ error: "#f44336",
21197
+ overlay: "rgba(0, 0, 0, 0.5)"
21198
+ };
21199
+ function getColors(theme) {
21200
+ return theme === "dark" ? DARK_THEME2 : LIGHT_THEME2;
21201
+ }
21202
+ async function fetchCustomerAttributes(customerId, token) {
21203
+ const url = `/api/plugins/telemetry/CUSTOMER/${customerId}/values/attributes/SERVER_SCOPE`;
21204
+ const response = await fetch(url, {
21205
+ method: "GET",
21206
+ headers: {
21207
+ "Content-Type": "application/json",
21208
+ "X-Authorization": `Bearer ${token}`
21209
+ }
21210
+ });
21211
+ if (!response.ok) {
21212
+ if (response.status === 404 || response.status === 400) {
21213
+ return { minTemperature: null, maxTemperature: null };
21214
+ }
21215
+ throw new Error(`Failed to fetch attributes: ${response.status}`);
21216
+ }
21217
+ const attributes = await response.json();
21218
+ let minTemperature = null;
21219
+ let maxTemperature = null;
21220
+ if (Array.isArray(attributes)) {
21221
+ for (const attr of attributes) {
21222
+ if (attr.key === "minTemperature") {
21223
+ minTemperature = Number(attr.value);
21224
+ } else if (attr.key === "maxTemperature") {
21225
+ maxTemperature = Number(attr.value);
21226
+ }
21227
+ }
21228
+ }
21229
+ return { minTemperature, maxTemperature };
21230
+ }
21231
+ async function saveCustomerAttributes(customerId, token, minTemperature, maxTemperature) {
21232
+ const url = `/api/plugins/telemetry/CUSTOMER/${customerId}/SERVER_SCOPE`;
21233
+ const attributes = {
21234
+ minTemperature,
21235
+ maxTemperature
21236
+ };
21237
+ const response = await fetch(url, {
21238
+ method: "POST",
21239
+ headers: {
21240
+ "Content-Type": "application/json",
21241
+ "X-Authorization": `Bearer ${token}`
21242
+ },
21243
+ body: JSON.stringify(attributes)
21244
+ });
21245
+ if (!response.ok) {
21246
+ throw new Error(`Failed to save attributes: ${response.status}`);
21247
+ }
21248
+ }
21249
+ function renderModal3(container, state, modalId, onClose, onSave) {
21250
+ const colors = getColors(state.theme);
21251
+ const minValue = state.minTemperature !== null ? state.minTemperature : "";
21252
+ const maxValue = state.maxTemperature !== null ? state.maxTemperature : "";
21253
+ container.innerHTML = `
21254
+ <style>
21255
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
21256
+ @keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
21257
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
21258
+
21259
+ #${modalId} {
21260
+ position: fixed;
21261
+ top: 0;
21262
+ left: 0;
21263
+ width: 100%;
21264
+ height: 100%;
21265
+ background: ${colors.overlay};
21266
+ z-index: 10000;
21267
+ display: flex;
21268
+ align-items: center;
21269
+ justify-content: center;
21270
+ animation: fadeIn 0.2s ease-out;
21271
+ }
21272
+
21273
+ #${modalId} .modal-content {
21274
+ background: ${colors.modalBg};
21275
+ border-radius: 16px;
21276
+ width: 90%;
21277
+ max-width: 480px;
21278
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
21279
+ border: 1px solid rgba(255, 255, 255, 0.1);
21280
+ animation: slideIn 0.3s ease-out;
21281
+ overflow: hidden;
21282
+ }
21283
+
21284
+ #${modalId} .modal-header {
21285
+ background: ${colors.headerBg};
21286
+ padding: 20px 24px;
21287
+ display: flex;
21288
+ align-items: center;
21289
+ justify-content: space-between;
21290
+ }
21291
+
21292
+ #${modalId} .modal-title {
21293
+ margin: 0;
21294
+ font-size: 18px;
21295
+ font-weight: 600;
21296
+ color: #fff;
21297
+ font-family: 'Roboto', sans-serif;
21298
+ display: flex;
21299
+ align-items: center;
21300
+ gap: 10px;
21301
+ }
21302
+
21303
+ #${modalId} .close-btn {
21304
+ width: 32px;
21305
+ height: 32px;
21306
+ background: rgba(255, 255, 255, 0.1);
21307
+ border: 1px solid rgba(255, 255, 255, 0.2);
21308
+ border-radius: 8px;
21309
+ cursor: pointer;
21310
+ display: flex;
21311
+ align-items: center;
21312
+ justify-content: center;
21313
+ transition: all 0.2s;
21314
+ color: #fff;
21315
+ font-size: 18px;
21316
+ }
21317
+
21318
+ #${modalId} .close-btn:hover {
21319
+ background: rgba(255, 68, 68, 0.25);
21320
+ border-color: rgba(255, 68, 68, 0.5);
21321
+ }
21322
+
21323
+ #${modalId} .modal-body {
21324
+ padding: 24px;
21325
+ }
21326
+
21327
+ #${modalId} .customer-info {
21328
+ margin-bottom: 24px;
21329
+ padding: 12px 16px;
21330
+ background: ${colors.inputBg};
21331
+ border-radius: 8px;
21332
+ border: 1px solid ${colors.inputBorder};
21333
+ }
21334
+
21335
+ #${modalId} .customer-label {
21336
+ font-size: 12px;
21337
+ color: ${colors.textMuted};
21338
+ margin-bottom: 4px;
21339
+ }
21340
+
21341
+ #${modalId} .customer-name {
21342
+ font-size: 16px;
21343
+ font-weight: 500;
21344
+ color: ${colors.textPrimary};
21345
+ }
21346
+
21347
+ #${modalId} .form-group {
21348
+ margin-bottom: 20px;
21349
+ }
21350
+
21351
+ #${modalId} .form-label {
21352
+ display: block;
21353
+ font-size: 14px;
21354
+ font-weight: 500;
21355
+ color: ${colors.textSecondary};
21356
+ margin-bottom: 8px;
21357
+ }
21358
+
21359
+ #${modalId} .form-input {
21360
+ width: 100%;
21361
+ padding: 12px 16px;
21362
+ font-size: 16px;
21363
+ background: ${colors.inputBg};
21364
+ border: 1px solid ${colors.inputBorder};
21365
+ border-radius: 8px;
21366
+ color: ${colors.inputText};
21367
+ outline: none;
21368
+ transition: border-color 0.2s;
21369
+ box-sizing: border-box;
21370
+ }
21371
+
21372
+ #${modalId} .form-input:focus {
21373
+ border-color: ${colors.buttonPrimary};
21374
+ }
21375
+
21376
+ #${modalId} .form-input::placeholder {
21377
+ color: ${colors.textMuted};
21378
+ }
21379
+
21380
+ #${modalId} .form-hint {
21381
+ font-size: 12px;
21382
+ color: ${colors.textMuted};
21383
+ margin-top: 6px;
21384
+ }
21385
+
21386
+ #${modalId} .temperature-range {
21387
+ display: grid;
21388
+ grid-template-columns: 1fr 1fr;
21389
+ gap: 16px;
21390
+ }
21391
+
21392
+ #${modalId} .range-preview {
21393
+ margin-top: 20px;
21394
+ padding: 16px;
21395
+ background: rgba(76, 175, 80, 0.1);
21396
+ border: 1px dashed ${colors.success};
21397
+ border-radius: 8px;
21398
+ text-align: center;
21399
+ }
21400
+
21401
+ #${modalId} .range-preview-label {
21402
+ font-size: 12px;
21403
+ color: ${colors.textMuted};
21404
+ margin-bottom: 8px;
21405
+ }
21406
+
21407
+ #${modalId} .range-preview-value {
21408
+ font-size: 24px;
21409
+ font-weight: 600;
21410
+ color: ${colors.success};
21411
+ }
21412
+
21413
+ #${modalId} .modal-footer {
21414
+ padding: 16px 24px;
21415
+ border-top: 1px solid ${colors.inputBorder};
21416
+ display: flex;
21417
+ justify-content: flex-end;
21418
+ gap: 12px;
21419
+ }
21420
+
21421
+ #${modalId} .btn {
21422
+ padding: 10px 24px;
21423
+ font-size: 14px;
21424
+ font-weight: 500;
21425
+ border-radius: 8px;
21426
+ cursor: pointer;
21427
+ transition: all 0.2s;
21428
+ border: none;
21429
+ display: flex;
21430
+ align-items: center;
21431
+ gap: 8px;
21432
+ }
21433
+
21434
+ #${modalId} .btn-secondary {
21435
+ background: ${colors.buttonSecondary};
21436
+ color: ${colors.textSecondary};
21437
+ border: 1px solid ${colors.inputBorder};
21438
+ }
21439
+
21440
+ #${modalId} .btn-secondary:hover {
21441
+ background: ${colors.inputBg};
21442
+ }
21443
+
21444
+ #${modalId} .btn-primary {
21445
+ background: ${colors.buttonPrimary};
21446
+ color: #fff;
21447
+ }
21448
+
21449
+ #${modalId} .btn-primary:hover {
21450
+ background: ${colors.buttonPrimaryHover};
21451
+ }
21452
+
21453
+ #${modalId} .btn-primary:disabled {
21454
+ opacity: 0.6;
21455
+ cursor: not-allowed;
21456
+ }
21457
+
21458
+ #${modalId} .spinner {
21459
+ width: 16px;
21460
+ height: 16px;
21461
+ border: 2px solid rgba(255,255,255,0.3);
21462
+ border-top-color: #fff;
21463
+ border-radius: 50%;
21464
+ animation: spin 1s linear infinite;
21465
+ }
21466
+
21467
+ #${modalId} .message {
21468
+ padding: 12px 16px;
21469
+ border-radius: 8px;
21470
+ margin-bottom: 16px;
21471
+ font-size: 14px;
21472
+ }
21473
+
21474
+ #${modalId} .message-error {
21475
+ background: rgba(244, 67, 54, 0.1);
21476
+ border: 1px solid ${colors.error};
21477
+ color: ${colors.error};
21478
+ }
21479
+
21480
+ #${modalId} .message-success {
21481
+ background: rgba(76, 175, 80, 0.1);
21482
+ border: 1px solid ${colors.success};
21483
+ color: ${colors.success};
21484
+ }
21485
+
21486
+ #${modalId} .loading-overlay {
21487
+ display: flex;
21488
+ flex-direction: column;
21489
+ align-items: center;
21490
+ justify-content: center;
21491
+ padding: 48px;
21492
+ color: ${colors.textSecondary};
21493
+ }
21494
+
21495
+ #${modalId} .loading-spinner {
21496
+ width: 40px;
21497
+ height: 40px;
21498
+ border: 3px solid ${colors.inputBorder};
21499
+ border-top-color: ${colors.buttonPrimary};
21500
+ border-radius: 50%;
21501
+ animation: spin 1s linear infinite;
21502
+ margin-bottom: 16px;
21503
+ }
21504
+ </style>
21505
+
21506
+ <div id="${modalId}" class="modal-overlay">
21507
+ <div class="modal-content">
21508
+ <div class="modal-header">
21509
+ <h2 class="modal-title">
21510
+ <span>\u{1F321}\uFE0F</span>
21511
+ Configurar Temperatura
21512
+ </h2>
21513
+ <button class="close-btn" id="${modalId}-close">&times;</button>
21514
+ </div>
21515
+
21516
+ <div class="modal-body">
21517
+ ${state.isLoading ? `
21518
+ <div class="loading-overlay">
21519
+ <div class="loading-spinner"></div>
21520
+ <div>Carregando configura\xE7\xF5es...</div>
21521
+ </div>
21522
+ ` : `
21523
+ ${state.error ? `
21524
+ <div class="message message-error">${state.error}</div>
21525
+ ` : ""}
21526
+
21527
+ ${state.successMessage ? `
21528
+ <div class="message message-success">${state.successMessage}</div>
21529
+ ` : ""}
21530
+
21531
+ <div class="customer-info">
21532
+ <div class="customer-label">Shopping / Cliente</div>
21533
+ <div class="customer-name">${state.customerName || "N\xE3o identificado"}</div>
21534
+ </div>
21535
+
21536
+ <div class="form-group">
21537
+ <label class="form-label">Faixa de Temperatura Ideal</label>
21538
+ <div class="temperature-range">
21539
+ <div>
21540
+ <input
21541
+ type="number"
21542
+ id="${modalId}-min"
21543
+ class="form-input"
21544
+ placeholder="M\xEDnima"
21545
+ value="${minValue}"
21546
+ step="0.5"
21547
+ min="0"
21548
+ max="50"
21549
+ />
21550
+ <div class="form-hint">Temperatura m\xEDnima (\xB0C)</div>
21551
+ </div>
21552
+ <div>
21553
+ <input
21554
+ type="number"
21555
+ id="${modalId}-max"
21556
+ class="form-input"
21557
+ placeholder="M\xE1xima"
21558
+ value="${maxValue}"
21559
+ step="0.5"
21560
+ min="0"
21561
+ max="50"
21562
+ />
21563
+ <div class="form-hint">Temperatura m\xE1xima (\xB0C)</div>
21564
+ </div>
21565
+ </div>
21566
+ </div>
21567
+
21568
+ <div class="range-preview" id="${modalId}-preview">
21569
+ <div class="range-preview-label">Faixa configurada</div>
21570
+ <div class="range-preview-value" id="${modalId}-preview-value">
21571
+ ${minValue && maxValue ? `${minValue}\xB0C - ${maxValue}\xB0C` : "N\xE3o definida"}
21572
+ </div>
21573
+ </div>
21574
+ `}
21575
+ </div>
21576
+
21577
+ ${!state.isLoading ? `
21578
+ <div class="modal-footer">
21579
+ <button class="btn btn-secondary" id="${modalId}-cancel">Cancelar</button>
21580
+ <button class="btn btn-primary" id="${modalId}-save" ${state.isSaving ? "disabled" : ""}>
21581
+ ${state.isSaving ? '<div class="spinner"></div> Salvando...' : "Salvar"}
21582
+ </button>
21583
+ </div>
21584
+ ` : ""}
21585
+ </div>
21586
+ </div>
21587
+ `;
21588
+ const closeBtn = document.getElementById(`${modalId}-close`);
21589
+ const cancelBtn = document.getElementById(`${modalId}-cancel`);
21590
+ const saveBtn = document.getElementById(`${modalId}-save`);
21591
+ const minInput = document.getElementById(`${modalId}-min`);
21592
+ const maxInput = document.getElementById(`${modalId}-max`);
21593
+ const previewValue = document.getElementById(`${modalId}-preview-value`);
21594
+ const overlay = document.getElementById(modalId);
21595
+ closeBtn?.addEventListener("click", onClose);
21596
+ cancelBtn?.addEventListener("click", onClose);
21597
+ overlay?.addEventListener("click", (e) => {
21598
+ if (e.target === overlay) onClose();
21599
+ });
21600
+ const updatePreview = () => {
21601
+ if (previewValue && minInput && maxInput) {
21602
+ const min = minInput.value;
21603
+ const max = maxInput.value;
21604
+ if (min && max) {
21605
+ previewValue.textContent = `${min}\xB0C - ${max}\xB0C`;
21606
+ } else {
21607
+ previewValue.textContent = "N\xE3o definida";
21608
+ }
21609
+ }
21610
+ };
21611
+ minInput?.addEventListener("input", updatePreview);
21612
+ maxInput?.addEventListener("input", updatePreview);
21613
+ saveBtn?.addEventListener("click", async () => {
21614
+ if (!minInput || !maxInput) return;
21615
+ const min = parseFloat(minInput.value);
21616
+ const max = parseFloat(maxInput.value);
21617
+ if (isNaN(min) || isNaN(max)) {
21618
+ state.error = "Por favor, preencha ambos os valores.";
21619
+ renderModal3(container, state, modalId, onClose, onSave);
21620
+ return;
21621
+ }
21622
+ if (min >= max) {
21623
+ state.error = "A temperatura m\xEDnima deve ser menor que a m\xE1xima.";
21624
+ renderModal3(container, state, modalId, onClose, onSave);
21625
+ return;
21626
+ }
21627
+ if (min < 0 || max > 50) {
21628
+ state.error = "Os valores devem estar entre 0\xB0C e 50\xB0C.";
21629
+ renderModal3(container, state, modalId, onClose, onSave);
21630
+ return;
21631
+ }
21632
+ await onSave(min, max);
21633
+ });
21634
+ }
21635
+ function openTemperatureSettingsModal(params) {
21636
+ const modalId = `myio-temp-settings-${Date.now()}`;
21637
+ const state = {
21638
+ customerId: params.customerId,
21639
+ customerName: params.customerName || "",
21640
+ token: params.token,
21641
+ theme: params.theme || "dark",
21642
+ minTemperature: null,
21643
+ maxTemperature: null,
21644
+ isLoading: true,
21645
+ isSaving: false,
21646
+ error: null,
21647
+ successMessage: null
21648
+ };
21649
+ const container = document.createElement("div");
21650
+ container.id = `${modalId}-container`;
21651
+ document.body.appendChild(container);
21652
+ const destroy = () => {
21653
+ container.remove();
21654
+ params.onClose?.();
21655
+ };
21656
+ const handleSave = async (min, max) => {
21657
+ state.isSaving = true;
21658
+ state.error = null;
21659
+ state.successMessage = null;
21660
+ renderModal3(container, state, modalId, destroy, handleSave);
21661
+ try {
21662
+ await saveCustomerAttributes(state.customerId, state.token, min, max);
21663
+ state.minTemperature = min;
21664
+ state.maxTemperature = max;
21665
+ state.isSaving = false;
21666
+ state.successMessage = "Configura\xE7\xF5es salvas com sucesso!";
21667
+ renderModal3(container, state, modalId, destroy, handleSave);
21668
+ params.onSave?.({ minTemperature: min, maxTemperature: max });
21669
+ setTimeout(() => {
21670
+ destroy();
21671
+ }, 1500);
21672
+ } catch (error) {
21673
+ state.isSaving = false;
21674
+ state.error = `Erro ao salvar: ${error.message}`;
21675
+ renderModal3(container, state, modalId, destroy, handleSave);
21676
+ }
21677
+ };
21678
+ renderModal3(container, state, modalId, destroy, handleSave);
21679
+ fetchCustomerAttributes(state.customerId, state.token).then(({ minTemperature, maxTemperature }) => {
21680
+ state.minTemperature = minTemperature;
21681
+ state.maxTemperature = maxTemperature;
21682
+ state.isLoading = false;
21683
+ renderModal3(container, state, modalId, destroy, handleSave);
21684
+ }).catch((error) => {
21685
+ state.isLoading = false;
21686
+ state.error = `Erro ao carregar: ${error.message}`;
21687
+ renderModal3(container, state, modalId, destroy, handleSave);
21688
+ });
21689
+ return { destroy };
21690
+ }
21691
+
21692
+ exports.CHART_COLORS = CHART_COLORS;
19370
21693
  exports.ConnectionStatusType = ConnectionStatusType;
21694
+ exports.DEFAULT_CLAMP_RANGE = DEFAULT_CLAMP_RANGE;
19371
21695
  exports.DeviceStatusType = DeviceStatusType;
19372
21696
  exports.MyIOChartModal = MyIOChartModal;
19373
21697
  exports.MyIODraggableCard = MyIODraggableCard;
@@ -19375,6 +21699,7 @@ ${rangeText}`;
19375
21699
  exports.MyIOToast = MyIOToast;
19376
21700
  exports.addDetectionContext = addDetectionContext;
19377
21701
  exports.addNamespace = addNamespace;
21702
+ exports.aggregateByDay = aggregateByDay;
19378
21703
  exports.averageByDay = averageByDay;
19379
21704
  exports.buildListItemsThingsboardByUniqueDatasource = buildListItemsThingsboardByUniqueDatasource;
19380
21705
  exports.buildMyioIngestionAuth = buildMyioIngestionAuth;
@@ -19383,6 +21708,8 @@ ${rangeText}`;
19383
21708
  exports.calcDeltaPercent = calcDeltaPercent;
19384
21709
  exports.calculateDeviceStatus = calculateDeviceStatus;
19385
21710
  exports.calculateDeviceStatusWithRanges = calculateDeviceStatusWithRanges;
21711
+ exports.calculateStats = calculateStats;
21712
+ exports.clampTemperature = clampTemperature;
19386
21713
  exports.classify = classify;
19387
21714
  exports.classifyWaterLabel = classifyWaterLabel;
19388
21715
  exports.classifyWaterLabels = classifyWaterLabels;
@@ -19395,9 +21722,11 @@ ${rangeText}`;
19395
21722
  exports.detectDeviceType = detectDeviceType;
19396
21723
  exports.determineInterval = determineInterval;
19397
21724
  exports.deviceStatusIcons = deviceStatusIcons;
21725
+ exports.exportTemperatureCSV = exportTemperatureCSV;
19398
21726
  exports.exportToCSV = exportToCSV;
19399
21727
  exports.exportToCSVAll = exportToCSVAll;
19400
21728
  exports.extractMyIOCredentials = extractMyIOCredentials;
21729
+ exports.fetchTemperatureData = fetchTemperatureData;
19401
21730
  exports.fetchThingsboardCustomerAttrsFromStorage = fetchThingsboardCustomerAttrsFromStorage;
19402
21731
  exports.fetchThingsboardCustomerServerScopeAttrs = fetchThingsboardCustomerServerScopeAttrs;
19403
21732
  exports.findValue = findValue;
@@ -19411,6 +21740,7 @@ ${rangeText}`;
19411
21740
  exports.formatEnergy = formatEnergy;
19412
21741
  exports.formatNumberReadable = formatNumberReadable;
19413
21742
  exports.formatTankHeadFromCm = formatTankHeadFromCm;
21743
+ exports.formatTemperature = formatTemperature;
19414
21744
  exports.formatWaterByGroup = formatWaterByGroup;
19415
21745
  exports.formatWaterVolumeM3 = formatWaterVolumeM3;
19416
21746
  exports.getAuthCacheStats = getAuthCacheStats;
@@ -19425,6 +21755,7 @@ ${rangeText}`;
19425
21755
  exports.getValueByDatakeyLegacy = getValueByDatakeyLegacy;
19426
21756
  exports.getWaterCategories = getWaterCategories;
19427
21757
  exports.groupByDay = groupByDay;
21758
+ exports.interpolateTemperature = interpolateTemperature;
19428
21759
  exports.isDeviceOffline = isDeviceOffline;
19429
21760
  exports.isValidConnectionStatus = isValidConnectionStatus;
19430
21761
  exports.isValidDeviceStatus = isValidDeviceStatus;
@@ -19442,6 +21773,9 @@ ${rangeText}`;
19442
21773
  exports.openDemandModal = openDemandModal;
19443
21774
  exports.openGoalsPanel = openGoalsPanel;
19444
21775
  exports.openRealTimeTelemetryModal = openRealTimeTelemetryModal;
21776
+ exports.openTemperatureComparisonModal = openTemperatureComparisonModal;
21777
+ exports.openTemperatureModal = openTemperatureModal;
21778
+ exports.openTemperatureSettingsModal = openTemperatureSettingsModal;
19445
21779
  exports.parseInputDateToDate = parseInputDateToDate;
19446
21780
  exports.renderCardComponent = renderCardComponent;
19447
21781
  exports.renderCardComponentEnhanced = renderCardComponent2;