myio-js-library 0.1.140 → 0.1.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -568,7 +568,9 @@ var init_template_card = __esm({
568
568
  // src/index.ts
569
569
  var index_exports = {};
570
570
  __export(index_exports, {
571
+ CHART_COLORS: () => CHART_COLORS,
571
572
  ConnectionStatusType: () => ConnectionStatusType,
573
+ DEFAULT_CLAMP_RANGE: () => DEFAULT_CLAMP_RANGE,
572
574
  DeviceStatusType: () => DeviceStatusType,
573
575
  MyIOChartModal: () => MyIOChartModal,
574
576
  MyIODraggableCard: () => MyIODraggableCard,
@@ -577,6 +579,7 @@ __export(index_exports, {
577
579
  MyIOToast: () => MyIOToast,
578
580
  addDetectionContext: () => addDetectionContext,
579
581
  addNamespace: () => addNamespace,
582
+ aggregateByDay: () => aggregateByDay,
580
583
  averageByDay: () => averageByDay,
581
584
  buildListItemsThingsboardByUniqueDatasource: () => buildListItemsThingsboardByUniqueDatasource,
582
585
  buildMyioIngestionAuth: () => buildMyioIngestionAuth,
@@ -585,6 +588,8 @@ __export(index_exports, {
585
588
  calcDeltaPercent: () => calcDeltaPercent,
586
589
  calculateDeviceStatus: () => calculateDeviceStatus,
587
590
  calculateDeviceStatusWithRanges: () => calculateDeviceStatusWithRanges,
591
+ calculateStats: () => calculateStats,
592
+ clampTemperature: () => clampTemperature,
588
593
  classify: () => classify,
589
594
  classifyWaterLabel: () => classifyWaterLabel,
590
595
  classifyWaterLabels: () => classifyWaterLabels,
@@ -597,9 +602,11 @@ __export(index_exports, {
597
602
  detectDeviceType: () => detectDeviceType,
598
603
  determineInterval: () => determineInterval,
599
604
  deviceStatusIcons: () => deviceStatusIcons,
605
+ exportTemperatureCSV: () => exportTemperatureCSV,
600
606
  exportToCSV: () => exportToCSV,
601
607
  exportToCSVAll: () => exportToCSVAll,
602
608
  extractMyIOCredentials: () => extractMyIOCredentials,
609
+ fetchTemperatureData: () => fetchTemperatureData,
603
610
  fetchThingsboardCustomerAttrsFromStorage: () => fetchThingsboardCustomerAttrsFromStorage,
604
611
  fetchThingsboardCustomerServerScopeAttrs: () => fetchThingsboardCustomerServerScopeAttrs,
605
612
  findValue: () => findValue,
@@ -613,6 +620,7 @@ __export(index_exports, {
613
620
  formatEnergy: () => formatEnergy,
614
621
  formatNumberReadable: () => formatNumberReadable,
615
622
  formatTankHeadFromCm: () => formatTankHeadFromCm,
623
+ formatTemperature: () => formatTemperature,
616
624
  formatWaterByGroup: () => formatWaterByGroup,
617
625
  formatWaterVolumeM3: () => formatWaterVolumeM3,
618
626
  getAuthCacheStats: () => getAuthCacheStats,
@@ -627,6 +635,7 @@ __export(index_exports, {
627
635
  getValueByDatakeyLegacy: () => getValueByDatakeyLegacy,
628
636
  getWaterCategories: () => getWaterCategories,
629
637
  groupByDay: () => groupByDay,
638
+ interpolateTemperature: () => interpolateTemperature,
630
639
  isDeviceOffline: () => isDeviceOffline,
631
640
  isValidConnectionStatus: () => isValidConnectionStatus,
632
641
  isValidDeviceStatus: () => isValidDeviceStatus,
@@ -644,6 +653,8 @@ __export(index_exports, {
644
653
  openDemandModal: () => openDemandModal,
645
654
  openGoalsPanel: () => openGoalsPanel,
646
655
  openRealTimeTelemetryModal: () => openRealTimeTelemetryModal,
656
+ openTemperatureComparisonModal: () => openTemperatureComparisonModal,
657
+ openTemperatureModal: () => openTemperatureModal,
647
658
  parseInputDateToDate: () => parseInputDateToDate,
648
659
  renderCardComponent: () => renderCardComponent,
649
660
  renderCardComponentEnhanced: () => renderCardComponent2,
@@ -18244,13 +18255,13 @@ function openGoalsPanel(params) {
18244
18255
  function initializeModal() {
18245
18256
  if (data) {
18246
18257
  modalState.goalsData = data;
18247
- renderModal();
18258
+ renderModal3();
18248
18259
  } else {
18249
- renderModal();
18260
+ renderModal3();
18250
18261
  loadGoalsData();
18251
18262
  }
18252
18263
  }
18253
- function renderModal() {
18264
+ function renderModal3() {
18254
18265
  const existing = document.getElementById("myio-goals-panel-modal");
18255
18266
  if (existing) {
18256
18267
  existing.remove();
@@ -19638,9 +19649,1730 @@ function openGoalsPanel(params) {
19638
19649
  };
19639
19650
  }
19640
19651
  }
19652
+
19653
+ // src/components/temperature/utils.ts
19654
+ var DAY_PERIODS = [
19655
+ { id: "madrugada", label: "Madrugada (00h-06h)", startHour: 0, endHour: 6 },
19656
+ { id: "manha", label: "Manh\xE3 (06h-12h)", startHour: 6, endHour: 12 },
19657
+ { id: "tarde", label: "Tarde (12h-18h)", startHour: 12, endHour: 18 },
19658
+ { id: "noite", label: "Noite (18h-24h)", startHour: 18, endHour: 24 }
19659
+ ];
19660
+ var DEFAULT_CLAMP_RANGE = { min: 15, max: 40 };
19661
+ function getTodaySoFar() {
19662
+ const now = /* @__PURE__ */ new Date();
19663
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
19664
+ return {
19665
+ startTs: startOfDay.getTime(),
19666
+ endTs: now.getTime()
19667
+ };
19668
+ }
19669
+ var CHART_COLORS = [
19670
+ "#1976d2",
19671
+ // Blue
19672
+ "#FF6B6B",
19673
+ // Red
19674
+ "#4CAF50",
19675
+ // Green
19676
+ "#FF9800",
19677
+ // Orange
19678
+ "#9C27B0",
19679
+ // Purple
19680
+ "#00BCD4",
19681
+ // Cyan
19682
+ "#E91E63",
19683
+ // Pink
19684
+ "#795548"
19685
+ // Brown
19686
+ ];
19687
+ async function fetchTemperatureData(token, deviceId, startTs, endTs) {
19688
+ const url = `/api/plugins/telemetry/DEVICE/${deviceId}/values/timeseries?keys=temperature&startTs=${encodeURIComponent(startTs)}&endTs=${encodeURIComponent(endTs)}&limit=50000&agg=NONE`;
19689
+ const response = await fetch(url, {
19690
+ headers: {
19691
+ "X-Authorization": `Bearer ${token}`,
19692
+ "Content-Type": "application/json"
19693
+ }
19694
+ });
19695
+ if (!response.ok) {
19696
+ throw new Error(`Failed to fetch temperature data: ${response.status}`);
19697
+ }
19698
+ const data = await response.json();
19699
+ return data?.temperature || [];
19700
+ }
19701
+ function clampTemperature(value, range = DEFAULT_CLAMP_RANGE) {
19702
+ const num = Number(value || 0);
19703
+ if (num < range.min) return range.min;
19704
+ if (num > range.max) return range.max;
19705
+ return num;
19706
+ }
19707
+ function calculateStats(data, clampRange = DEFAULT_CLAMP_RANGE) {
19708
+ if (data.length === 0) {
19709
+ return { avg: 0, min: 0, max: 0, count: 0 };
19710
+ }
19711
+ const values = data.map((item) => clampTemperature(item.value, clampRange));
19712
+ const sum = values.reduce((acc, v) => acc + v, 0);
19713
+ return {
19714
+ avg: sum / values.length,
19715
+ min: Math.min(...values),
19716
+ max: Math.max(...values),
19717
+ count: values.length
19718
+ };
19719
+ }
19720
+ function interpolateTemperature(data, options) {
19721
+ const { intervalMinutes, startTs, endTs, clampRange = DEFAULT_CLAMP_RANGE } = options;
19722
+ const intervalMs = intervalMinutes * 60 * 1e3;
19723
+ if (data.length === 0) {
19724
+ return [];
19725
+ }
19726
+ const sortedData = [...data].sort((a, b) => a.ts - b.ts);
19727
+ const result = [];
19728
+ let lastKnownValue = clampTemperature(sortedData[0].value, clampRange);
19729
+ let dataIndex = 0;
19730
+ for (let ts = startTs; ts <= endTs; ts += intervalMs) {
19731
+ while (dataIndex < sortedData.length - 1 && sortedData[dataIndex + 1].ts <= ts) {
19732
+ dataIndex++;
19733
+ }
19734
+ const currentData = sortedData[dataIndex];
19735
+ if (currentData && Math.abs(currentData.ts - ts) < intervalMs) {
19736
+ lastKnownValue = clampTemperature(currentData.value, clampRange);
19737
+ }
19738
+ result.push({
19739
+ ts,
19740
+ value: lastKnownValue
19741
+ });
19742
+ }
19743
+ return result;
19744
+ }
19745
+ function aggregateByDay(data, clampRange = DEFAULT_CLAMP_RANGE) {
19746
+ if (data.length === 0) {
19747
+ return [];
19748
+ }
19749
+ const dayMap = /* @__PURE__ */ new Map();
19750
+ data.forEach((item) => {
19751
+ const date = new Date(item.ts);
19752
+ const dateKey = date.toISOString().split("T")[0];
19753
+ if (!dayMap.has(dateKey)) {
19754
+ dayMap.set(dateKey, []);
19755
+ }
19756
+ dayMap.get(dateKey).push(item);
19757
+ });
19758
+ const result = [];
19759
+ dayMap.forEach((dayData, dateKey) => {
19760
+ const values = dayData.map((item) => clampTemperature(item.value, clampRange));
19761
+ const sum = values.reduce((acc, v) => acc + v, 0);
19762
+ result.push({
19763
+ date: dateKey,
19764
+ dateTs: new Date(dateKey).getTime(),
19765
+ avg: sum / values.length,
19766
+ min: Math.min(...values),
19767
+ max: Math.max(...values),
19768
+ count: values.length
19769
+ });
19770
+ });
19771
+ return result.sort((a, b) => a.dateTs - b.dateTs);
19772
+ }
19773
+ function filterByDayPeriods(data, selectedPeriods) {
19774
+ if (selectedPeriods.length === 0 || selectedPeriods.length === DAY_PERIODS.length) {
19775
+ return data;
19776
+ }
19777
+ return data.filter((item) => {
19778
+ const date = new Date(item.ts);
19779
+ const hour = date.getHours();
19780
+ return selectedPeriods.some((periodId) => {
19781
+ const period = DAY_PERIODS.find((p) => p.id === periodId);
19782
+ if (!period) return false;
19783
+ return hour >= period.startHour && hour < period.endHour;
19784
+ });
19785
+ });
19786
+ }
19787
+ function getSelectedPeriodsLabel(selectedPeriods) {
19788
+ if (selectedPeriods.length === 0 || selectedPeriods.length === DAY_PERIODS.length) {
19789
+ return "Todos os per\xEDodos";
19790
+ }
19791
+ if (selectedPeriods.length === 1) {
19792
+ const period = DAY_PERIODS.find((p) => p.id === selectedPeriods[0]);
19793
+ return period?.label || "";
19794
+ }
19795
+ return `${selectedPeriods.length} per\xEDodos selecionados`;
19796
+ }
19797
+ function formatTemperature(value, decimals = 1) {
19798
+ return `${value.toFixed(decimals)}\xB0C`;
19799
+ }
19800
+ function exportTemperatureCSV(data, deviceLabel, stats, startDate, endDate) {
19801
+ if (data.length === 0) {
19802
+ console.warn("No data to export");
19803
+ return;
19804
+ }
19805
+ const BOM = "\uFEFF";
19806
+ let csvContent = BOM;
19807
+ csvContent += `Relat\xF3rio de Temperatura - ${deviceLabel}
19808
+ `;
19809
+ csvContent += `Per\xEDodo: ${startDate} at\xE9 ${endDate}
19810
+ `;
19811
+ csvContent += `M\xE9dia: ${formatTemperature(stats.avg)}
19812
+ `;
19813
+ csvContent += `M\xEDnima: ${formatTemperature(stats.min)}
19814
+ `;
19815
+ csvContent += `M\xE1xima: ${formatTemperature(stats.max)}
19816
+ `;
19817
+ csvContent += `Total de leituras: ${stats.count}
19818
+ `;
19819
+ csvContent += "\n";
19820
+ csvContent += "Data/Hora,Temperatura (\xB0C)\n";
19821
+ data.forEach((item) => {
19822
+ const date = new Date(item.ts).toLocaleString("pt-BR");
19823
+ const temp = Number(item.value).toFixed(2);
19824
+ csvContent += `"${date}",${temp}
19825
+ `;
19826
+ });
19827
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
19828
+ const url = URL.createObjectURL(blob);
19829
+ const link = document.createElement("a");
19830
+ link.href = url;
19831
+ link.download = `temperatura_${deviceLabel.replace(/\s+/g, "_")}_${startDate}_${endDate}.csv`;
19832
+ document.body.appendChild(link);
19833
+ link.click();
19834
+ document.body.removeChild(link);
19835
+ URL.revokeObjectURL(url);
19836
+ }
19837
+ var DARK_THEME = {
19838
+ background: "rgba(0, 0, 0, 0.85)",
19839
+ surface: "#1a1f28",
19840
+ text: "#ffffff",
19841
+ textMuted: "rgba(255, 255, 255, 0.7)",
19842
+ border: "rgba(255, 255, 255, 0.1)",
19843
+ primary: "#1976d2",
19844
+ success: "#4CAF50",
19845
+ warning: "#FF9800",
19846
+ danger: "#f44336",
19847
+ chartLine: "#1976d2",
19848
+ chartGrid: "rgba(255, 255, 255, 0.1)"
19849
+ };
19850
+ var LIGHT_THEME = {
19851
+ background: "rgba(0, 0, 0, 0.6)",
19852
+ surface: "#ffffff",
19853
+ text: "#333333",
19854
+ textMuted: "#666666",
19855
+ border: "#e0e0e0",
19856
+ primary: "#1976d2",
19857
+ success: "#4CAF50",
19858
+ warning: "#FF9800",
19859
+ danger: "#f44336",
19860
+ chartLine: "#1976d2",
19861
+ chartGrid: "#e0e0e0"
19862
+ };
19863
+ function getThemeColors(theme) {
19864
+ return theme === "dark" ? DARK_THEME : LIGHT_THEME;
19865
+ }
19866
+
19867
+ // src/components/temperature/TemperatureModal.ts
19868
+ async function openTemperatureModal(params) {
19869
+ const modalId = `myio-temp-modal-${Date.now()}`;
19870
+ const defaultDateRange = getTodaySoFar();
19871
+ const startTs = params.startDate ? new Date(params.startDate).getTime() : defaultDateRange.startTs;
19872
+ const endTs = params.endDate ? new Date(params.endDate).getTime() : defaultDateRange.endTs;
19873
+ const state = {
19874
+ token: params.token,
19875
+ deviceId: params.deviceId,
19876
+ label: params.label || "Sensor de Temperatura",
19877
+ currentTemperature: params.currentTemperature ?? null,
19878
+ temperatureMin: params.temperatureMin ?? null,
19879
+ temperatureMax: params.temperatureMax ?? null,
19880
+ temperatureStatus: params.temperatureStatus ?? null,
19881
+ startTs,
19882
+ endTs,
19883
+ granularity: params.granularity || "hour",
19884
+ theme: params.theme || "light",
19885
+ clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
19886
+ locale: params.locale || "pt-BR",
19887
+ data: [],
19888
+ stats: { avg: 0, min: 0, max: 0, count: 0 },
19889
+ isLoading: true,
19890
+ dateRangePicker: null,
19891
+ selectedPeriods: ["madrugada", "manha", "tarde", "noite"]
19892
+ // All periods selected by default
19893
+ };
19894
+ const savedGranularity = localStorage.getItem("myio-temp-modal-granularity");
19895
+ const savedTheme = localStorage.getItem("myio-temp-modal-theme");
19896
+ if (savedGranularity) state.granularity = savedGranularity;
19897
+ if (savedTheme) state.theme = savedTheme;
19898
+ const modalContainer = document.createElement("div");
19899
+ modalContainer.id = modalId;
19900
+ document.body.appendChild(modalContainer);
19901
+ renderModal(modalContainer, state, modalId);
19902
+ try {
19903
+ state.data = await fetchTemperatureData(state.token, state.deviceId, state.startTs, state.endTs);
19904
+ state.stats = calculateStats(state.data, state.clampRange);
19905
+ state.isLoading = false;
19906
+ renderModal(modalContainer, state, modalId);
19907
+ drawChart(modalId, state);
19908
+ } catch (error) {
19909
+ console.error("[TemperatureModal] Error fetching data:", error);
19910
+ state.isLoading = false;
19911
+ renderModal(modalContainer, state, modalId, error);
19912
+ }
19913
+ await setupEventListeners(modalContainer, state, modalId, params.onClose);
19914
+ return {
19915
+ destroy: () => {
19916
+ modalContainer.remove();
19917
+ params.onClose?.();
19918
+ },
19919
+ updateData: async (startDate, endDate, granularity) => {
19920
+ state.startTs = new Date(startDate).getTime();
19921
+ state.endTs = new Date(endDate).getTime();
19922
+ if (granularity) state.granularity = granularity;
19923
+ state.isLoading = true;
19924
+ renderModal(modalContainer, state, modalId);
19925
+ try {
19926
+ state.data = await fetchTemperatureData(state.token, state.deviceId, state.startTs, state.endTs);
19927
+ state.stats = calculateStats(state.data, state.clampRange);
19928
+ state.isLoading = false;
19929
+ renderModal(modalContainer, state, modalId);
19930
+ drawChart(modalId, state);
19931
+ } catch (error) {
19932
+ console.error("[TemperatureModal] Error updating data:", error);
19933
+ state.isLoading = false;
19934
+ renderModal(modalContainer, state, modalId, error);
19935
+ }
19936
+ }
19937
+ };
19938
+ }
19939
+ function renderModal(container, state, modalId, error) {
19940
+ const colors = getThemeColors(state.theme);
19941
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale);
19942
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale);
19943
+ const statusText = state.temperatureStatus === "ok" ? "Dentro da faixa" : state.temperatureStatus === "above" ? "Acima do limite" : state.temperatureStatus === "below" ? "Abaixo do limite" : "N/A";
19944
+ const statusColor = state.temperatureStatus === "ok" ? colors.success : state.temperatureStatus === "above" ? colors.danger : state.temperatureStatus === "below" ? colors.primary : colors.textMuted;
19945
+ const rangeText = state.temperatureMin !== null && state.temperatureMax !== null ? `${state.temperatureMin}\xB0C - ${state.temperatureMax}\xB0C` : "N\xE3o definida";
19946
+ const startDateInput = new Date(state.startTs).toISOString().slice(0, 16);
19947
+ const endDateInput = new Date(state.endTs).toISOString().slice(0, 16);
19948
+ const isMaximized = container.__isMaximized || false;
19949
+ const contentMaxWidth = isMaximized ? "100%" : "900px";
19950
+ const contentMaxHeight = isMaximized ? "100vh" : "95vh";
19951
+ const contentBorderRadius = isMaximized ? "0" : "10px";
19952
+ container.innerHTML = `
19953
+ <div class="myio-temp-modal-overlay" style="
19954
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
19955
+ background: rgba(0, 0, 0, 0.5); z-index: 9998;
19956
+ display: flex; justify-content: center; align-items: center;
19957
+ backdrop-filter: blur(2px);
19958
+ ">
19959
+ <div class="myio-temp-modal-content" style="
19960
+ background: ${colors.surface}; border-radius: ${contentBorderRadius};
19961
+ max-width: ${contentMaxWidth}; width: ${isMaximized ? "100%" : "95%"};
19962
+ max-height: ${contentMaxHeight}; height: ${isMaximized ? "100%" : "auto"};
19963
+ overflow: hidden; display: flex; flex-direction: column;
19964
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
19965
+ font-family: 'Roboto', Arial, sans-serif;
19966
+ ">
19967
+ <!-- Header - MyIO Premium Style -->
19968
+ <div style="
19969
+ padding: 4px 8px; display: flex; align-items: center; justify-content: space-between;
19970
+ background: #3e1a7d; color: white; border-radius: ${isMaximized ? "0" : "10px 10px 0 0"};
19971
+ min-height: 20px;
19972
+ ">
19973
+ <h2 style="margin: 6px; font-size: 18px; font-weight: 600; color: white; line-height: 2;">
19974
+ \u{1F321}\uFE0F ${state.label} - Hist\xF3rico de Temperatura
19975
+ </h2>
19976
+ <div style="display: flex; gap: 4px; align-items: center;">
19977
+ <!-- Theme Toggle -->
19978
+ <button id="${modalId}-theme-toggle" title="Alternar tema" style="
19979
+ background: none; border: none; font-size: 16px; cursor: pointer;
19980
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
19981
+ transition: background-color 0.2s;
19982
+ ">${state.theme === "dark" ? "\u2600\uFE0F" : "\u{1F319}"}</button>
19983
+ <!-- Maximize Button -->
19984
+ <button id="${modalId}-maximize" title="${isMaximized ? "Restaurar" : "Maximizar"}" style="
19985
+ background: none; border: none; font-size: 16px; cursor: pointer;
19986
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
19987
+ transition: background-color 0.2s;
19988
+ ">${isMaximized ? "\u{1F5D7}" : "\u{1F5D6}"}</button>
19989
+ <!-- Close Button -->
19990
+ <button id="${modalId}-close" title="Fechar" style="
19991
+ background: none; border: none; font-size: 20px; cursor: pointer;
19992
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
19993
+ transition: background-color 0.2s;
19994
+ ">\xD7</button>
19995
+ </div>
19996
+ </div>
19997
+
19998
+ <!-- Body -->
19999
+ <div style="flex: 1; overflow-y: auto; padding: 16px;">
20000
+
20001
+ <!-- Controls Row -->
20002
+ <div style="
20003
+ display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end;
20004
+ margin-bottom: 16px; padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#f7f7f7"};
20005
+ border-radius: 6px; border: 1px solid ${colors.border};
20006
+ ">
20007
+ <!-- Granularity Select -->
20008
+ <div>
20009
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20010
+ Granularidade
20011
+ </label>
20012
+ <select id="${modalId}-granularity" style="
20013
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20014
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20015
+ cursor: pointer; min-width: 130px;
20016
+ ">
20017
+ <option value="hour" ${state.granularity === "hour" ? "selected" : ""}>Hora (30 min)</option>
20018
+ <option value="day" ${state.granularity === "day" ? "selected" : ""}>Dia (m\xE9dia)</option>
20019
+ </select>
20020
+ </div>
20021
+ <!-- Day Period Filter (Multiselect) -->
20022
+ <div style="position: relative;">
20023
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20024
+ Per\xEDodos do Dia
20025
+ </label>
20026
+ <button id="${modalId}-period-btn" type="button" style="
20027
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20028
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20029
+ cursor: pointer; min-width: 180px; text-align: left;
20030
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
20031
+ ">
20032
+ <span>${getSelectedPeriodsLabel(state.selectedPeriods)}</span>
20033
+ <span style="font-size: 10px;">\u25BC</span>
20034
+ </button>
20035
+ <div id="${modalId}-period-dropdown" style="
20036
+ display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
20037
+ background: ${colors.surface}; border: 1px solid ${colors.border};
20038
+ border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20039
+ min-width: 200px; margin-top: 4px; padding: 8px 0;
20040
+ ">
20041
+ ${DAY_PERIODS.map((period) => `
20042
+ <label style="
20043
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
20044
+ cursor: pointer; font-size: 13px; color: ${colors.text};
20045
+ " onmouseover="this.style.background='${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"}'"
20046
+ onmouseout="this.style.background='transparent'">
20047
+ <input type="checkbox"
20048
+ name="${modalId}-period"
20049
+ value="${period.id}"
20050
+ ${state.selectedPeriods.includes(period.id) ? "checked" : ""}
20051
+ style="width: 16px; height: 16px; cursor: pointer; accent-color: #3e1a7d;">
20052
+ ${period.label}
20053
+ </label>
20054
+ `).join("")}
20055
+ <div style="border-top: 1px solid ${colors.border}; margin-top: 8px; padding-top: 8px;">
20056
+ <button id="${modalId}-period-select-all" type="button" style="
20057
+ width: calc(100% - 16px); margin: 0 8px 4px; padding: 6px;
20058
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
20059
+ border: none; border-radius: 4px; cursor: pointer;
20060
+ font-size: 12px; color: ${colors.text};
20061
+ ">Selecionar Todos</button>
20062
+ <button id="${modalId}-period-clear" type="button" style="
20063
+ width: calc(100% - 16px); margin: 0 8px; padding: 6px;
20064
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
20065
+ border: none; border-radius: 4px; cursor: pointer;
20066
+ font-size: 12px; color: ${colors.text};
20067
+ ">Limpar Sele\xE7\xE3o</button>
20068
+ </div>
20069
+ </div>
20070
+ </div>
20071
+ <!-- Date Range Picker -->
20072
+ <div style="flex: 1; min-width: 220px;">
20073
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20074
+ Per\xEDodo
20075
+ </label>
20076
+ <input type="text" id="${modalId}-date-range" readonly placeholder="Selecione o per\xEDodo..." style="
20077
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20078
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20079
+ width: 100%; cursor: pointer; box-sizing: border-box;
20080
+ "/>
20081
+ </div>
20082
+ <!-- Query Button -->
20083
+ <button id="${modalId}-query" style="
20084
+ background: #3e1a7d; color: white; border: none;
20085
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20086
+ font-size: 14px; font-weight: 500; height: 38px;
20087
+ display: flex; align-items: center; gap: 8px;
20088
+ font-family: 'Roboto', Arial, sans-serif;
20089
+ " ${state.isLoading ? "disabled" : ""}>
20090
+ ${state.isLoading ? '<span style="animation: spin 1s linear infinite; display: inline-block;">\u21BB</span> Carregando...' : "Carregar"}
20091
+ </button>
20092
+ </div>
20093
+
20094
+ <!-- Stats Cards -->
20095
+ <div style="
20096
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
20097
+ gap: 12px; margin-bottom: 16px;
20098
+ ">
20099
+ <!-- Current Temperature -->
20100
+ <div style="
20101
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
20102
+ border-radius: 12px; border: 1px solid ${colors.border};
20103
+ ">
20104
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">Temperatura Atual</span>
20105
+ <div style="font-weight: 700; font-size: 24px; color: ${statusColor}; margin-top: 4px;">
20106
+ ${state.currentTemperature !== null ? formatTemperature(state.currentTemperature) : "N/A"}
20107
+ </div>
20108
+ <div style="font-size: 11px; color: ${statusColor}; margin-top: 2px;">${statusText}</div>
20109
+ </div>
20110
+ <!-- Average -->
20111
+ <div style="
20112
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
20113
+ border-radius: 12px; border: 1px solid ${colors.border};
20114
+ ">
20115
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">M\xE9dia do Per\xEDodo</span>
20116
+ <div id="${modalId}-avg" style="font-weight: 600; font-size: 20px; color: ${colors.text}; margin-top: 4px;">
20117
+ ${state.stats.count > 0 ? formatTemperature(state.stats.avg) : "N/A"}
20118
+ </div>
20119
+ <div style="font-size: 11px; color: ${colors.textMuted}; margin-top: 2px;">${startDateStr} - ${endDateStr}</div>
20120
+ </div>
20121
+ <!-- Min/Max -->
20122
+ <div style="
20123
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
20124
+ border-radius: 12px; border: 1px solid ${colors.border};
20125
+ ">
20126
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">Min / Max</span>
20127
+ <div id="${modalId}-minmax" style="font-weight: 600; font-size: 20px; color: ${colors.text}; margin-top: 4px;">
20128
+ ${state.stats.count > 0 ? `${formatTemperature(state.stats.min)} / ${formatTemperature(state.stats.max)}` : "N/A"}
20129
+ </div>
20130
+ <div style="font-size: 11px; color: ${colors.textMuted}; margin-top: 2px;">${state.stats.count} leituras</div>
20131
+ </div>
20132
+ <!-- Ideal Range -->
20133
+ <div style="
20134
+ padding: 16px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
20135
+ border-radius: 12px; border: 1px solid ${colors.border};
20136
+ ">
20137
+ <span style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500;">Faixa Ideal</span>
20138
+ <div style="font-weight: 600; font-size: 20px; color: ${colors.success}; margin-top: 4px;">
20139
+ ${rangeText}
20140
+ </div>
20141
+ </div>
20142
+ </div>
20143
+
20144
+ <!-- Chart Container -->
20145
+ <div style="margin-bottom: 20px;">
20146
+ <h3 style="margin: 0 0 12px 0; font-size: 14px; color: ${colors.textMuted}; font-weight: 500;">
20147
+ Hist\xF3rico de Temperatura
20148
+ </h3>
20149
+ <div id="${modalId}-chart" style="
20150
+ height: 320px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.03)" : "#fafafa"};
20151
+ border-radius: 12px; display: flex; justify-content: center; align-items: center;
20152
+ border: 1px solid ${colors.border}; position: relative;
20153
+ ">
20154
+ ${state.isLoading ? `<div style="text-align: center; color: ${colors.textMuted};">
20155
+ <div style="animation: spin 1s linear infinite; font-size: 32px; margin-bottom: 8px;">\u21BB</div>
20156
+ <div>Carregando dados...</div>
20157
+ </div>` : error ? `<div style="text-align: center; color: ${colors.danger};">
20158
+ <div style="font-size: 32px; margin-bottom: 8px;">\u26A0\uFE0F</div>
20159
+ <div>Erro ao carregar dados</div>
20160
+ <div style="font-size: 12px; margin-top: 4px;">${error.message}</div>
20161
+ </div>` : state.data.length === 0 ? `<div style="text-align: center; color: ${colors.textMuted};">
20162
+ <div style="font-size: 32px; margin-bottom: 8px;">\u{1F4ED}</div>
20163
+ <div>Sem dados para o per\xEDodo selecionado</div>
20164
+ </div>` : `<canvas id="${modalId}-canvas" style="width: 100%; height: 100%;"></canvas>`}
20165
+ </div>
20166
+ </div>
20167
+
20168
+ <!-- Actions -->
20169
+ <div style="display: flex; justify-content: flex-end; gap: 12px;">
20170
+ <button id="${modalId}-export" style="
20171
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f7f7f7"};
20172
+ color: ${colors.text}; border: 1px solid ${colors.border};
20173
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20174
+ font-size: 14px; display: flex; align-items: center; gap: 8px;
20175
+ font-family: 'Roboto', Arial, sans-serif;
20176
+ " ${state.data.length === 0 ? "disabled" : ""}>
20177
+ \u{1F4E5} Exportar CSV
20178
+ </button>
20179
+ <button id="${modalId}-close-btn" style="
20180
+ background: #3e1a7d; color: white; border: none;
20181
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20182
+ font-size: 14px; font-weight: 500;
20183
+ font-family: 'Roboto', Arial, sans-serif;
20184
+ ">
20185
+ Fechar
20186
+ </button>
20187
+ </div>
20188
+ </div><!-- End Body -->
20189
+ </div>
20190
+ </div>
20191
+ <style>
20192
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
20193
+ #${modalId} select:focus, #${modalId} input:focus {
20194
+ outline: 2px solid #3e1a7d;
20195
+ outline-offset: 2px;
20196
+ }
20197
+ #${modalId} button:hover:not(:disabled) {
20198
+ opacity: 0.9;
20199
+ }
20200
+ #${modalId} button:disabled {
20201
+ opacity: 0.5;
20202
+ cursor: not-allowed;
20203
+ }
20204
+ #${modalId} .myio-temp-modal-content > div:first-child button:hover {
20205
+ background: rgba(255, 255, 255, 0.1) !important;
20206
+ color: white !important;
20207
+ }
20208
+ </style>
20209
+ `;
20210
+ }
20211
+ function drawChart(modalId, state) {
20212
+ const chartContainer = document.getElementById(`${modalId}-chart`);
20213
+ const canvas = document.getElementById(`${modalId}-canvas`);
20214
+ if (!chartContainer || !canvas || state.data.length === 0) return;
20215
+ const ctx = canvas.getContext("2d");
20216
+ if (!ctx) return;
20217
+ const colors = getThemeColors(state.theme);
20218
+ const filteredData = filterByDayPeriods(state.data, state.selectedPeriods);
20219
+ if (filteredData.length === 0) {
20220
+ canvas.width = chartContainer.clientWidth;
20221
+ canvas.height = chartContainer.clientHeight;
20222
+ ctx.fillStyle = colors.textMuted;
20223
+ ctx.font = "14px Roboto, Arial, sans-serif";
20224
+ ctx.textAlign = "center";
20225
+ ctx.fillText("Nenhum dado para os per\xEDodos selecionados", canvas.width / 2, canvas.height / 2);
20226
+ return;
20227
+ }
20228
+ let chartData;
20229
+ if (state.granularity === "hour") {
20230
+ const interpolated = interpolateTemperature(filteredData, {
20231
+ intervalMinutes: 30,
20232
+ startTs: state.startTs,
20233
+ endTs: state.endTs,
20234
+ clampRange: state.clampRange
20235
+ });
20236
+ const filteredInterpolated = filterByDayPeriods(interpolated, state.selectedPeriods);
20237
+ chartData = filteredInterpolated.map((item) => ({
20238
+ x: item.ts,
20239
+ y: Number(item.value),
20240
+ screenX: 0,
20241
+ screenY: 0
20242
+ }));
20243
+ } else {
20244
+ const daily = aggregateByDay(filteredData, state.clampRange);
20245
+ chartData = daily.map((item) => ({
20246
+ x: item.dateTs,
20247
+ y: item.avg,
20248
+ screenX: 0,
20249
+ screenY: 0,
20250
+ label: item.date
20251
+ }));
20252
+ }
20253
+ if (chartData.length === 0) return;
20254
+ const width = chartContainer.clientWidth - 2;
20255
+ const height = 320;
20256
+ canvas.width = width;
20257
+ canvas.height = height;
20258
+ const paddingLeft = 60;
20259
+ const paddingRight = 20;
20260
+ const paddingTop = 20;
20261
+ const paddingBottom = 55;
20262
+ const isPeriodsFiltered = state.selectedPeriods.length < 4 && state.selectedPeriods.length > 0;
20263
+ const values = chartData.map((d) => d.y);
20264
+ const minY = Math.min(...values) - 1;
20265
+ const maxY = Math.max(...values) + 1;
20266
+ const chartWidth = width - paddingLeft - paddingRight;
20267
+ const chartHeight = height - paddingTop - paddingBottom;
20268
+ const scaleY = chartHeight / (maxY - minY || 1);
20269
+ if (isPeriodsFiltered) {
20270
+ const pointSpacing = chartWidth / Math.max(1, chartData.length - 1);
20271
+ chartData.forEach((point, index) => {
20272
+ point.screenX = paddingLeft + index * pointSpacing;
20273
+ point.screenY = height - paddingBottom - (point.y - minY) * scaleY;
20274
+ });
20275
+ } else {
20276
+ const minX = chartData[0].x;
20277
+ const maxX = chartData[chartData.length - 1].x;
20278
+ const timeRange = maxX - minX || 1;
20279
+ const scaleX = chartWidth / timeRange;
20280
+ chartData.forEach((point) => {
20281
+ point.screenX = paddingLeft + (point.x - minX) * scaleX;
20282
+ point.screenY = height - paddingBottom - (point.y - minY) * scaleY;
20283
+ });
20284
+ }
20285
+ ctx.clearRect(0, 0, width, height);
20286
+ ctx.strokeStyle = colors.chartGrid;
20287
+ ctx.lineWidth = 1;
20288
+ for (let i = 0; i <= 4; i++) {
20289
+ const y = paddingTop + chartHeight * i / 4;
20290
+ ctx.beginPath();
20291
+ ctx.moveTo(paddingLeft, y);
20292
+ ctx.lineTo(width - paddingRight, y);
20293
+ ctx.stroke();
20294
+ }
20295
+ if (state.temperatureMin !== null && state.temperatureMax !== null) {
20296
+ const rangeMinY = height - paddingBottom - (state.temperatureMin - minY) * scaleY;
20297
+ const rangeMaxY = height - paddingBottom - (state.temperatureMax - minY) * scaleY;
20298
+ ctx.fillStyle = "rgba(76, 175, 80, 0.1)";
20299
+ ctx.fillRect(paddingLeft, rangeMaxY, chartWidth, rangeMinY - rangeMaxY);
20300
+ ctx.strokeStyle = colors.success;
20301
+ ctx.setLineDash([5, 5]);
20302
+ ctx.beginPath();
20303
+ ctx.moveTo(paddingLeft, rangeMinY);
20304
+ ctx.lineTo(width - paddingRight, rangeMinY);
20305
+ ctx.moveTo(paddingLeft, rangeMaxY);
20306
+ ctx.lineTo(width - paddingRight, rangeMaxY);
20307
+ ctx.stroke();
20308
+ ctx.setLineDash([]);
20309
+ }
20310
+ ctx.strokeStyle = colors.chartLine;
20311
+ ctx.lineWidth = 2;
20312
+ ctx.beginPath();
20313
+ chartData.forEach((point, i) => {
20314
+ if (i === 0) ctx.moveTo(point.screenX, point.screenY);
20315
+ else ctx.lineTo(point.screenX, point.screenY);
20316
+ });
20317
+ ctx.stroke();
20318
+ ctx.fillStyle = colors.chartLine;
20319
+ chartData.forEach((point) => {
20320
+ ctx.beginPath();
20321
+ ctx.arc(point.screenX, point.screenY, 4, 0, Math.PI * 2);
20322
+ ctx.fill();
20323
+ });
20324
+ ctx.fillStyle = colors.textMuted;
20325
+ ctx.font = "11px system-ui, sans-serif";
20326
+ ctx.textAlign = "right";
20327
+ for (let i = 0; i <= 4; i++) {
20328
+ const val = minY + (maxY - minY) * (4 - i) / 4;
20329
+ const y = paddingTop + chartHeight * i / 4;
20330
+ ctx.fillText(val.toFixed(1) + "\xB0C", paddingLeft - 8, y + 4);
20331
+ }
20332
+ ctx.textAlign = "center";
20333
+ const numLabels = Math.min(8, chartData.length);
20334
+ const labelInterval = Math.max(1, Math.floor(chartData.length / numLabels));
20335
+ for (let i = 0; i < chartData.length; i += labelInterval) {
20336
+ const point = chartData[i];
20337
+ const date = new Date(point.x);
20338
+ let label;
20339
+ if (state.granularity === "hour") {
20340
+ label = date.toLocaleTimeString(state.locale, { hour: "2-digit", minute: "2-digit" });
20341
+ } else {
20342
+ label = date.toLocaleDateString(state.locale, { day: "2-digit", month: "2-digit" });
20343
+ }
20344
+ ctx.strokeStyle = colors.chartGrid;
20345
+ ctx.lineWidth = 1;
20346
+ ctx.beginPath();
20347
+ ctx.moveTo(point.screenX, paddingTop);
20348
+ ctx.lineTo(point.screenX, height - paddingBottom);
20349
+ ctx.stroke();
20350
+ ctx.fillStyle = colors.textMuted;
20351
+ ctx.fillText(label, point.screenX, height - paddingBottom + 18);
20352
+ }
20353
+ ctx.strokeStyle = colors.border;
20354
+ ctx.lineWidth = 1;
20355
+ ctx.beginPath();
20356
+ ctx.moveTo(paddingLeft, paddingTop);
20357
+ ctx.lineTo(paddingLeft, height - paddingBottom);
20358
+ ctx.lineTo(width - paddingRight, height - paddingBottom);
20359
+ ctx.stroke();
20360
+ setupChartTooltip(canvas, chartContainer, chartData, state, colors);
20361
+ }
20362
+ function setupChartTooltip(canvas, container, chartData, state, colors) {
20363
+ const existingTooltip = container.querySelector(".myio-chart-tooltip");
20364
+ if (existingTooltip) existingTooltip.remove();
20365
+ const tooltip = document.createElement("div");
20366
+ tooltip.className = "myio-chart-tooltip";
20367
+ tooltip.style.cssText = `
20368
+ position: absolute;
20369
+ background: ${state.theme === "dark" ? "rgba(30, 30, 40, 0.95)" : "rgba(255, 255, 255, 0.98)"};
20370
+ border: 1px solid ${colors.border};
20371
+ border-radius: 8px;
20372
+ padding: 10px 14px;
20373
+ font-size: 13px;
20374
+ color: ${colors.text};
20375
+ pointer-events: none;
20376
+ opacity: 0;
20377
+ transition: opacity 0.15s;
20378
+ z-index: 1000;
20379
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20380
+ min-width: 140px;
20381
+ `;
20382
+ container.appendChild(tooltip);
20383
+ const findNearestPoint = (mouseX, mouseY) => {
20384
+ const threshold = 20;
20385
+ let nearest = null;
20386
+ let minDist = Infinity;
20387
+ for (const point of chartData) {
20388
+ const dist = Math.sqrt(
20389
+ Math.pow(mouseX - point.screenX, 2) + Math.pow(mouseY - point.screenY, 2)
20390
+ );
20391
+ if (dist < minDist && dist < threshold) {
20392
+ minDist = dist;
20393
+ nearest = point;
20394
+ }
20395
+ }
20396
+ return nearest;
20397
+ };
20398
+ canvas.addEventListener("mousemove", (e) => {
20399
+ const rect = canvas.getBoundingClientRect();
20400
+ const mouseX = e.clientX - rect.left;
20401
+ const mouseY = e.clientY - rect.top;
20402
+ const point = findNearestPoint(mouseX, mouseY);
20403
+ if (point) {
20404
+ const date = new Date(point.x);
20405
+ let dateStr;
20406
+ if (state.granularity === "hour") {
20407
+ dateStr = date.toLocaleDateString(state.locale, {
20408
+ day: "2-digit",
20409
+ month: "2-digit",
20410
+ year: "numeric"
20411
+ }) + " " + date.toLocaleTimeString(state.locale, {
20412
+ hour: "2-digit",
20413
+ minute: "2-digit"
20414
+ });
20415
+ } else {
20416
+ dateStr = date.toLocaleDateString(state.locale, {
20417
+ day: "2-digit",
20418
+ month: "2-digit",
20419
+ year: "numeric"
20420
+ });
20421
+ }
20422
+ tooltip.innerHTML = `
20423
+ <div style="font-weight: 600; margin-bottom: 6px; color: ${colors.primary};">
20424
+ ${formatTemperature(point.y)}
20425
+ </div>
20426
+ <div style="font-size: 11px; color: ${colors.textMuted};">
20427
+ \u{1F4C5} ${dateStr}
20428
+ </div>
20429
+ `;
20430
+ let tooltipX = point.screenX + 15;
20431
+ let tooltipY = point.screenY - 15;
20432
+ const tooltipRect = tooltip.getBoundingClientRect();
20433
+ const containerRect = container.getBoundingClientRect();
20434
+ if (tooltipX + tooltipRect.width > containerRect.width - 10) {
20435
+ tooltipX = point.screenX - tooltipRect.width - 15;
20436
+ }
20437
+ if (tooltipY < 10) {
20438
+ tooltipY = point.screenY + 15;
20439
+ }
20440
+ tooltip.style.left = `${tooltipX}px`;
20441
+ tooltip.style.top = `${tooltipY}px`;
20442
+ tooltip.style.opacity = "1";
20443
+ canvas.style.cursor = "pointer";
20444
+ } else {
20445
+ tooltip.style.opacity = "0";
20446
+ canvas.style.cursor = "default";
20447
+ }
20448
+ });
20449
+ canvas.addEventListener("mouseleave", () => {
20450
+ tooltip.style.opacity = "0";
20451
+ canvas.style.cursor = "default";
20452
+ });
20453
+ }
20454
+ async function setupEventListeners(container, state, modalId, onClose) {
20455
+ const closeModal = () => {
20456
+ container.remove();
20457
+ onClose?.();
20458
+ };
20459
+ container.querySelector(".myio-temp-modal-overlay")?.addEventListener("click", (e) => {
20460
+ if (e.target === e.currentTarget) closeModal();
20461
+ });
20462
+ document.getElementById(`${modalId}-close`)?.addEventListener("click", closeModal);
20463
+ document.getElementById(`${modalId}-close-btn`)?.addEventListener("click", closeModal);
20464
+ const dateRangeInput = document.getElementById(`${modalId}-date-range`);
20465
+ if (dateRangeInput && !state.dateRangePicker) {
20466
+ try {
20467
+ state.dateRangePicker = await createDateRangePicker2(dateRangeInput, {
20468
+ presetStart: new Date(state.startTs).toISOString(),
20469
+ presetEnd: new Date(state.endTs).toISOString(),
20470
+ includeTime: true,
20471
+ timePrecision: "minute",
20472
+ maxRangeDays: 90,
20473
+ locale: state.locale,
20474
+ parentEl: container.querySelector(".myio-temp-modal-content"),
20475
+ onApply: (result) => {
20476
+ state.startTs = new Date(result.startISO).getTime();
20477
+ state.endTs = new Date(result.endISO).getTime();
20478
+ console.log("[TemperatureModal] Date range applied:", result);
20479
+ }
20480
+ });
20481
+ } catch (error) {
20482
+ console.warn("[TemperatureModal] DateRangePicker initialization failed:", error);
20483
+ }
20484
+ }
20485
+ document.getElementById(`${modalId}-theme-toggle`)?.addEventListener("click", async () => {
20486
+ state.theme = state.theme === "dark" ? "light" : "dark";
20487
+ localStorage.setItem("myio-temp-modal-theme", state.theme);
20488
+ state.dateRangePicker = null;
20489
+ renderModal(container, state, modalId);
20490
+ if (state.data.length > 0) drawChart(modalId, state);
20491
+ await setupEventListeners(container, state, modalId, onClose);
20492
+ });
20493
+ document.getElementById(`${modalId}-maximize`)?.addEventListener("click", async () => {
20494
+ container.__isMaximized = !container.__isMaximized;
20495
+ state.dateRangePicker = null;
20496
+ renderModal(container, state, modalId);
20497
+ if (state.data.length > 0) drawChart(modalId, state);
20498
+ await setupEventListeners(container, state, modalId, onClose);
20499
+ });
20500
+ const periodBtn = document.getElementById(`${modalId}-period-btn`);
20501
+ const periodDropdown = document.getElementById(`${modalId}-period-dropdown`);
20502
+ periodBtn?.addEventListener("click", (e) => {
20503
+ e.stopPropagation();
20504
+ if (periodDropdown) {
20505
+ periodDropdown.style.display = periodDropdown.style.display === "none" ? "block" : "none";
20506
+ }
20507
+ });
20508
+ document.addEventListener("click", (e) => {
20509
+ if (periodDropdown && !periodDropdown.contains(e.target) && e.target !== periodBtn) {
20510
+ periodDropdown.style.display = "none";
20511
+ }
20512
+ });
20513
+ const periodCheckboxes = document.querySelectorAll(`input[name="${modalId}-period"]`);
20514
+ periodCheckboxes.forEach((checkbox) => {
20515
+ checkbox.addEventListener("change", () => {
20516
+ const checked = Array.from(periodCheckboxes).filter((cb) => cb.checked).map((cb) => cb.value);
20517
+ state.selectedPeriods = checked;
20518
+ const btnLabel = periodBtn?.querySelector("span:first-child");
20519
+ if (btnLabel) {
20520
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
20521
+ }
20522
+ if (state.data.length > 0) drawChart(modalId, state);
20523
+ });
20524
+ });
20525
+ document.getElementById(`${modalId}-period-select-all`)?.addEventListener("click", () => {
20526
+ periodCheckboxes.forEach((cb) => {
20527
+ cb.checked = true;
20528
+ });
20529
+ state.selectedPeriods = ["madrugada", "manha", "tarde", "noite"];
20530
+ const btnLabel = periodBtn?.querySelector("span:first-child");
20531
+ if (btnLabel) {
20532
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
20533
+ }
20534
+ if (state.data.length > 0) drawChart(modalId, state);
20535
+ });
20536
+ document.getElementById(`${modalId}-period-clear`)?.addEventListener("click", () => {
20537
+ periodCheckboxes.forEach((cb) => {
20538
+ cb.checked = false;
20539
+ });
20540
+ state.selectedPeriods = [];
20541
+ const btnLabel = periodBtn?.querySelector("span:first-child");
20542
+ if (btnLabel) {
20543
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
20544
+ }
20545
+ if (state.data.length > 0) drawChart(modalId, state);
20546
+ });
20547
+ document.getElementById(`${modalId}-granularity`)?.addEventListener("change", (e) => {
20548
+ state.granularity = e.target.value;
20549
+ localStorage.setItem("myio-temp-modal-granularity", state.granularity);
20550
+ if (state.data.length > 0) drawChart(modalId, state);
20551
+ });
20552
+ document.getElementById(`${modalId}-query`)?.addEventListener("click", async () => {
20553
+ if (state.startTs >= state.endTs) {
20554
+ alert("Por favor, selecione um per\xEDodo v\xE1lido");
20555
+ return;
20556
+ }
20557
+ state.isLoading = true;
20558
+ state.dateRangePicker = null;
20559
+ renderModal(container, state, modalId);
20560
+ try {
20561
+ state.data = await fetchTemperatureData(state.token, state.deviceId, state.startTs, state.endTs);
20562
+ state.stats = calculateStats(state.data, state.clampRange);
20563
+ state.isLoading = false;
20564
+ renderModal(container, state, modalId);
20565
+ drawChart(modalId, state);
20566
+ await setupEventListeners(container, state, modalId, onClose);
20567
+ } catch (error) {
20568
+ console.error("[TemperatureModal] Error fetching data:", error);
20569
+ state.isLoading = false;
20570
+ renderModal(container, state, modalId, error);
20571
+ await setupEventListeners(container, state, modalId, onClose);
20572
+ }
20573
+ });
20574
+ document.getElementById(`${modalId}-export`)?.addEventListener("click", () => {
20575
+ if (state.data.length === 0) return;
20576
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale).replace(/\//g, "-");
20577
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale).replace(/\//g, "-");
20578
+ exportTemperatureCSV(
20579
+ state.data,
20580
+ state.label,
20581
+ state.stats,
20582
+ startDateStr,
20583
+ endDateStr
20584
+ );
20585
+ });
20586
+ }
20587
+
20588
+ // src/components/temperature/TemperatureComparisonModal.ts
20589
+ async function openTemperatureComparisonModal(params) {
20590
+ const modalId = `myio-temp-comparison-modal-${Date.now()}`;
20591
+ const defaultDateRange = getTodaySoFar();
20592
+ const startTs = params.startDate ? new Date(params.startDate).getTime() : defaultDateRange.startTs;
20593
+ const endTs = params.endDate ? new Date(params.endDate).getTime() : defaultDateRange.endTs;
20594
+ const state = {
20595
+ token: params.token,
20596
+ devices: params.devices,
20597
+ startTs,
20598
+ endTs,
20599
+ granularity: params.granularity || "hour",
20600
+ theme: params.theme || "dark",
20601
+ clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
20602
+ locale: params.locale || "pt-BR",
20603
+ deviceData: [],
20604
+ isLoading: true,
20605
+ dateRangePicker: null,
20606
+ selectedPeriods: ["madrugada", "manha", "tarde", "noite"]
20607
+ // All periods selected by default
20608
+ };
20609
+ const savedGranularity = localStorage.getItem("myio-temp-comparison-granularity");
20610
+ const savedTheme = localStorage.getItem("myio-temp-comparison-theme");
20611
+ if (savedGranularity) state.granularity = savedGranularity;
20612
+ if (savedTheme) state.theme = savedTheme;
20613
+ const modalContainer = document.createElement("div");
20614
+ modalContainer.id = modalId;
20615
+ document.body.appendChild(modalContainer);
20616
+ renderModal2(modalContainer, state, modalId);
20617
+ await fetchAllDevicesData(state);
20618
+ renderModal2(modalContainer, state, modalId);
20619
+ drawComparisonChart(modalId, state);
20620
+ await setupEventListeners2(modalContainer, state, modalId, params.onClose);
20621
+ return {
20622
+ destroy: () => {
20623
+ modalContainer.remove();
20624
+ params.onClose?.();
20625
+ },
20626
+ updateData: async (startDate, endDate, granularity) => {
20627
+ state.startTs = new Date(startDate).getTime();
20628
+ state.endTs = new Date(endDate).getTime();
20629
+ if (granularity) state.granularity = granularity;
20630
+ state.isLoading = true;
20631
+ renderModal2(modalContainer, state, modalId);
20632
+ await fetchAllDevicesData(state);
20633
+ renderModal2(modalContainer, state, modalId);
20634
+ drawComparisonChart(modalId, state);
20635
+ setupEventListeners2(modalContainer, state, modalId, params.onClose);
20636
+ }
20637
+ };
20638
+ }
20639
+ async function fetchAllDevicesData(state) {
20640
+ state.isLoading = true;
20641
+ state.deviceData = [];
20642
+ try {
20643
+ const results = await Promise.all(
20644
+ state.devices.map(async (device, index) => {
20645
+ const deviceId = device.tbId || device.id;
20646
+ try {
20647
+ const data = await fetchTemperatureData(state.token, deviceId, state.startTs, state.endTs);
20648
+ const stats = calculateStats(data, state.clampRange);
20649
+ return {
20650
+ device,
20651
+ data,
20652
+ stats,
20653
+ color: CHART_COLORS[index % CHART_COLORS.length]
20654
+ };
20655
+ } catch (error) {
20656
+ console.error(`[TemperatureComparisonModal] Error fetching data for ${device.label}:`, error);
20657
+ return {
20658
+ device,
20659
+ data: [],
20660
+ stats: { avg: 0, min: 0, max: 0, count: 0 },
20661
+ color: CHART_COLORS[index % CHART_COLORS.length]
20662
+ };
20663
+ }
20664
+ })
20665
+ );
20666
+ state.deviceData = results;
20667
+ } catch (error) {
20668
+ console.error("[TemperatureComparisonModal] Error fetching data:", error);
20669
+ }
20670
+ state.isLoading = false;
20671
+ }
20672
+ function renderModal2(container, state, modalId) {
20673
+ const colors = getThemeColors(state.theme);
20674
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale);
20675
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale);
20676
+ const startDateInput = new Date(state.startTs).toISOString().slice(0, 16);
20677
+ const endDateInput = new Date(state.endTs).toISOString().slice(0, 16);
20678
+ const legendHTML = state.deviceData.map((dd) => `
20679
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px;
20680
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)"};
20681
+ border-radius: 8px;">
20682
+ <span style="width: 12px; height: 12px; border-radius: 50%; background: ${dd.color};"></span>
20683
+ <span style="color: ${colors.text}; font-size: 13px;">${dd.device.label}</span>
20684
+ <span style="color: ${colors.textMuted}; font-size: 11px; margin-left: auto;">
20685
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.avg) : "N/A"}
20686
+ </span>
20687
+ </div>
20688
+ `).join("");
20689
+ const statsHTML = state.deviceData.map((dd) => `
20690
+ <div style="
20691
+ padding: 12px; background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#fafafa"};
20692
+ border-radius: 10px; border-left: 4px solid ${dd.color};
20693
+ min-width: 150px;
20694
+ ">
20695
+ <div style="font-weight: 600; color: ${colors.text}; font-size: 13px; margin-bottom: 8px;">
20696
+ ${dd.device.label}
20697
+ </div>
20698
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; font-size: 11px;">
20699
+ <span style="color: ${colors.textMuted};">M\xE9dia:</span>
20700
+ <span style="color: ${colors.text}; font-weight: 500;">
20701
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.avg) : "N/A"}
20702
+ </span>
20703
+ <span style="color: ${colors.textMuted};">Min:</span>
20704
+ <span style="color: ${colors.text};">
20705
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.min) : "N/A"}
20706
+ </span>
20707
+ <span style="color: ${colors.textMuted};">Max:</span>
20708
+ <span style="color: ${colors.text};">
20709
+ ${dd.stats.count > 0 ? formatTemperature(dd.stats.max) : "N/A"}
20710
+ </span>
20711
+ <span style="color: ${colors.textMuted};">Leituras:</span>
20712
+ <span style="color: ${colors.text};">${dd.stats.count}</span>
20713
+ </div>
20714
+ </div>
20715
+ `).join("");
20716
+ const isMaximized = container.__isMaximized || false;
20717
+ const contentMaxWidth = isMaximized ? "100%" : "1100px";
20718
+ const contentMaxHeight = isMaximized ? "100vh" : "95vh";
20719
+ const contentBorderRadius = isMaximized ? "0" : "10px";
20720
+ container.innerHTML = `
20721
+ <div class="myio-temp-comparison-overlay" style="
20722
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
20723
+ background: rgba(0, 0, 0, 0.5); z-index: 9998;
20724
+ display: flex; justify-content: center; align-items: center;
20725
+ backdrop-filter: blur(2px);
20726
+ ">
20727
+ <div class="myio-temp-comparison-content" style="
20728
+ background: ${colors.surface}; border-radius: ${contentBorderRadius};
20729
+ max-width: ${contentMaxWidth}; width: ${isMaximized ? "100%" : "95%"};
20730
+ max-height: ${contentMaxHeight}; height: ${isMaximized ? "100%" : "auto"};
20731
+ overflow: hidden; display: flex; flex-direction: column;
20732
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20733
+ font-family: 'Roboto', Arial, sans-serif;
20734
+ ">
20735
+ <!-- Header - MyIO Premium Style -->
20736
+ <div style="
20737
+ padding: 4px 8px; display: flex; align-items: center; justify-content: space-between;
20738
+ background: #3e1a7d; color: white; border-radius: ${isMaximized ? "0" : "10px 10px 0 0"};
20739
+ min-height: 20px;
20740
+ ">
20741
+ <h2 style="margin: 6px; font-size: 18px; font-weight: 600; color: white; line-height: 2;">
20742
+ \u{1F321}\uFE0F Compara\xE7\xE3o de Temperatura - ${state.devices.length} sensores
20743
+ </h2>
20744
+ <div style="display: flex; gap: 4px; align-items: center;">
20745
+ <!-- Theme Toggle -->
20746
+ <button id="${modalId}-theme-toggle" title="Alternar tema" style="
20747
+ background: none; border: none; font-size: 16px; cursor: pointer;
20748
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
20749
+ transition: background-color 0.2s;
20750
+ ">${state.theme === "dark" ? "\u2600\uFE0F" : "\u{1F319}"}</button>
20751
+ <!-- Maximize Button -->
20752
+ <button id="${modalId}-maximize" title="${isMaximized ? "Restaurar" : "Maximizar"}" style="
20753
+ background: none; border: none; font-size: 16px; cursor: pointer;
20754
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
20755
+ transition: background-color 0.2s;
20756
+ ">${isMaximized ? "\u{1F5D7}" : "\u{1F5D6}"}</button>
20757
+ <!-- Close Button -->
20758
+ <button id="${modalId}-close" title="Fechar" style="
20759
+ background: none; border: none; font-size: 20px; cursor: pointer;
20760
+ padding: 4px 8px; border-radius: 6px; color: rgba(255,255,255,0.8);
20761
+ transition: background-color 0.2s;
20762
+ ">\xD7</button>
20763
+ </div>
20764
+ </div>
20765
+
20766
+ <!-- Body -->
20767
+ <div style="flex: 1; overflow-y: auto; padding: 16px;">
20768
+
20769
+ <!-- Controls Row -->
20770
+ <div style="
20771
+ display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end;
20772
+ margin-bottom: 16px; padding: 16px;
20773
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.05)" : "#f7f7f7"};
20774
+ border-radius: 6px; border: 1px solid ${colors.border};
20775
+ ">
20776
+ <!-- Granularity Select -->
20777
+ <div>
20778
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20779
+ Granularidade
20780
+ </label>
20781
+ <select id="${modalId}-granularity" style="
20782
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20783
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20784
+ cursor: pointer; min-width: 130px;
20785
+ ">
20786
+ <option value="hour" ${state.granularity === "hour" ? "selected" : ""}>Hora (30 min)</option>
20787
+ <option value="day" ${state.granularity === "day" ? "selected" : ""}>Dia (m\xE9dia)</option>
20788
+ </select>
20789
+ </div>
20790
+ <!-- Day Period Filter (Multiselect) -->
20791
+ <div style="position: relative;">
20792
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20793
+ Per\xEDodos do Dia
20794
+ </label>
20795
+ <button id="${modalId}-period-btn" type="button" style="
20796
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20797
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20798
+ cursor: pointer; min-width: 180px; text-align: left;
20799
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
20800
+ ">
20801
+ <span>${getSelectedPeriodsLabel(state.selectedPeriods)}</span>
20802
+ <span style="font-size: 10px;">\u25BC</span>
20803
+ </button>
20804
+ <div id="${modalId}-period-dropdown" style="
20805
+ display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
20806
+ background: ${colors.surface}; border: 1px solid ${colors.border};
20807
+ border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
20808
+ min-width: 200px; margin-top: 4px; padding: 8px 0;
20809
+ ">
20810
+ ${DAY_PERIODS.map((period) => `
20811
+ <label style="
20812
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
20813
+ cursor: pointer; font-size: 13px; color: ${colors.text};
20814
+ " onmouseover="this.style.background='${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"}'"
20815
+ onmouseout="this.style.background='transparent'">
20816
+ <input type="checkbox"
20817
+ name="${modalId}-period"
20818
+ value="${period.id}"
20819
+ ${state.selectedPeriods.includes(period.id) ? "checked" : ""}
20820
+ style="width: 16px; height: 16px; cursor: pointer; accent-color: #3e1a7d;">
20821
+ ${period.label}
20822
+ </label>
20823
+ `).join("")}
20824
+ <div style="border-top: 1px solid ${colors.border}; margin-top: 8px; padding-top: 8px;">
20825
+ <button id="${modalId}-period-select-all" type="button" style="
20826
+ width: calc(100% - 16px); margin: 0 8px 4px; padding: 6px;
20827
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
20828
+ border: none; border-radius: 4px; cursor: pointer;
20829
+ font-size: 12px; color: ${colors.text};
20830
+ ">Selecionar Todos</button>
20831
+ <button id="${modalId}-period-clear" type="button" style="
20832
+ width: calc(100% - 16px); margin: 0 8px; padding: 6px;
20833
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f0f0f0"};
20834
+ border: none; border-radius: 4px; cursor: pointer;
20835
+ font-size: 12px; color: ${colors.text};
20836
+ ">Limpar Sele\xE7\xE3o</button>
20837
+ </div>
20838
+ </div>
20839
+ </div>
20840
+ <!-- Date Range Picker -->
20841
+ <div style="flex: 1; min-width: 220px;">
20842
+ <label style="color: ${colors.textMuted}; font-size: 12px; font-weight: 500; display: block; margin-bottom: 4px;">
20843
+ Per\xEDodo
20844
+ </label>
20845
+ <input type="text" id="${modalId}-date-range" readonly placeholder="Selecione o per\xEDodo..." style="
20846
+ padding: 8px 12px; border: 1px solid ${colors.border}; border-radius: 6px;
20847
+ font-size: 14px; color: ${colors.text}; background: ${colors.surface};
20848
+ width: 100%; cursor: pointer; box-sizing: border-box;
20849
+ "/>
20850
+ </div>
20851
+ <!-- Query Button -->
20852
+ <button id="${modalId}-query" style="
20853
+ background: #3e1a7d; color: white; border: none;
20854
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20855
+ font-size: 14px; font-weight: 500; height: 38px;
20856
+ display: flex; align-items: center; gap: 8px;
20857
+ font-family: 'Roboto', Arial, sans-serif;
20858
+ " ${state.isLoading ? "disabled" : ""}>
20859
+ ${state.isLoading ? '<span style="animation: spin 1s linear infinite; display: inline-block;">\u21BB</span> Carregando...' : "Carregar"}
20860
+ </button>
20861
+ </div>
20862
+
20863
+ <!-- Legend -->
20864
+ <div style="
20865
+ display: flex; flex-wrap: wrap; gap: 10px;
20866
+ margin-bottom: 20px;
20867
+ ">
20868
+ ${legendHTML}
20869
+ </div>
20870
+
20871
+ <!-- Chart Container -->
20872
+ <div style="margin-bottom: 24px;">
20873
+ <div id="${modalId}-chart" style="
20874
+ height: 380px;
20875
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.03)" : "#fafafa"};
20876
+ border-radius: 14px; display: flex; justify-content: center; align-items: center;
20877
+ border: 1px solid ${colors.border}; position: relative;
20878
+ ">
20879
+ ${state.isLoading ? `<div style="text-align: center; color: ${colors.textMuted};">
20880
+ <div style="animation: spin 1s linear infinite; font-size: 36px; margin-bottom: 12px;">\u21BB</div>
20881
+ <div style="font-size: 15px;">Carregando dados de ${state.devices.length} sensores...</div>
20882
+ </div>` : state.deviceData.every((dd) => dd.data.length === 0) ? `<div style="text-align: center; color: ${colors.textMuted};">
20883
+ <div style="font-size: 48px; margin-bottom: 12px;">\u{1F4ED}</div>
20884
+ <div style="font-size: 16px;">Sem dados para o per\xEDodo selecionado</div>
20885
+ </div>` : `<canvas id="${modalId}-canvas" style="width: 100%; height: 100%;"></canvas>`}
20886
+ </div>
20887
+ </div>
20888
+
20889
+ <!-- Stats Cards -->
20890
+ <div style="
20891
+ display: flex; flex-wrap: wrap; gap: 12px;
20892
+ margin-bottom: 24px;
20893
+ ">
20894
+ ${statsHTML}
20895
+ </div>
20896
+
20897
+ <!-- Actions -->
20898
+ <div style="display: flex; justify-content: flex-end; gap: 12px;">
20899
+ <button id="${modalId}-export" style="
20900
+ background: ${state.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f7f7f7"};
20901
+ color: ${colors.text}; border: 1px solid ${colors.border};
20902
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20903
+ font-size: 14px; display: flex; align-items: center; gap: 8px;
20904
+ font-family: 'Roboto', Arial, sans-serif;
20905
+ " ${state.deviceData.every((dd) => dd.data.length === 0) ? "disabled" : ""}>
20906
+ \u{1F4E5} Exportar CSV
20907
+ </button>
20908
+ <button id="${modalId}-close-btn" style="
20909
+ background: #3e1a7d; color: white; border: none;
20910
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
20911
+ font-size: 14px; font-weight: 500;
20912
+ font-family: 'Roboto', Arial, sans-serif;
20913
+ ">
20914
+ Fechar
20915
+ </button>
20916
+ </div>
20917
+ </div><!-- End Body -->
20918
+ </div>
20919
+ </div>
20920
+ <style>
20921
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
20922
+ #${modalId} select:focus, #${modalId} input:focus {
20923
+ outline: 2px solid #3e1a7d;
20924
+ outline-offset: 2px;
20925
+ }
20926
+ #${modalId} button:hover:not(:disabled) {
20927
+ opacity: 0.9;
20928
+ }
20929
+ #${modalId} button:disabled {
20930
+ opacity: 0.5;
20931
+ cursor: not-allowed;
20932
+ }
20933
+ #${modalId} .myio-temp-comparison-content > div:first-child button:hover {
20934
+ background: rgba(255, 255, 255, 0.1) !important;
20935
+ color: white !important;
20936
+ }
20937
+ </style>
20938
+ `;
20939
+ }
20940
+ function drawComparisonChart(modalId, state) {
20941
+ const chartContainer = document.getElementById(`${modalId}-chart`);
20942
+ const canvas = document.getElementById(`${modalId}-canvas`);
20943
+ if (!chartContainer || !canvas) return;
20944
+ const hasData = state.deviceData.some((dd) => dd.data.length > 0);
20945
+ if (!hasData) return;
20946
+ const ctx = canvas.getContext("2d");
20947
+ if (!ctx) return;
20948
+ const colors = getThemeColors(state.theme);
20949
+ const width = chartContainer.clientWidth - 2;
20950
+ const height = 380;
20951
+ canvas.width = width;
20952
+ canvas.height = height;
20953
+ const paddingLeft = 65;
20954
+ const paddingRight = 25;
20955
+ const paddingTop = 25;
20956
+ const paddingBottom = 55;
20957
+ ctx.clearRect(0, 0, width, height);
20958
+ const processedData = [];
20959
+ state.deviceData.forEach((dd) => {
20960
+ if (dd.data.length === 0) return;
20961
+ const filteredData = filterByDayPeriods(dd.data, state.selectedPeriods);
20962
+ if (filteredData.length === 0) return;
20963
+ let points;
20964
+ if (state.granularity === "hour") {
20965
+ const interpolated = interpolateTemperature(filteredData, {
20966
+ intervalMinutes: 30,
20967
+ startTs: state.startTs,
20968
+ endTs: state.endTs,
20969
+ clampRange: state.clampRange
20970
+ });
20971
+ const filteredInterpolated = filterByDayPeriods(interpolated, state.selectedPeriods);
20972
+ points = filteredInterpolated.map((item) => ({
20973
+ x: item.ts,
20974
+ y: Number(item.value),
20975
+ screenX: 0,
20976
+ screenY: 0,
20977
+ deviceLabel: dd.device.label,
20978
+ deviceColor: dd.color
20979
+ }));
20980
+ } else {
20981
+ const daily = aggregateByDay(filteredData, state.clampRange);
20982
+ points = daily.map((item) => ({
20983
+ x: item.dateTs,
20984
+ y: item.avg,
20985
+ screenX: 0,
20986
+ screenY: 0,
20987
+ deviceLabel: dd.device.label,
20988
+ deviceColor: dd.color
20989
+ }));
20990
+ }
20991
+ if (points.length > 0) {
20992
+ processedData.push({ device: dd, points });
20993
+ }
20994
+ });
20995
+ if (processedData.length === 0) {
20996
+ ctx.fillStyle = colors.textMuted;
20997
+ ctx.font = "14px Roboto, Arial, sans-serif";
20998
+ ctx.textAlign = "center";
20999
+ ctx.fillText("Nenhum dado para os per\xEDodos selecionados", width / 2, height / 2);
21000
+ return;
21001
+ }
21002
+ const isPeriodsFiltered = state.selectedPeriods.length < 4 && state.selectedPeriods.length > 0;
21003
+ let globalMinY = Infinity;
21004
+ let globalMaxY = -Infinity;
21005
+ processedData.forEach(({ points }) => {
21006
+ points.forEach((point) => {
21007
+ if (point.y < globalMinY) globalMinY = point.y;
21008
+ if (point.y > globalMaxY) globalMaxY = point.y;
21009
+ });
21010
+ });
21011
+ globalMinY = Math.floor(globalMinY) - 1;
21012
+ globalMaxY = Math.ceil(globalMaxY) + 1;
21013
+ const chartWidth = width - paddingLeft - paddingRight;
21014
+ const chartHeight = height - paddingTop - paddingBottom;
21015
+ const scaleY = chartHeight / (globalMaxY - globalMinY || 1);
21016
+ if (isPeriodsFiltered) {
21017
+ const maxPoints = Math.max(...processedData.map(({ points }) => points.length));
21018
+ const pointSpacing = chartWidth / Math.max(1, maxPoints - 1);
21019
+ processedData.forEach(({ points }) => {
21020
+ points.forEach((point, index) => {
21021
+ point.screenX = paddingLeft + index * pointSpacing;
21022
+ point.screenY = height - paddingBottom - (point.y - globalMinY) * scaleY;
21023
+ });
21024
+ });
21025
+ } else {
21026
+ let globalMinX = Infinity;
21027
+ let globalMaxX = -Infinity;
21028
+ processedData.forEach(({ points }) => {
21029
+ points.forEach((point) => {
21030
+ if (point.x < globalMinX) globalMinX = point.x;
21031
+ if (point.x > globalMaxX) globalMaxX = point.x;
21032
+ });
21033
+ });
21034
+ const timeRange = globalMaxX - globalMinX || 1;
21035
+ const scaleX = chartWidth / timeRange;
21036
+ processedData.forEach(({ points }) => {
21037
+ points.forEach((point) => {
21038
+ point.screenX = paddingLeft + (point.x - globalMinX) * scaleX;
21039
+ point.screenY = height - paddingBottom - (point.y - globalMinY) * scaleY;
21040
+ });
21041
+ });
21042
+ }
21043
+ ctx.strokeStyle = colors.chartGrid;
21044
+ ctx.lineWidth = 1;
21045
+ for (let i = 0; i <= 5; i++) {
21046
+ const y = paddingTop + chartHeight * i / 5;
21047
+ ctx.beginPath();
21048
+ ctx.moveTo(paddingLeft, y);
21049
+ ctx.lineTo(width - paddingRight, y);
21050
+ ctx.stroke();
21051
+ }
21052
+ processedData.forEach(({ device, points }) => {
21053
+ ctx.strokeStyle = device.color;
21054
+ ctx.lineWidth = 2.5;
21055
+ ctx.beginPath();
21056
+ points.forEach((point, i) => {
21057
+ if (i === 0) ctx.moveTo(point.screenX, point.screenY);
21058
+ else ctx.lineTo(point.screenX, point.screenY);
21059
+ });
21060
+ ctx.stroke();
21061
+ ctx.fillStyle = device.color;
21062
+ points.forEach((point) => {
21063
+ ctx.beginPath();
21064
+ ctx.arc(point.screenX, point.screenY, 4, 0, Math.PI * 2);
21065
+ ctx.fill();
21066
+ });
21067
+ });
21068
+ ctx.fillStyle = colors.textMuted;
21069
+ ctx.font = "12px system-ui, sans-serif";
21070
+ ctx.textAlign = "right";
21071
+ for (let i = 0; i <= 5; i++) {
21072
+ const val = globalMinY + (globalMaxY - globalMinY) * (5 - i) / 5;
21073
+ const y = paddingTop + chartHeight * i / 5;
21074
+ ctx.fillText(val.toFixed(1) + "\xB0C", paddingLeft - 10, y + 4);
21075
+ }
21076
+ ctx.textAlign = "center";
21077
+ const xAxisPoints = processedData[0]?.points || [];
21078
+ const numLabels = Math.min(8, xAxisPoints.length);
21079
+ const labelInterval = Math.max(1, Math.floor(xAxisPoints.length / numLabels));
21080
+ for (let i = 0; i < xAxisPoints.length; i += labelInterval) {
21081
+ const point = xAxisPoints[i];
21082
+ const date = new Date(point.x);
21083
+ let label;
21084
+ if (state.granularity === "hour") {
21085
+ label = date.toLocaleTimeString(state.locale, { hour: "2-digit", minute: "2-digit" });
21086
+ } else {
21087
+ label = date.toLocaleDateString(state.locale, { day: "2-digit", month: "2-digit" });
21088
+ }
21089
+ ctx.strokeStyle = colors.chartGrid;
21090
+ ctx.lineWidth = 1;
21091
+ ctx.beginPath();
21092
+ ctx.moveTo(point.screenX, paddingTop);
21093
+ ctx.lineTo(point.screenX, height - paddingBottom);
21094
+ ctx.stroke();
21095
+ ctx.fillStyle = colors.textMuted;
21096
+ ctx.fillText(label, point.screenX, height - paddingBottom + 18);
21097
+ }
21098
+ ctx.strokeStyle = colors.border;
21099
+ ctx.lineWidth = 1;
21100
+ ctx.beginPath();
21101
+ ctx.moveTo(paddingLeft, paddingTop);
21102
+ ctx.lineTo(paddingLeft, height - paddingBottom);
21103
+ ctx.lineTo(width - paddingRight, height - paddingBottom);
21104
+ ctx.stroke();
21105
+ const allChartPoints = processedData.flatMap((pd) => pd.points);
21106
+ setupComparisonChartTooltip(canvas, chartContainer, allChartPoints, state, colors);
21107
+ }
21108
+ function setupComparisonChartTooltip(canvas, container, chartData, state, colors) {
21109
+ const existingTooltip = container.querySelector(".myio-chart-tooltip");
21110
+ if (existingTooltip) existingTooltip.remove();
21111
+ const tooltip = document.createElement("div");
21112
+ tooltip.className = "myio-chart-tooltip";
21113
+ tooltip.style.cssText = `
21114
+ position: absolute;
21115
+ background: ${state.theme === "dark" ? "rgba(30, 30, 40, 0.95)" : "rgba(255, 255, 255, 0.98)"};
21116
+ border: 1px solid ${colors.border};
21117
+ border-radius: 8px;
21118
+ padding: 10px 14px;
21119
+ font-size: 13px;
21120
+ color: ${colors.text};
21121
+ pointer-events: none;
21122
+ opacity: 0;
21123
+ transition: opacity 0.15s;
21124
+ z-index: 1000;
21125
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
21126
+ min-width: 160px;
21127
+ `;
21128
+ container.appendChild(tooltip);
21129
+ const findNearestPoint = (mouseX, mouseY) => {
21130
+ const threshold = 20;
21131
+ let nearest = null;
21132
+ let minDist = Infinity;
21133
+ for (const point of chartData) {
21134
+ const dist = Math.sqrt(
21135
+ Math.pow(mouseX - point.screenX, 2) + Math.pow(mouseY - point.screenY, 2)
21136
+ );
21137
+ if (dist < minDist && dist < threshold) {
21138
+ minDist = dist;
21139
+ nearest = point;
21140
+ }
21141
+ }
21142
+ return nearest;
21143
+ };
21144
+ canvas.addEventListener("mousemove", (e) => {
21145
+ const rect = canvas.getBoundingClientRect();
21146
+ const mouseX = e.clientX - rect.left;
21147
+ const mouseY = e.clientY - rect.top;
21148
+ const point = findNearestPoint(mouseX, mouseY);
21149
+ if (point) {
21150
+ const date = new Date(point.x);
21151
+ let dateStr;
21152
+ if (state.granularity === "hour") {
21153
+ dateStr = date.toLocaleDateString(state.locale, {
21154
+ day: "2-digit",
21155
+ month: "2-digit",
21156
+ year: "numeric"
21157
+ }) + " " + date.toLocaleTimeString(state.locale, {
21158
+ hour: "2-digit",
21159
+ minute: "2-digit"
21160
+ });
21161
+ } else {
21162
+ dateStr = date.toLocaleDateString(state.locale, {
21163
+ day: "2-digit",
21164
+ month: "2-digit",
21165
+ year: "numeric"
21166
+ });
21167
+ }
21168
+ tooltip.innerHTML = `
21169
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
21170
+ <span style="width: 10px; height: 10px; border-radius: 50%; background: ${point.deviceColor};"></span>
21171
+ <span style="font-weight: 600;">${point.deviceLabel}</span>
21172
+ </div>
21173
+ <div style="font-weight: 600; font-size: 16px; color: ${point.deviceColor}; margin-bottom: 4px;">
21174
+ ${formatTemperature(point.y)}
21175
+ </div>
21176
+ <div style="font-size: 11px; color: ${colors.textMuted};">
21177
+ \u{1F4C5} ${dateStr}
21178
+ </div>
21179
+ `;
21180
+ let tooltipX = point.screenX + 15;
21181
+ let tooltipY = point.screenY - 15;
21182
+ const tooltipRect = tooltip.getBoundingClientRect();
21183
+ const containerRect = container.getBoundingClientRect();
21184
+ if (tooltipX + tooltipRect.width > containerRect.width - 10) {
21185
+ tooltipX = point.screenX - tooltipRect.width - 15;
21186
+ }
21187
+ if (tooltipY < 10) {
21188
+ tooltipY = point.screenY + 15;
21189
+ }
21190
+ tooltip.style.left = `${tooltipX}px`;
21191
+ tooltip.style.top = `${tooltipY}px`;
21192
+ tooltip.style.opacity = "1";
21193
+ canvas.style.cursor = "pointer";
21194
+ } else {
21195
+ tooltip.style.opacity = "0";
21196
+ canvas.style.cursor = "default";
21197
+ }
21198
+ });
21199
+ canvas.addEventListener("mouseleave", () => {
21200
+ tooltip.style.opacity = "0";
21201
+ canvas.style.cursor = "default";
21202
+ });
21203
+ }
21204
+ async function setupEventListeners2(container, state, modalId, onClose) {
21205
+ const closeModal = () => {
21206
+ container.remove();
21207
+ onClose?.();
21208
+ };
21209
+ container.querySelector(".myio-temp-comparison-overlay")?.addEventListener("click", (e) => {
21210
+ if (e.target === e.currentTarget) closeModal();
21211
+ });
21212
+ document.getElementById(`${modalId}-close`)?.addEventListener("click", closeModal);
21213
+ document.getElementById(`${modalId}-close-btn`)?.addEventListener("click", closeModal);
21214
+ const dateRangeInput = document.getElementById(`${modalId}-date-range`);
21215
+ if (dateRangeInput && !state.dateRangePicker) {
21216
+ try {
21217
+ state.dateRangePicker = await createDateRangePicker2(dateRangeInput, {
21218
+ presetStart: new Date(state.startTs).toISOString(),
21219
+ presetEnd: new Date(state.endTs).toISOString(),
21220
+ includeTime: true,
21221
+ timePrecision: "minute",
21222
+ maxRangeDays: 90,
21223
+ locale: state.locale,
21224
+ parentEl: container.querySelector(".myio-temp-comparison-content"),
21225
+ onApply: (result) => {
21226
+ state.startTs = new Date(result.startISO).getTime();
21227
+ state.endTs = new Date(result.endISO).getTime();
21228
+ console.log("[TemperatureComparisonModal] Date range applied:", result);
21229
+ }
21230
+ });
21231
+ } catch (error) {
21232
+ console.warn("[TemperatureComparisonModal] DateRangePicker initialization failed:", error);
21233
+ }
21234
+ }
21235
+ document.getElementById(`${modalId}-theme-toggle`)?.addEventListener("click", async () => {
21236
+ state.theme = state.theme === "dark" ? "light" : "dark";
21237
+ localStorage.setItem("myio-temp-comparison-theme", state.theme);
21238
+ state.dateRangePicker = null;
21239
+ renderModal2(container, state, modalId);
21240
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21241
+ drawComparisonChart(modalId, state);
21242
+ }
21243
+ await setupEventListeners2(container, state, modalId, onClose);
21244
+ });
21245
+ document.getElementById(`${modalId}-maximize`)?.addEventListener("click", async () => {
21246
+ container.__isMaximized = !container.__isMaximized;
21247
+ state.dateRangePicker = null;
21248
+ renderModal2(container, state, modalId);
21249
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21250
+ drawComparisonChart(modalId, state);
21251
+ }
21252
+ await setupEventListeners2(container, state, modalId, onClose);
21253
+ });
21254
+ const periodBtn = document.getElementById(`${modalId}-period-btn`);
21255
+ const periodDropdown = document.getElementById(`${modalId}-period-dropdown`);
21256
+ periodBtn?.addEventListener("click", (e) => {
21257
+ e.stopPropagation();
21258
+ if (periodDropdown) {
21259
+ periodDropdown.style.display = periodDropdown.style.display === "none" ? "block" : "none";
21260
+ }
21261
+ });
21262
+ document.addEventListener("click", (e) => {
21263
+ if (periodDropdown && !periodDropdown.contains(e.target) && e.target !== periodBtn) {
21264
+ periodDropdown.style.display = "none";
21265
+ }
21266
+ });
21267
+ const periodCheckboxes = document.querySelectorAll(`input[name="${modalId}-period"]`);
21268
+ periodCheckboxes.forEach((checkbox) => {
21269
+ checkbox.addEventListener("change", () => {
21270
+ const checked = Array.from(periodCheckboxes).filter((cb) => cb.checked).map((cb) => cb.value);
21271
+ state.selectedPeriods = checked;
21272
+ const btnLabel = periodBtn?.querySelector("span:first-child");
21273
+ if (btnLabel) {
21274
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
21275
+ }
21276
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21277
+ drawComparisonChart(modalId, state);
21278
+ }
21279
+ });
21280
+ });
21281
+ document.getElementById(`${modalId}-period-select-all`)?.addEventListener("click", () => {
21282
+ periodCheckboxes.forEach((cb) => {
21283
+ cb.checked = true;
21284
+ });
21285
+ state.selectedPeriods = ["madrugada", "manha", "tarde", "noite"];
21286
+ const btnLabel = periodBtn?.querySelector("span:first-child");
21287
+ if (btnLabel) {
21288
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
21289
+ }
21290
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21291
+ drawComparisonChart(modalId, state);
21292
+ }
21293
+ });
21294
+ document.getElementById(`${modalId}-period-clear`)?.addEventListener("click", () => {
21295
+ periodCheckboxes.forEach((cb) => {
21296
+ cb.checked = false;
21297
+ });
21298
+ state.selectedPeriods = [];
21299
+ const btnLabel = periodBtn?.querySelector("span:first-child");
21300
+ if (btnLabel) {
21301
+ btnLabel.textContent = getSelectedPeriodsLabel(state.selectedPeriods);
21302
+ }
21303
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21304
+ drawComparisonChart(modalId, state);
21305
+ }
21306
+ });
21307
+ document.getElementById(`${modalId}-granularity`)?.addEventListener("change", (e) => {
21308
+ state.granularity = e.target.value;
21309
+ localStorage.setItem("myio-temp-comparison-granularity", state.granularity);
21310
+ if (state.deviceData.some((dd) => dd.data.length > 0)) {
21311
+ drawComparisonChart(modalId, state);
21312
+ }
21313
+ });
21314
+ document.getElementById(`${modalId}-query`)?.addEventListener("click", async () => {
21315
+ if (state.startTs >= state.endTs) {
21316
+ alert("Por favor, selecione um per\xEDodo v\xE1lido");
21317
+ return;
21318
+ }
21319
+ state.isLoading = true;
21320
+ state.dateRangePicker = null;
21321
+ renderModal2(container, state, modalId);
21322
+ await fetchAllDevicesData(state);
21323
+ renderModal2(container, state, modalId);
21324
+ drawComparisonChart(modalId, state);
21325
+ await setupEventListeners2(container, state, modalId, onClose);
21326
+ });
21327
+ document.getElementById(`${modalId}-export`)?.addEventListener("click", () => {
21328
+ if (state.deviceData.every((dd) => dd.data.length === 0)) return;
21329
+ exportComparisonCSV(state);
21330
+ });
21331
+ }
21332
+ function exportComparisonCSV(state) {
21333
+ const startDateStr = new Date(state.startTs).toLocaleDateString(state.locale).replace(/\//g, "-");
21334
+ const endDateStr = new Date(state.endTs).toLocaleDateString(state.locale).replace(/\//g, "-");
21335
+ const BOM = "\uFEFF";
21336
+ let csvContent = BOM;
21337
+ csvContent += `Compara\xE7\xE3o de Temperatura
21338
+ `;
21339
+ csvContent += `Per\xEDodo: ${startDateStr} at\xE9 ${endDateStr}
21340
+ `;
21341
+ csvContent += `Sensores: ${state.devices.map((d) => d.label).join(", ")}
21342
+ `;
21343
+ csvContent += "\n";
21344
+ csvContent += "Estat\xEDsticas por Sensor:\n";
21345
+ csvContent += "Sensor,M\xE9dia (\xB0C),Min (\xB0C),Max (\xB0C),Leituras\n";
21346
+ state.deviceData.forEach((dd) => {
21347
+ csvContent += `"${dd.device.label}",${dd.stats.avg.toFixed(2)},${dd.stats.min.toFixed(2)},${dd.stats.max.toFixed(2)},${dd.stats.count}
21348
+ `;
21349
+ });
21350
+ csvContent += "\n";
21351
+ csvContent += "Dados Detalhados:\n";
21352
+ csvContent += "Data/Hora,Sensor,Temperatura (\xB0C)\n";
21353
+ state.deviceData.forEach((dd) => {
21354
+ dd.data.forEach((item) => {
21355
+ const date = new Date(item.ts).toLocaleString(state.locale);
21356
+ const temp = Number(item.value).toFixed(2);
21357
+ csvContent += `"${date}","${dd.device.label}",${temp}
21358
+ `;
21359
+ });
21360
+ });
21361
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
21362
+ const url = URL.createObjectURL(blob);
21363
+ const link = document.createElement("a");
21364
+ link.href = url;
21365
+ link.download = `comparacao_temperatura_${startDateStr}_${endDateStr}.csv`;
21366
+ document.body.appendChild(link);
21367
+ link.click();
21368
+ document.body.removeChild(link);
21369
+ URL.revokeObjectURL(url);
21370
+ }
19641
21371
  // Annotate the CommonJS export names for ESM import in node:
19642
21372
  0 && (module.exports = {
21373
+ CHART_COLORS,
19643
21374
  ConnectionStatusType,
21375
+ DEFAULT_CLAMP_RANGE,
19644
21376
  DeviceStatusType,
19645
21377
  MyIOChartModal,
19646
21378
  MyIODraggableCard,
@@ -19649,6 +21381,7 @@ function openGoalsPanel(params) {
19649
21381
  MyIOToast,
19650
21382
  addDetectionContext,
19651
21383
  addNamespace,
21384
+ aggregateByDay,
19652
21385
  averageByDay,
19653
21386
  buildListItemsThingsboardByUniqueDatasource,
19654
21387
  buildMyioIngestionAuth,
@@ -19657,6 +21390,8 @@ function openGoalsPanel(params) {
19657
21390
  calcDeltaPercent,
19658
21391
  calculateDeviceStatus,
19659
21392
  calculateDeviceStatusWithRanges,
21393
+ calculateStats,
21394
+ clampTemperature,
19660
21395
  classify,
19661
21396
  classifyWaterLabel,
19662
21397
  classifyWaterLabels,
@@ -19669,9 +21404,11 @@ function openGoalsPanel(params) {
19669
21404
  detectDeviceType,
19670
21405
  determineInterval,
19671
21406
  deviceStatusIcons,
21407
+ exportTemperatureCSV,
19672
21408
  exportToCSV,
19673
21409
  exportToCSVAll,
19674
21410
  extractMyIOCredentials,
21411
+ fetchTemperatureData,
19675
21412
  fetchThingsboardCustomerAttrsFromStorage,
19676
21413
  fetchThingsboardCustomerServerScopeAttrs,
19677
21414
  findValue,
@@ -19685,6 +21422,7 @@ function openGoalsPanel(params) {
19685
21422
  formatEnergy,
19686
21423
  formatNumberReadable,
19687
21424
  formatTankHeadFromCm,
21425
+ formatTemperature,
19688
21426
  formatWaterByGroup,
19689
21427
  formatWaterVolumeM3,
19690
21428
  getAuthCacheStats,
@@ -19699,6 +21437,7 @@ function openGoalsPanel(params) {
19699
21437
  getValueByDatakeyLegacy,
19700
21438
  getWaterCategories,
19701
21439
  groupByDay,
21440
+ interpolateTemperature,
19702
21441
  isDeviceOffline,
19703
21442
  isValidConnectionStatus,
19704
21443
  isValidDeviceStatus,
@@ -19716,6 +21455,8 @@ function openGoalsPanel(params) {
19716
21455
  openDemandModal,
19717
21456
  openGoalsPanel,
19718
21457
  openRealTimeTelemetryModal,
21458
+ openTemperatureComparisonModal,
21459
+ openTemperatureModal,
19719
21460
  parseInputDateToDate,
19720
21461
  renderCardComponent,
19721
21462
  renderCardComponentEnhanced,