myio-js-library 0.1.139 → 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/README.md +180 -0
- package/dist/index.cjs +1748 -4
- package/dist/index.d.cts +167 -1
- package/dist/index.js +1737 -4
- package/dist/myio-js-library.umd.js +1737 -4
- package/dist/myio-js-library.umd.min.js +1 -1
- package/package.json +1 -1
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,
|
|
@@ -10944,8 +10955,11 @@ var EnergyModalView = class {
|
|
|
10944
10955
|
*/
|
|
10945
10956
|
reRenderChart() {
|
|
10946
10957
|
const mode = this.config.params.mode || "single";
|
|
10958
|
+
console.log("[EnergyModalView] reRenderChart called, mode:", mode, "barMode:", this.currentBarMode, "theme:", this.currentTheme);
|
|
10947
10959
|
if (mode === "comparison") {
|
|
10948
|
-
|
|
10960
|
+
console.log("[EnergyModalView] Calling renderComparisonChart...");
|
|
10961
|
+
const result = this.renderComparisonChart();
|
|
10962
|
+
console.log("[EnergyModalView] renderComparisonChart result:", result);
|
|
10949
10963
|
} else {
|
|
10950
10964
|
if (this.currentEnergyData) {
|
|
10951
10965
|
this.renderChart(this.currentEnergyData);
|
|
@@ -18241,13 +18255,13 @@ function openGoalsPanel(params) {
|
|
|
18241
18255
|
function initializeModal() {
|
|
18242
18256
|
if (data) {
|
|
18243
18257
|
modalState.goalsData = data;
|
|
18244
|
-
|
|
18258
|
+
renderModal3();
|
|
18245
18259
|
} else {
|
|
18246
|
-
|
|
18260
|
+
renderModal3();
|
|
18247
18261
|
loadGoalsData();
|
|
18248
18262
|
}
|
|
18249
18263
|
}
|
|
18250
|
-
function
|
|
18264
|
+
function renderModal3() {
|
|
18251
18265
|
const existing = document.getElementById("myio-goals-panel-modal");
|
|
18252
18266
|
if (existing) {
|
|
18253
18267
|
existing.remove();
|
|
@@ -19635,9 +19649,1730 @@ function openGoalsPanel(params) {
|
|
|
19635
19649
|
};
|
|
19636
19650
|
}
|
|
19637
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
|
+
}
|
|
19638
21371
|
// Annotate the CommonJS export names for ESM import in node:
|
|
19639
21372
|
0 && (module.exports = {
|
|
21373
|
+
CHART_COLORS,
|
|
19640
21374
|
ConnectionStatusType,
|
|
21375
|
+
DEFAULT_CLAMP_RANGE,
|
|
19641
21376
|
DeviceStatusType,
|
|
19642
21377
|
MyIOChartModal,
|
|
19643
21378
|
MyIODraggableCard,
|
|
@@ -19646,6 +21381,7 @@ function openGoalsPanel(params) {
|
|
|
19646
21381
|
MyIOToast,
|
|
19647
21382
|
addDetectionContext,
|
|
19648
21383
|
addNamespace,
|
|
21384
|
+
aggregateByDay,
|
|
19649
21385
|
averageByDay,
|
|
19650
21386
|
buildListItemsThingsboardByUniqueDatasource,
|
|
19651
21387
|
buildMyioIngestionAuth,
|
|
@@ -19654,6 +21390,8 @@ function openGoalsPanel(params) {
|
|
|
19654
21390
|
calcDeltaPercent,
|
|
19655
21391
|
calculateDeviceStatus,
|
|
19656
21392
|
calculateDeviceStatusWithRanges,
|
|
21393
|
+
calculateStats,
|
|
21394
|
+
clampTemperature,
|
|
19657
21395
|
classify,
|
|
19658
21396
|
classifyWaterLabel,
|
|
19659
21397
|
classifyWaterLabels,
|
|
@@ -19666,9 +21404,11 @@ function openGoalsPanel(params) {
|
|
|
19666
21404
|
detectDeviceType,
|
|
19667
21405
|
determineInterval,
|
|
19668
21406
|
deviceStatusIcons,
|
|
21407
|
+
exportTemperatureCSV,
|
|
19669
21408
|
exportToCSV,
|
|
19670
21409
|
exportToCSVAll,
|
|
19671
21410
|
extractMyIOCredentials,
|
|
21411
|
+
fetchTemperatureData,
|
|
19672
21412
|
fetchThingsboardCustomerAttrsFromStorage,
|
|
19673
21413
|
fetchThingsboardCustomerServerScopeAttrs,
|
|
19674
21414
|
findValue,
|
|
@@ -19682,6 +21422,7 @@ function openGoalsPanel(params) {
|
|
|
19682
21422
|
formatEnergy,
|
|
19683
21423
|
formatNumberReadable,
|
|
19684
21424
|
formatTankHeadFromCm,
|
|
21425
|
+
formatTemperature,
|
|
19685
21426
|
formatWaterByGroup,
|
|
19686
21427
|
formatWaterVolumeM3,
|
|
19687
21428
|
getAuthCacheStats,
|
|
@@ -19696,6 +21437,7 @@ function openGoalsPanel(params) {
|
|
|
19696
21437
|
getValueByDatakeyLegacy,
|
|
19697
21438
|
getWaterCategories,
|
|
19698
21439
|
groupByDay,
|
|
21440
|
+
interpolateTemperature,
|
|
19699
21441
|
isDeviceOffline,
|
|
19700
21442
|
isValidConnectionStatus,
|
|
19701
21443
|
isValidDeviceStatus,
|
|
@@ -19713,6 +21455,8 @@ function openGoalsPanel(params) {
|
|
|
19713
21455
|
openDemandModal,
|
|
19714
21456
|
openGoalsPanel,
|
|
19715
21457
|
openRealTimeTelemetryModal,
|
|
21458
|
+
openTemperatureComparisonModal,
|
|
21459
|
+
openTemperatureModal,
|
|
19716
21460
|
parseInputDateToDate,
|
|
19717
21461
|
renderCardComponent,
|
|
19718
21462
|
renderCardComponentEnhanced,
|