myio-js-library 0.1.547 → 0.1.549
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +499 -82
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +499 -82
- package/dist/myio-js-library.umd.js +499 -82
- package/dist/myio-js-library.umd.min.js +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -899,7 +899,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
899
899
|
// package.json
|
|
900
900
|
var package_default = {
|
|
901
901
|
name: "myio-js-library",
|
|
902
|
-
version: "0.1.
|
|
902
|
+
version: "0.1.549",
|
|
903
903
|
description: "A clean, standalone JS SDK for MYIO projects",
|
|
904
904
|
license: "MIT",
|
|
905
905
|
repository: "github:gh-myio/myio-js-library",
|
|
@@ -18649,6 +18649,12 @@ function renderCardComponentV5({
|
|
|
18649
18649
|
temperatureMax,
|
|
18650
18650
|
temperatureStatus,
|
|
18651
18651
|
// 'ok' | 'above' | 'below' | undefined
|
|
18652
|
+
// Temperatura atual vinda do Ingestion = média das últimas 2 h; true = nenhuma leitura na janela
|
|
18653
|
+
temperatureNoRecentReading = false,
|
|
18654
|
+
temperatureSource,
|
|
18655
|
+
// 'ingestion-avg-2h' | 'thingsboard' | undefined
|
|
18656
|
+
temperatureFetchedAt,
|
|
18657
|
+
// ms da consulta (rótulo "Atualizado às HH:MM")
|
|
18652
18658
|
// Per-device exclude_groups_totals attribute (SERVER_SCOPE) — drives the orange marker
|
|
18653
18659
|
excludeGroupsTotals
|
|
18654
18660
|
} = entityObject;
|
|
@@ -18808,7 +18814,14 @@ function renderCardComponentV5({
|
|
|
18808
18814
|
const percentageForDisplay = isTankDevice2 ? (waterPercentage || 0) * 100 : perc;
|
|
18809
18815
|
const tankLevelCm = isTankDevice2 ? Number(waterLevel ?? val) : null;
|
|
18810
18816
|
const _pctDecimals = resolvePercentDecimals(percentDecimals);
|
|
18817
|
+
const showNoTempReading = !!temperatureNoRecentReading && isTemperatureDevice2(deviceType);
|
|
18818
|
+
const tempValueTitle = (() => {
|
|
18819
|
+
if (!isTemperatureDevice2(deviceType) || temperatureSource !== "ingestion-avg-2h") return "";
|
|
18820
|
+
const at = temperatureFetchedAt ? ` \xB7 Atualizado \xE0s ${new Date(temperatureFetchedAt).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}` : "";
|
|
18821
|
+
return showNoTempReading ? `Nenhuma leitura nas \xFAltimas 2 h${at}` : `Temperatura (m\xE9dia das \xFAltimas 2 h)${at}`;
|
|
18822
|
+
})();
|
|
18811
18823
|
const formatCardValue = (value, deviceType2) => {
|
|
18824
|
+
if (showNoTempReading) return "Sem leitura recente";
|
|
18812
18825
|
if (isTankDevice2) {
|
|
18813
18826
|
return `${percentageForDisplay.toFixed(_pctDecimals).replace(".", ",")}%`;
|
|
18814
18827
|
}
|
|
@@ -19111,7 +19124,7 @@ function renderCardComponentV5({
|
|
|
19111
19124
|
}
|
|
19112
19125
|
return "ok";
|
|
19113
19126
|
};
|
|
19114
|
-
const tempStatus = isTermostatoDevice ? calculateTempStatus2() : null;
|
|
19127
|
+
const tempStatus = isTermostatoDevice && !showNoTempReading ? calculateTempStatus2() : null;
|
|
19115
19128
|
const deviceImageUrl = getDeviceImageUrl(deviceType, percentageForDisplay, {
|
|
19116
19129
|
tempStatus,
|
|
19117
19130
|
isOffline
|
|
@@ -19131,7 +19144,7 @@ function renderCardComponentV5({
|
|
|
19131
19144
|
}
|
|
19132
19145
|
return null;
|
|
19133
19146
|
};
|
|
19134
|
-
const tempDeviationPercent = isTermostatoDevice ? calculateTempDeviationPercent() : null;
|
|
19147
|
+
const tempDeviationPercent = isTermostatoDevice && !showNoTempReading ? calculateTempDeviationPercent() : null;
|
|
19135
19148
|
const cardHTML = `
|
|
19136
19149
|
<div class="device-card-centered clickable ${cardEntity.status === "offline" ? "offline" : ""}"
|
|
19137
19150
|
data-entity-id="${entityId}"
|
|
@@ -19176,7 +19189,7 @@ function renderCardComponentV5({
|
|
|
19176
19189
|
<span class="flash-icon ${shouldFlashIcon2 ? "flash" : ""}">
|
|
19177
19190
|
${icon2}
|
|
19178
19191
|
</span>
|
|
19179
|
-
<span class="consumption-value">${formatCardValue(cardEntity.lastValue, deviceType)}</span>
|
|
19192
|
+
<span class="consumption-value"${showNoTempReading ? ' style="color:#9ca3af;font-weight:500;font-size:0.85em;"' : ""}${tempValueTitle ? ` title="${tempValueTitle}"` : ""}>${formatCardValue(cardEntity.lastValue, deviceType)}</span>
|
|
19180
19193
|
</div>
|
|
19181
19194
|
</div>
|
|
19182
19195
|
${isTankDevice2 ? `<span class="device-percentage-badge device-tank-level-badge" style="position: absolute; bottom: 12px; right: 12px; z-index: 20; background: none !important;">${formatTankHeadFromCm(tankLevelCm)}</span>` : !isTermostatoDevice ? `<span class="device-percentage-badge percentage-tooltip-trigger" style="position: absolute; bottom: 12px; right: 12px; z-index: 20; background: none !important; cursor: help;">${percentageForDisplay.toFixed(_pctDecimals).replace(".", ",")}%</span>` : tempDeviationPercent ? `<span class="device-percentage-badge temp-deviation-badge temp-comparison-tooltip-trigger" style="position: absolute; bottom: 12px; right: 12px; z-index: 20; background: none !important; color: ${tempDeviationPercent.isAbove ? "#ef4444" : tempDeviationPercent.isBelow ? "#3b82f6" : "#6b7280"}; font-weight: 600; cursor: help;">${tempDeviationPercent.sign}${tempDeviationPercent.value.toFixed(1)}%</span>` : ""}
|
|
@@ -30329,8 +30342,8 @@ function normalizeToSaoPauloISO(dateLike, endOfDay = false) {
|
|
|
30329
30342
|
if (typeof dateLike === "string") {
|
|
30330
30343
|
ymd = dateLike.split("T")[0];
|
|
30331
30344
|
} else {
|
|
30332
|
-
const
|
|
30333
|
-
const d = new Date(dateLike.getTime() -
|
|
30345
|
+
const SP_OFFSET_MS2 = 3 * 60 * 60 * 1e3;
|
|
30346
|
+
const d = new Date(dateLike.getTime() - SP_OFFSET_MS2);
|
|
30334
30347
|
const y = d.getUTCFullYear();
|
|
30335
30348
|
const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
30336
30349
|
const da = String(d.getUTCDate()).padStart(2, "0");
|
|
@@ -33551,10 +33564,12 @@ function makeCols(unit, colOpts) {
|
|
|
33551
33564
|
{ key: "idx", label: "#", pdfW: 10 },
|
|
33552
33565
|
{ key: "nome", label: colOpts?.nameLabel || "Nome", pdfW: 100 },
|
|
33553
33566
|
{ key: "identificador", label: "Identificador", pdfW: 60 },
|
|
33554
|
-
{ key: "consumo", label: unit ? `Consumo (${unit})` : "Consumo", pdfW: 50 },
|
|
33567
|
+
{ key: "consumo", label: colOpts?.valueLabel || (unit ? `Consumo (${unit})` : "Consumo"), pdfW: 50 },
|
|
33555
33568
|
{ key: "perc", label: "%", pdfW: 20 }
|
|
33556
33569
|
];
|
|
33557
|
-
return
|
|
33570
|
+
return cols.filter(
|
|
33571
|
+
(c) => !(colOpts?.hideIdentifier && c.key === "identificador") && !(colOpts?.hidePerc && c.key === "perc")
|
|
33572
|
+
);
|
|
33558
33573
|
}
|
|
33559
33574
|
function buildRow(d, idx) {
|
|
33560
33575
|
const fmtVal = () => {
|
|
@@ -35142,13 +35157,60 @@ var DeviceReportModal = class {
|
|
|
35142
35157
|
participationChart = null;
|
|
35143
35158
|
// Período do último load — alimenta os headers dos exports PDF/XLS.
|
|
35144
35159
|
exportPeriod = null;
|
|
35160
|
+
// Header: "Relatório - <identifier> - <label>" + nome do device (sutil, com copiar).
|
|
35161
|
+
// O customer já aparece no footer premium. Texto escapado — o shell usa innerHTML no título.
|
|
35162
|
+
buildHeaderTitleHTML() {
|
|
35163
|
+
const esc8 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
35164
|
+
const base = esc8(
|
|
35165
|
+
`Relat\xF3rio - ${this.params.identifier || "SEM IDENTIFICADOR"} - ${this.params.label || "SEM ETIQUETA"}`
|
|
35166
|
+
);
|
|
35167
|
+
const deviceName = String(this.params.deviceName || "").trim();
|
|
35168
|
+
const deviceHTML = deviceName ? `<span class="myio-dr-devname" style="margin-left:10px;font-size:0.72em;font-weight:400;opacity:.75;display:inline-flex;align-items:center;gap:4px;vertical-align:middle;">
|
|
35169
|
+
${esc8(deviceName)}
|
|
35170
|
+
<button type="button" class="myio-dr-copy" data-copy="${esc8(deviceName)}" title="Copiar nome do dispositivo"
|
|
35171
|
+
aria-label="Copiar nome do dispositivo"
|
|
35172
|
+
style="border:none;background:transparent;color:inherit;cursor:pointer;padding:0 2px;line-height:1;opacity:.9;display:inline-flex;">
|
|
35173
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
35174
|
+
</button>
|
|
35175
|
+
</span>` : "";
|
|
35176
|
+
return `${base}${deviceHTML}`;
|
|
35177
|
+
}
|
|
35178
|
+
bindCopyDeviceName() {
|
|
35179
|
+
const root = this.modal?.element;
|
|
35180
|
+
const btn = root?.querySelector(".myio-dr-copy");
|
|
35181
|
+
if (!btn) return;
|
|
35182
|
+
btn.addEventListener("click", async (e) => {
|
|
35183
|
+
e.stopPropagation();
|
|
35184
|
+
const text2 = btn.dataset.copy || "";
|
|
35185
|
+
try {
|
|
35186
|
+
await navigator.clipboard.writeText(text2);
|
|
35187
|
+
} catch {
|
|
35188
|
+
const ta = document.createElement("textarea");
|
|
35189
|
+
ta.value = text2;
|
|
35190
|
+
ta.style.position = "fixed";
|
|
35191
|
+
ta.style.opacity = "0";
|
|
35192
|
+
document.body.appendChild(ta);
|
|
35193
|
+
ta.select();
|
|
35194
|
+
document.execCommand("copy");
|
|
35195
|
+
ta.remove();
|
|
35196
|
+
}
|
|
35197
|
+
const prev = btn.innerHTML;
|
|
35198
|
+
btn.textContent = "\u2713";
|
|
35199
|
+
btn.title = "Copiado!";
|
|
35200
|
+
setTimeout(() => {
|
|
35201
|
+
btn.innerHTML = prev;
|
|
35202
|
+
btn.title = "Copiar nome do dispositivo";
|
|
35203
|
+
}, 1200);
|
|
35204
|
+
});
|
|
35205
|
+
}
|
|
35145
35206
|
show() {
|
|
35146
35207
|
this.modal = createModal({
|
|
35147
|
-
title:
|
|
35208
|
+
title: this.buildHeaderTitleHTML(),
|
|
35148
35209
|
width: "80vw",
|
|
35149
35210
|
height: "90vh",
|
|
35150
35211
|
theme: this.params.ui?.theme || "light"
|
|
35151
35212
|
});
|
|
35213
|
+
this.bindCopyDeviceName();
|
|
35152
35214
|
this.renderContent();
|
|
35153
35215
|
this.mountFooter();
|
|
35154
35216
|
this.modal.on("close", () => {
|
|
@@ -37577,6 +37639,11 @@ function createMoneyVarianceColumn(config2) {
|
|
|
37577
37639
|
}
|
|
37578
37640
|
|
|
37579
37641
|
// src/components/premium-modals/report-all/AllReportModal.ts
|
|
37642
|
+
var SP_OFFSET_MS = 3 * 60 * 60 * 1e3;
|
|
37643
|
+
var spDayStart = (ts) => {
|
|
37644
|
+
const d = new Date(ts - SP_OFFSET_MS);
|
|
37645
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) + SP_OFFSET_MS;
|
|
37646
|
+
};
|
|
37580
37647
|
var DOMAIN_CONFIG2 = {
|
|
37581
37648
|
energy: {
|
|
37582
37649
|
endpoint: "energy",
|
|
@@ -37607,6 +37674,7 @@ var AllReportModal = class {
|
|
|
37607
37674
|
});
|
|
37608
37675
|
const domain = params.domain || "energy";
|
|
37609
37676
|
this.domainConfig = DOMAIN_CONFIG2[domain];
|
|
37677
|
+
this.granularity = params.granularity ?? (domain === "temperature" ? "1h" : "1d");
|
|
37610
37678
|
this.debugEnabled = params.debug === 1;
|
|
37611
37679
|
this.moneyColumn = createReportMoneyColumn(params.money ?? null);
|
|
37612
37680
|
if (this.moneyColumn.enabled && typeof document !== "undefined") {
|
|
@@ -37653,6 +37721,12 @@ var AllReportModal = class {
|
|
|
37653
37721
|
domainConfig;
|
|
37654
37722
|
// Granularity: '1d' (daily) | '1h' (hourly)
|
|
37655
37723
|
granularity = "1d";
|
|
37724
|
+
// Temperatura: estatísticas por sensor (key = ingestionId) da última carga
|
|
37725
|
+
temperatureStats = /* @__PURE__ */ new Map();
|
|
37726
|
+
// Linhas expandidas (série do device na granularidade escolhida) — key = row.id
|
|
37727
|
+
expandedIds = /* @__PURE__ */ new Set();
|
|
37728
|
+
// Série por device p/ energia/água (expansão) — key = `${granularity}|${id}`, por período
|
|
37729
|
+
deviceSeriesCache = /* @__PURE__ */ new Map();
|
|
37656
37730
|
// Hourly series fetched device-by-device for the 1h CSV export (the /totals endpoint
|
|
37657
37731
|
// has no hourly granularity). Keyed by StoreReading.id; cache key = period, so a new
|
|
37658
37732
|
// load or granularity switch invalidates it.
|
|
@@ -37686,6 +37760,36 @@ var AllReportModal = class {
|
|
|
37686
37760
|
console.log(`[AllReportModal DEBUG] ${message}`, data || "");
|
|
37687
37761
|
}
|
|
37688
37762
|
}
|
|
37763
|
+
get isTemperature() {
|
|
37764
|
+
return (this.params.domain || "energy") === "temperature";
|
|
37765
|
+
}
|
|
37766
|
+
// Faixa de leituras válidas (°C, já com offset) — fora dela a leitura é descartada
|
|
37767
|
+
get temperatureValidRange() {
|
|
37768
|
+
const r = this.params.temperatureValidRange;
|
|
37769
|
+
return r && Number.isFinite(r.min) && Number.isFinite(r.max) ? r : DEFAULT_CLAMP_RANGE;
|
|
37770
|
+
}
|
|
37771
|
+
get temperatureIdealRange() {
|
|
37772
|
+
const r = this.params.temperatureIdealRange;
|
|
37773
|
+
return r && Number.isFinite(Number(r.min)) && Number.isFinite(Number(r.max)) ? { min: Number(r.min), max: Number(r.max) } : null;
|
|
37774
|
+
}
|
|
37775
|
+
// offSetTemperature do sensor (vem do itemsList do MENU); 0 quando ausente
|
|
37776
|
+
temperatureOffsetFor(id) {
|
|
37777
|
+
const item = this.params.itemsList?.find((i) => String(i.id) === String(id));
|
|
37778
|
+
const off = Number(item?.temperatureOffset ?? 0);
|
|
37779
|
+
return Number.isFinite(off) ? off : 0;
|
|
37780
|
+
}
|
|
37781
|
+
// Identificador genérico (ex.: todo sensor de temperatura tem identifier "Temperatura")
|
|
37782
|
+
// não identifica nada → a coluna só aparece se algum device tiver um código de verdade.
|
|
37783
|
+
showIdentifierColumn(rows = this.data) {
|
|
37784
|
+
if (!this.isTemperature) return true;
|
|
37785
|
+
return rows.some((r) => {
|
|
37786
|
+
const id = String(r.identifier || "").trim();
|
|
37787
|
+
return !!id && !/^temperatura$/i.test(id) && id !== r.name;
|
|
37788
|
+
});
|
|
37789
|
+
}
|
|
37790
|
+
fmtTemp(v) {
|
|
37791
|
+
return v === null || v === void 0 || !Number.isFinite(v) ? "\u2014" : fmtPt(v);
|
|
37792
|
+
}
|
|
37689
37793
|
// Helper: normalize identifiers (upper, strip spaces and non-alphanum)
|
|
37690
37794
|
normalizeId(v) {
|
|
37691
37795
|
return (v || "").toString().normalize("NFKC").toUpperCase().replace(/\s+/g, "").replace(/[0300-036f]/g, "");
|
|
@@ -37951,12 +38055,13 @@ var AllReportModal = class {
|
|
|
37951
38055
|
tooltip: {
|
|
37952
38056
|
enabled: true,
|
|
37953
38057
|
title: "Granularidade",
|
|
37954
|
-
text: "
|
|
38058
|
+
text: this.isTemperature ? "A tabela mostra a <b>m\xE9dia do per\xEDodo</b> de cada sensor. Clique em \u25B8 no sensor para ver a s\xE9rie <b>por hora</b> ou <b>por dia</b>; o CSV segue a mesma granularidade." : "A tabela mostra os <b>totais do per\xEDodo</b>. Clique em \u25B8 no dispositivo para ver a s\xE9rie <b>por hora</b> ou <b>por dia</b>; em Hora, o CSV traz uma linha por dispositivo \xD7 hora."
|
|
37955
38059
|
}
|
|
37956
38060
|
},
|
|
37957
38061
|
onChange: (value) => {
|
|
37958
38062
|
this.granularity = value;
|
|
37959
38063
|
this.hourlySeriesCache = null;
|
|
38064
|
+
if (this.data.length && this.expandedIds.size) this.renderTable();
|
|
37960
38065
|
}
|
|
37961
38066
|
});
|
|
37962
38067
|
}
|
|
@@ -38021,6 +38126,9 @@ var AllReportModal = class {
|
|
|
38021
38126
|
this.debugLog("\u{1F4C5} Date range selected", { startISO, endISO });
|
|
38022
38127
|
this.exportPeriod = { startISO, endISO };
|
|
38023
38128
|
this.hourlySeriesCache = null;
|
|
38129
|
+
this.deviceSeriesCache.clear();
|
|
38130
|
+
this.temperatureStats.clear();
|
|
38131
|
+
this.expandedIds.clear();
|
|
38024
38132
|
if (!startISO || !endISO) {
|
|
38025
38133
|
this.showError("Selecione um per\xEDodo v\xE1lido");
|
|
38026
38134
|
return;
|
|
@@ -38085,6 +38193,7 @@ var AllReportModal = class {
|
|
|
38085
38193
|
});
|
|
38086
38194
|
}
|
|
38087
38195
|
return filtered.sort((a, b) => {
|
|
38196
|
+
if (!!a.noData !== !!b.noData) return a.noData ? 1 : -1;
|
|
38088
38197
|
const aVal = a[this.sortField];
|
|
38089
38198
|
const bVal = b[this.sortField];
|
|
38090
38199
|
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
@@ -38117,6 +38226,7 @@ var AllReportModal = class {
|
|
|
38117
38226
|
// Máximo/Mínimo com nome do device; mínimo só entre devices COM consumo;
|
|
38118
38227
|
// "Sem Consumo" oculto para temperature (média °C não tem semântica de zero).
|
|
38119
38228
|
computeKpis() {
|
|
38229
|
+
if (this.isTemperature) return this.computeTemperatureKpis();
|
|
38120
38230
|
const totalConsumption = this.calculateTotalConsumption();
|
|
38121
38231
|
const storeCount = Math.max(1, this.data.length);
|
|
38122
38232
|
const maxRow = this.data.reduce(
|
|
@@ -38149,6 +38259,51 @@ var AllReportModal = class {
|
|
|
38149
38259
|
if (moneyKpi) kpis.push(moneyKpi);
|
|
38150
38260
|
return kpis;
|
|
38151
38261
|
}
|
|
38262
|
+
// Temperatura: média é MÉDIA (nunca soma); só sensores com leitura válida entram.
|
|
38263
|
+
computeTemperatureKpis() {
|
|
38264
|
+
const withData = this.data.filter((r) => !r.noData);
|
|
38265
|
+
const noData = this.data.length - withData.length;
|
|
38266
|
+
const avg = withData.length ? withData.reduce((s, r) => s + r.consumption, 0) / withData.length : null;
|
|
38267
|
+
const maxRow = withData.reduce((b, r) => !b || r.consumption > b.consumption ? r : b, null);
|
|
38268
|
+
const minRow = withData.reduce((b, r) => !b || r.consumption < b.consumption ? r : b, null);
|
|
38269
|
+
const kpis = [
|
|
38270
|
+
{
|
|
38271
|
+
value: String(this.data.length),
|
|
38272
|
+
label: "Sensores",
|
|
38273
|
+
...noData ? { sub: `${noData} sem leitura no per\xEDodo` } : {}
|
|
38274
|
+
},
|
|
38275
|
+
{ value: avg === null ? "\u2014" : `${fmtPt(avg)} \xB0C`, label: "M\xE9dia geral", sub: "m\xE9dia das m\xE9dias dos sensores" },
|
|
38276
|
+
{
|
|
38277
|
+
value: maxRow ? `${fmtPt(maxRow.consumption)} \xB0C` : "\u2014",
|
|
38278
|
+
label: "Maior m\xE9dia",
|
|
38279
|
+
...maxRow?.name ? { sub: maxRow.name } : {}
|
|
38280
|
+
},
|
|
38281
|
+
{
|
|
38282
|
+
value: minRow ? `${fmtPt(minRow.consumption)} \xB0C` : "\u2014",
|
|
38283
|
+
label: "Menor m\xE9dia",
|
|
38284
|
+
...minRow?.name ? { sub: minRow.name } : {}
|
|
38285
|
+
}
|
|
38286
|
+
];
|
|
38287
|
+
const ideal = this.temperatureIdealRange;
|
|
38288
|
+
if (ideal) {
|
|
38289
|
+
const out = withData.filter((r) => r.consumption < ideal.min || r.consumption > ideal.max).length;
|
|
38290
|
+
kpis.push({
|
|
38291
|
+
value: String(out),
|
|
38292
|
+
label: "Fora da faixa ideal",
|
|
38293
|
+
sub: `faixa ${fmtPt(ideal.min)}\u2013${fmtPt(ideal.max)} \xB0C`
|
|
38294
|
+
});
|
|
38295
|
+
}
|
|
38296
|
+
const discarded = this.data.reduce((s, r) => s + (r.discarded || 0), 0);
|
|
38297
|
+
if (discarded > 0) {
|
|
38298
|
+
const v = this.temperatureValidRange;
|
|
38299
|
+
kpis.push({
|
|
38300
|
+
value: String(discarded),
|
|
38301
|
+
label: "Leituras descartadas",
|
|
38302
|
+
sub: `fora de ${fmtPt(v.min)}\u2013${fmtPt(v.max)} \xB0C`
|
|
38303
|
+
});
|
|
38304
|
+
}
|
|
38305
|
+
return kpis;
|
|
38306
|
+
}
|
|
38152
38307
|
// RFC-0228 A6 — the "Custo projetado (R$)" summary KPI, or null when money is off.
|
|
38153
38308
|
// Reuses the A6 column's honest-coverage resolution: a formatted R$ total only when
|
|
38154
38309
|
// the overlay is confidently priced (DEC-8), otherwise a coverage state label.
|
|
@@ -38180,18 +38335,8 @@ var AllReportModal = class {
|
|
|
38180
38335
|
}
|
|
38181
38336
|
const isGrouped = paginatedData.some((r) => r.groupLabel);
|
|
38182
38337
|
const grandTotal = this.calculateTotalConsumption();
|
|
38183
|
-
const
|
|
38184
|
-
const
|
|
38185
|
-
const tableRows = isGrouped ? this.renderGroupedRows(paginatedData, grandTotal) : paginatedData.map(
|
|
38186
|
-
(row) => `
|
|
38187
|
-
<tr>
|
|
38188
|
-
<td data-label="Identificador" style="font-family: monospace; font-weight: bold; text-transform: uppercase;">${row.identifier}</td>
|
|
38189
|
-
<td data-label="Nome"><strong>${row.name}</strong></td>
|
|
38190
|
-
<td data-label="${this.domainConfig.label}" style="text-align: right; font-weight: bold;">${fmtPt(row.consumption)}</td>
|
|
38191
|
-
${moneyCell(row)}${this.varianceColumn.bodyCellHTML(row.id)}<td data-label="%" style="text-align: right; color: var(--myio-text-muted);">${pct(row.consumption)}</td>
|
|
38192
|
-
</tr>
|
|
38193
|
-
`
|
|
38194
|
-
).join("");
|
|
38338
|
+
const showId = this.showIdentifierColumn();
|
|
38339
|
+
const tableRows = isGrouped ? this.renderGroupedRows(paginatedData, grandTotal) : paginatedData.map((row) => this.renderRowHTML(row, grandTotal, showId)).join("");
|
|
38195
38340
|
container.innerHTML = `
|
|
38196
38341
|
<div style="max-height: 500px; overflow-y: auto; border: 1px solid var(--myio-border); border-radius: 6px;">
|
|
38197
38342
|
<style>
|
|
@@ -38211,6 +38356,21 @@ var AllReportModal = class {
|
|
|
38211
38356
|
color: var(--myio-primary, #1565c0);
|
|
38212
38357
|
margin-left: 8px;
|
|
38213
38358
|
}
|
|
38359
|
+
.rp-expand {
|
|
38360
|
+
border: none; background: transparent; cursor: pointer; padding: 0 6px 0 0;
|
|
38361
|
+
font-size: 12px; color: var(--myio-text-muted, #6b7280); line-height: 1;
|
|
38362
|
+
transition: transform .15s ease;
|
|
38363
|
+
}
|
|
38364
|
+
.rp-expand[aria-expanded="true"] { transform: rotate(90deg); color: var(--myio-brand-700, #3e1a7d); }
|
|
38365
|
+
.rp-series-row > td { background: var(--myio-bg, #f8fafc); padding: 8px 12px 12px 32px !important; }
|
|
38366
|
+
.rp-series { max-height: 260px; overflow-y: auto; font-size: 12px; }
|
|
38367
|
+
.rp-series table { width: 100%; border-collapse: collapse; }
|
|
38368
|
+
.rp-series th, .rp-series td { padding: 3px 8px; border-bottom: 1px solid var(--myio-border, #e5e7eb); }
|
|
38369
|
+
.rp-series th { text-align: left; font-weight: 600; color: var(--myio-text-muted, #6b7280); position: sticky; top: 0; background: var(--myio-bg, #f8fafc); }
|
|
38370
|
+
.rp-series td.num, .rp-series th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
38371
|
+
.rp-muted { color: var(--myio-text-muted, #9ca3af); font-weight: 500; }
|
|
38372
|
+
.rp-temp-above { color: #dc2626; }
|
|
38373
|
+
.rp-temp-below { color: #2563eb; }
|
|
38214
38374
|
@media (max-width: 768px) {
|
|
38215
38375
|
.myio-table-mobile { display: block !important; }
|
|
38216
38376
|
.myio-table-mobile thead,
|
|
@@ -38226,29 +38386,145 @@ var AllReportModal = class {
|
|
|
38226
38386
|
</style>
|
|
38227
38387
|
<table class="myio-table myio-table-mobile" style="table-layout: fixed; width: 100%;">
|
|
38228
38388
|
<thead style="position: sticky; top: 0; background: var(--myio-bg); z-index: 1;">
|
|
38229
|
-
<tr>
|
|
38230
|
-
<th style="cursor: pointer; width: 22%;" data-sort="identifier">
|
|
38231
|
-
Identificador
|
|
38232
|
-
<span style="margin-left: 4px; opacity: ${this.getSortOpacity("identifier")};">${this.getSortIcon("identifier")}</span>
|
|
38233
|
-
</th>
|
|
38234
|
-
<th style="cursor: pointer; width: 40%;" data-sort="name">
|
|
38235
|
-
Nome
|
|
38236
|
-
<span style="margin-left: 4px; opacity: ${this.getSortOpacity("name")};">${this.getSortIcon("name")}</span>
|
|
38237
|
-
</th>
|
|
38238
|
-
<th style="cursor: pointer; text-align: right; width: 24%;" data-sort="consumption">
|
|
38239
|
-
${this.domainConfig.label}
|
|
38240
|
-
<span style="margin-left: 4px; opacity: ${this.getSortOpacity("consumption")};">${this.getSortIcon("consumption")}</span>
|
|
38241
|
-
</th>
|
|
38242
|
-
${this.moneyColumn.headerCellHTML()}${this.varianceColumn.headerCellHTML()}<th style="text-align: right; width: 14%;">%</th>
|
|
38243
|
-
</tr>
|
|
38389
|
+
<tr>${this.renderHeaderCellsHTML(showId)}</tr>
|
|
38244
38390
|
</thead>
|
|
38245
38391
|
<tbody>${tableRows}</tbody>
|
|
38246
38392
|
</table>
|
|
38247
38393
|
</div>
|
|
38248
38394
|
`;
|
|
38249
38395
|
this.setupTableSorting();
|
|
38396
|
+
this.setupRowExpansion(container);
|
|
38250
38397
|
this.updateParticipationChart();
|
|
38251
38398
|
}
|
|
38399
|
+
sortableTh(field, label, style) {
|
|
38400
|
+
return `<th style="cursor: pointer; ${style}" data-sort="${field}">
|
|
38401
|
+
${label}
|
|
38402
|
+
<span style="margin-left: 4px; opacity: ${this.getSortOpacity(field)};">${this.getSortIcon(field)}</span>
|
|
38403
|
+
</th>`;
|
|
38404
|
+
}
|
|
38405
|
+
// Colunas: energia/água = [Identificador] Nome | Consumo | (R$) | % ;
|
|
38406
|
+
// temperatura = [Identificador] Nome | Média | Mín | Máx (sem % — participação não se aplica).
|
|
38407
|
+
renderHeaderCellsHTML(showId) {
|
|
38408
|
+
const idTh = showId ? this.sortableTh("identifier", "Identificador", "width: 22%;") : "";
|
|
38409
|
+
if (this.isTemperature) {
|
|
38410
|
+
return idTh + this.sortableTh("name", "Sensor", `width: ${showId ? 34 : 52}%;`) + this.sortableTh("consumption", "M\xE9dia (\xB0C)", "text-align: right; width: 16%;") + this.sortableTh("min", "M\xEDn (\xB0C)", "text-align: right; width: 14%;") + this.sortableTh("max", "M\xE1x (\xB0C)", "text-align: right; width: 14%;");
|
|
38411
|
+
}
|
|
38412
|
+
return idTh + this.sortableTh("name", "Nome", `width: ${showId ? 40 : 62}%;`) + this.sortableTh("consumption", this.domainConfig.label, "text-align: right; width: 24%;") + `${this.moneyColumn.headerCellHTML()}${this.varianceColumn.headerCellHTML()}<th style="text-align: right; width: 14%;">%</th>`;
|
|
38413
|
+
}
|
|
38414
|
+
columnCount(showId) {
|
|
38415
|
+
const base = (showId ? 1 : 0) + 1;
|
|
38416
|
+
if (this.isTemperature) return base + 3;
|
|
38417
|
+
return base + 2 + (this.moneyColumn.enabled ? 1 : 0) + (this.varianceColumn.enabled ? 1 : 0);
|
|
38418
|
+
}
|
|
38419
|
+
renderRowHTML(row, grandTotal, showId) {
|
|
38420
|
+
const expandable = !!row.id;
|
|
38421
|
+
const expanded = expandable && this.expandedIds.has(row.id);
|
|
38422
|
+
const expandBtn = expandable ? `<button type="button" class="rp-expand" data-expand-id="${row.id}" aria-expanded="${expanded}" title="Ver s\xE9rie por ${this.granularity === "1h" ? "hora" : "dia"}">\u25B8</button>` : "";
|
|
38423
|
+
const idTd = showId ? `<td data-label="Identificador" style="font-family: monospace; font-weight: bold; text-transform: uppercase;">${row.identifier}</td>` : "";
|
|
38424
|
+
const nameTd = `<td data-label="Nome">${expandBtn}<strong>${row.name}</strong></td>`;
|
|
38425
|
+
let valueTds;
|
|
38426
|
+
if (this.isTemperature) {
|
|
38427
|
+
const ideal = this.temperatureIdealRange;
|
|
38428
|
+
const cls = !row.noData && ideal ? row.consumption > ideal.max ? "rp-temp-above" : row.consumption < ideal.min ? "rp-temp-below" : "" : "";
|
|
38429
|
+
const avgTd = row.noData ? `<td data-label="M\xE9dia (\xB0C)" style="text-align: right;" class="rp-muted">Sem leitura</td>` : `<td data-label="M\xE9dia (\xB0C)" style="text-align: right; font-weight: bold;" class="${cls}">${fmtPt(row.consumption)}</td>`;
|
|
38430
|
+
valueTds = avgTd + `<td data-label="M\xEDn (\xB0C)" style="text-align: right;">${this.fmtTemp(row.noData ? null : row.min)}</td><td data-label="M\xE1x (\xB0C)" style="text-align: right;">${this.fmtTemp(row.noData ? null : row.max)}</td>`;
|
|
38431
|
+
} else {
|
|
38432
|
+
const pct = grandTotal > 0 ? `${fmtPt(row.consumption / grandTotal * 100)}%` : "\u2014";
|
|
38433
|
+
valueTds = `<td data-label="${this.domainConfig.label}" style="text-align: right; font-weight: bold;">${fmtPt(row.consumption)}</td>${this.moneyColumn.bodyCellHTML(row.id)}${this.varianceColumn.bodyCellHTML(row.id)}<td data-label="%" style="text-align: right; color: var(--myio-text-muted);">${pct}</td>`;
|
|
38434
|
+
}
|
|
38435
|
+
const seriesRow = expanded ? `<tr class="rp-series-row" data-series-for="${row.id}"><td colspan="${this.columnCount(showId)}"><div class="rp-series">Carregando s\xE9rie\u2026</div></td></tr>` : "";
|
|
38436
|
+
return `<tr>${idTd}${nameTd}${valueTds}</tr>${seriesRow}`;
|
|
38437
|
+
}
|
|
38438
|
+
// ▸ no nome do device: abre/fecha a série (hora ou dia, conforme a granularidade)
|
|
38439
|
+
setupRowExpansion(container) {
|
|
38440
|
+
container.querySelectorAll(".rp-expand").forEach((btn) => {
|
|
38441
|
+
btn.addEventListener("click", (e) => {
|
|
38442
|
+
e.stopPropagation();
|
|
38443
|
+
const id = btn.dataset.expandId;
|
|
38444
|
+
if (!id) return;
|
|
38445
|
+
if (this.expandedIds.has(id)) this.expandedIds.delete(id);
|
|
38446
|
+
else this.expandedIds.add(id);
|
|
38447
|
+
this.renderTable();
|
|
38448
|
+
});
|
|
38449
|
+
});
|
|
38450
|
+
container.querySelectorAll(".rp-series-row").forEach((tr) => {
|
|
38451
|
+
const id = tr.dataset.seriesFor;
|
|
38452
|
+
const box = tr.querySelector(".rp-series");
|
|
38453
|
+
if (id && box) void this.fillSeries(id, box);
|
|
38454
|
+
});
|
|
38455
|
+
}
|
|
38456
|
+
async fillSeries(id, box) {
|
|
38457
|
+
const row = this.data.find((r) => r.id === id);
|
|
38458
|
+
const gran = this.granularity;
|
|
38459
|
+
try {
|
|
38460
|
+
const points = await this.getDeviceSeries(id, gran);
|
|
38461
|
+
if (!box.isConnected) return;
|
|
38462
|
+
if (!points.length) {
|
|
38463
|
+
box.innerHTML = `<span class="rp-muted">Sem dados ${gran === "1h" ? "hor\xE1rios" : "di\xE1rios"} no per\xEDodo.</span>`;
|
|
38464
|
+
return;
|
|
38465
|
+
}
|
|
38466
|
+
const fmtTs = (ts) => gran === "1h" ? new Date(ts).toLocaleString("pt-BR", {
|
|
38467
|
+
day: "2-digit",
|
|
38468
|
+
month: "2-digit",
|
|
38469
|
+
year: "numeric",
|
|
38470
|
+
hour: "2-digit",
|
|
38471
|
+
minute: "2-digit",
|
|
38472
|
+
timeZone: "America/Sao_Paulo"
|
|
38473
|
+
}) : new Date(ts).toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });
|
|
38474
|
+
const valueLabel = this.isTemperature ? gran === "1h" ? "Temperatura (\xB0C)" : "M\xE9dia do dia (\xB0C)" : this.domainConfig.label;
|
|
38475
|
+
box.innerHTML = `
|
|
38476
|
+
<table>
|
|
38477
|
+
<thead><tr><th>${gran === "1h" ? "Data/Hora" : "Dia"}</th><th class="num">${valueLabel}</th></tr></thead>
|
|
38478
|
+
<tbody>${points.map((p) => `<tr><td>${fmtTs(p.timestamp)}</td><td class="num">${fmtPt(p.value)}</td></tr>`).join("")}</tbody>
|
|
38479
|
+
</table>`;
|
|
38480
|
+
} catch (err) {
|
|
38481
|
+
this.debugLog("\u274C fillSeries", err);
|
|
38482
|
+
if (box.isConnected) {
|
|
38483
|
+
box.innerHTML = `<span class="rp-muted">N\xE3o foi poss\xEDvel carregar a s\xE9rie de ${row?.name || "dispositivo"}.</span>`;
|
|
38484
|
+
}
|
|
38485
|
+
}
|
|
38486
|
+
}
|
|
38487
|
+
// Série do device na granularidade pedida. Temperatura: da série horária já calculada
|
|
38488
|
+
// (offset + descarte), agregada por dia em 1d. Energia/água: endpoint por device.
|
|
38489
|
+
async getDeviceSeries(id, gran) {
|
|
38490
|
+
if (this.isTemperature) {
|
|
38491
|
+
const hourly = this.temperatureStats.get(id)?.series || [];
|
|
38492
|
+
return gran === "1h" ? hourly : this.aggregateDaily(hourly, "avg");
|
|
38493
|
+
}
|
|
38494
|
+
const key = `${gran}|${id}`;
|
|
38495
|
+
const cached = this.deviceSeriesCache.get(key);
|
|
38496
|
+
if (cached) return cached;
|
|
38497
|
+
const startISO = this.exportPeriod?.startISO;
|
|
38498
|
+
const endISO = this.exportPeriod?.endISO;
|
|
38499
|
+
const token = this.params.api.ingestionToken;
|
|
38500
|
+
const baseUrl = this.params.api.dataApiBaseUrl;
|
|
38501
|
+
if (!startISO || !endISO || !token || !baseUrl) throw new Error("API/per\xEDodo indispon\xEDvel");
|
|
38502
|
+
const url = `${baseUrl}/telemetry/devices/${id}/${this.domainConfig.endpoint}?startTime=${encodeURIComponent(startISO)}&endTime=${encodeURIComponent(endISO)}&granularity=${gran}&deep=0`;
|
|
38503
|
+
const res = await fetch(url, {
|
|
38504
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
|
|
38505
|
+
});
|
|
38506
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
38507
|
+
const body = await res.json();
|
|
38508
|
+
const points = this.parseSeries(body);
|
|
38509
|
+
this.deviceSeriesCache.set(key, points);
|
|
38510
|
+
return points;
|
|
38511
|
+
}
|
|
38512
|
+
parseSeries(body) {
|
|
38513
|
+
const ent = Array.isArray(body) ? body[0] : body;
|
|
38514
|
+
return (ent?.consumption || []).map((p) => ({ timestamp: new Date(p?.timestamp).getTime(), value: Number(p?.value) })).filter((p) => Number.isFinite(p.timestamp) && Number.isFinite(p.value)).sort((a, b) => a.timestamp - b.timestamp);
|
|
38515
|
+
}
|
|
38516
|
+
// Agrega série horária por dia civil (São Paulo): média (temperatura) ou soma (consumo)
|
|
38517
|
+
aggregateDaily(points, mode) {
|
|
38518
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
38519
|
+
for (const p of points) {
|
|
38520
|
+
const day = spDayStart(p.timestamp);
|
|
38521
|
+
const acc = byDay.get(day) || { sum: 0, n: 0 };
|
|
38522
|
+
acc.sum += p.value;
|
|
38523
|
+
acc.n += 1;
|
|
38524
|
+
byDay.set(day, acc);
|
|
38525
|
+
}
|
|
38526
|
+
return [...byDay.entries()].sort((a, b) => a[0] - b[0]).map(([day, { sum: sum2, n }]) => ({ timestamp: day, value: mode === "avg" ? sum2 / n : sum2 }));
|
|
38527
|
+
}
|
|
38252
38528
|
// Resolve the chart palette from the host theme: createMyIOTheme exposes
|
|
38253
38529
|
// tones(n) — solid hex tones derived from the dashboard accent. When absent
|
|
38254
38530
|
// (or the accent is not hex), the component falls back to the MYIO palette.
|
|
@@ -38266,6 +38542,10 @@ var AllReportModal = class {
|
|
|
38266
38542
|
updateParticipationChart() {
|
|
38267
38543
|
const container = document.getElementById("participation-chart-container");
|
|
38268
38544
|
if (!container) return;
|
|
38545
|
+
if (this.isTemperature) {
|
|
38546
|
+
this.renderTemperatureChart(container);
|
|
38547
|
+
return;
|
|
38548
|
+
}
|
|
38269
38549
|
const items = this.getFilteredData().map((row) => ({
|
|
38270
38550
|
id: row.id || this.generateStoreId(row.identifier),
|
|
38271
38551
|
label: row.name,
|
|
@@ -38293,6 +38573,40 @@ var AllReportModal = class {
|
|
|
38293
38573
|
if (palette) this.participationChart.updateSettings({ palette });
|
|
38294
38574
|
this.participationChart.updateData(items);
|
|
38295
38575
|
}
|
|
38576
|
+
// Barras horizontais (HTML puro) da média por sensor, escala comum, faixa ideal sombreada
|
|
38577
|
+
renderTemperatureChart(container) {
|
|
38578
|
+
const rows = this.getFilteredData();
|
|
38579
|
+
if (!rows.length) return;
|
|
38580
|
+
const withData = rows.filter((r) => !r.noData);
|
|
38581
|
+
const ideal = this.temperatureIdealRange;
|
|
38582
|
+
const values = withData.length ? withData.map((r) => r.consumption) : [20, 30];
|
|
38583
|
+
const lo = Math.floor(Math.min(...values, ideal ? ideal.min : Infinity) - 1);
|
|
38584
|
+
const hi = Math.ceil(Math.max(...values, ideal ? ideal.max : -Infinity) + 1);
|
|
38585
|
+
const span = Math.max(1, hi - lo);
|
|
38586
|
+
const pos = (v) => `${((v - lo) / span * 100).toFixed(2)}%`;
|
|
38587
|
+
const band = ideal ? `<div style="position:absolute;top:0;bottom:0;left:${pos(ideal.min)};width:calc(${pos(ideal.max)} - ${pos(ideal.min)});background:rgba(34,197,94,.15);border-left:1px dashed #22c55e;border-right:1px dashed #22c55e;"></div>` : "";
|
|
38588
|
+
const barColor = (v) => ideal && v > ideal.max ? "#ef4444" : ideal && v < ideal.min ? "#3b82f6" : "var(--myio-brand-700, #3e1a7d)";
|
|
38589
|
+
const lines = rows.map((r) => {
|
|
38590
|
+
const track = r.noData ? `<span class="rp-muted" style="font-size:11px;">Sem leitura</span>` : `<div style="position:relative;height:12px;background:var(--myio-border,#e5e7eb);border-radius:6px;overflow:hidden;">
|
|
38591
|
+
${band}
|
|
38592
|
+
<div style="position:absolute;top:2px;bottom:2px;left:0;width:${pos(r.consumption)};background:${barColor(r.consumption)};border-radius:4px;opacity:.85;"></div>
|
|
38593
|
+
</div>`;
|
|
38594
|
+
return `
|
|
38595
|
+
<div style="display:grid;grid-template-columns:minmax(0,38%) 1fr 52px;gap:8px;align-items:center;margin-bottom:6px;font-size:12px;">
|
|
38596
|
+
<span title="${r.name}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${r.name}</span>
|
|
38597
|
+
${track}
|
|
38598
|
+
<span style="text-align:right;font-variant-numeric:tabular-nums;font-weight:600;">${r.noData ? "\u2014" : fmtPt(r.consumption)}</span>
|
|
38599
|
+
</div>`;
|
|
38600
|
+
}).join("");
|
|
38601
|
+
container.innerHTML = `
|
|
38602
|
+
<div style="border:1px solid var(--myio-border,#e5e7eb);border-radius:10px;padding:12px 14px;max-height:620px;overflow-y:auto;">
|
|
38603
|
+
<div style="font-weight:700;font-size:14px;margin-bottom:2px;">Temperatura m\xE9dia por sensor</div>
|
|
38604
|
+
<div style="font-size:11px;color:var(--myio-text-muted,#6b7280);margin-bottom:10px;">
|
|
38605
|
+
Escala ${fmtPt(lo)}\u2013${fmtPt(hi)} \xB0C${ideal ? ` \xB7 <span style="color:#16a34a;">faixa ideal ${fmtPt(ideal.min)}\u2013${fmtPt(ideal.max)} \xB0C</span>` : ""}
|
|
38606
|
+
</div>
|
|
38607
|
+
${lines}
|
|
38608
|
+
</div>`;
|
|
38609
|
+
}
|
|
38296
38610
|
// RFC-0182: Render rows grouped by groupLabel with section headers (Option B)
|
|
38297
38611
|
renderGroupedRows(rows, grandTotal) {
|
|
38298
38612
|
const groupOrder = [];
|
|
@@ -38305,27 +38619,26 @@ var AllReportModal = class {
|
|
|
38305
38619
|
}
|
|
38306
38620
|
byGroup.get(g).push(row);
|
|
38307
38621
|
}
|
|
38308
|
-
const
|
|
38622
|
+
const showId = this.showIdentifierColumn();
|
|
38309
38623
|
return groupOrder.map((groupLabel) => {
|
|
38310
38624
|
const items = byGroup.get(groupLabel);
|
|
38311
|
-
|
|
38625
|
+
let summary;
|
|
38626
|
+
if (this.isTemperature) {
|
|
38627
|
+
const withData = items.filter((r) => !r.noData);
|
|
38628
|
+
const avg = withData.length ? withData.reduce((s, r) => s + r.consumption, 0) / withData.length : null;
|
|
38629
|
+
summary = `${items.length} sensores \xB7 m\xE9dia ${this.fmtTemp(avg)} \xB0C`;
|
|
38630
|
+
} else {
|
|
38631
|
+
const groupTotal = items.reduce((s, r) => s + r.consumption, 0);
|
|
38632
|
+
summary = `${items.length} dispositivos \xB7 ${fmtPt(groupTotal)} ${this.domainConfig.unit}`;
|
|
38633
|
+
}
|
|
38312
38634
|
const header = `
|
|
38313
38635
|
<tr class="rp-group-header">
|
|
38314
|
-
<td colspan="${
|
|
38636
|
+
<td colspan="${this.columnCount(showId)}">
|
|
38315
38637
|
${groupLabel}
|
|
38316
|
-
<span class="rp-group-total">${
|
|
38638
|
+
<span class="rp-group-total">${summary}</span>
|
|
38317
38639
|
</td>
|
|
38318
38640
|
</tr>`;
|
|
38319
|
-
|
|
38320
|
-
(row) => `
|
|
38321
|
-
<tr>
|
|
38322
|
-
<td data-label="Identificador" style="font-family: monospace; font-weight: bold; text-transform: uppercase;">${row.identifier}</td>
|
|
38323
|
-
<td data-label="Nome"><strong>${row.name}</strong></td>
|
|
38324
|
-
<td data-label="${this.domainConfig.label}" style="text-align: right; font-weight: bold;">${fmtPt(row.consumption)}</td>
|
|
38325
|
-
${this.moneyColumn.bodyCellHTML(row.id)}${this.varianceColumn.bodyCellHTML(row.id)}<td data-label="%" style="text-align: right; color: var(--myio-text-muted);">${pct(row.consumption)}</td>
|
|
38326
|
-
</tr>`
|
|
38327
|
-
).join("");
|
|
38328
|
-
return header + dataRows;
|
|
38641
|
+
return header + items.map((row) => this.renderRowHTML(row, grandTotal, showId)).join("");
|
|
38329
38642
|
}).join("");
|
|
38330
38643
|
}
|
|
38331
38644
|
getSortIcon(field) {
|
|
@@ -38433,6 +38746,10 @@ var AllReportModal = class {
|
|
|
38433
38746
|
}
|
|
38434
38747
|
async exportCSV() {
|
|
38435
38748
|
const sortedData = [...this.data].sort((a, b) => b.consumption - a.consumption);
|
|
38749
|
+
if (this.isTemperature) {
|
|
38750
|
+
this.exportTemperatureCSV(sortedData);
|
|
38751
|
+
return;
|
|
38752
|
+
}
|
|
38436
38753
|
if (this.granularity === "1h") {
|
|
38437
38754
|
await this.exportHourlyCSV(sortedData);
|
|
38438
38755
|
return;
|
|
@@ -38446,6 +38763,44 @@ var AllReportModal = class {
|
|
|
38446
38763
|
const csvContent = toCsv(csvData);
|
|
38447
38764
|
this.downloadCSV(csvContent, `relatorio-geral-lojas-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`);
|
|
38448
38765
|
}
|
|
38766
|
+
// Temperatura: valores já com offset e sem leituras inválidas (mesma base da tabela).
|
|
38767
|
+
// 1d → uma linha por sensor por DIA; 1h → uma linha por sensor por HORA.
|
|
38768
|
+
exportTemperatureCSV(rows) {
|
|
38769
|
+
const showId = this.showIdentifierColumn(rows);
|
|
38770
|
+
const idCol = (r) => showId ? [r.identifier] : [];
|
|
38771
|
+
const idHead = showId ? ["Identificador"] : [];
|
|
38772
|
+
const n2 = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : v.toFixed(2);
|
|
38773
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
38774
|
+
const summary = [
|
|
38775
|
+
[...idHead, "Sensor", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "M\xE1x (\xB0C)", "Leituras", "Leituras descartadas"],
|
|
38776
|
+
...rows.map((r) => [
|
|
38777
|
+
...idCol(r),
|
|
38778
|
+
r.name,
|
|
38779
|
+
r.noData ? "Sem leitura" : n2(r.consumption),
|
|
38780
|
+
r.noData ? "" : n2(r.min),
|
|
38781
|
+
r.noData ? "" : n2(r.max),
|
|
38782
|
+
String(r.readings ?? ""),
|
|
38783
|
+
String(r.discarded ?? "")
|
|
38784
|
+
])
|
|
38785
|
+
];
|
|
38786
|
+
const gran = this.granularity;
|
|
38787
|
+
const series = [[...idHead, "Sensor", gran === "1h" ? "Data/Hora" : "Dia", "Temperatura (\xB0C)"]];
|
|
38788
|
+
const fmtTs = (ts) => gran === "1h" ? new Date(ts).toLocaleString("pt-BR", {
|
|
38789
|
+
day: "2-digit",
|
|
38790
|
+
month: "2-digit",
|
|
38791
|
+
year: "numeric",
|
|
38792
|
+
hour: "2-digit",
|
|
38793
|
+
minute: "2-digit",
|
|
38794
|
+
timeZone: "America/Sao_Paulo"
|
|
38795
|
+
}) : new Date(ts).toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });
|
|
38796
|
+
for (const r of rows) {
|
|
38797
|
+
const hourly = r.id && this.temperatureStats.get(r.id)?.series || [];
|
|
38798
|
+
const pts = gran === "1h" ? hourly : this.aggregateDaily(hourly, "avg");
|
|
38799
|
+
for (const p of pts) series.push([...idCol(r), r.name, fmtTs(p.timestamp), p.value.toFixed(2)]);
|
|
38800
|
+
}
|
|
38801
|
+
const csvContent = toCsv([...summary, [], ...series]);
|
|
38802
|
+
this.downloadCSV(csvContent, `relatorio-temperatura-${gran}-${date}.csv`);
|
|
38803
|
+
}
|
|
38449
38804
|
// 1h CSV: one row per device × hour. The series comes from per-device requests
|
|
38450
38805
|
// (/telemetry/devices/{id}/{endpoint}?granularity=1h) fetched lazily at export time
|
|
38451
38806
|
// and cached per period — /totals cannot provide hourly data.
|
|
@@ -38533,16 +38888,29 @@ var AllReportModal = class {
|
|
|
38533
38888
|
// Maps the report rows to the TelemetryDevice shape consumed by the shared
|
|
38534
38889
|
// TELEMETRY grid exporters (only labelOrName/name/deviceIdentifier/val/perc are read).
|
|
38535
38890
|
buildExportDevices() {
|
|
38536
|
-
const sorted = [...this.data].sort(
|
|
38891
|
+
const sorted = [...this.data].sort(
|
|
38892
|
+
(a, b) => !!a.noData !== !!b.noData ? a.noData ? 1 : -1 : b.consumption - a.consumption
|
|
38893
|
+
);
|
|
38537
38894
|
const total = sorted.reduce((s, r) => s + (r.consumption || 0), 0);
|
|
38538
38895
|
return sorted.map((r) => ({
|
|
38539
38896
|
labelOrName: r.name,
|
|
38540
38897
|
name: r.name,
|
|
38541
38898
|
deviceIdentifier: r.identifier,
|
|
38542
|
-
|
|
38543
|
-
|
|
38899
|
+
// Temperatura: média do sensor (sem leitura → "—"); sem % de participação
|
|
38900
|
+
val: this.isTemperature && r.noData ? null : r.consumption,
|
|
38901
|
+
perc: this.isTemperature ? void 0 : total > 0 ? r.consumption / total * 100 : 0
|
|
38544
38902
|
}));
|
|
38545
38903
|
}
|
|
38904
|
+
// Colunas do PDF/XLS: temperatura = Sensor | Temperatura média (sem %, Identificador só se útil)
|
|
38905
|
+
exportColumns() {
|
|
38906
|
+
if (!this.isTemperature) return null;
|
|
38907
|
+
return {
|
|
38908
|
+
nameLabel: "Sensor",
|
|
38909
|
+
valueLabel: "Temperatura m\xE9dia (\xB0C)",
|
|
38910
|
+
hidePerc: true,
|
|
38911
|
+
hideIdentifier: !this.showIdentifierColumn()
|
|
38912
|
+
};
|
|
38913
|
+
}
|
|
38546
38914
|
// Accent hex da paleta do dashboard (tema efetivo) para o PDF/XLS — cai no
|
|
38547
38915
|
// roxo MYIO default quando não há tema configurado.
|
|
38548
38916
|
resolveAccentHex() {
|
|
@@ -38568,6 +38936,7 @@ var AllReportModal = class {
|
|
|
38568
38936
|
{
|
|
38569
38937
|
accentColor: this.resolveAccentHex(),
|
|
38570
38938
|
kpis: this.computeKpis(),
|
|
38939
|
+
columns: this.exportColumns(),
|
|
38571
38940
|
chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null
|
|
38572
38941
|
}
|
|
38573
38942
|
);
|
|
@@ -38581,7 +38950,7 @@ var AllReportModal = class {
|
|
|
38581
38950
|
this.domainConfig.unit,
|
|
38582
38951
|
this.exportPeriod,
|
|
38583
38952
|
null,
|
|
38584
|
-
{ accentColor: this.resolveAccentHex() }
|
|
38953
|
+
{ accentColor: this.resolveAccentHex(), columns: this.exportColumns() }
|
|
38585
38954
|
);
|
|
38586
38955
|
}
|
|
38587
38956
|
async fetchCustomerTotals(startISO, endISO) {
|
|
@@ -38634,34 +39003,59 @@ var AllReportModal = class {
|
|
|
38634
39003
|
// em total_value (in-place). Sensores sem série ficam com o 0 original.
|
|
38635
39004
|
async enrichTemperatureAverages(data, startISO, endISO, token, baseUrl) {
|
|
38636
39005
|
const rows = Array.isArray(data?.data) ? data.data : [];
|
|
38637
|
-
|
|
39006
|
+
let tempRows = rows.filter((d) => String(d?.deviceType || "").toLowerCase() === "temperature");
|
|
39007
|
+
if (this.params.itemsList) {
|
|
39008
|
+
const ids = new Set(this.params.itemsList.map((i) => String(i.id)));
|
|
39009
|
+
tempRows = tempRows.filter((d) => ids.has(String(d?.id)));
|
|
39010
|
+
}
|
|
38638
39011
|
if (!tempRows.length) return;
|
|
38639
|
-
const gran = this.granularity === "1h" ? "1h" : "1d";
|
|
38640
39012
|
const startTime = encodeURIComponent(startISO);
|
|
38641
39013
|
const endTime = encodeURIComponent(endISO);
|
|
39014
|
+
const valid = this.temperatureValidRange;
|
|
38642
39015
|
const BATCH = 6;
|
|
38643
39016
|
for (let i = 0; i < tempRows.length; i += BATCH) {
|
|
38644
39017
|
await Promise.all(
|
|
38645
39018
|
tempRows.slice(i, i + BATCH).map(async (dev) => {
|
|
39019
|
+
const id = String(dev.id);
|
|
39020
|
+
const offset = this.temperatureOffsetFor(id);
|
|
38646
39021
|
try {
|
|
38647
|
-
const url = `${baseUrl}/telemetry/devices/${
|
|
39022
|
+
const url = `${baseUrl}/telemetry/devices/${id}/temperature?startTime=${startTime}&endTime=${endTime}&granularity=1h&deep=0`;
|
|
38648
39023
|
const res = await fetch(url, {
|
|
38649
39024
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
|
|
38650
39025
|
});
|
|
38651
|
-
if (!res.ok)
|
|
38652
|
-
const
|
|
38653
|
-
|
|
38654
|
-
|
|
38655
|
-
if (values.length) {
|
|
38656
|
-
dev.total_value = values.reduce((s, v) => s + v, 0) / values.length;
|
|
38657
|
-
}
|
|
39026
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
39027
|
+
const stats = this.computeTemperatureStats(this.parseSeries(await res.json()), offset, valid);
|
|
39028
|
+
this.temperatureStats.set(id, stats);
|
|
39029
|
+
dev.total_value = stats.avg;
|
|
38658
39030
|
} catch {
|
|
39031
|
+
const avg = dev.total_value === null || dev.total_value === void 0 ? NaN : Number(dev.total_value);
|
|
39032
|
+
dev.total_value = Number.isFinite(avg) ? avg + offset : null;
|
|
38659
39033
|
}
|
|
38660
39034
|
})
|
|
38661
39035
|
);
|
|
38662
39036
|
}
|
|
38663
39037
|
this.debugLog(`[AllReportModal] ED-996: temperaturas m\xE9dias aplicadas a ${tempRows.length} sensores`);
|
|
38664
39038
|
}
|
|
39039
|
+
// Offset somado a cada leitura; leituras fora da faixa válida são descartadas (contadas)
|
|
39040
|
+
computeTemperatureStats(raw, offset, valid) {
|
|
39041
|
+
const series = [];
|
|
39042
|
+
let discarded = 0;
|
|
39043
|
+
for (const p of raw) {
|
|
39044
|
+
const v = p.value + offset;
|
|
39045
|
+
if (v < valid.min || v > valid.max) discarded++;
|
|
39046
|
+
else series.push({ timestamp: p.timestamp, value: v });
|
|
39047
|
+
}
|
|
39048
|
+
if (!series.length) return { avg: null, min: null, max: null, readings: 0, discarded, series };
|
|
39049
|
+
let sum2 = 0;
|
|
39050
|
+
let min = Infinity;
|
|
39051
|
+
let max = -Infinity;
|
|
39052
|
+
for (const p of series) {
|
|
39053
|
+
sum2 += p.value;
|
|
39054
|
+
if (p.value < min) min = p.value;
|
|
39055
|
+
if (p.value > max) max = p.value;
|
|
39056
|
+
}
|
|
39057
|
+
return { avg: sum2 / series.length, min, max, readings: series.length, discarded, series };
|
|
39058
|
+
}
|
|
38665
39059
|
// Re-map the cached API response under the current exclusion flag and refresh the UI.
|
|
38666
39060
|
// No-op until data has been loaded at least once.
|
|
38667
39061
|
remapAndRender() {
|
|
@@ -38760,12 +39154,17 @@ var AllReportModal = class {
|
|
|
38760
39154
|
}
|
|
38761
39155
|
if (!this.params.itemsList) {
|
|
38762
39156
|
this.debugLog("\u{1F4CB} No itemsList provided \u2014 mapping directly from API array");
|
|
38763
|
-
return apiArray.map(
|
|
38764
|
-
|
|
38765
|
-
|
|
38766
|
-
|
|
38767
|
-
|
|
38768
|
-
|
|
39157
|
+
return apiArray.map(
|
|
39158
|
+
(item) => this.decorateTemperatureRow(
|
|
39159
|
+
{
|
|
39160
|
+
identifier: item.assetName || this.resolveStoreIdentifierFromApi(item) || item.id || "",
|
|
39161
|
+
name: item.name || item.assetName || item.id || "",
|
|
39162
|
+
consumption: this.pickConsumption(item),
|
|
39163
|
+
...item.id ? { id: String(item.id) } : {}
|
|
39164
|
+
},
|
|
39165
|
+
item
|
|
39166
|
+
)
|
|
39167
|
+
);
|
|
38769
39168
|
}
|
|
38770
39169
|
const orchIdSet = new Set(this.params.itemsList.map((item) => String(item.id)));
|
|
38771
39170
|
const orchMeta = new Map(this.params.itemsList.map((item) => [String(item.id), item]));
|
|
@@ -38782,14 +39181,17 @@ var AllReportModal = class {
|
|
|
38782
39181
|
continue;
|
|
38783
39182
|
}
|
|
38784
39183
|
const consumption = Math.round(this.pickConsumption(apiItem) * 100) / 100;
|
|
38785
|
-
const result =
|
|
38786
|
-
|
|
38787
|
-
|
|
38788
|
-
|
|
38789
|
-
|
|
38790
|
-
|
|
38791
|
-
|
|
38792
|
-
|
|
39184
|
+
const result = this.decorateTemperatureRow(
|
|
39185
|
+
{
|
|
39186
|
+
identifier: meta?.identifier || apiItem.name || apiId,
|
|
39187
|
+
name: meta?.label || apiItem.name || apiId,
|
|
39188
|
+
consumption,
|
|
39189
|
+
id: apiId,
|
|
39190
|
+
...meta?.groupLabel ? { groupLabel: meta.groupLabel } : {}
|
|
39191
|
+
},
|
|
39192
|
+
apiItem
|
|
39193
|
+
);
|
|
39194
|
+
totalMappedConsumption += result.consumption;
|
|
38793
39195
|
rows.push(result);
|
|
38794
39196
|
}
|
|
38795
39197
|
this.debugLog("[AllReportModal] API-driven filter", {
|
|
@@ -38799,6 +39201,21 @@ var AllReportModal = class {
|
|
|
38799
39201
|
});
|
|
38800
39202
|
return rows;
|
|
38801
39203
|
}
|
|
39204
|
+
// Temperatura: média null/NaN → "Sem leitura" (nunca 0 °C); anexa mín/máx/contagens
|
|
39205
|
+
decorateTemperatureRow(row, apiItem) {
|
|
39206
|
+
if (!this.isTemperature) return row;
|
|
39207
|
+
const avg = apiItem?.total_value;
|
|
39208
|
+
const hasAvg = avg !== null && avg !== void 0 && Number.isFinite(Number(avg));
|
|
39209
|
+
const stats = row.id ? this.temperatureStats.get(row.id) : void 0;
|
|
39210
|
+
return {
|
|
39211
|
+
...row,
|
|
39212
|
+
consumption: hasAvg ? Math.round(Number(avg) * 100) / 100 : 0,
|
|
39213
|
+
noData: !hasAvg,
|
|
39214
|
+
...stats && stats.min !== null ? { min: Math.round(stats.min * 100) / 100 } : {},
|
|
39215
|
+
...stats && stats.max !== null ? { max: Math.round(stats.max * 100) / 100 } : {},
|
|
39216
|
+
...stats ? { readings: stats.readings, discarded: stats.discarded } : {}
|
|
39217
|
+
};
|
|
39218
|
+
}
|
|
38802
39219
|
parseConsumptionValue(item) {
|
|
38803
39220
|
const possibleFields = ["total_value", "totalValue", "consumption", "value", "total", "energy", "kwh"];
|
|
38804
39221
|
for (const field of possibleFields) {
|
|
@@ -78966,7 +79383,7 @@ async function openTemperatureModal(params) {
|
|
|
78966
79383
|
const savedGranularity = localStorage.getItem("myio-temp-modal-granularity");
|
|
78967
79384
|
const savedTheme = localStorage.getItem("myio-temp-modal-theme");
|
|
78968
79385
|
if (savedGranularity) state6.granularity = savedGranularity;
|
|
78969
|
-
if (savedTheme) state6.theme = savedTheme;
|
|
79386
|
+
if (savedTheme && !params.theme) state6.theme = savedTheme;
|
|
78970
79387
|
const modalContainer = document.createElement("div");
|
|
78971
79388
|
modalContainer.id = modalId;
|
|
78972
79389
|
document.body.appendChild(modalContainer);
|
|
@@ -79677,7 +80094,7 @@ async function openTemperatureComparisonModal(params) {
|
|
|
79677
80094
|
startTs,
|
|
79678
80095
|
endTs,
|
|
79679
80096
|
granularity: params.granularity || "hour",
|
|
79680
|
-
theme: params.theme || "
|
|
80097
|
+
theme: params.theme || "light",
|
|
79681
80098
|
clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
|
|
79682
80099
|
locale: params.locale || "pt-BR",
|
|
79683
80100
|
deviceData: [],
|
|
@@ -79691,7 +80108,7 @@ async function openTemperatureComparisonModal(params) {
|
|
|
79691
80108
|
const savedGranularity = localStorage.getItem("myio-temp-comparison-granularity");
|
|
79692
80109
|
const savedTheme = localStorage.getItem("myio-temp-comparison-theme");
|
|
79693
80110
|
if (savedGranularity) state6.granularity = savedGranularity;
|
|
79694
|
-
if (savedTheme) state6.theme = savedTheme;
|
|
80111
|
+
if (savedTheme && !params.theme) state6.theme = savedTheme;
|
|
79695
80112
|
const modalContainer = document.createElement("div");
|
|
79696
80113
|
modalContainer.id = modalId;
|
|
79697
80114
|
document.body.appendChild(modalContainer);
|