myio-js-library 0.1.548 → 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 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.548",
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",
@@ -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 SP_OFFSET_MS = 3 * 60 * 60 * 1e3;
30346
- const d = new Date(dateLike.getTime() - SP_OFFSET_MS);
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 colOpts?.hideIdentifier ? cols.filter((c) => c.key !== "identificador") : cols;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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: `Relat\xF3rio - ${this.params.identifier || "SEM IDENTIFICADOR"} - ${this.params.label || "SEM ETIQUETA"}`,
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", () => {
@@ -37590,6 +37639,11 @@ function createMoneyVarianceColumn(config2) {
37590
37639
  }
37591
37640
 
37592
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
+ };
37593
37647
  var DOMAIN_CONFIG2 = {
37594
37648
  energy: {
37595
37649
  endpoint: "energy",
@@ -37620,6 +37674,7 @@ var AllReportModal = class {
37620
37674
  });
37621
37675
  const domain = params.domain || "energy";
37622
37676
  this.domainConfig = DOMAIN_CONFIG2[domain];
37677
+ this.granularity = params.granularity ?? (domain === "temperature" ? "1h" : "1d");
37623
37678
  this.debugEnabled = params.debug === 1;
37624
37679
  this.moneyColumn = createReportMoneyColumn(params.money ?? null);
37625
37680
  if (this.moneyColumn.enabled && typeof document !== "undefined") {
@@ -37666,6 +37721,12 @@ var AllReportModal = class {
37666
37721
  domainConfig;
37667
37722
  // Granularity: '1d' (daily) | '1h' (hourly)
37668
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();
37669
37730
  // Hourly series fetched device-by-device for the 1h CSV export (the /totals endpoint
37670
37731
  // has no hourly granularity). Keyed by StoreReading.id; cache key = period, so a new
37671
37732
  // load or granularity switch invalidates it.
@@ -37699,6 +37760,36 @@ var AllReportModal = class {
37699
37760
  console.log(`[AllReportModal DEBUG] ${message}`, data || "");
37700
37761
  }
37701
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
+ }
37702
37793
  // Helper: normalize identifiers (upper, strip spaces and non-alphanum)
37703
37794
  normalizeId(v) {
37704
37795
  return (v || "").toString().normalize("NFKC").toUpperCase().replace(/\s+/g, "").replace(/[0300-036f]/g, "");
@@ -37964,12 +38055,13 @@ var AllReportModal = class {
37964
38055
  tooltip: {
37965
38056
  enabled: true,
37966
38057
  title: "Granularidade",
37967
- text: "Em <b>Hora</b>, o CSV exportado traz uma linha por dispositivo \xD7 hora (busca device a device). A tabela sempre mostra os totais do per\xEDodo."
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."
37968
38059
  }
37969
38060
  },
37970
38061
  onChange: (value) => {
37971
38062
  this.granularity = value;
37972
38063
  this.hourlySeriesCache = null;
38064
+ if (this.data.length && this.expandedIds.size) this.renderTable();
37973
38065
  }
37974
38066
  });
37975
38067
  }
@@ -38034,6 +38126,9 @@ var AllReportModal = class {
38034
38126
  this.debugLog("\u{1F4C5} Date range selected", { startISO, endISO });
38035
38127
  this.exportPeriod = { startISO, endISO };
38036
38128
  this.hourlySeriesCache = null;
38129
+ this.deviceSeriesCache.clear();
38130
+ this.temperatureStats.clear();
38131
+ this.expandedIds.clear();
38037
38132
  if (!startISO || !endISO) {
38038
38133
  this.showError("Selecione um per\xEDodo v\xE1lido");
38039
38134
  return;
@@ -38098,6 +38193,7 @@ var AllReportModal = class {
38098
38193
  });
38099
38194
  }
38100
38195
  return filtered.sort((a, b) => {
38196
+ if (!!a.noData !== !!b.noData) return a.noData ? 1 : -1;
38101
38197
  const aVal = a[this.sortField];
38102
38198
  const bVal = b[this.sortField];
38103
38199
  if (typeof aVal === "string" && typeof bVal === "string") {
@@ -38130,6 +38226,7 @@ var AllReportModal = class {
38130
38226
  // Máximo/Mínimo com nome do device; mínimo só entre devices COM consumo;
38131
38227
  // "Sem Consumo" oculto para temperature (média °C não tem semântica de zero).
38132
38228
  computeKpis() {
38229
+ if (this.isTemperature) return this.computeTemperatureKpis();
38133
38230
  const totalConsumption = this.calculateTotalConsumption();
38134
38231
  const storeCount = Math.max(1, this.data.length);
38135
38232
  const maxRow = this.data.reduce(
@@ -38162,6 +38259,51 @@ var AllReportModal = class {
38162
38259
  if (moneyKpi) kpis.push(moneyKpi);
38163
38260
  return kpis;
38164
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
+ }
38165
38307
  // RFC-0228 A6 — the "Custo projetado (R$)" summary KPI, or null when money is off.
38166
38308
  // Reuses the A6 column's honest-coverage resolution: a formatted R$ total only when
38167
38309
  // the overlay is confidently priced (DEC-8), otherwise a coverage state label.
@@ -38193,18 +38335,8 @@ var AllReportModal = class {
38193
38335
  }
38194
38336
  const isGrouped = paginatedData.some((r) => r.groupLabel);
38195
38337
  const grandTotal = this.calculateTotalConsumption();
38196
- const pct = (v) => grandTotal > 0 ? `${fmtPt(v / grandTotal * 100)}%` : "\u2014";
38197
- const moneyCell = (row) => this.moneyColumn.bodyCellHTML(row.id);
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("");
38338
+ const showId = this.showIdentifierColumn();
38339
+ const tableRows = isGrouped ? this.renderGroupedRows(paginatedData, grandTotal) : paginatedData.map((row) => this.renderRowHTML(row, grandTotal, showId)).join("");
38208
38340
  container.innerHTML = `
38209
38341
  <div style="max-height: 500px; overflow-y: auto; border: 1px solid var(--myio-border); border-radius: 6px;">
38210
38342
  <style>
@@ -38224,6 +38356,21 @@ var AllReportModal = class {
38224
38356
  color: var(--myio-primary, #1565c0);
38225
38357
  margin-left: 8px;
38226
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; }
38227
38374
  @media (max-width: 768px) {
38228
38375
  .myio-table-mobile { display: block !important; }
38229
38376
  .myio-table-mobile thead,
@@ -38239,29 +38386,145 @@ var AllReportModal = class {
38239
38386
  </style>
38240
38387
  <table class="myio-table myio-table-mobile" style="table-layout: fixed; width: 100%;">
38241
38388
  <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>
38389
+ <tr>${this.renderHeaderCellsHTML(showId)}</tr>
38257
38390
  </thead>
38258
38391
  <tbody>${tableRows}</tbody>
38259
38392
  </table>
38260
38393
  </div>
38261
38394
  `;
38262
38395
  this.setupTableSorting();
38396
+ this.setupRowExpansion(container);
38263
38397
  this.updateParticipationChart();
38264
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
+ }
38265
38528
  // Resolve the chart palette from the host theme: createMyIOTheme exposes
38266
38529
  // tones(n) — solid hex tones derived from the dashboard accent. When absent
38267
38530
  // (or the accent is not hex), the component falls back to the MYIO palette.
@@ -38279,6 +38542,10 @@ var AllReportModal = class {
38279
38542
  updateParticipationChart() {
38280
38543
  const container = document.getElementById("participation-chart-container");
38281
38544
  if (!container) return;
38545
+ if (this.isTemperature) {
38546
+ this.renderTemperatureChart(container);
38547
+ return;
38548
+ }
38282
38549
  const items = this.getFilteredData().map((row) => ({
38283
38550
  id: row.id || this.generateStoreId(row.identifier),
38284
38551
  label: row.name,
@@ -38306,6 +38573,40 @@ var AllReportModal = class {
38306
38573
  if (palette) this.participationChart.updateSettings({ palette });
38307
38574
  this.participationChart.updateData(items);
38308
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
+ }
38309
38610
  // RFC-0182: Render rows grouped by groupLabel with section headers (Option B)
38310
38611
  renderGroupedRows(rows, grandTotal) {
38311
38612
  const groupOrder = [];
@@ -38318,27 +38619,26 @@ var AllReportModal = class {
38318
38619
  }
38319
38620
  byGroup.get(g).push(row);
38320
38621
  }
38321
- const pct = (v) => grandTotal > 0 ? `${fmtPt(v / grandTotal * 100)}%` : "\u2014";
38622
+ const showId = this.showIdentifierColumn();
38322
38623
  return groupOrder.map((groupLabel) => {
38323
38624
  const items = byGroup.get(groupLabel);
38324
- const groupTotal = items.reduce((s, r) => s + r.consumption, 0);
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
+ }
38325
38634
  const header = `
38326
38635
  <tr class="rp-group-header">
38327
- <td colspan="${4 + (this.moneyColumn.enabled ? 1 : 0) + (this.varianceColumn.enabled ? 1 : 0)}">
38636
+ <td colspan="${this.columnCount(showId)}">
38328
38637
  ${groupLabel}
38329
- <span class="rp-group-total">${items.length} dispositivos \xB7 ${fmtPt(groupTotal)} ${this.domainConfig.unit}</span>
38638
+ <span class="rp-group-total">${summary}</span>
38330
38639
  </td>
38331
38640
  </tr>`;
38332
- const dataRows = items.map(
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;
38641
+ return header + items.map((row) => this.renderRowHTML(row, grandTotal, showId)).join("");
38342
38642
  }).join("");
38343
38643
  }
38344
38644
  getSortIcon(field) {
@@ -38446,6 +38746,10 @@ var AllReportModal = class {
38446
38746
  }
38447
38747
  async exportCSV() {
38448
38748
  const sortedData = [...this.data].sort((a, b) => b.consumption - a.consumption);
38749
+ if (this.isTemperature) {
38750
+ this.exportTemperatureCSV(sortedData);
38751
+ return;
38752
+ }
38449
38753
  if (this.granularity === "1h") {
38450
38754
  await this.exportHourlyCSV(sortedData);
38451
38755
  return;
@@ -38459,6 +38763,44 @@ var AllReportModal = class {
38459
38763
  const csvContent = toCsv(csvData);
38460
38764
  this.downloadCSV(csvContent, `relatorio-geral-lojas-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`);
38461
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
+ }
38462
38804
  // 1h CSV: one row per device × hour. The series comes from per-device requests
38463
38805
  // (/telemetry/devices/{id}/{endpoint}?granularity=1h) fetched lazily at export time
38464
38806
  // and cached per period — /totals cannot provide hourly data.
@@ -38546,16 +38888,29 @@ var AllReportModal = class {
38546
38888
  // Maps the report rows to the TelemetryDevice shape consumed by the shared
38547
38889
  // TELEMETRY grid exporters (only labelOrName/name/deviceIdentifier/val/perc are read).
38548
38890
  buildExportDevices() {
38549
- const sorted = [...this.data].sort((a, b) => b.consumption - a.consumption);
38891
+ const sorted = [...this.data].sort(
38892
+ (a, b) => !!a.noData !== !!b.noData ? a.noData ? 1 : -1 : b.consumption - a.consumption
38893
+ );
38550
38894
  const total = sorted.reduce((s, r) => s + (r.consumption || 0), 0);
38551
38895
  return sorted.map((r) => ({
38552
38896
  labelOrName: r.name,
38553
38897
  name: r.name,
38554
38898
  deviceIdentifier: r.identifier,
38555
- val: r.consumption,
38556
- perc: total > 0 ? r.consumption / total * 100 : 0
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
38557
38902
  }));
38558
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
+ }
38559
38914
  // Accent hex da paleta do dashboard (tema efetivo) para o PDF/XLS — cai no
38560
38915
  // roxo MYIO default quando não há tema configurado.
38561
38916
  resolveAccentHex() {
@@ -38581,6 +38936,7 @@ var AllReportModal = class {
38581
38936
  {
38582
38937
  accentColor: this.resolveAccentHex(),
38583
38938
  kpis: this.computeKpis(),
38939
+ columns: this.exportColumns(),
38584
38940
  chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null
38585
38941
  }
38586
38942
  );
@@ -38594,7 +38950,7 @@ var AllReportModal = class {
38594
38950
  this.domainConfig.unit,
38595
38951
  this.exportPeriod,
38596
38952
  null,
38597
- { accentColor: this.resolveAccentHex() }
38953
+ { accentColor: this.resolveAccentHex(), columns: this.exportColumns() }
38598
38954
  );
38599
38955
  }
38600
38956
  async fetchCustomerTotals(startISO, endISO) {
@@ -38647,34 +39003,59 @@ var AllReportModal = class {
38647
39003
  // em total_value (in-place). Sensores sem série ficam com o 0 original.
38648
39004
  async enrichTemperatureAverages(data, startISO, endISO, token, baseUrl) {
38649
39005
  const rows = Array.isArray(data?.data) ? data.data : [];
38650
- const tempRows = rows.filter((d) => String(d?.deviceType || "").toLowerCase() === "temperature");
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
+ }
38651
39011
  if (!tempRows.length) return;
38652
- const gran = this.granularity === "1h" ? "1h" : "1d";
38653
39012
  const startTime = encodeURIComponent(startISO);
38654
39013
  const endTime = encodeURIComponent(endISO);
39014
+ const valid = this.temperatureValidRange;
38655
39015
  const BATCH = 6;
38656
39016
  for (let i = 0; i < tempRows.length; i += BATCH) {
38657
39017
  await Promise.all(
38658
39018
  tempRows.slice(i, i + BATCH).map(async (dev) => {
39019
+ const id = String(dev.id);
39020
+ const offset = this.temperatureOffsetFor(id);
38659
39021
  try {
38660
- const url = `${baseUrl}/telemetry/devices/${dev.id}/temperature?startTime=${startTime}&endTime=${endTime}&granularity=${gran}&deep=0`;
39022
+ const url = `${baseUrl}/telemetry/devices/${id}/temperature?startTime=${startTime}&endTime=${endTime}&granularity=1h&deep=0`;
38661
39023
  const res = await fetch(url, {
38662
39024
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
38663
39025
  });
38664
- if (!res.ok) return;
38665
- const body = await res.json();
38666
- const ent = Array.isArray(body) ? body[0] : body;
38667
- const values = (ent?.consumption || []).map((p) => Number(p?.value)).filter((v) => Number.isFinite(v));
38668
- if (values.length) {
38669
- dev.total_value = values.reduce((s, v) => s + v, 0) / values.length;
38670
- }
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;
38671
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;
38672
39033
  }
38673
39034
  })
38674
39035
  );
38675
39036
  }
38676
39037
  this.debugLog(`[AllReportModal] ED-996: temperaturas m\xE9dias aplicadas a ${tempRows.length} sensores`);
38677
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
+ }
38678
39059
  // Re-map the cached API response under the current exclusion flag and refresh the UI.
38679
39060
  // No-op until data has been loaded at least once.
38680
39061
  remapAndRender() {
@@ -38773,12 +39154,17 @@ var AllReportModal = class {
38773
39154
  }
38774
39155
  if (!this.params.itemsList) {
38775
39156
  this.debugLog("\u{1F4CB} No itemsList provided \u2014 mapping directly from API array");
38776
- return apiArray.map((item) => ({
38777
- identifier: item.assetName || this.resolveStoreIdentifierFromApi(item) || item.id || "",
38778
- name: item.name || item.assetName || item.id || "",
38779
- consumption: this.pickConsumption(item),
38780
- ...item.id ? { id: String(item.id) } : {}
38781
- }));
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
+ );
38782
39168
  }
38783
39169
  const orchIdSet = new Set(this.params.itemsList.map((item) => String(item.id)));
38784
39170
  const orchMeta = new Map(this.params.itemsList.map((item) => [String(item.id), item]));
@@ -38795,14 +39181,17 @@ var AllReportModal = class {
38795
39181
  continue;
38796
39182
  }
38797
39183
  const consumption = Math.round(this.pickConsumption(apiItem) * 100) / 100;
38798
- const result = {
38799
- identifier: meta?.identifier || apiItem.name || apiId,
38800
- name: meta?.label || apiItem.name || apiId,
38801
- consumption,
38802
- id: apiId,
38803
- ...meta?.groupLabel ? { groupLabel: meta.groupLabel } : {}
38804
- };
38805
- totalMappedConsumption += consumption;
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;
38806
39195
  rows.push(result);
38807
39196
  }
38808
39197
  this.debugLog("[AllReportModal] API-driven filter", {
@@ -38812,6 +39201,21 @@ var AllReportModal = class {
38812
39201
  });
38813
39202
  return rows;
38814
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
+ }
38815
39219
  parseConsumptionValue(item) {
38816
39220
  const possibleFields = ["total_value", "totalValue", "consumption", "value", "total", "energy", "kwh"];
38817
39221
  for (const field of possibleFields) {
@@ -78979,7 +79383,7 @@ async function openTemperatureModal(params) {
78979
79383
  const savedGranularity = localStorage.getItem("myio-temp-modal-granularity");
78980
79384
  const savedTheme = localStorage.getItem("myio-temp-modal-theme");
78981
79385
  if (savedGranularity) state6.granularity = savedGranularity;
78982
- if (savedTheme) state6.theme = savedTheme;
79386
+ if (savedTheme && !params.theme) state6.theme = savedTheme;
78983
79387
  const modalContainer = document.createElement("div");
78984
79388
  modalContainer.id = modalId;
78985
79389
  document.body.appendChild(modalContainer);
@@ -79690,7 +80094,7 @@ async function openTemperatureComparisonModal(params) {
79690
80094
  startTs,
79691
80095
  endTs,
79692
80096
  granularity: params.granularity || "hour",
79693
- theme: params.theme || "dark",
80097
+ theme: params.theme || "light",
79694
80098
  clampRange: params.clampRange || DEFAULT_CLAMP_RANGE,
79695
80099
  locale: params.locale || "pt-BR",
79696
80100
  deviceData: [],
@@ -79704,7 +80108,7 @@ async function openTemperatureComparisonModal(params) {
79704
80108
  const savedGranularity = localStorage.getItem("myio-temp-comparison-granularity");
79705
80109
  const savedTheme = localStorage.getItem("myio-temp-comparison-theme");
79706
80110
  if (savedGranularity) state6.granularity = savedGranularity;
79707
- if (savedTheme) state6.theme = savedTheme;
80111
+ if (savedTheme && !params.theme) state6.theme = savedTheme;
79708
80112
  const modalContainer = document.createElement("div");
79709
80113
  modalContainer.id = modalId;
79710
80114
  document.body.appendChild(modalContainer);