myio-js-library 0.1.552 → 0.1.554

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.552",
902
+ version: "0.1.554",
903
903
  description: "A clean, standalone JS SDK for MYIO projects",
904
904
  license: "MIT",
905
905
  repository: "github:gh-myio/myio-js-library",
@@ -18667,9 +18667,17 @@ function renderCardComponentV5({
18667
18667
  // Temperatura atual vinda do Ingestion = média das últimas 2 h; true = nenhuma leitura na janela
18668
18668
  temperatureNoRecentReading = false,
18669
18669
  temperatureSource,
18670
- // 'ingestion-avg-2h' | 'thingsboard' | undefined
18670
+ // 'ingestion-hourly' | 'ingestion-avg-2h' | 'thingsboard' | undefined
18671
18671
  temperatureFetchedAt,
18672
18672
  // ms da consulta (rótulo "Atualizado às HH:MM")
18673
+ // Idade da última leitura: 'ok' | 'warning' (10–12 h) | 'stale' (12–24 h) | 'offline' (> 24 h)
18674
+ temperatureFreshness,
18675
+ temperatureHourTs,
18676
+ // início da hora fechada exibida (ms)
18677
+ temperatureLastTs,
18678
+ // instante da última leitura válida (ms)
18679
+ temperatureOffset = 0,
18680
+ // offset aplicado (°C) — marcador igual ao da exclusão de totais
18673
18681
  // Per-device exclude_groups_totals attribute (SERVER_SCOPE) — drives the orange marker
18674
18682
  excludeGroupsTotals
18675
18683
  } = entityObject;
@@ -18830,8 +18838,29 @@ function renderCardComponentV5({
18830
18838
  const tankLevelCm = isTankDevice2 ? Number(waterLevel ?? val) : null;
18831
18839
  const _pctDecimals = resolvePercentDecimals(percentDecimals);
18832
18840
  const showNoTempReading = !!temperatureNoRecentReading && isTemperatureDevice2(deviceType);
18841
+ const _isTempCard = isTemperatureDevice2(deviceType);
18842
+ const showTempWarning = _isTempCard && temperatureFreshness === "warning";
18843
+ const _off = Number(temperatureOffset) || 0;
18833
18844
  const tempValueTitle = (() => {
18834
- if (!isTemperatureDevice2(deviceType) || temperatureSource !== "ingestion-avg-2h") return "";
18845
+ if (_isTempCard && temperatureSource === "ingestion-hourly") {
18846
+ const tz = { timeZone: "America/Sao_Paulo" };
18847
+ const hm = (ts) => new Date(ts).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", ...tz });
18848
+ const dm = (ts) => new Date(ts).toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", ...tz });
18849
+ const ago = (ts) => {
18850
+ const h3 = Math.floor((Date.now() - ts) / 36e5);
18851
+ const m = Math.floor((Date.now() - ts) % 36e5 / 6e4);
18852
+ return h3 > 0 ? `${h3} h ${m} min` : `${m} min`;
18853
+ };
18854
+ const offTxt = _off ? ` \xB7 offset ${_off > 0 ? "+" : "\u2212"}${String(Math.abs(_off)).replace(".", ",")} \xB0C` : "";
18855
+ if (temperatureFreshness === "offline") return "Nenhuma leitura nas \xFAltimas 24 h";
18856
+ if (!temperatureLastTs) return "";
18857
+ const last = `\xFAltima leitura ${dm(temperatureLastTs)} ${hm(temperatureLastTs)} (h\xE1 ${ago(temperatureLastTs)})`;
18858
+ if (temperatureFreshness === "stale") return `Sem leitura nas \xFAltimas 12 h \u2014 ${last}`;
18859
+ const hour = temperatureHourTs ? `M\xE9dia da hora ${dm(temperatureHourTs)} ${hm(temperatureHourTs)}` : "Temperatura";
18860
+ const warn = temperatureFreshness === "warning" ? "\u26A0 Leitura atrasada (10\u201312 h) \u2014 " : "";
18861
+ return `${warn}${hour} \xB7 ${last}${offTxt}`;
18862
+ }
18863
+ if (!_isTempCard || temperatureSource !== "ingestion-avg-2h") return "";
18835
18864
  const at = temperatureFetchedAt ? ` \xB7 Atualizado \xE0s ${new Date(temperatureFetchedAt).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}` : "";
18836
18865
  return showNoTempReading ? `Nenhuma leitura nas \xFAltimas 2 h${at}` : `Temperatura (m\xE9dia das \xFAltimas 2 h)${at}`;
18837
18866
  })();
@@ -18915,6 +18944,22 @@ function renderCardComponentV5({
18915
18944
  height: 100%;
18916
18945
  }
18917
18946
 
18947
+ /* Temperatura: leitura atrasada (10\u201312 h) \u2014 borda laranja + \u26A0 piscando */
18948
+ .myio-enhanced-card-container-v5.myio-card-temp-warning .device-card-centered {
18949
+ border: 2px solid #f59e0b !important;
18950
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.18) !important;
18951
+ }
18952
+ .myio-temp-warn-icon {
18953
+ margin-left: 4px;
18954
+ font-size: 0.9em;
18955
+ cursor: help;
18956
+ animation: myio-temp-warn-blink 1.2s ease-in-out infinite;
18957
+ }
18958
+ @keyframes myio-temp-warn-blink {
18959
+ 0%, 100% { opacity: 1; }
18960
+ 50% { opacity: 0.2; }
18961
+ }
18962
+
18918
18963
  /* Subtle orange marker \u2014 device flagged in exclude_groups_totals */
18919
18964
  .myio-enhanced-card-container-v5.myio-card-excluded::after {
18920
18965
  content: '';
@@ -19204,7 +19249,7 @@ function renderCardComponentV5({
19204
19249
  <span class="flash-icon ${shouldFlashIcon2 ? "flash" : ""}">
19205
19250
  ${icon2}
19206
19251
  </span>
19207
- <span class="consumption-value"${showNoTempReading ? ' style="color:#9ca3af;font-weight:500;font-size:0.85em;"' : ""}${tempValueTitle ? ` title="${tempValueTitle}"` : ""}>${formatCardValue(cardEntity.lastValue, deviceType)}</span>
19252
+ <span class="consumption-value"${showNoTempReading ? ' style="color:#9ca3af;font-weight:500;font-size:0.85em;"' : ""}${tempValueTitle ? ` title="${tempValueTitle}"` : ""}>${formatCardValue(cardEntity.lastValue, deviceType)}</span>${showTempWarning ? `<span class="myio-temp-warn-icon" title="${tempValueTitle}" aria-label="Leitura atrasada">\u26A0\uFE0F</span>` : ""}
19208
19253
  </div>
19209
19254
  </div>
19210
19255
  ${isTankDevice2 ? `<span class="device-percentage-badge device-tank-level-badge" style="position: absolute; bottom: 12px; right: 12px; z-index: 20; background: none !important;">${formatTankHeadFromCm(tankLevelCm)}</span>` : !isTermostatoDevice ? `<span class="device-percentage-badge percentage-tooltip-trigger" style="position: absolute; bottom: 12px; right: 12px; z-index: 20; background: none !important; cursor: help;">${percentageForDisplay.toFixed(_pctDecimals).replace(".", ",")}%</span>` : tempDeviationPercent ? `<span class="device-percentage-badge temp-deviation-badge temp-comparison-tooltip-trigger" style="position: absolute; bottom: 12px; right: 12px; z-index: 20; background: none !important; color: ${tempDeviationPercent.isAbove ? "#ef4444" : tempDeviationPercent.isBelow ? "#3b82f6" : "#6b7280"}; font-weight: 600; cursor: help;">${tempDeviationPercent.sign}${tempDeviationPercent.value.toFixed(1)}%</span>` : ""}
@@ -19224,6 +19269,8 @@ function renderCardComponentV5({
19224
19269
  if (_isExcluded) container.classList.add("myio-card-excluded");
19225
19270
  } catch (e) {
19226
19271
  }
19272
+ if (_isTempCard && _off !== 0) container.classList.add("myio-card-excluded", "myio-card-temp-offset");
19273
+ if (showTempWarning) container.classList.add("myio-card-temp-warning");
19227
19274
  if (!document.getElementById("myio-enhanced-card-layout-styles-v5")) {
19228
19275
  const layoutStyle = document.createElement("style");
19229
19276
  layoutStyle.id = "myio-enhanced-card-layout-styles-v5";
@@ -33574,31 +33621,56 @@ var fmtPt = (n) => new Intl.NumberFormat("pt-BR", { minimumFractionDigits: 2, ma
33574
33621
 
33575
33622
  // src/components/telemetry-grid-shopping/export.ts
33576
33623
  var import_jspdf = require("jspdf");
33624
+ function extras(d) {
33625
+ return d;
33626
+ }
33577
33627
  function makeCols(unit, colOpts) {
33578
33628
  const cols = [
33579
33629
  { key: "idx", label: "#", pdfW: 10 },
33580
33630
  { key: "nome", label: colOpts?.nameLabel || "Nome", pdfW: 100 },
33581
33631
  { key: "identificador", label: "Identificador", pdfW: 60 },
33582
33632
  { key: "consumo", label: colOpts?.valueLabel || (unit ? `Consumo (${unit})` : "Consumo"), pdfW: 50 },
33633
+ ...(colOpts?.extraColumns || []).map((c, i) => ({ key: `x${i}`, label: c.label, pdfW: c.pdfW ?? 40 })),
33583
33634
  { key: "perc", label: "%", pdfW: 20 }
33584
33635
  ];
33585
33636
  return cols.filter(
33586
33637
  (c) => !(colOpts?.hideIdentifier && c.key === "identificador") && !(colOpts?.hidePerc && c.key === "perc")
33587
33638
  );
33588
33639
  }
33589
- function buildRow(d, idx) {
33640
+ function buildRow(d, idx, colOpts) {
33641
+ const x = extras(d);
33590
33642
  const fmtVal = () => {
33591
- if (d.val === null || d.val === void 0) return "\u2014";
33592
- return Number(d.val).toLocaleString("pt-BR", { maximumFractionDigits: 3, useGrouping: false });
33643
+ if (d.val === null || d.val === void 0) return colOpts?.emptyValueText || "\u2014";
33644
+ const dec = colOpts?.valueDecimals;
33645
+ return Number(d.val).toLocaleString(
33646
+ "pt-BR",
33647
+ dec !== void 0 ? { minimumFractionDigits: dec, maximumFractionDigits: dec, useGrouping: false } : { maximumFractionDigits: 3, useGrouping: false }
33648
+ );
33593
33649
  };
33594
33650
  const pd = resolvePercentDecimals();
33595
- return {
33651
+ const name = d.labelOrName || d.name || "\u2014";
33652
+ const row = {
33596
33653
  idx: String(idx + 1),
33597
- nome: d.labelOrName || d.name || "\u2014",
33654
+ nome: x.nameNote ? `${name} (${x.nameNote})` : name,
33598
33655
  identificador: d.deviceIdentifier || "\u2014",
33599
33656
  consumo: fmtVal(),
33600
33657
  perc: d.perc !== void 0 ? `${d.perc.toFixed(pd).replace(".", ",")}%` : "\u2014"
33601
33658
  };
33659
+ (colOpts?.extraColumns || []).forEach((_, i) => {
33660
+ row[`x${i}`] = x.extraCells?.[i] ?? "\u2014";
33661
+ });
33662
+ return row;
33663
+ }
33664
+ function forEachRow(devices, onGroup, onRow) {
33665
+ let n = 0;
33666
+ devices.forEach((d) => {
33667
+ const g = extras(d).groupHeader;
33668
+ if (g) onGroup(g);
33669
+ else onRow(d, n++);
33670
+ });
33671
+ }
33672
+ function countDataRows(devices) {
33673
+ return devices.filter((d) => !extras(d).groupHeader).length;
33602
33674
  }
33603
33675
  function fmtPeriod(period) {
33604
33676
  if (!period?.startISO) return "";
@@ -33635,6 +33707,9 @@ function escXml(s) {
33635
33707
  function truncate(s, max) {
33636
33708
  return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
33637
33709
  }
33710
+ function pdfSafe(s) {
33711
+ return String(s).replace(/−/g, "-").replace(/[‐‑]/g, "-").replace(/ /g, " ");
33712
+ }
33638
33713
  function exportGridCsv(devices, label, unit, period, customerName) {
33639
33714
  const periodLabel2 = fmtPeriod(period);
33640
33715
  const metaRows = [
@@ -33646,10 +33721,15 @@ function exportGridCsv(devices, label, unit, period, customerName) {
33646
33721
  ].filter((v) => v !== null);
33647
33722
  const cols = makeCols(unit);
33648
33723
  const header = cols.map((c) => `"${c.label}"`).join(";");
33649
- const rows = devices.map((d, i) => {
33650
- const r = buildRow(d, i);
33651
- return cols.map((c) => `"${String(r[c.key]).replace(/"/g, '""')}"`).join(";");
33652
- });
33724
+ const rows = [];
33725
+ forEachRow(
33726
+ devices,
33727
+ (g) => rows.push(`"${g.replace(/"/g, '""')}"`),
33728
+ (d, i) => {
33729
+ const r = buildRow(d, i);
33730
+ rows.push(cols.map((c) => `"${String(r[c.key]).replace(/"/g, '""')}"`).join(";"));
33731
+ }
33732
+ );
33653
33733
  const csv = "\uFEFF" + [...metaRows, header, ...rows].join("\r\n");
33654
33734
  triggerDownload(
33655
33735
  new Blob([csv], { type: "text/csv;charset=utf-8;" }),
@@ -33665,14 +33745,23 @@ function exportGridXls(devices, label, unit, period, customerName, options) {
33665
33745
  const headerCells = cols.map(
33666
33746
  (c) => `<Cell ss:StyleID="h"><Data ss:Type="String">${escXml(c.label)}</Data></Cell>`
33667
33747
  ).join("");
33668
- const dataRows = devices.map((d, i) => {
33669
- const r = buildRow(d, i);
33670
- const cells = cols.map((c) => {
33671
- const v = escXml(String(r[c.key]));
33672
- return `<Cell><Data ss:Type="String">${v}</Data></Cell>`;
33673
- }).join("");
33674
- return `<Row>${cells}</Row>`;
33675
- }).join("\n");
33748
+ const dataRowsArr = [];
33749
+ forEachRow(
33750
+ devices,
33751
+ (g) => dataRowsArr.push(
33752
+ `<Row><Cell ss:StyleID="g" ss:MergeAcross="${cols.length - 1}"><Data ss:Type="String">${escXml(g)}</Data></Cell></Row>`
33753
+ ),
33754
+ (d, i) => {
33755
+ const r = buildRow(d, i, options?.columns);
33756
+ const cells = cols.map((c) => {
33757
+ const v = escXml(String(r[c.key]));
33758
+ return `<Cell><Data ss:Type="String">${v}</Data></Cell>`;
33759
+ }).join("");
33760
+ dataRowsArr.push(`<Row>${cells}</Row>`);
33761
+ }
33762
+ );
33763
+ const dataRows = dataRowsArr.join("\n");
33764
+ const kpiRows = (options?.kpis || []).map((k) => metaRow(k.label, k.sub ? `${k.value} (${k.sub})` : k.value)).join("\n");
33676
33765
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
33677
33766
  <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
33678
33767
  xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
@@ -33685,6 +33774,10 @@ function exportGridXls(devices, label, unit, period, customerName, options) {
33685
33774
  <Font ss:Bold="1"/>
33686
33775
  <Interior ss:Color="#F0EDF9" ss:Pattern="Solid"/>
33687
33776
  </Style>
33777
+ <Style ss:ID="g">
33778
+ <Font ss:Bold="1" ss:Color="#555555"/>
33779
+ <Interior ss:Color="#EEEEEE" ss:Pattern="Solid"/>
33780
+ </Style>
33688
33781
  </Styles>
33689
33782
  <Worksheet ss:Name="${escXml(label.slice(0, 31))}">
33690
33783
  <Table>
@@ -33692,6 +33785,8 @@ function exportGridXls(devices, label, unit, period, customerName, options) {
33692
33785
  ${customerName ? metaRow("Cliente", customerName) : ""}
33693
33786
  ${periodLabel2 ? metaRow("Per\xEDodo", periodLabel2) : ""}
33694
33787
  ${metaRow("Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR"))}
33788
+ ${kpiRows ? `<Row/>
33789
+ ${kpiRows}` : ""}
33695
33790
  <Row/>
33696
33791
  <Row>${headerCells}</Row>
33697
33792
  ${dataRows}
@@ -33741,13 +33836,14 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33741
33836
  doc.setFont("helvetica", "bold");
33742
33837
  doc.setFontSize(10);
33743
33838
  const titleText = customerName ? `${customerName} \u2014 ${label}` : label;
33744
- doc.text(titleText, MARGIN, HDR_H / 2 + 1.5);
33839
+ doc.text(pdfSafe(titleText), MARGIN, HDR_H / 2 + 1.5);
33745
33840
  doc.setFont("helvetica", "normal");
33746
33841
  doc.setFontSize(8);
33747
33842
  const periodLabel2 = fmtPeriod(period);
33748
33843
  const periodPart = periodLabel2 ? `Per\xEDodo: ${periodLabel2} \u2022 ` : "";
33749
- const info = `${periodPart}Gerado em: ${generatedAt} \u2022 ${devices.length} dispositivo(s) \u2022 Unidade: ${unit} \u2022 P\xE1g. ${pageNo2}`;
33750
- doc.text(info, PW - MARGIN, HDR_H / 2 + 1.5, { align: "right" });
33844
+ const countLabel = options?.columns?.countLabel || "dispositivo(s)";
33845
+ const info = `${periodPart}Gerado em: ${generatedAt} \u2022 ${countDataRows(devices)} ${countLabel} \u2022 Unidade: ${unit} \u2022 P\xE1g. ${pageNo2}`;
33846
+ doc.text(pdfSafe(info), PW - MARGIN, HDR_H / 2 + 1.5, { align: "right" });
33751
33847
  }
33752
33848
  function drawColumnHeaders(y) {
33753
33849
  doc.setFillColor(...ACCENT_SOFT);
@@ -33757,7 +33853,7 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33757
33853
  doc.setFontSize(7);
33758
33854
  cols.forEach((c, ci) => {
33759
33855
  const x = colX(ci) + 1.5;
33760
- doc.text(c.label, x, y + HEAD_H / 2 + 2.5);
33856
+ doc.text(pdfSafe(c.label), x, y + HEAD_H / 2 + 2.5);
33761
33857
  });
33762
33858
  doc.setDrawColor(200, 195, 220);
33763
33859
  doc.setLineWidth(0.2);
@@ -33774,13 +33870,21 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33774
33870
  cols.forEach((c, ci) => {
33775
33871
  const x = colX(ci) + 1.5;
33776
33872
  const maxChars = Math.floor(colWidths[ci] / 1.8);
33777
- const text2 = truncate(String(r[c.key]), maxChars);
33873
+ const text2 = truncate(pdfSafe(String(r[c.key])), maxChars);
33778
33874
  doc.text(text2, x, y + ROW_H / 2 + 2.2);
33779
33875
  });
33780
33876
  doc.setDrawColor(230, 228, 240);
33781
33877
  doc.setLineWidth(0.1);
33782
33878
  doc.line(MARGIN, y + ROW_H, MARGIN + TABLE_W, y + ROW_H);
33783
33879
  }
33880
+ function drawGroupRow(text2, y) {
33881
+ doc.setFillColor(...ACCENT_SOFT);
33882
+ doc.rect(MARGIN, y, TABLE_W, ROW_H, "F");
33883
+ doc.setTextColor(...ACCENT2);
33884
+ doc.setFont("helvetica", "bold");
33885
+ doc.setFontSize(6.5);
33886
+ doc.text(truncate(pdfSafe(text2), 160), MARGIN + 1.5, y + ROW_H / 2 + 2.2);
33887
+ }
33784
33888
  function drawFooter2() {
33785
33889
  doc.setFillColor(250, 249, 255);
33786
33890
  doc.rect(0, PH - FTR_H, PW, FTR_H, "F");
@@ -33807,14 +33911,14 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33807
33911
  doc.setTextColor(...ACCENT2);
33808
33912
  doc.setFont("helvetica", "bold");
33809
33913
  doc.setFontSize(11);
33810
- doc.text(truncate(kpi.value, 24), cx, y + 6.5, { align: "center" });
33914
+ doc.text(truncate(pdfSafe(kpi.value), 24), cx, y + 6.5, { align: "center" });
33811
33915
  doc.setTextColor(110, 110, 120);
33812
33916
  doc.setFont("helvetica", "normal");
33813
33917
  doc.setFontSize(6.5);
33814
- doc.text(truncate(kpi.label, 34), cx, y + 10.5, { align: "center" });
33918
+ doc.text(truncate(pdfSafe(kpi.label), 34), cx, y + 10.5, { align: "center" });
33815
33919
  if (kpi.sub) {
33816
33920
  doc.setFontSize(5.5);
33817
- doc.text(truncate(kpi.sub, 40), cx, y + 13.5, { align: "center" });
33921
+ doc.text(truncate(pdfSafe(kpi.sub), 40), cx, y + 13.5, { align: "center" });
33818
33922
  }
33819
33923
  });
33820
33924
  }
@@ -33827,7 +33931,7 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33827
33931
  }
33828
33932
  drawColumnHeaders(currentY);
33829
33933
  currentY += HEAD_H;
33830
- devices.forEach((d, i) => {
33934
+ const ensureSpace = () => {
33831
33935
  if (currentY + ROW_H > MAX_Y) {
33832
33936
  drawFooter2();
33833
33937
  doc.addPage();
@@ -33837,9 +33941,20 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33837
33941
  drawColumnHeaders(currentY);
33838
33942
  currentY += HEAD_H;
33839
33943
  }
33840
- drawDataRow(buildRow(d, i), currentY, i % 2 === 0);
33841
- currentY += ROW_H;
33842
- });
33944
+ };
33945
+ forEachRow(
33946
+ devices,
33947
+ (g) => {
33948
+ ensureSpace();
33949
+ drawGroupRow(g, currentY);
33950
+ currentY += ROW_H;
33951
+ },
33952
+ (d, i) => {
33953
+ ensureSpace();
33954
+ drawDataRow(buildRow(d, i, options?.columns), currentY, i % 2 === 0);
33955
+ currentY += ROW_H;
33956
+ }
33957
+ );
33843
33958
  drawFooter2();
33844
33959
  const chart = options?.chartImage;
33845
33960
  if (chart?.dataUrl) {
@@ -33850,7 +33965,7 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33850
33965
  doc.setTextColor(...ACCENT2);
33851
33966
  doc.setFont("helvetica", "bold");
33852
33967
  doc.setFontSize(12);
33853
- doc.text(chart.title || "Participa\xE7\xE3o por Dispositivo", MARGIN, titleY);
33968
+ doc.text(pdfSafe(chart.title || "Participa\xE7\xE3o por Dispositivo"), MARGIN, titleY);
33854
33969
  const imgTop = titleY + 4;
33855
33970
  const availW = TABLE_W;
33856
33971
  const availH = MAX_Y - imgTop;
@@ -35090,6 +35205,81 @@ function createModalFooter(params = {}) {
35090
35205
  };
35091
35206
  }
35092
35207
 
35208
+ // src/components/premium-modals/internal/temperatureBarsPng.ts
35209
+ function renderTemperatureBarsPng(opts) {
35210
+ if (typeof document === "undefined" || !opts.items.length) return null;
35211
+ const W = 1400;
35212
+ const ROW = 34;
35213
+ const TOP = 56;
35214
+ const H = TOP + opts.items.length * ROW + 20;
35215
+ const canvas = document.createElement("canvas");
35216
+ canvas.width = W;
35217
+ canvas.height = H;
35218
+ let ctx = null;
35219
+ try {
35220
+ ctx = canvas.getContext("2d");
35221
+ } catch {
35222
+ ctx = null;
35223
+ }
35224
+ if (!ctx) return null;
35225
+ const fmt2 = (v) => v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
35226
+ const ideal = opts.ideal && Number.isFinite(opts.ideal.min) && Number.isFinite(opts.ideal.max) ? opts.ideal : null;
35227
+ const values = opts.items.map((i) => i.value).filter((v) => v !== null && Number.isFinite(v));
35228
+ const base = values.length ? values : [20, 30];
35229
+ const lo = Math.floor(Math.min(...base, ideal ? ideal.min : Infinity) - 1);
35230
+ const hi = Math.ceil(Math.max(...base, ideal ? ideal.max : -Infinity) + 1);
35231
+ const span = Math.max(1, hi - lo);
35232
+ const LABEL_W = 460;
35233
+ const VALUE_W = 120;
35234
+ const trackX = LABEL_W + 20;
35235
+ const trackW = W - trackX - VALUE_W - 20;
35236
+ const xOf = (v) => trackX + (v - lo) / span * trackW;
35237
+ ctx.fillStyle = "#ffffff";
35238
+ ctx.fillRect(0, 0, W, H);
35239
+ ctx.fillStyle = "#6b7280";
35240
+ ctx.font = "20px Helvetica, Arial, sans-serif";
35241
+ ctx.textBaseline = "middle";
35242
+ const scale = `Escala ${fmt2(lo)}\u2013${fmt2(hi)} \xB0C`;
35243
+ ctx.fillText(ideal ? `${scale} \xB7 faixa ideal ${fmt2(ideal.min)}\u2013${fmt2(ideal.max)} \xB0C` : scale, 16, 26);
35244
+ opts.items.forEach((item, i) => {
35245
+ const y = TOP + i * ROW;
35246
+ const cy = y + ROW / 2;
35247
+ ctx.fillStyle = "#1f2937";
35248
+ ctx.font = "20px Helvetica, Arial, sans-serif";
35249
+ ctx.textAlign = "left";
35250
+ let label = item.label;
35251
+ while (ctx.measureText(label).width > LABEL_W - 10 && label.length > 4) label = label.slice(0, -2);
35252
+ if (label !== item.label) label = label.slice(0, -1) + "\u2026";
35253
+ ctx.fillText(label, 16, cy);
35254
+ if (item.value === null || !Number.isFinite(item.value)) {
35255
+ ctx.fillStyle = "#9ca3af";
35256
+ ctx.font = "italic 18px Helvetica, Arial, sans-serif";
35257
+ ctx.fillText("Sem leitura", trackX, cy);
35258
+ ctx.textAlign = "right";
35259
+ ctx.fillText("\u2014", W - 16, cy);
35260
+ return;
35261
+ }
35262
+ ctx.fillStyle = "#e5e7eb";
35263
+ ctx.fillRect(trackX, cy - 9, trackW, 18);
35264
+ if (ideal) {
35265
+ ctx.fillStyle = "rgba(34,197,94,0.18)";
35266
+ ctx.fillRect(xOf(ideal.min), cy - 9, xOf(ideal.max) - xOf(ideal.min), 18);
35267
+ }
35268
+ const v = item.value;
35269
+ ctx.fillStyle = ideal && v > ideal.max ? "#ef4444" : ideal && v < ideal.min ? "#3b82f6" : opts.accent || "#3e1a7d";
35270
+ ctx.fillRect(trackX, cy - 6, Math.max(2, xOf(v) - trackX), 12);
35271
+ ctx.fillStyle = "#111827";
35272
+ ctx.font = "bold 20px Helvetica, Arial, sans-serif";
35273
+ ctx.textAlign = "right";
35274
+ ctx.fillText(fmt2(v), W - 16, cy);
35275
+ });
35276
+ try {
35277
+ return { dataUrl: canvas.toDataURL("image/png"), width: W, height: H };
35278
+ } catch {
35279
+ return null;
35280
+ }
35281
+ }
35282
+
35093
35283
  // src/components/premium-modals/report-device/DeviceReportModal.ts
35094
35284
  var DOMAIN_CONFIG = {
35095
35285
  energy: {
@@ -35883,10 +36073,22 @@ var DeviceReportModal = class {
35883
36073
  const summaryValue = this.domainConfig.summaryType === "average" ? valid > 0 ? total / valid : 0 : total;
35884
36074
  const now = /* @__PURE__ */ new Date();
35885
36075
  const timestamp = now.toLocaleDateString("pt-BR") + " - " + now.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
36076
+ const p = this.exportPeriod;
36077
+ const fmtD = (iso) => iso ? new Date(iso).toLocaleDateString("pt-BR") : "";
36078
+ const off = Number(this.params.temperatureOffset) || 0;
36079
+ const isTemp = this.domainConfig.summaryType === "average";
36080
+ void summaryValue;
35886
36081
  const csvData = [
35887
36082
  ["Dispositivo/Loja", this.params.identifier || "N/A", this.params.label || ""],
36083
+ ...this.params.deviceName ? [["Dispositivo (TB)", this.params.deviceName, ""]] : [],
36084
+ ...isTemp && off !== 0 ? [["Offset aplicado (\xB0C)", this.domainConfig.formatter(off), ""]] : [],
36085
+ ...this.resolveCustomerName() ? [["Cliente", this.resolveCustomerName(), ""]] : [],
36086
+ ...p?.startISO ? [["Per\xEDodo", `${fmtD(p.startISO)} \u2014 ${fmtD(p.endISO)}`, ""]] : [],
36087
+ ["Granularidade", this.granularity === "1h" ? "Hora" : "Dia", ""],
35888
36088
  ["DATA EMISS\xC3O", timestamp, ""],
35889
- [this.domainConfig.summaryLabel, this.domainConfig.formatter(summaryValue), this.domainConfig.unit],
36089
+ [],
36090
+ ...this.computeKpis().map((k) => [k.label, k.value, k.sub || ""]),
36091
+ [],
35890
36092
  [this.granularity === "1h" ? "Data/Hora" : "Data", this.domainConfig.label, ""],
35891
36093
  ...this.data.map((row) => [this.formatDate(row.date), row.noData ? "Sem leitura" : this.domainConfig.formatter(row.consumption)])
35892
36094
  ];
@@ -35907,7 +36109,11 @@ var DeviceReportModal = class {
35907
36109
  }
35908
36110
  // Título dos exports — mesmo da modal (identificador + etiqueta do device).
35909
36111
  resolveExportTitle() {
35910
- return `Relat\xF3rio - ${this.params.identifier || "SEM IDENTIFICADOR"} - ${this.params.label || "SEM ETIQUETA"}`;
36112
+ const base = `Relat\xF3rio - ${this.params.identifier || "SEM IDENTIFICADOR"} - ${this.params.label || "SEM ETIQUETA"}`;
36113
+ const dev = this.params.deviceName ? ` (${this.params.deviceName})` : "";
36114
+ const off = Number(this.params.temperatureOffset) || 0;
36115
+ const offTxt = this.domainConfig.summaryType === "average" && off !== 0 ? ` \xB7 offset ${off > 0 ? "+" : "\u2212"}${Math.abs(off).toLocaleString("pt-BR", { maximumFractionDigits: 2 })} \xB0C` : "";
36116
+ return `${base}${dev}${offTxt}`;
35911
36117
  }
35912
36118
  // Mapeia as linhas do relatório (dia/hora × consumo) para o shape TelemetryDevice
35913
36119
  // dos exporters compartilhados do grid: labelOrName = data formatada, val = consumo,
@@ -35926,16 +36132,41 @@ var DeviceReportModal = class {
35926
36132
  // Opções de coluna dos exporters: relatório single-device → Data | Consumo | %
35927
36133
  // (Identificador redundante — o device já está no título/filename).
35928
36134
  exportColumnOptions() {
36135
+ const isTemp = this.domainConfig.summaryType === "average";
35929
36136
  return {
35930
36137
  nameLabel: this.granularity === "1h" ? "Data/Hora" : "Data",
35931
- hideIdentifier: true
36138
+ hideIdentifier: true,
36139
+ // Mesmo rótulo da coluna da tela (antes saía "Consumo (°C)" em temperatura)
36140
+ valueLabel: this.domainConfig.label,
36141
+ countLabel: this.granularity === "1h" ? "hora(s)" : "dia(s)",
36142
+ ...isTemp ? { hidePerc: true, emptyValueText: "Sem leitura", valueDecimals: 2 } : {}
35932
36143
  };
35933
36144
  }
36145
+ // Gráfico da página final do PDF = painel da direita da tela
36146
+ async buildExportChart() {
36147
+ if (this.domainConfig.summaryType === "average") {
36148
+ const isHourly = this.granularity === "1h";
36149
+ const ranked = [...this.validRows].sort((a, b) => b.consumption - a.consumption);
36150
+ const shown = isHourly ? ranked.slice(0, 24) : ranked;
36151
+ const noData = isHourly ? [] : this.data.filter((r) => r.noData);
36152
+ const png = renderTemperatureBarsPng({
36153
+ items: [
36154
+ ...shown.map((r) => ({ label: this.formatDate(r.date), value: r.consumption })),
36155
+ ...noData.map((r) => ({ label: this.formatDate(r.date), value: null }))
36156
+ ],
36157
+ ideal: this.params.temperatureIdealRange ?? null,
36158
+ accent: this.resolveAccentHex()
36159
+ });
36160
+ const title = isHourly ? `Horas mais quentes${ranked.length > 24 ? " (top 24)" : ""}` : "Dias mais quentes (m\xE9dia do dia)";
36161
+ return png ? { ...png, title } : null;
36162
+ }
36163
+ const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
36164
+ return chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dia" } : null;
36165
+ }
35934
36166
  // PDF export — layout premium do grid + paleta do dashboard + faixa de KPIs +
35935
36167
  // página dedicada com o gráfico "Participação por Dia" da modal.
35936
36168
  async exportPDF() {
35937
36169
  if (!this.data.length) return;
35938
- const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
35939
36170
  exportGridPdf(
35940
36171
  this.buildExportDevices(),
35941
36172
  this.resolveExportTitle(),
@@ -35945,7 +36176,7 @@ var DeviceReportModal = class {
35945
36176
  {
35946
36177
  accentColor: this.resolveAccentHex(),
35947
36178
  kpis: this.computeKpis(),
35948
- chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dia" } : null,
36179
+ chartImage: await this.buildExportChart(),
35949
36180
  columns: this.exportColumnOptions()
35950
36181
  }
35951
36182
  );
@@ -35961,7 +36192,8 @@ var DeviceReportModal = class {
35961
36192
  this.resolveCustomerName() || null,
35962
36193
  {
35963
36194
  accentColor: this.resolveAccentHex(),
35964
- columns: this.exportColumnOptions()
36195
+ columns: this.exportColumnOptions(),
36196
+ kpis: this.computeKpis()
35965
36197
  }
35966
36198
  );
35967
36199
  }
@@ -38945,28 +39177,43 @@ var AllReportModal = class {
38945
39177
  // Header row only
38946
39178
  ["Identificador", "Nome", `Consumo (${this.domainConfig.unit})`],
38947
39179
  // Data rows
38948
- ...sortedData.map((row) => [row.identifier, row.name, row.consumption.toFixed(2)])
39180
+ ...sortedData.map((row) => [row.identifier, row.name, row.consumption.toFixed(2).replace(".", ",")])
38949
39181
  ];
38950
39182
  const csvContent = toCsv(csvData);
38951
39183
  this.downloadCSV(csvContent, `relatorio-geral-lojas-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`);
38952
39184
  }
38953
39185
  // Temperatura: valores já com offset e sem leituras inválidas (mesma base da tabela).
38954
39186
  // 1d → uma linha por sensor por DIA; 1h → uma linha por sensor por HORA.
38955
- exportTemperatureCSV(rows) {
39187
+ exportTemperatureCSV(_rows) {
39188
+ const rows = this.getFilteredData();
38956
39189
  const showId = this.showIdentifierColumn(rows);
38957
39190
  const idCol = (r) => showId ? [r.identifier] : [];
38958
39191
  const idHead = showId ? ["Identificador"] : [];
38959
- const n2 = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : v.toFixed(2);
39192
+ const n2 = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : v.toFixed(2).replace(".", ",");
38960
39193
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
39194
+ const gran = this.granularity;
38961
39195
  const fmtStamp = (ts) => {
38962
39196
  const { date: d, time } = this.formatSpDateTime(ts);
38963
39197
  return `${d} ${time}`;
38964
39198
  };
39199
+ const p = this.exportPeriod;
39200
+ const fmtD = (iso) => iso ? new Date(iso).toLocaleDateString("pt-BR") : "";
39201
+ const meta = [
39202
+ ["Relat\xF3rio", this.resolveTitle()],
39203
+ ...this.params.customerName ? [["Cliente", this.params.customerName]] : [],
39204
+ ...p?.startISO ? [["Per\xEDodo", `${fmtD(p.startISO)} \u2014 ${fmtD(p.endISO)}`]] : [],
39205
+ ["Granularidade", gran === "1h" ? "Hora" : "Dia"],
39206
+ ["Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR")],
39207
+ [],
39208
+ ...this.computeKpis().map((k) => [k.label, k.value, k.sub || ""]),
39209
+ []
39210
+ ];
38965
39211
  const summary = [
38966
- [...idHead, "Sensor", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "Data/Hora M\xEDn", "M\xE1x (\xB0C)", "Data/Hora M\xE1x", "Leituras", "Leituras descartadas"],
39212
+ [...idHead, "Sensor", "Offset (\xB0C)", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "Data/Hora M\xEDn", "M\xE1x (\xB0C)", "Data/Hora M\xE1x", "Leituras", "Leituras descartadas"],
38967
39213
  ...rows.map((r) => [
38968
39214
  ...idCol(r),
38969
39215
  r.name,
39216
+ r.id ? n2(this.temperatureOffsetFor(r.id)) : "",
38970
39217
  r.noData ? "Sem leitura" : n2(r.consumption),
38971
39218
  r.noData ? "" : n2(r.min),
38972
39219
  r.noData || !r.minTs ? "" : fmtStamp(r.minTs),
@@ -38976,7 +39223,6 @@ var AllReportModal = class {
38976
39223
  String(r.discarded ?? "")
38977
39224
  ])
38978
39225
  ];
38979
- const gran = this.granularity;
38980
39226
  const series = [[...idHead, "Sensor", gran === "1h" ? "Data/Hora" : "Dia", "Temperatura (\xB0C)"]];
38981
39227
  const fmtTs = (ts) => gran === "1h" ? new Date(ts).toLocaleString("pt-BR", {
38982
39228
  day: "2-digit",
@@ -38989,9 +39235,9 @@ var AllReportModal = class {
38989
39235
  for (const r of rows) {
38990
39236
  const raw = r.id && this.temperatureStats.get(r.id)?.series || [];
38991
39237
  const pts = gran === "1h" ? this.aggregateHourly(raw, "avg") : this.aggregateDaily(raw, "avg");
38992
- for (const p of pts) series.push([...idCol(r), r.name, fmtTs(p.timestamp), p.value.toFixed(2)]);
39238
+ for (const p2 of pts) series.push([...idCol(r), r.name, fmtTs(p2.timestamp), n2(p2.value)]);
38993
39239
  }
38994
- const csvContent = toCsv([...summary, [], ...series]);
39240
+ const csvContent = toCsv([...meta, ...summary, [], ...series]);
38995
39241
  this.downloadCSV(csvContent, `relatorio-temperatura-${gran}-${date}.csv`);
38996
39242
  }
38997
39243
  // 1h CSV: one row per device × hour. The series comes from per-device requests
@@ -39016,7 +39262,7 @@ var AllReportModal = class {
39016
39262
  for (const row of sortedData) {
39017
39263
  const points = row.id && series.get(row.id) || [];
39018
39264
  for (const p of points) {
39019
- csvData.push([row.identifier, row.name, fmtTs(p.timestamp), p.value.toFixed(2)]);
39265
+ csvData.push([row.identifier, row.name, fmtTs(p.timestamp), p.value.toFixed(2).replace(".", ",")]);
39020
39266
  }
39021
39267
  }
39022
39268
  if (csvData.length === 1) {
@@ -39080,28 +39326,75 @@ var AllReportModal = class {
39080
39326
  }
39081
39327
  // Maps the report rows to the TelemetryDevice shape consumed by the shared
39082
39328
  // TELEMETRY grid exporters (only labelOrName/name/deviceIdentifier/val/perc are read).
39329
+ // Linhas dos exports = a TABELA DA TELA: mesma ordem/filtros (getFilteredData), cabeçalhos
39330
+ // de grupo com o mesmo resumo, e — em temperatura — Mín/Máx, offset e "Sem leitura".
39083
39331
  buildExportDevices() {
39084
- const sorted = [...this.data].sort(
39085
- (a, b) => !!a.noData !== !!b.noData ? a.noData ? 1 : -1 : b.consumption - a.consumption
39086
- );
39087
- const total = sorted.reduce((s, r) => s + (r.consumption || 0), 0);
39088
- return sorted.map((r) => ({
39089
- labelOrName: r.name,
39090
- name: r.name,
39091
- deviceIdentifier: r.identifier,
39092
- // Temperatura: média do sensor (sem leitura → "—"); sem % de participação
39093
- val: this.isTemperature && r.noData ? null : r.consumption,
39094
- perc: this.isTemperature ? void 0 : total > 0 ? r.consumption / total * 100 : 0
39095
- }));
39332
+ const rows = this.getFilteredData();
39333
+ const grandTotal = this.calculateTotalConsumption();
39334
+ const toDevice = (r) => {
39335
+ const base = {
39336
+ labelOrName: r.name,
39337
+ name: r.name,
39338
+ deviceIdentifier: r.identifier,
39339
+ val: this.isTemperature && r.noData ? null : r.consumption,
39340
+ perc: this.isTemperature ? void 0 : grandTotal > 0 ? r.consumption / grandTotal * 100 : 0
39341
+ };
39342
+ if (!this.isTemperature) return base;
39343
+ const off = r.id && !r.noData ? this.temperatureOffsetFor(r.id) : 0;
39344
+ return {
39345
+ ...base,
39346
+ extraCells: [this.fmtTemp(r.noData ? null : r.min), this.fmtTemp(r.noData ? null : r.max)],
39347
+ ...off ? { nameNote: `offset ${off > 0 ? "+" : "\u2212"}${fmtPt(Math.abs(off))} \xB0C` } : {}
39348
+ };
39349
+ };
39350
+ const out = [];
39351
+ if (rows.some((r) => r.groupLabel)) {
39352
+ const order = [];
39353
+ const byGroup = /* @__PURE__ */ new Map();
39354
+ for (const r of rows) {
39355
+ const g = r.groupLabel || "\u2014";
39356
+ if (!byGroup.has(g)) {
39357
+ byGroup.set(g, []);
39358
+ order.push(g);
39359
+ }
39360
+ byGroup.get(g).push(r);
39361
+ }
39362
+ for (const g of order) {
39363
+ const items = byGroup.get(g);
39364
+ out.push({ groupHeader: `${g.toUpperCase()} \xB7 ${this.groupSummaryText(items)}` });
39365
+ items.forEach((r) => out.push(toDevice(r)));
39366
+ }
39367
+ } else {
39368
+ rows.forEach((r) => out.push(toDevice(r)));
39369
+ }
39370
+ return out;
39371
+ }
39372
+ // Mesmo texto do cabeçalho de grupo da tabela
39373
+ groupSummaryText(items) {
39374
+ if (this.isTemperature) {
39375
+ const withData = items.filter((r) => !r.noData);
39376
+ const avg = withData.length ? withData.reduce((s, r) => s + r.consumption, 0) / withData.length : null;
39377
+ return `${items.length} sensores \xB7 m\xE9dia ${this.fmtTemp(avg)} \xB0C`;
39378
+ }
39379
+ const total = items.reduce((s, r) => s + r.consumption, 0);
39380
+ return `${items.length} dispositivos \xB7 ${fmtPt(total)} ${this.domainConfig.unit}`;
39096
39381
  }
39097
- // Colunas do PDF/XLS: temperatura = Sensor | Temperatura média (sem %, Identificador só se útil)
39382
+ // Colunas do PDF/XLS = colunas da tela. Temperatura: Sensor | Média | Mín | Máx (sem %).
39098
39383
  exportColumns() {
39099
39384
  if (!this.isTemperature) return null;
39100
39385
  return {
39101
39386
  nameLabel: "Sensor",
39102
- valueLabel: "Temperatura m\xE9dia (\xB0C)",
39387
+ valueLabel: "M\xE9dia (\xB0C)",
39103
39388
  hidePerc: true,
39104
- hideIdentifier: !this.showIdentifierColumn()
39389
+ hideIdentifier: !this.showIdentifierColumn(),
39390
+ extraColumns: [
39391
+ { label: "M\xEDn (\xB0C)", pdfW: 35 },
39392
+ { label: "M\xE1x (\xB0C)", pdfW: 35 }
39393
+ ],
39394
+ emptyValueText: "Sem leitura",
39395
+ countLabel: "sensor(es)",
39396
+ valueDecimals: 2
39397
+ // "30,00" como na tela (não "30")
39105
39398
  };
39106
39399
  }
39107
39400
  // Accent hex da paleta do dashboard (tema efetivo) para o PDF/XLS — cai no
@@ -39115,26 +39408,38 @@ var AllReportModal = class {
39115
39408
  const vars = typeof theme.cssVars === "function" ? theme.cssVars() : theme;
39116
39409
  return vars?.["--myio-brand-700"];
39117
39410
  }
39411
+ // Gráfico da página final do PDF = o painel da direita da tela
39412
+ async buildExportChart() {
39413
+ if (this.isTemperature) {
39414
+ const png = renderTemperatureBarsPng({
39415
+ items: this.getFilteredData().map((r) => ({ label: r.name, value: r.noData ? null : r.consumption })),
39416
+ ideal: this.temperatureIdealRange,
39417
+ accent: this.resolveAccentHex()
39418
+ });
39419
+ return png ? { ...png, title: "Temperatura m\xE9dia por sensor" } : null;
39420
+ }
39421
+ const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
39422
+ return chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null;
39423
+ }
39118
39424
  // PDF export — layout premium do grid + paleta do dashboard + faixa de KPIs +
39119
- // página dedicada com o gráfico de participação da modal.
39425
+ // página dedicada com o mesmo gráfico do painel da modal.
39120
39426
  async exportPDF() {
39121
39427
  if (!this.data.length) return;
39122
- const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
39123
39428
  exportGridPdf(
39124
39429
  this.buildExportDevices(),
39125
39430
  this.resolveTitle(),
39126
39431
  this.domainConfig.unit,
39127
39432
  this.exportPeriod,
39128
- null,
39433
+ this.params.customerName || null,
39129
39434
  {
39130
39435
  accentColor: this.resolveAccentHex(),
39131
39436
  kpis: this.computeKpis(),
39132
39437
  columns: this.exportColumns(),
39133
- chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null
39438
+ chartImage: await this.buildExportChart()
39134
39439
  }
39135
39440
  );
39136
39441
  }
39137
- // XLS export (XML Spreadsheet) — same as the TELEMETRY grid export.
39442
+ // XLS export (XML Spreadsheet) — mesma tabela + KPIs da tela.
39138
39443
  exportXLS() {
39139
39444
  if (!this.data.length) return;
39140
39445
  exportGridXls(
@@ -39142,8 +39447,8 @@ var AllReportModal = class {
39142
39447
  this.resolveTitle(),
39143
39448
  this.domainConfig.unit,
39144
39449
  this.exportPeriod,
39145
- null,
39146
- { accentColor: this.resolveAccentHex(), columns: this.exportColumns() }
39450
+ this.params.customerName || null,
39451
+ { accentColor: this.resolveAccentHex(), columns: this.exportColumns(), kpis: this.computeKpis() }
39147
39452
  );
39148
39453
  }
39149
39454
  async fetchCustomerTotals(startISO, endISO) {
@@ -79616,7 +79921,10 @@ async function openTemperatureModal(params) {
79616
79921
  document.body.appendChild(modalContainer);
79617
79922
  renderModal3(modalContainer, state6, modalId);
79618
79923
  try {
79619
- state6.data = await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs));
79924
+ state6.data = discardInvalid(
79925
+ await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs)),
79926
+ state6
79927
+ );
79620
79928
  state6.stats = calculateStats(state6.data, state6.clampRange, state6.temperatureOffset);
79621
79929
  state6.isLoading = false;
79622
79930
  renderModal3(modalContainer, state6, modalId);
@@ -79641,7 +79949,10 @@ async function openTemperatureModal(params) {
79641
79949
  state6.isLoading = true;
79642
79950
  renderModal3(modalContainer, state6, modalId);
79643
79951
  try {
79644
- state6.data = await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs));
79952
+ state6.data = discardInvalid(
79953
+ await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs)),
79954
+ state6
79955
+ );
79645
79956
  state6.stats = calculateStats(state6.data, state6.clampRange, state6.temperatureOffset);
79646
79957
  state6.isLoading = false;
79647
79958
  renderModal3(modalContainer, state6, modalId);
@@ -80269,7 +80580,10 @@ async function setupEventListeners3(container, state6, modalId, onClose) {
80269
80580
  state6.dateRangePicker = null;
80270
80581
  renderModal3(container, state6, modalId);
80271
80582
  try {
80272
- state6.data = await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs));
80583
+ state6.data = discardInvalid(
80584
+ await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs)),
80585
+ state6
80586
+ );
80273
80587
  state6.stats = calculateStats(state6.data, state6.clampRange, state6.temperatureOffset);
80274
80588
  state6.isLoading = false;
80275
80589
  renderModal3(container, state6, modalId);
@@ -80283,19 +80597,64 @@ async function setupEventListeners3(container, state6, modalId, onClose) {
80283
80597
  }
80284
80598
  });
80285
80599
  }
80600
+ function discardInvalid(data, state6) {
80601
+ const off = Number(state6.temperatureOffset) || 0;
80602
+ return data.filter((p) => {
80603
+ const v = Number(p.value) + off;
80604
+ return Number.isFinite(v) && v >= state6.clampRange.min && v <= state6.clampRange.max;
80605
+ });
80606
+ }
80286
80607
  function exportCsv(state6) {
80287
80608
  if (state6.data.length === 0) return;
80288
- const startDateStr = new Date(state6.startTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
80289
- const endDateStr = new Date(state6.endTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
80290
- exportTemperatureCSV(
80291
- state6.data,
80292
- state6.label,
80293
- state6.stats,
80294
- startDateStr,
80295
- endDateStr,
80296
- state6.clampRange,
80297
- state6.temperatureOffset
80298
- );
80609
+ const off = Number(state6.temperatureOffset) || 0;
80610
+ const n2 = (v) => v.toFixed(2).replace(".", ",");
80611
+ const q = (v) => `"${String(v).replace(/"/g, '""')}"`;
80612
+ const dt = (ts) => new Date(ts).toLocaleString("pt-BR", {
80613
+ day: "2-digit",
80614
+ month: "2-digit",
80615
+ year: "numeric",
80616
+ hour: "2-digit",
80617
+ minute: "2-digit",
80618
+ timeZone: state6.timezone || "America/Sao_Paulo"
80619
+ });
80620
+ const fmtD = (ts) => new Date(ts).toLocaleDateString("pt-BR", { timeZone: state6.timezone || "America/Sao_Paulo" });
80621
+ const filtered = filterByDayPeriods(state6.data, state6.selectedPeriods);
80622
+ const lines = [
80623
+ ["Relat\xF3rio", `${state6.label} - Hist\xF3rico de Temperatura`],
80624
+ ...state6.deviceName ? [["Dispositivo (TB)", state6.deviceName]] : [],
80625
+ ...state6.customerName ? [["Cliente", state6.customerName]] : [],
80626
+ ["Per\xEDodo", `${fmtD(state6.startTs)} \u2014 ${fmtD(state6.endTs)}`],
80627
+ ["Granularidade", state6.granularity === "hour" ? "Hora (m\xE9dia da hora fechada)" : "Dia (m\xE9dia do dia)"],
80628
+ ["Per\xEDodos do dia", getSelectedPeriodsLabel(state6.selectedPeriods)],
80629
+ ...off !== 0 ? [["Offset aplicado (\xB0C)", n2(off)]] : [],
80630
+ ["Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR")],
80631
+ [],
80632
+ ["Temperatura atual (\xB0C)", state6.currentTemperature !== null ? n2(state6.currentTemperature) : "N/A"],
80633
+ ["M\xE9dia do per\xEDodo (\xB0C)", state6.stats.count > 0 ? n2(state6.stats.avg) : "N/A"],
80634
+ ["M\xEDnima (\xB0C)", state6.stats.count > 0 ? n2(state6.stats.min) : "N/A"],
80635
+ ["M\xE1xima (\xB0C)", state6.stats.count > 0 ? n2(state6.stats.max) : "N/A"],
80636
+ ["Leituras", String(state6.stats.count)],
80637
+ []
80638
+ ];
80639
+ if (state6.granularity === "hour") {
80640
+ lines.push(["Data/Hora", "Temperatura (\xB0C)"]);
80641
+ aggregateByHour(filtered, state6.clampRange, off).forEach((p) => lines.push([dt(p.ts), n2(Number(p.value))]));
80642
+ } else {
80643
+ lines.push(["Dia", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "M\xE1x (\xB0C)", "Leituras"]);
80644
+ aggregateByDay(filtered, state6.clampRange, off).forEach(
80645
+ (d) => lines.push([fmtD(d.dateTs), n2(d.avg), n2(d.min), n2(d.max), String(d.count)])
80646
+ );
80647
+ }
80648
+ const csv = "\uFEFF" + lines.map((r) => r.map(q).join(";")).join("\r\n");
80649
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
80650
+ const url = URL.createObjectURL(blob);
80651
+ const a = document.createElement("a");
80652
+ a.href = url;
80653
+ a.download = `temperatura_${state6.label.replace(/\s+/g, "_")}_${state6.granularity === "hour" ? "1h" : "1d"}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
80654
+ document.body.appendChild(a);
80655
+ a.click();
80656
+ document.body.removeChild(a);
80657
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
80299
80658
  }
80300
80659
  function buildHeaderTitle(state6) {
80301
80660
  const base = `${escHtml3(state6.label)} - Hist\xF3rico de Temperatura`;
@@ -81202,43 +81561,63 @@ function mountCmpFooter(container, state6, modalId) {
81202
81561
  _cmpFooters.set(modalId, footer);
81203
81562
  }
81204
81563
  function exportComparisonCSV2(state6) {
81205
- const startDateStr = new Date(state6.startTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
81206
- const endDateStr = new Date(state6.endTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
81207
- const BOM = "\uFEFF";
81208
- let csvContent = BOM;
81209
- csvContent += `Compara\xE7\xE3o de Temperatura
81210
- `;
81211
- csvContent += `Per\xEDodo: ${startDateStr} at\xE9 ${endDateStr}
81212
- `;
81213
- csvContent += `Sensores: ${state6.devices.map((d) => d.label).join(", ")}
81214
- `;
81215
- csvContent += "\n";
81216
- csvContent += "Estat\xEDsticas por Sensor:\n";
81217
- csvContent += "Sensor,M\xE9dia (\xB0C),Min (\xB0C),Max (\xB0C),Leituras\n";
81218
- state6.deviceData.forEach((dd) => {
81219
- csvContent += `"${dd.device.label}",${dd.stats.avg.toFixed(2)},${dd.stats.min.toFixed(2)},${dd.stats.max.toFixed(2)},${dd.stats.count}
81220
- `;
81564
+ const n2 = (v) => v.toFixed(2).replace(".", ",");
81565
+ const q = (v) => `"${String(v).replace(/"/g, '""')}"`;
81566
+ const fmtD = (ts) => new Date(ts).toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });
81567
+ const dt = (ts) => new Date(ts).toLocaleString("pt-BR", {
81568
+ day: "2-digit",
81569
+ month: "2-digit",
81570
+ year: "numeric",
81571
+ hour: "2-digit",
81572
+ minute: "2-digit",
81573
+ timeZone: "America/Sao_Paulo"
81221
81574
  });
81222
- csvContent += "\n";
81223
- csvContent += "Dados Detalhados:\n";
81224
- csvContent += "Data/Hora,Sensor,Temperatura (\xB0C)\n";
81225
- state6.deviceData.forEach((dd) => {
81226
- dd.data.forEach((item) => {
81227
- const date = new Date(item.ts).toLocaleString(state6.locale);
81228
- const temp = Number(item.value).toFixed(2);
81229
- csvContent += `"${date}","${dd.device.label}",${temp}
81230
- `;
81231
- });
81575
+ const offTxt = (d) => {
81576
+ const o = Number(d.temperatureOffset) || 0;
81577
+ return o !== 0 ? n2(o) : "";
81578
+ };
81579
+ const lines = [
81580
+ ["Relat\xF3rio", `Compara\xE7\xE3o de Temperatura - ${state6.devices.length} sensores`],
81581
+ ...state6.customerName ? [["Cliente", state6.customerName]] : [],
81582
+ ["Per\xEDodo", `${fmtD(state6.startTs)} \u2014 ${fmtD(state6.endTs)}`],
81583
+ ["Granularidade", state6.granularity === "hour" ? "Hora (m\xE9dia da hora fechada)" : "Dia (m\xE9dia do dia)"],
81584
+ ["Per\xEDodos do dia", getSelectedPeriodsLabel(state6.selectedPeriods)],
81585
+ ["Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR")],
81586
+ [],
81587
+ ["Sensor", "Offset (\xB0C)", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "M\xE1x (\xB0C)", "Leituras"],
81588
+ ...state6.deviceData.map((dd) => [
81589
+ dd.device.label,
81590
+ offTxt(dd.device),
81591
+ dd.stats.count > 0 ? n2(dd.stats.avg) : "N/A",
81592
+ dd.stats.count > 0 ? n2(dd.stats.min) : "N/A",
81593
+ dd.stats.count > 0 ? n2(dd.stats.max) : "N/A",
81594
+ String(dd.stats.count)
81595
+ ]),
81596
+ []
81597
+ ];
81598
+ const perDevice = state6.deviceData.map((dd) => {
81599
+ const filtered = filterByDayPeriods(dd.data, state6.selectedPeriods);
81600
+ const pts = state6.granularity === "hour" ? aggregateByHour(filtered, state6.clampRange).map((p) => [p.ts, Number(p.value)]) : aggregateByDay(filtered, state6.clampRange).map((d) => [d.dateTs, d.avg]);
81601
+ return new Map(pts);
81232
81602
  });
81233
- const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
81603
+ const allTs = [...new Set(perDevice.flatMap((m) => [...m.keys()]))].sort((a, b) => a - b);
81604
+ lines.push([state6.granularity === "hour" ? "Data/Hora" : "Dia", ...state6.deviceData.map((dd) => `${dd.device.label} (\xB0C)`)]);
81605
+ allTs.forEach(
81606
+ (ts) => lines.push([
81607
+ state6.granularity === "hour" ? dt(ts) : fmtD(ts),
81608
+ ...perDevice.map((m) => m.has(ts) ? n2(m.get(ts)) : "")
81609
+ ])
81610
+ );
81611
+ const csv = "\uFEFF" + lines.map((r) => r.map(q).join(";")).join("\r\n");
81612
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
81234
81613
  const url = URL.createObjectURL(blob);
81235
81614
  const link = document.createElement("a");
81236
81615
  link.href = url;
81237
- link.download = `comparacao_temperatura_${startDateStr}_${endDateStr}.csv`;
81616
+ link.download = `comparacao_temperatura_${state6.granularity === "hour" ? "1h" : "1d"}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
81238
81617
  document.body.appendChild(link);
81239
81618
  link.click();
81240
81619
  document.body.removeChild(link);
81241
- URL.revokeObjectURL(url);
81620
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
81242
81621
  }
81243
81622
 
81244
81623
  // src/components/temperature/TemperatureSettingsModal.ts