myio-js-library 0.1.548 → 0.1.550
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 +523 -91
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +523 -91
- package/dist/myio-js-library.umd.js +523 -91
- 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.550",
|
|
903
903
|
description: "A clean, standalone JS SDK for MYIO projects",
|
|
904
904
|
license: "MIT",
|
|
905
905
|
repository: "github:gh-myio/myio-js-library",
|
|
@@ -30342,8 +30342,8 @@ function normalizeToSaoPauloISO(dateLike, endOfDay = false) {
|
|
|
30342
30342
|
if (typeof dateLike === "string") {
|
|
30343
30343
|
ymd = dateLike.split("T")[0];
|
|
30344
30344
|
} else {
|
|
30345
|
-
const
|
|
30346
|
-
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);
|
|
30347
30347
|
const y = d.getUTCFullYear();
|
|
30348
30348
|
const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
30349
30349
|
const da = String(d.getUTCDate()).padStart(2, "0");
|
|
@@ -33564,10 +33564,12 @@ function makeCols(unit, colOpts) {
|
|
|
33564
33564
|
{ key: "idx", label: "#", pdfW: 10 },
|
|
33565
33565
|
{ key: "nome", label: colOpts?.nameLabel || "Nome", pdfW: 100 },
|
|
33566
33566
|
{ key: "identificador", label: "Identificador", pdfW: 60 },
|
|
33567
|
-
{ key: "consumo", label: unit ? `Consumo (${unit})` : "Consumo", pdfW: 50 },
|
|
33567
|
+
{ key: "consumo", label: colOpts?.valueLabel || (unit ? `Consumo (${unit})` : "Consumo"), pdfW: 50 },
|
|
33568
33568
|
{ key: "perc", label: "%", pdfW: 20 }
|
|
33569
33569
|
];
|
|
33570
|
-
return
|
|
33570
|
+
return cols.filter(
|
|
33571
|
+
(c) => !(colOpts?.hideIdentifier && c.key === "identificador") && !(colOpts?.hidePerc && c.key === "perc")
|
|
33572
|
+
);
|
|
33571
33573
|
}
|
|
33572
33574
|
function buildRow(d, idx) {
|
|
33573
33575
|
const fmtVal = () => {
|
|
@@ -35155,13 +35157,60 @@ var DeviceReportModal = class {
|
|
|
35155
35157
|
participationChart = null;
|
|
35156
35158
|
// Período do último load — alimenta os headers dos exports PDF/XLS.
|
|
35157
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
|
+
}
|
|
35158
35206
|
show() {
|
|
35159
35207
|
this.modal = createModal({
|
|
35160
|
-
title:
|
|
35208
|
+
title: this.buildHeaderTitleHTML(),
|
|
35161
35209
|
width: "80vw",
|
|
35162
35210
|
height: "90vh",
|
|
35163
35211
|
theme: this.params.ui?.theme || "light"
|
|
35164
35212
|
});
|
|
35213
|
+
this.bindCopyDeviceName();
|
|
35165
35214
|
this.renderContent();
|
|
35166
35215
|
this.mountFooter();
|
|
35167
35216
|
this.modal.on("close", () => {
|
|
@@ -35453,7 +35502,8 @@ var DeviceReportModal = class {
|
|
|
35453
35502
|
baseUrl: this.params.api.dataApiBaseUrl || "https://api.data.apps.myio-bas.com",
|
|
35454
35503
|
ingestionId: this.params.ingestionId,
|
|
35455
35504
|
startISO,
|
|
35456
|
-
endISO
|
|
35505
|
+
endISO,
|
|
35506
|
+
granularity: this.granularity
|
|
35457
35507
|
});
|
|
35458
35508
|
this.data = this.processApiResponse(apiResponse, dateRange);
|
|
35459
35509
|
this.renderSummary();
|
|
@@ -35483,10 +35533,25 @@ var DeviceReportModal = class {
|
|
|
35483
35533
|
if (!Array.isArray(dataArray) || dataArray.length === 0) {
|
|
35484
35534
|
console.warn("[DeviceReportModal] API returned empty or invalid response, zero-filling date range");
|
|
35485
35535
|
if (isHourly) return [];
|
|
35486
|
-
|
|
35536
|
+
const noData = this.domainConfig.summaryType === "average";
|
|
35537
|
+
return dateRange.map((date) => ({ date, consumption: 0, ...noData ? { noData: true } : {} }));
|
|
35487
35538
|
}
|
|
35488
35539
|
const deviceData = dataArray[0];
|
|
35489
35540
|
const consumption = deviceData.consumption || [];
|
|
35541
|
+
if (!isHourly && this.domainConfig.summaryType === "average") {
|
|
35542
|
+
const acc = {};
|
|
35543
|
+
consumption.forEach((item) => {
|
|
35544
|
+
if (item.timestamp && item.value != null && Number.isFinite(Number(item.value))) {
|
|
35545
|
+
const date = String(item.timestamp).slice(0, 10);
|
|
35546
|
+
const a = acc[date] || (acc[date] = { sum: 0, n: 0 });
|
|
35547
|
+
a.sum += Number(item.value);
|
|
35548
|
+
a.n += 1;
|
|
35549
|
+
}
|
|
35550
|
+
});
|
|
35551
|
+
return dateRange.map(
|
|
35552
|
+
(date) => acc[date] ? { date, consumption: acc[date].sum / acc[date].n } : { date, consumption: 0, noData: true }
|
|
35553
|
+
);
|
|
35554
|
+
}
|
|
35490
35555
|
if (isHourly) {
|
|
35491
35556
|
return consumption.filter((item) => item.timestamp && item.value != null).map((item) => ({
|
|
35492
35557
|
date: item.timestamp,
|
|
@@ -35548,14 +35613,15 @@ var DeviceReportModal = class {
|
|
|
35548
35613
|
const isHourly = this.granularity === "1h";
|
|
35549
35614
|
const isTemperature = this.domainConfig.summaryType === "average";
|
|
35550
35615
|
const total = this.calculateTotal();
|
|
35551
|
-
const
|
|
35552
|
-
const
|
|
35616
|
+
const rows = this.validRows;
|
|
35617
|
+
const summaryValue = isTemperature ? rows.length > 0 ? total / rows.length : 0 : total;
|
|
35618
|
+
const dayKeys = new Set(rows.map((r) => r.date.slice(0, 10)));
|
|
35553
35619
|
const dayCount = Math.max(1, dayKeys.size);
|
|
35554
|
-
const maxRow =
|
|
35620
|
+
const maxRow = rows.reduce(
|
|
35555
35621
|
(best, r) => !best || r.consumption > best.consumption ? r : best,
|
|
35556
35622
|
null
|
|
35557
35623
|
);
|
|
35558
|
-
const minPool = isTemperature ?
|
|
35624
|
+
const minPool = isTemperature ? rows : rows.filter((r) => r.consumption > 0);
|
|
35559
35625
|
const minRow = minPool.reduce(
|
|
35560
35626
|
(best, r) => !best || r.consumption < best.consumption ? r : best,
|
|
35561
35627
|
null
|
|
@@ -35567,7 +35633,7 @@ var DeviceReportModal = class {
|
|
|
35567
35633
|
{ value: fmt2(total / dayCount), label: `M\xE9dia por Dia (${unit})` }
|
|
35568
35634
|
];
|
|
35569
35635
|
if (isHourly) {
|
|
35570
|
-
kpis.push({ value: fmt2(total /
|
|
35636
|
+
kpis.push({ value: fmt2(rows.length ? total / rows.length : 0), label: `M\xE9dia por Hora (${unit})` });
|
|
35571
35637
|
}
|
|
35572
35638
|
kpis.push(
|
|
35573
35639
|
{
|
|
@@ -35583,6 +35649,9 @@ var DeviceReportModal = class {
|
|
|
35583
35649
|
);
|
|
35584
35650
|
if (!isTemperature) {
|
|
35585
35651
|
kpis.push({ value: String(zeroCount), label: isHourly ? "Horas sem Consumo" : "Dias sem Consumo" });
|
|
35652
|
+
} else {
|
|
35653
|
+
const noData = this.data.length - rows.length;
|
|
35654
|
+
if (noData > 0) kpis.push({ value: String(noData), label: isHourly ? "Horas sem Leitura" : "Dias sem Leitura" });
|
|
35586
35655
|
}
|
|
35587
35656
|
return kpis;
|
|
35588
35657
|
}
|
|
@@ -35668,7 +35737,7 @@ var DeviceReportModal = class {
|
|
|
35668
35737
|
${this.data.map((row) => `
|
|
35669
35738
|
<tr>
|
|
35670
35739
|
<td>${this.formatDate(row.date)}</td>
|
|
35671
|
-
<td style="text-align: right;">${this.domainConfig.formatter(row.consumption)}</td>
|
|
35740
|
+
<td style="text-align: right;${row.noData ? " color: var(--myio-text-muted);" : ""}">${row.noData ? "Sem leitura" : this.domainConfig.formatter(row.consumption)}</td>
|
|
35672
35741
|
</tr>
|
|
35673
35742
|
`).join("")}
|
|
35674
35743
|
</tbody>
|
|
@@ -35699,13 +35768,18 @@ var DeviceReportModal = class {
|
|
|
35699
35768
|
if (key === "date") {
|
|
35700
35769
|
comparison = new Date(a.date).getTime() - new Date(b.date).getTime();
|
|
35701
35770
|
} else {
|
|
35771
|
+
if (!!a.noData !== !!b.noData) return a.noData ? 1 : -1;
|
|
35702
35772
|
comparison = a.consumption - b.consumption;
|
|
35703
35773
|
}
|
|
35704
35774
|
return this.sortState.direction === "desc" ? -comparison : comparison;
|
|
35705
35775
|
});
|
|
35706
35776
|
}
|
|
35777
|
+
// Linhas com leitura (temperatura: dia sem leitura fica fora de totais/médias/KPIs)
|
|
35778
|
+
get validRows() {
|
|
35779
|
+
return this.data.filter((r) => !r.noData);
|
|
35780
|
+
}
|
|
35707
35781
|
calculateTotal() {
|
|
35708
|
-
return this.
|
|
35782
|
+
return this.validRows.reduce((sum2, row) => sum2 + row.consumption, 0);
|
|
35709
35783
|
}
|
|
35710
35784
|
formatDate(dateStr) {
|
|
35711
35785
|
if (!dateStr) return "";
|
|
@@ -35718,7 +35792,8 @@ var DeviceReportModal = class {
|
|
|
35718
35792
|
}
|
|
35719
35793
|
exportCSV() {
|
|
35720
35794
|
const total = this.calculateTotal();
|
|
35721
|
-
const
|
|
35795
|
+
const valid = this.validRows.length;
|
|
35796
|
+
const summaryValue = this.domainConfig.summaryType === "average" ? valid > 0 ? total / valid : 0 : total;
|
|
35722
35797
|
const now = /* @__PURE__ */ new Date();
|
|
35723
35798
|
const timestamp = now.toLocaleDateString("pt-BR") + " - " + now.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
35724
35799
|
const csvData = [
|
|
@@ -35726,7 +35801,7 @@ var DeviceReportModal = class {
|
|
|
35726
35801
|
["DATA EMISS\xC3O", timestamp, ""],
|
|
35727
35802
|
[this.domainConfig.summaryLabel, this.domainConfig.formatter(summaryValue), this.domainConfig.unit],
|
|
35728
35803
|
[this.granularity === "1h" ? "Data/Hora" : "Data", this.domainConfig.label, ""],
|
|
35729
|
-
...this.data.map((row) => [this.formatDate(row.date), this.domainConfig.formatter(row.consumption)])
|
|
35804
|
+
...this.data.map((row) => [this.formatDate(row.date), row.noData ? "Sem leitura" : this.domainConfig.formatter(row.consumption)])
|
|
35730
35805
|
];
|
|
35731
35806
|
const csvContent = toCsv(csvData);
|
|
35732
35807
|
const granSuffix = this.granularity === "1h" ? "-1h" : "";
|
|
@@ -35757,7 +35832,7 @@ var DeviceReportModal = class {
|
|
|
35757
35832
|
return this.data.map((row) => ({
|
|
35758
35833
|
labelOrName: this.formatDate(row.date),
|
|
35759
35834
|
name: this.formatDate(row.date),
|
|
35760
|
-
val: row.consumption,
|
|
35835
|
+
val: row.noData ? null : row.consumption,
|
|
35761
35836
|
...isTemperature || total <= 0 ? {} : { perc: row.consumption / total * 100 }
|
|
35762
35837
|
}));
|
|
35763
35838
|
}
|
|
@@ -37590,6 +37665,11 @@ function createMoneyVarianceColumn(config2) {
|
|
|
37590
37665
|
}
|
|
37591
37666
|
|
|
37592
37667
|
// src/components/premium-modals/report-all/AllReportModal.ts
|
|
37668
|
+
var SP_OFFSET_MS = 3 * 60 * 60 * 1e3;
|
|
37669
|
+
var spDayStart = (ts) => {
|
|
37670
|
+
const d = new Date(ts - SP_OFFSET_MS);
|
|
37671
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) + SP_OFFSET_MS;
|
|
37672
|
+
};
|
|
37593
37673
|
var DOMAIN_CONFIG2 = {
|
|
37594
37674
|
energy: {
|
|
37595
37675
|
endpoint: "energy",
|
|
@@ -37620,6 +37700,7 @@ var AllReportModal = class {
|
|
|
37620
37700
|
});
|
|
37621
37701
|
const domain = params.domain || "energy";
|
|
37622
37702
|
this.domainConfig = DOMAIN_CONFIG2[domain];
|
|
37703
|
+
this.granularity = params.granularity ?? (domain === "temperature" ? "1h" : "1d");
|
|
37623
37704
|
this.debugEnabled = params.debug === 1;
|
|
37624
37705
|
this.moneyColumn = createReportMoneyColumn(params.money ?? null);
|
|
37625
37706
|
if (this.moneyColumn.enabled && typeof document !== "undefined") {
|
|
@@ -37666,6 +37747,12 @@ var AllReportModal = class {
|
|
|
37666
37747
|
domainConfig;
|
|
37667
37748
|
// Granularity: '1d' (daily) | '1h' (hourly)
|
|
37668
37749
|
granularity = "1d";
|
|
37750
|
+
// Temperatura: estatísticas por sensor (key = ingestionId) da última carga
|
|
37751
|
+
temperatureStats = /* @__PURE__ */ new Map();
|
|
37752
|
+
// Linhas expandidas (série do device na granularidade escolhida) — key = row.id
|
|
37753
|
+
expandedIds = /* @__PURE__ */ new Set();
|
|
37754
|
+
// Série por device p/ energia/água (expansão) — key = `${granularity}|${id}`, por período
|
|
37755
|
+
deviceSeriesCache = /* @__PURE__ */ new Map();
|
|
37669
37756
|
// Hourly series fetched device-by-device for the 1h CSV export (the /totals endpoint
|
|
37670
37757
|
// has no hourly granularity). Keyed by StoreReading.id; cache key = period, so a new
|
|
37671
37758
|
// load or granularity switch invalidates it.
|
|
@@ -37699,6 +37786,36 @@ var AllReportModal = class {
|
|
|
37699
37786
|
console.log(`[AllReportModal DEBUG] ${message}`, data || "");
|
|
37700
37787
|
}
|
|
37701
37788
|
}
|
|
37789
|
+
get isTemperature() {
|
|
37790
|
+
return (this.params.domain || "energy") === "temperature";
|
|
37791
|
+
}
|
|
37792
|
+
// Faixa de leituras válidas (°C, já com offset) — fora dela a leitura é descartada
|
|
37793
|
+
get temperatureValidRange() {
|
|
37794
|
+
const r = this.params.temperatureValidRange;
|
|
37795
|
+
return r && Number.isFinite(r.min) && Number.isFinite(r.max) ? r : DEFAULT_CLAMP_RANGE;
|
|
37796
|
+
}
|
|
37797
|
+
get temperatureIdealRange() {
|
|
37798
|
+
const r = this.params.temperatureIdealRange;
|
|
37799
|
+
return r && Number.isFinite(Number(r.min)) && Number.isFinite(Number(r.max)) ? { min: Number(r.min), max: Number(r.max) } : null;
|
|
37800
|
+
}
|
|
37801
|
+
// offSetTemperature do sensor (vem do itemsList do MENU); 0 quando ausente
|
|
37802
|
+
temperatureOffsetFor(id) {
|
|
37803
|
+
const item = this.params.itemsList?.find((i) => String(i.id) === String(id));
|
|
37804
|
+
const off = Number(item?.temperatureOffset ?? 0);
|
|
37805
|
+
return Number.isFinite(off) ? off : 0;
|
|
37806
|
+
}
|
|
37807
|
+
// Identificador genérico (ex.: todo sensor de temperatura tem identifier "Temperatura")
|
|
37808
|
+
// não identifica nada → a coluna só aparece se algum device tiver um código de verdade.
|
|
37809
|
+
showIdentifierColumn(rows = this.data) {
|
|
37810
|
+
if (!this.isTemperature) return true;
|
|
37811
|
+
return rows.some((r) => {
|
|
37812
|
+
const id = String(r.identifier || "").trim();
|
|
37813
|
+
return !!id && !/^temperatura$/i.test(id) && id !== r.name;
|
|
37814
|
+
});
|
|
37815
|
+
}
|
|
37816
|
+
fmtTemp(v) {
|
|
37817
|
+
return v === null || v === void 0 || !Number.isFinite(v) ? "\u2014" : fmtPt(v);
|
|
37818
|
+
}
|
|
37702
37819
|
// Helper: normalize identifiers (upper, strip spaces and non-alphanum)
|
|
37703
37820
|
normalizeId(v) {
|
|
37704
37821
|
return (v || "").toString().normalize("NFKC").toUpperCase().replace(/\s+/g, "").replace(/[0300-036f]/g, "");
|
|
@@ -37964,12 +38081,13 @@ var AllReportModal = class {
|
|
|
37964
38081
|
tooltip: {
|
|
37965
38082
|
enabled: true,
|
|
37966
38083
|
title: "Granularidade",
|
|
37967
|
-
text: "
|
|
38084
|
+
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."
|
|
37968
38085
|
}
|
|
37969
38086
|
},
|
|
37970
38087
|
onChange: (value) => {
|
|
37971
38088
|
this.granularity = value;
|
|
37972
38089
|
this.hourlySeriesCache = null;
|
|
38090
|
+
if (this.data.length && this.expandedIds.size) this.renderTable();
|
|
37973
38091
|
}
|
|
37974
38092
|
});
|
|
37975
38093
|
}
|
|
@@ -38034,6 +38152,9 @@ var AllReportModal = class {
|
|
|
38034
38152
|
this.debugLog("\u{1F4C5} Date range selected", { startISO, endISO });
|
|
38035
38153
|
this.exportPeriod = { startISO, endISO };
|
|
38036
38154
|
this.hourlySeriesCache = null;
|
|
38155
|
+
this.deviceSeriesCache.clear();
|
|
38156
|
+
this.temperatureStats.clear();
|
|
38157
|
+
this.expandedIds.clear();
|
|
38037
38158
|
if (!startISO || !endISO) {
|
|
38038
38159
|
this.showError("Selecione um per\xEDodo v\xE1lido");
|
|
38039
38160
|
return;
|
|
@@ -38098,6 +38219,7 @@ var AllReportModal = class {
|
|
|
38098
38219
|
});
|
|
38099
38220
|
}
|
|
38100
38221
|
return filtered.sort((a, b) => {
|
|
38222
|
+
if (!!a.noData !== !!b.noData) return a.noData ? 1 : -1;
|
|
38101
38223
|
const aVal = a[this.sortField];
|
|
38102
38224
|
const bVal = b[this.sortField];
|
|
38103
38225
|
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
@@ -38130,6 +38252,7 @@ var AllReportModal = class {
|
|
|
38130
38252
|
// Máximo/Mínimo com nome do device; mínimo só entre devices COM consumo;
|
|
38131
38253
|
// "Sem Consumo" oculto para temperature (média °C não tem semântica de zero).
|
|
38132
38254
|
computeKpis() {
|
|
38255
|
+
if (this.isTemperature) return this.computeTemperatureKpis();
|
|
38133
38256
|
const totalConsumption = this.calculateTotalConsumption();
|
|
38134
38257
|
const storeCount = Math.max(1, this.data.length);
|
|
38135
38258
|
const maxRow = this.data.reduce(
|
|
@@ -38162,6 +38285,51 @@ var AllReportModal = class {
|
|
|
38162
38285
|
if (moneyKpi) kpis.push(moneyKpi);
|
|
38163
38286
|
return kpis;
|
|
38164
38287
|
}
|
|
38288
|
+
// Temperatura: média é MÉDIA (nunca soma); só sensores com leitura válida entram.
|
|
38289
|
+
computeTemperatureKpis() {
|
|
38290
|
+
const withData = this.data.filter((r) => !r.noData);
|
|
38291
|
+
const noData = this.data.length - withData.length;
|
|
38292
|
+
const avg = withData.length ? withData.reduce((s, r) => s + r.consumption, 0) / withData.length : null;
|
|
38293
|
+
const maxRow = withData.reduce((b, r) => !b || r.consumption > b.consumption ? r : b, null);
|
|
38294
|
+
const minRow = withData.reduce((b, r) => !b || r.consumption < b.consumption ? r : b, null);
|
|
38295
|
+
const kpis = [
|
|
38296
|
+
{
|
|
38297
|
+
value: String(this.data.length),
|
|
38298
|
+
label: "Sensores",
|
|
38299
|
+
...noData ? { sub: `${noData} sem leitura no per\xEDodo` } : {}
|
|
38300
|
+
},
|
|
38301
|
+
{ value: avg === null ? "\u2014" : `${fmtPt(avg)} \xB0C`, label: "M\xE9dia geral", sub: "m\xE9dia das m\xE9dias dos sensores" },
|
|
38302
|
+
{
|
|
38303
|
+
value: maxRow ? `${fmtPt(maxRow.consumption)} \xB0C` : "\u2014",
|
|
38304
|
+
label: "Maior m\xE9dia",
|
|
38305
|
+
...maxRow?.name ? { sub: maxRow.name } : {}
|
|
38306
|
+
},
|
|
38307
|
+
{
|
|
38308
|
+
value: minRow ? `${fmtPt(minRow.consumption)} \xB0C` : "\u2014",
|
|
38309
|
+
label: "Menor m\xE9dia",
|
|
38310
|
+
...minRow?.name ? { sub: minRow.name } : {}
|
|
38311
|
+
}
|
|
38312
|
+
];
|
|
38313
|
+
const ideal = this.temperatureIdealRange;
|
|
38314
|
+
if (ideal) {
|
|
38315
|
+
const out = withData.filter((r) => r.consumption < ideal.min || r.consumption > ideal.max).length;
|
|
38316
|
+
kpis.push({
|
|
38317
|
+
value: String(out),
|
|
38318
|
+
label: "Fora da faixa ideal",
|
|
38319
|
+
sub: `faixa ${fmtPt(ideal.min)}\u2013${fmtPt(ideal.max)} \xB0C`
|
|
38320
|
+
});
|
|
38321
|
+
}
|
|
38322
|
+
const discarded = this.data.reduce((s, r) => s + (r.discarded || 0), 0);
|
|
38323
|
+
if (discarded > 0) {
|
|
38324
|
+
const v = this.temperatureValidRange;
|
|
38325
|
+
kpis.push({
|
|
38326
|
+
value: String(discarded),
|
|
38327
|
+
label: "Leituras descartadas",
|
|
38328
|
+
sub: `fora de ${fmtPt(v.min)}\u2013${fmtPt(v.max)} \xB0C`
|
|
38329
|
+
});
|
|
38330
|
+
}
|
|
38331
|
+
return kpis;
|
|
38332
|
+
}
|
|
38165
38333
|
// RFC-0228 A6 — the "Custo projetado (R$)" summary KPI, or null when money is off.
|
|
38166
38334
|
// Reuses the A6 column's honest-coverage resolution: a formatted R$ total only when
|
|
38167
38335
|
// the overlay is confidently priced (DEC-8), otherwise a coverage state label.
|
|
@@ -38193,18 +38361,8 @@ var AllReportModal = class {
|
|
|
38193
38361
|
}
|
|
38194
38362
|
const isGrouped = paginatedData.some((r) => r.groupLabel);
|
|
38195
38363
|
const grandTotal = this.calculateTotalConsumption();
|
|
38196
|
-
const
|
|
38197
|
-
const
|
|
38198
|
-
const tableRows = isGrouped ? this.renderGroupedRows(paginatedData, grandTotal) : paginatedData.map(
|
|
38199
|
-
(row) => `
|
|
38200
|
-
<tr>
|
|
38201
|
-
<td data-label="Identificador" style="font-family: monospace; font-weight: bold; text-transform: uppercase;">${row.identifier}</td>
|
|
38202
|
-
<td data-label="Nome"><strong>${row.name}</strong></td>
|
|
38203
|
-
<td data-label="${this.domainConfig.label}" style="text-align: right; font-weight: bold;">${fmtPt(row.consumption)}</td>
|
|
38204
|
-
${moneyCell(row)}${this.varianceColumn.bodyCellHTML(row.id)}<td data-label="%" style="text-align: right; color: var(--myio-text-muted);">${pct(row.consumption)}</td>
|
|
38205
|
-
</tr>
|
|
38206
|
-
`
|
|
38207
|
-
).join("");
|
|
38364
|
+
const showId = this.showIdentifierColumn();
|
|
38365
|
+
const tableRows = isGrouped ? this.renderGroupedRows(paginatedData, grandTotal) : paginatedData.map((row) => this.renderRowHTML(row, grandTotal, showId)).join("");
|
|
38208
38366
|
container.innerHTML = `
|
|
38209
38367
|
<div style="max-height: 500px; overflow-y: auto; border: 1px solid var(--myio-border); border-radius: 6px;">
|
|
38210
38368
|
<style>
|
|
@@ -38224,6 +38382,21 @@ var AllReportModal = class {
|
|
|
38224
38382
|
color: var(--myio-primary, #1565c0);
|
|
38225
38383
|
margin-left: 8px;
|
|
38226
38384
|
}
|
|
38385
|
+
.rp-expand {
|
|
38386
|
+
border: none; background: transparent; cursor: pointer; padding: 0 6px 0 0;
|
|
38387
|
+
font-size: 12px; color: var(--myio-text-muted, #6b7280); line-height: 1;
|
|
38388
|
+
transition: transform .15s ease;
|
|
38389
|
+
}
|
|
38390
|
+
.rp-expand[aria-expanded="true"] { transform: rotate(90deg); color: var(--myio-brand-700, #3e1a7d); }
|
|
38391
|
+
.rp-series-row > td { background: var(--myio-bg, #f8fafc); padding: 8px 12px 12px 32px !important; }
|
|
38392
|
+
.rp-series { max-height: 260px; overflow-y: auto; font-size: 12px; }
|
|
38393
|
+
.rp-series table { width: 100%; border-collapse: collapse; }
|
|
38394
|
+
.rp-series th, .rp-series td { padding: 3px 8px; border-bottom: 1px solid var(--myio-border, #e5e7eb); }
|
|
38395
|
+
.rp-series th { text-align: left; font-weight: 600; color: var(--myio-text-muted, #6b7280); position: sticky; top: 0; background: var(--myio-bg, #f8fafc); }
|
|
38396
|
+
.rp-series td.num, .rp-series th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
38397
|
+
.rp-muted { color: var(--myio-text-muted, #9ca3af); font-weight: 500; }
|
|
38398
|
+
.rp-temp-above { color: #dc2626; }
|
|
38399
|
+
.rp-temp-below { color: #2563eb; }
|
|
38227
38400
|
@media (max-width: 768px) {
|
|
38228
38401
|
.myio-table-mobile { display: block !important; }
|
|
38229
38402
|
.myio-table-mobile thead,
|
|
@@ -38239,29 +38412,145 @@ var AllReportModal = class {
|
|
|
38239
38412
|
</style>
|
|
38240
38413
|
<table class="myio-table myio-table-mobile" style="table-layout: fixed; width: 100%;">
|
|
38241
38414
|
<thead style="position: sticky; top: 0; background: var(--myio-bg); z-index: 1;">
|
|
38242
|
-
<tr>
|
|
38243
|
-
<th style="cursor: pointer; width: 22%;" data-sort="identifier">
|
|
38244
|
-
Identificador
|
|
38245
|
-
<span style="margin-left: 4px; opacity: ${this.getSortOpacity("identifier")};">${this.getSortIcon("identifier")}</span>
|
|
38246
|
-
</th>
|
|
38247
|
-
<th style="cursor: pointer; width: 40%;" data-sort="name">
|
|
38248
|
-
Nome
|
|
38249
|
-
<span style="margin-left: 4px; opacity: ${this.getSortOpacity("name")};">${this.getSortIcon("name")}</span>
|
|
38250
|
-
</th>
|
|
38251
|
-
<th style="cursor: pointer; text-align: right; width: 24%;" data-sort="consumption">
|
|
38252
|
-
${this.domainConfig.label}
|
|
38253
|
-
<span style="margin-left: 4px; opacity: ${this.getSortOpacity("consumption")};">${this.getSortIcon("consumption")}</span>
|
|
38254
|
-
</th>
|
|
38255
|
-
${this.moneyColumn.headerCellHTML()}${this.varianceColumn.headerCellHTML()}<th style="text-align: right; width: 14%;">%</th>
|
|
38256
|
-
</tr>
|
|
38415
|
+
<tr>${this.renderHeaderCellsHTML(showId)}</tr>
|
|
38257
38416
|
</thead>
|
|
38258
38417
|
<tbody>${tableRows}</tbody>
|
|
38259
38418
|
</table>
|
|
38260
38419
|
</div>
|
|
38261
38420
|
`;
|
|
38262
38421
|
this.setupTableSorting();
|
|
38422
|
+
this.setupRowExpansion(container);
|
|
38263
38423
|
this.updateParticipationChart();
|
|
38264
38424
|
}
|
|
38425
|
+
sortableTh(field, label, style) {
|
|
38426
|
+
return `<th style="cursor: pointer; ${style}" data-sort="${field}">
|
|
38427
|
+
${label}
|
|
38428
|
+
<span style="margin-left: 4px; opacity: ${this.getSortOpacity(field)};">${this.getSortIcon(field)}</span>
|
|
38429
|
+
</th>`;
|
|
38430
|
+
}
|
|
38431
|
+
// Colunas: energia/água = [Identificador] Nome | Consumo | (R$) | % ;
|
|
38432
|
+
// temperatura = [Identificador] Nome | Média | Mín | Máx (sem % — participação não se aplica).
|
|
38433
|
+
renderHeaderCellsHTML(showId) {
|
|
38434
|
+
const idTh = showId ? this.sortableTh("identifier", "Identificador", "width: 22%;") : "";
|
|
38435
|
+
if (this.isTemperature) {
|
|
38436
|
+
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%;");
|
|
38437
|
+
}
|
|
38438
|
+
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>`;
|
|
38439
|
+
}
|
|
38440
|
+
columnCount(showId) {
|
|
38441
|
+
const base = (showId ? 1 : 0) + 1;
|
|
38442
|
+
if (this.isTemperature) return base + 3;
|
|
38443
|
+
return base + 2 + (this.moneyColumn.enabled ? 1 : 0) + (this.varianceColumn.enabled ? 1 : 0);
|
|
38444
|
+
}
|
|
38445
|
+
renderRowHTML(row, grandTotal, showId) {
|
|
38446
|
+
const expandable = !!row.id;
|
|
38447
|
+
const expanded = expandable && this.expandedIds.has(row.id);
|
|
38448
|
+
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>` : "";
|
|
38449
|
+
const idTd = showId ? `<td data-label="Identificador" style="font-family: monospace; font-weight: bold; text-transform: uppercase;">${row.identifier}</td>` : "";
|
|
38450
|
+
const nameTd = `<td data-label="Nome">${expandBtn}<strong>${row.name}</strong></td>`;
|
|
38451
|
+
let valueTds;
|
|
38452
|
+
if (this.isTemperature) {
|
|
38453
|
+
const ideal = this.temperatureIdealRange;
|
|
38454
|
+
const cls = !row.noData && ideal ? row.consumption > ideal.max ? "rp-temp-above" : row.consumption < ideal.min ? "rp-temp-below" : "" : "";
|
|
38455
|
+
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>`;
|
|
38456
|
+
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>`;
|
|
38457
|
+
} else {
|
|
38458
|
+
const pct = grandTotal > 0 ? `${fmtPt(row.consumption / grandTotal * 100)}%` : "\u2014";
|
|
38459
|
+
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>`;
|
|
38460
|
+
}
|
|
38461
|
+
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>` : "";
|
|
38462
|
+
return `<tr>${idTd}${nameTd}${valueTds}</tr>${seriesRow}`;
|
|
38463
|
+
}
|
|
38464
|
+
// ▸ no nome do device: abre/fecha a série (hora ou dia, conforme a granularidade)
|
|
38465
|
+
setupRowExpansion(container) {
|
|
38466
|
+
container.querySelectorAll(".rp-expand").forEach((btn) => {
|
|
38467
|
+
btn.addEventListener("click", (e) => {
|
|
38468
|
+
e.stopPropagation();
|
|
38469
|
+
const id = btn.dataset.expandId;
|
|
38470
|
+
if (!id) return;
|
|
38471
|
+
if (this.expandedIds.has(id)) this.expandedIds.delete(id);
|
|
38472
|
+
else this.expandedIds.add(id);
|
|
38473
|
+
this.renderTable();
|
|
38474
|
+
});
|
|
38475
|
+
});
|
|
38476
|
+
container.querySelectorAll(".rp-series-row").forEach((tr) => {
|
|
38477
|
+
const id = tr.dataset.seriesFor;
|
|
38478
|
+
const box = tr.querySelector(".rp-series");
|
|
38479
|
+
if (id && box) void this.fillSeries(id, box);
|
|
38480
|
+
});
|
|
38481
|
+
}
|
|
38482
|
+
async fillSeries(id, box) {
|
|
38483
|
+
const row = this.data.find((r) => r.id === id);
|
|
38484
|
+
const gran = this.granularity;
|
|
38485
|
+
try {
|
|
38486
|
+
const points = await this.getDeviceSeries(id, gran);
|
|
38487
|
+
if (!box.isConnected) return;
|
|
38488
|
+
if (!points.length) {
|
|
38489
|
+
box.innerHTML = `<span class="rp-muted">Sem dados ${gran === "1h" ? "hor\xE1rios" : "di\xE1rios"} no per\xEDodo.</span>`;
|
|
38490
|
+
return;
|
|
38491
|
+
}
|
|
38492
|
+
const fmtTs = (ts) => gran === "1h" ? new Date(ts).toLocaleString("pt-BR", {
|
|
38493
|
+
day: "2-digit",
|
|
38494
|
+
month: "2-digit",
|
|
38495
|
+
year: "numeric",
|
|
38496
|
+
hour: "2-digit",
|
|
38497
|
+
minute: "2-digit",
|
|
38498
|
+
timeZone: "America/Sao_Paulo"
|
|
38499
|
+
}) : new Date(ts).toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });
|
|
38500
|
+
const valueLabel = this.isTemperature ? gran === "1h" ? "Temperatura (\xB0C)" : "M\xE9dia do dia (\xB0C)" : this.domainConfig.label;
|
|
38501
|
+
box.innerHTML = `
|
|
38502
|
+
<table>
|
|
38503
|
+
<thead><tr><th>${gran === "1h" ? "Data/Hora" : "Dia"}</th><th class="num">${valueLabel}</th></tr></thead>
|
|
38504
|
+
<tbody>${points.map((p) => `<tr><td>${fmtTs(p.timestamp)}</td><td class="num">${fmtPt(p.value)}</td></tr>`).join("")}</tbody>
|
|
38505
|
+
</table>`;
|
|
38506
|
+
} catch (err) {
|
|
38507
|
+
this.debugLog("\u274C fillSeries", err);
|
|
38508
|
+
if (box.isConnected) {
|
|
38509
|
+
box.innerHTML = `<span class="rp-muted">N\xE3o foi poss\xEDvel carregar a s\xE9rie de ${row?.name || "dispositivo"}.</span>`;
|
|
38510
|
+
}
|
|
38511
|
+
}
|
|
38512
|
+
}
|
|
38513
|
+
// Série do device na granularidade pedida. Temperatura: da série horária já calculada
|
|
38514
|
+
// (offset + descarte), agregada por dia em 1d. Energia/água: endpoint por device.
|
|
38515
|
+
async getDeviceSeries(id, gran) {
|
|
38516
|
+
if (this.isTemperature) {
|
|
38517
|
+
const hourly = this.temperatureStats.get(id)?.series || [];
|
|
38518
|
+
return gran === "1h" ? hourly : this.aggregateDaily(hourly, "avg");
|
|
38519
|
+
}
|
|
38520
|
+
const key = `${gran}|${id}`;
|
|
38521
|
+
const cached = this.deviceSeriesCache.get(key);
|
|
38522
|
+
if (cached) return cached;
|
|
38523
|
+
const startISO = this.exportPeriod?.startISO;
|
|
38524
|
+
const endISO = this.exportPeriod?.endISO;
|
|
38525
|
+
const token = this.params.api.ingestionToken;
|
|
38526
|
+
const baseUrl = this.params.api.dataApiBaseUrl;
|
|
38527
|
+
if (!startISO || !endISO || !token || !baseUrl) throw new Error("API/per\xEDodo indispon\xEDvel");
|
|
38528
|
+
const url = `${baseUrl}/telemetry/devices/${id}/${this.domainConfig.endpoint}?startTime=${encodeURIComponent(startISO)}&endTime=${encodeURIComponent(endISO)}&granularity=${gran}&deep=0`;
|
|
38529
|
+
const res = await fetch(url, {
|
|
38530
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
|
|
38531
|
+
});
|
|
38532
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
38533
|
+
const body = await res.json();
|
|
38534
|
+
const points = this.parseSeries(body);
|
|
38535
|
+
this.deviceSeriesCache.set(key, points);
|
|
38536
|
+
return points;
|
|
38537
|
+
}
|
|
38538
|
+
parseSeries(body) {
|
|
38539
|
+
const ent = Array.isArray(body) ? body[0] : body;
|
|
38540
|
+
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);
|
|
38541
|
+
}
|
|
38542
|
+
// Agrega série horária por dia civil (São Paulo): média (temperatura) ou soma (consumo)
|
|
38543
|
+
aggregateDaily(points, mode) {
|
|
38544
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
38545
|
+
for (const p of points) {
|
|
38546
|
+
const day = spDayStart(p.timestamp);
|
|
38547
|
+
const acc = byDay.get(day) || { sum: 0, n: 0 };
|
|
38548
|
+
acc.sum += p.value;
|
|
38549
|
+
acc.n += 1;
|
|
38550
|
+
byDay.set(day, acc);
|
|
38551
|
+
}
|
|
38552
|
+
return [...byDay.entries()].sort((a, b) => a[0] - b[0]).map(([day, { sum: sum2, n }]) => ({ timestamp: day, value: mode === "avg" ? sum2 / n : sum2 }));
|
|
38553
|
+
}
|
|
38265
38554
|
// Resolve the chart palette from the host theme: createMyIOTheme exposes
|
|
38266
38555
|
// tones(n) — solid hex tones derived from the dashboard accent. When absent
|
|
38267
38556
|
// (or the accent is not hex), the component falls back to the MYIO palette.
|
|
@@ -38279,6 +38568,10 @@ var AllReportModal = class {
|
|
|
38279
38568
|
updateParticipationChart() {
|
|
38280
38569
|
const container = document.getElementById("participation-chart-container");
|
|
38281
38570
|
if (!container) return;
|
|
38571
|
+
if (this.isTemperature) {
|
|
38572
|
+
this.renderTemperatureChart(container);
|
|
38573
|
+
return;
|
|
38574
|
+
}
|
|
38282
38575
|
const items = this.getFilteredData().map((row) => ({
|
|
38283
38576
|
id: row.id || this.generateStoreId(row.identifier),
|
|
38284
38577
|
label: row.name,
|
|
@@ -38306,6 +38599,40 @@ var AllReportModal = class {
|
|
|
38306
38599
|
if (palette) this.participationChart.updateSettings({ palette });
|
|
38307
38600
|
this.participationChart.updateData(items);
|
|
38308
38601
|
}
|
|
38602
|
+
// Barras horizontais (HTML puro) da média por sensor, escala comum, faixa ideal sombreada
|
|
38603
|
+
renderTemperatureChart(container) {
|
|
38604
|
+
const rows = this.getFilteredData();
|
|
38605
|
+
if (!rows.length) return;
|
|
38606
|
+
const withData = rows.filter((r) => !r.noData);
|
|
38607
|
+
const ideal = this.temperatureIdealRange;
|
|
38608
|
+
const values = withData.length ? withData.map((r) => r.consumption) : [20, 30];
|
|
38609
|
+
const lo = Math.floor(Math.min(...values, ideal ? ideal.min : Infinity) - 1);
|
|
38610
|
+
const hi = Math.ceil(Math.max(...values, ideal ? ideal.max : -Infinity) + 1);
|
|
38611
|
+
const span = Math.max(1, hi - lo);
|
|
38612
|
+
const pos = (v) => `${((v - lo) / span * 100).toFixed(2)}%`;
|
|
38613
|
+
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>` : "";
|
|
38614
|
+
const barColor = (v) => ideal && v > ideal.max ? "#ef4444" : ideal && v < ideal.min ? "#3b82f6" : "var(--myio-brand-700, #3e1a7d)";
|
|
38615
|
+
const lines = rows.map((r) => {
|
|
38616
|
+
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;">
|
|
38617
|
+
${band}
|
|
38618
|
+
<div style="position:absolute;top:2px;bottom:2px;left:0;width:${pos(r.consumption)};background:${barColor(r.consumption)};border-radius:4px;opacity:.85;"></div>
|
|
38619
|
+
</div>`;
|
|
38620
|
+
return `
|
|
38621
|
+
<div style="display:grid;grid-template-columns:minmax(0,38%) 1fr 52px;gap:8px;align-items:center;margin-bottom:6px;font-size:12px;">
|
|
38622
|
+
<span title="${r.name}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${r.name}</span>
|
|
38623
|
+
${track}
|
|
38624
|
+
<span style="text-align:right;font-variant-numeric:tabular-nums;font-weight:600;">${r.noData ? "\u2014" : fmtPt(r.consumption)}</span>
|
|
38625
|
+
</div>`;
|
|
38626
|
+
}).join("");
|
|
38627
|
+
container.innerHTML = `
|
|
38628
|
+
<div style="border:1px solid var(--myio-border,#e5e7eb);border-radius:10px;padding:12px 14px;max-height:620px;overflow-y:auto;">
|
|
38629
|
+
<div style="font-weight:700;font-size:14px;margin-bottom:2px;">Temperatura m\xE9dia por sensor</div>
|
|
38630
|
+
<div style="font-size:11px;color:var(--myio-text-muted,#6b7280);margin-bottom:10px;">
|
|
38631
|
+
Escala ${fmtPt(lo)}\u2013${fmtPt(hi)} \xB0C${ideal ? ` \xB7 <span style="color:#16a34a;">faixa ideal ${fmtPt(ideal.min)}\u2013${fmtPt(ideal.max)} \xB0C</span>` : ""}
|
|
38632
|
+
</div>
|
|
38633
|
+
${lines}
|
|
38634
|
+
</div>`;
|
|
38635
|
+
}
|
|
38309
38636
|
// RFC-0182: Render rows grouped by groupLabel with section headers (Option B)
|
|
38310
38637
|
renderGroupedRows(rows, grandTotal) {
|
|
38311
38638
|
const groupOrder = [];
|
|
@@ -38318,27 +38645,26 @@ var AllReportModal = class {
|
|
|
38318
38645
|
}
|
|
38319
38646
|
byGroup.get(g).push(row);
|
|
38320
38647
|
}
|
|
38321
|
-
const
|
|
38648
|
+
const showId = this.showIdentifierColumn();
|
|
38322
38649
|
return groupOrder.map((groupLabel) => {
|
|
38323
38650
|
const items = byGroup.get(groupLabel);
|
|
38324
|
-
|
|
38651
|
+
let summary;
|
|
38652
|
+
if (this.isTemperature) {
|
|
38653
|
+
const withData = items.filter((r) => !r.noData);
|
|
38654
|
+
const avg = withData.length ? withData.reduce((s, r) => s + r.consumption, 0) / withData.length : null;
|
|
38655
|
+
summary = `${items.length} sensores \xB7 m\xE9dia ${this.fmtTemp(avg)} \xB0C`;
|
|
38656
|
+
} else {
|
|
38657
|
+
const groupTotal = items.reduce((s, r) => s + r.consumption, 0);
|
|
38658
|
+
summary = `${items.length} dispositivos \xB7 ${fmtPt(groupTotal)} ${this.domainConfig.unit}`;
|
|
38659
|
+
}
|
|
38325
38660
|
const header = `
|
|
38326
38661
|
<tr class="rp-group-header">
|
|
38327
|
-
<td colspan="${
|
|
38662
|
+
<td colspan="${this.columnCount(showId)}">
|
|
38328
38663
|
${groupLabel}
|
|
38329
|
-
<span class="rp-group-total">${
|
|
38664
|
+
<span class="rp-group-total">${summary}</span>
|
|
38330
38665
|
</td>
|
|
38331
38666
|
</tr>`;
|
|
38332
|
-
|
|
38333
|
-
(row) => `
|
|
38334
|
-
<tr>
|
|
38335
|
-
<td data-label="Identificador" style="font-family: monospace; font-weight: bold; text-transform: uppercase;">${row.identifier}</td>
|
|
38336
|
-
<td data-label="Nome"><strong>${row.name}</strong></td>
|
|
38337
|
-
<td data-label="${this.domainConfig.label}" style="text-align: right; font-weight: bold;">${fmtPt(row.consumption)}</td>
|
|
38338
|
-
${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>
|
|
38339
|
-
</tr>`
|
|
38340
|
-
).join("");
|
|
38341
|
-
return header + dataRows;
|
|
38667
|
+
return header + items.map((row) => this.renderRowHTML(row, grandTotal, showId)).join("");
|
|
38342
38668
|
}).join("");
|
|
38343
38669
|
}
|
|
38344
38670
|
getSortIcon(field) {
|
|
@@ -38446,6 +38772,10 @@ var AllReportModal = class {
|
|
|
38446
38772
|
}
|
|
38447
38773
|
async exportCSV() {
|
|
38448
38774
|
const sortedData = [...this.data].sort((a, b) => b.consumption - a.consumption);
|
|
38775
|
+
if (this.isTemperature) {
|
|
38776
|
+
this.exportTemperatureCSV(sortedData);
|
|
38777
|
+
return;
|
|
38778
|
+
}
|
|
38449
38779
|
if (this.granularity === "1h") {
|
|
38450
38780
|
await this.exportHourlyCSV(sortedData);
|
|
38451
38781
|
return;
|
|
@@ -38459,6 +38789,44 @@ var AllReportModal = class {
|
|
|
38459
38789
|
const csvContent = toCsv(csvData);
|
|
38460
38790
|
this.downloadCSV(csvContent, `relatorio-geral-lojas-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`);
|
|
38461
38791
|
}
|
|
38792
|
+
// Temperatura: valores já com offset e sem leituras inválidas (mesma base da tabela).
|
|
38793
|
+
// 1d → uma linha por sensor por DIA; 1h → uma linha por sensor por HORA.
|
|
38794
|
+
exportTemperatureCSV(rows) {
|
|
38795
|
+
const showId = this.showIdentifierColumn(rows);
|
|
38796
|
+
const idCol = (r) => showId ? [r.identifier] : [];
|
|
38797
|
+
const idHead = showId ? ["Identificador"] : [];
|
|
38798
|
+
const n2 = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : v.toFixed(2);
|
|
38799
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
38800
|
+
const summary = [
|
|
38801
|
+
[...idHead, "Sensor", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "M\xE1x (\xB0C)", "Leituras", "Leituras descartadas"],
|
|
38802
|
+
...rows.map((r) => [
|
|
38803
|
+
...idCol(r),
|
|
38804
|
+
r.name,
|
|
38805
|
+
r.noData ? "Sem leitura" : n2(r.consumption),
|
|
38806
|
+
r.noData ? "" : n2(r.min),
|
|
38807
|
+
r.noData ? "" : n2(r.max),
|
|
38808
|
+
String(r.readings ?? ""),
|
|
38809
|
+
String(r.discarded ?? "")
|
|
38810
|
+
])
|
|
38811
|
+
];
|
|
38812
|
+
const gran = this.granularity;
|
|
38813
|
+
const series = [[...idHead, "Sensor", gran === "1h" ? "Data/Hora" : "Dia", "Temperatura (\xB0C)"]];
|
|
38814
|
+
const fmtTs = (ts) => gran === "1h" ? new Date(ts).toLocaleString("pt-BR", {
|
|
38815
|
+
day: "2-digit",
|
|
38816
|
+
month: "2-digit",
|
|
38817
|
+
year: "numeric",
|
|
38818
|
+
hour: "2-digit",
|
|
38819
|
+
minute: "2-digit",
|
|
38820
|
+
timeZone: "America/Sao_Paulo"
|
|
38821
|
+
}) : new Date(ts).toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });
|
|
38822
|
+
for (const r of rows) {
|
|
38823
|
+
const hourly = r.id && this.temperatureStats.get(r.id)?.series || [];
|
|
38824
|
+
const pts = gran === "1h" ? hourly : this.aggregateDaily(hourly, "avg");
|
|
38825
|
+
for (const p of pts) series.push([...idCol(r), r.name, fmtTs(p.timestamp), p.value.toFixed(2)]);
|
|
38826
|
+
}
|
|
38827
|
+
const csvContent = toCsv([...summary, [], ...series]);
|
|
38828
|
+
this.downloadCSV(csvContent, `relatorio-temperatura-${gran}-${date}.csv`);
|
|
38829
|
+
}
|
|
38462
38830
|
// 1h CSV: one row per device × hour. The series comes from per-device requests
|
|
38463
38831
|
// (/telemetry/devices/{id}/{endpoint}?granularity=1h) fetched lazily at export time
|
|
38464
38832
|
// and cached per period — /totals cannot provide hourly data.
|
|
@@ -38546,16 +38914,29 @@ var AllReportModal = class {
|
|
|
38546
38914
|
// Maps the report rows to the TelemetryDevice shape consumed by the shared
|
|
38547
38915
|
// TELEMETRY grid exporters (only labelOrName/name/deviceIdentifier/val/perc are read).
|
|
38548
38916
|
buildExportDevices() {
|
|
38549
|
-
const sorted = [...this.data].sort(
|
|
38917
|
+
const sorted = [...this.data].sort(
|
|
38918
|
+
(a, b) => !!a.noData !== !!b.noData ? a.noData ? 1 : -1 : b.consumption - a.consumption
|
|
38919
|
+
);
|
|
38550
38920
|
const total = sorted.reduce((s, r) => s + (r.consumption || 0), 0);
|
|
38551
38921
|
return sorted.map((r) => ({
|
|
38552
38922
|
labelOrName: r.name,
|
|
38553
38923
|
name: r.name,
|
|
38554
38924
|
deviceIdentifier: r.identifier,
|
|
38555
|
-
|
|
38556
|
-
|
|
38925
|
+
// Temperatura: média do sensor (sem leitura → "—"); sem % de participação
|
|
38926
|
+
val: this.isTemperature && r.noData ? null : r.consumption,
|
|
38927
|
+
perc: this.isTemperature ? void 0 : total > 0 ? r.consumption / total * 100 : 0
|
|
38557
38928
|
}));
|
|
38558
38929
|
}
|
|
38930
|
+
// Colunas do PDF/XLS: temperatura = Sensor | Temperatura média (sem %, Identificador só se útil)
|
|
38931
|
+
exportColumns() {
|
|
38932
|
+
if (!this.isTemperature) return null;
|
|
38933
|
+
return {
|
|
38934
|
+
nameLabel: "Sensor",
|
|
38935
|
+
valueLabel: "Temperatura m\xE9dia (\xB0C)",
|
|
38936
|
+
hidePerc: true,
|
|
38937
|
+
hideIdentifier: !this.showIdentifierColumn()
|
|
38938
|
+
};
|
|
38939
|
+
}
|
|
38559
38940
|
// Accent hex da paleta do dashboard (tema efetivo) para o PDF/XLS — cai no
|
|
38560
38941
|
// roxo MYIO default quando não há tema configurado.
|
|
38561
38942
|
resolveAccentHex() {
|
|
@@ -38581,6 +38962,7 @@ var AllReportModal = class {
|
|
|
38581
38962
|
{
|
|
38582
38963
|
accentColor: this.resolveAccentHex(),
|
|
38583
38964
|
kpis: this.computeKpis(),
|
|
38965
|
+
columns: this.exportColumns(),
|
|
38584
38966
|
chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null
|
|
38585
38967
|
}
|
|
38586
38968
|
);
|
|
@@ -38594,7 +38976,7 @@ var AllReportModal = class {
|
|
|
38594
38976
|
this.domainConfig.unit,
|
|
38595
38977
|
this.exportPeriod,
|
|
38596
38978
|
null,
|
|
38597
|
-
{ accentColor: this.resolveAccentHex() }
|
|
38979
|
+
{ accentColor: this.resolveAccentHex(), columns: this.exportColumns() }
|
|
38598
38980
|
);
|
|
38599
38981
|
}
|
|
38600
38982
|
async fetchCustomerTotals(startISO, endISO) {
|
|
@@ -38647,34 +39029,61 @@ var AllReportModal = class {
|
|
|
38647
39029
|
// em total_value (in-place). Sensores sem série ficam com o 0 original.
|
|
38648
39030
|
async enrichTemperatureAverages(data, startISO, endISO, token, baseUrl) {
|
|
38649
39031
|
const rows = Array.isArray(data?.data) ? data.data : [];
|
|
38650
|
-
|
|
39032
|
+
let tempRows;
|
|
39033
|
+
if (this.params.itemsList) {
|
|
39034
|
+
const ids = new Set(this.params.itemsList.map((i) => String(i.id)));
|
|
39035
|
+
tempRows = rows.filter((d) => ids.has(String(d?.id)));
|
|
39036
|
+
} else {
|
|
39037
|
+
tempRows = rows.filter((d) => String(d?.deviceType || "").toLowerCase() === "temperature");
|
|
39038
|
+
}
|
|
38651
39039
|
if (!tempRows.length) return;
|
|
38652
|
-
const gran = this.granularity === "1h" ? "1h" : "1d";
|
|
38653
39040
|
const startTime = encodeURIComponent(startISO);
|
|
38654
39041
|
const endTime = encodeURIComponent(endISO);
|
|
39042
|
+
const valid = this.temperatureValidRange;
|
|
38655
39043
|
const BATCH = 6;
|
|
38656
39044
|
for (let i = 0; i < tempRows.length; i += BATCH) {
|
|
38657
39045
|
await Promise.all(
|
|
38658
39046
|
tempRows.slice(i, i + BATCH).map(async (dev) => {
|
|
39047
|
+
const id = String(dev.id);
|
|
39048
|
+
const offset = this.temperatureOffsetFor(id);
|
|
38659
39049
|
try {
|
|
38660
|
-
const url = `${baseUrl}/telemetry/devices/${
|
|
39050
|
+
const url = `${baseUrl}/telemetry/devices/${id}/temperature?startTime=${startTime}&endTime=${endTime}&granularity=1h&deep=0`;
|
|
38661
39051
|
const res = await fetch(url, {
|
|
38662
39052
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
|
|
38663
39053
|
});
|
|
38664
|
-
if (!res.ok)
|
|
38665
|
-
const
|
|
38666
|
-
|
|
38667
|
-
|
|
38668
|
-
if (values.length) {
|
|
38669
|
-
dev.total_value = values.reduce((s, v) => s + v, 0) / values.length;
|
|
38670
|
-
}
|
|
39054
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
39055
|
+
const stats = this.computeTemperatureStats(this.parseSeries(await res.json()), offset, valid);
|
|
39056
|
+
this.temperatureStats.set(id, stats);
|
|
39057
|
+
dev.total_value = stats.avg;
|
|
38671
39058
|
} catch {
|
|
39059
|
+
const avg = dev.total_value === null || dev.total_value === void 0 ? NaN : Number(dev.total_value);
|
|
39060
|
+
dev.total_value = Number.isFinite(avg) ? avg + offset : null;
|
|
38672
39061
|
}
|
|
38673
39062
|
})
|
|
38674
39063
|
);
|
|
38675
39064
|
}
|
|
38676
39065
|
this.debugLog(`[AllReportModal] ED-996: temperaturas m\xE9dias aplicadas a ${tempRows.length} sensores`);
|
|
38677
39066
|
}
|
|
39067
|
+
// Offset somado a cada leitura; leituras fora da faixa válida são descartadas (contadas)
|
|
39068
|
+
computeTemperatureStats(raw, offset, valid) {
|
|
39069
|
+
const series = [];
|
|
39070
|
+
let discarded = 0;
|
|
39071
|
+
for (const p of raw) {
|
|
39072
|
+
const v = p.value + offset;
|
|
39073
|
+
if (v < valid.min || v > valid.max) discarded++;
|
|
39074
|
+
else series.push({ timestamp: p.timestamp, value: v });
|
|
39075
|
+
}
|
|
39076
|
+
if (!series.length) return { avg: null, min: null, max: null, readings: 0, discarded, series };
|
|
39077
|
+
let sum2 = 0;
|
|
39078
|
+
let min = Infinity;
|
|
39079
|
+
let max = -Infinity;
|
|
39080
|
+
for (const p of series) {
|
|
39081
|
+
sum2 += p.value;
|
|
39082
|
+
if (p.value < min) min = p.value;
|
|
39083
|
+
if (p.value > max) max = p.value;
|
|
39084
|
+
}
|
|
39085
|
+
return { avg: sum2 / series.length, min, max, readings: series.length, discarded, series };
|
|
39086
|
+
}
|
|
38678
39087
|
// Re-map the cached API response under the current exclusion flag and refresh the UI.
|
|
38679
39088
|
// No-op until data has been loaded at least once.
|
|
38680
39089
|
remapAndRender() {
|
|
@@ -38773,12 +39182,17 @@ var AllReportModal = class {
|
|
|
38773
39182
|
}
|
|
38774
39183
|
if (!this.params.itemsList) {
|
|
38775
39184
|
this.debugLog("\u{1F4CB} No itemsList provided \u2014 mapping directly from API array");
|
|
38776
|
-
return apiArray.map(
|
|
38777
|
-
|
|
38778
|
-
|
|
38779
|
-
|
|
38780
|
-
|
|
38781
|
-
|
|
39185
|
+
return apiArray.map(
|
|
39186
|
+
(item) => this.decorateTemperatureRow(
|
|
39187
|
+
{
|
|
39188
|
+
identifier: item.assetName || this.resolveStoreIdentifierFromApi(item) || item.id || "",
|
|
39189
|
+
name: item.name || item.assetName || item.id || "",
|
|
39190
|
+
consumption: this.pickConsumption(item),
|
|
39191
|
+
...item.id ? { id: String(item.id) } : {}
|
|
39192
|
+
},
|
|
39193
|
+
item
|
|
39194
|
+
)
|
|
39195
|
+
);
|
|
38782
39196
|
}
|
|
38783
39197
|
const orchIdSet = new Set(this.params.itemsList.map((item) => String(item.id)));
|
|
38784
39198
|
const orchMeta = new Map(this.params.itemsList.map((item) => [String(item.id), item]));
|
|
@@ -38795,14 +39209,17 @@ var AllReportModal = class {
|
|
|
38795
39209
|
continue;
|
|
38796
39210
|
}
|
|
38797
39211
|
const consumption = Math.round(this.pickConsumption(apiItem) * 100) / 100;
|
|
38798
|
-
const result =
|
|
38799
|
-
|
|
38800
|
-
|
|
38801
|
-
|
|
38802
|
-
|
|
38803
|
-
|
|
38804
|
-
|
|
38805
|
-
|
|
39212
|
+
const result = this.decorateTemperatureRow(
|
|
39213
|
+
{
|
|
39214
|
+
identifier: meta?.identifier || apiItem.name || apiId,
|
|
39215
|
+
name: meta?.label || apiItem.name || apiId,
|
|
39216
|
+
consumption,
|
|
39217
|
+
id: apiId,
|
|
39218
|
+
...meta?.groupLabel ? { groupLabel: meta.groupLabel } : {}
|
|
39219
|
+
},
|
|
39220
|
+
apiItem
|
|
39221
|
+
);
|
|
39222
|
+
totalMappedConsumption += result.consumption;
|
|
38806
39223
|
rows.push(result);
|
|
38807
39224
|
}
|
|
38808
39225
|
this.debugLog("[AllReportModal] API-driven filter", {
|
|
@@ -38812,6 +39229,21 @@ var AllReportModal = class {
|
|
|
38812
39229
|
});
|
|
38813
39230
|
return rows;
|
|
38814
39231
|
}
|
|
39232
|
+
// Temperatura: média null/NaN → "Sem leitura" (nunca 0 °C); anexa mín/máx/contagens
|
|
39233
|
+
decorateTemperatureRow(row, apiItem) {
|
|
39234
|
+
if (!this.isTemperature) return row;
|
|
39235
|
+
const avg = apiItem?.total_value;
|
|
39236
|
+
const hasAvg = avg !== null && avg !== void 0 && Number.isFinite(Number(avg));
|
|
39237
|
+
const stats = row.id ? this.temperatureStats.get(row.id) : void 0;
|
|
39238
|
+
return {
|
|
39239
|
+
...row,
|
|
39240
|
+
consumption: hasAvg ? Math.round(Number(avg) * 100) / 100 : 0,
|
|
39241
|
+
noData: !hasAvg,
|
|
39242
|
+
...stats && stats.min !== null ? { min: Math.round(stats.min * 100) / 100 } : {},
|
|
39243
|
+
...stats && stats.max !== null ? { max: Math.round(stats.max * 100) / 100 } : {},
|
|
39244
|
+
...stats ? { readings: stats.readings, discarded: stats.discarded } : {}
|
|
39245
|
+
};
|
|
39246
|
+
}
|
|
38815
39247
|
parseConsumptionValue(item) {
|
|
38816
39248
|
const possibleFields = ["total_value", "totalValue", "consumption", "value", "total", "energy", "kwh"];
|
|
38817
39249
|
for (const field of possibleFields) {
|
|
@@ -78979,7 +79411,7 @@ async function openTemperatureModal(params) {
|
|
|
78979
79411
|
const savedGranularity = localStorage.getItem("myio-temp-modal-granularity");
|
|
78980
79412
|
const savedTheme = localStorage.getItem("myio-temp-modal-theme");
|
|
78981
79413
|
if (savedGranularity) state6.granularity = savedGranularity;
|
|
78982
|
-
if (savedTheme) state6.theme = savedTheme;
|
|
79414
|
+
if (savedTheme && !params.theme) state6.theme = savedTheme;
|
|
78983
79415
|
const modalContainer = document.createElement("div");
|
|
78984
79416
|
modalContainer.id = modalId;
|
|
78985
79417
|
document.body.appendChild(modalContainer);
|
|
@@ -79690,7 +80122,7 @@ async function openTemperatureComparisonModal(params) {
|
|
|
79690
80122
|
startTs,
|
|
79691
80123
|
endTs,
|
|
79692
80124
|
granularity: params.granularity || "hour",
|
|
79693
|
-
theme: params.theme || "
|
|
80125
|
+
theme: params.theme || "light",
|
|
79694
80126
|
clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
|
|
79695
80127
|
locale: params.locale || "pt-BR",
|
|
79696
80128
|
deviceData: [],
|
|
@@ -79704,7 +80136,7 @@ async function openTemperatureComparisonModal(params) {
|
|
|
79704
80136
|
const savedGranularity = localStorage.getItem("myio-temp-comparison-granularity");
|
|
79705
80137
|
const savedTheme = localStorage.getItem("myio-temp-comparison-theme");
|
|
79706
80138
|
if (savedGranularity) state6.granularity = savedGranularity;
|
|
79707
|
-
if (savedTheme) state6.theme = savedTheme;
|
|
80139
|
+
if (savedTheme && !params.theme) state6.theme = savedTheme;
|
|
79708
80140
|
const modalContainer = document.createElement("div");
|
|
79709
80141
|
modalContainer.id = modalId;
|
|
79710
80142
|
document.body.appendChild(modalContainer);
|