myio-js-library 0.1.551 → 0.1.553

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.551",
902
+ version: "0.1.553",
903
903
  description: "A clean, standalone JS SDK for MYIO projects",
904
904
  license: "MIT",
905
905
  repository: "github:gh-myio/myio-js-library",
@@ -5568,12 +5568,14 @@ var ModalHeader = {
5568
5568
  showClose = true,
5569
5569
  primaryColor = "#3e1a7d",
5570
5570
  borderRadius = "10px 10px 0 0",
5571
- draggable = true
5571
+ draggable = true,
5572
+ solid = false
5572
5573
  } = options;
5573
5574
  const isDark = theme === "dark";
5574
- const bgColor = isDark ? primaryColor : "linear-gradient(90deg, #f1f5f9 0%, #e2e8f0 100%)";
5575
- const textColor = isDark ? "white" : "#475569";
5576
- const btnColor = isDark ? "rgba(255,255,255,0.8)" : "rgba(71,85,105,0.8)";
5575
+ const solidStyle = isDark || solid;
5576
+ const bgColor = solidStyle ? primaryColor : "linear-gradient(90deg, #f1f5f9 0%, #e2e8f0 100%)";
5577
+ const textColor = solidStyle ? "white" : "#475569";
5578
+ const btnColor = solidStyle ? "rgba(255,255,255,0.8)" : "rgba(71,85,105,0.8)";
5577
5579
  const headerStyle = `
5578
5580
  padding: 8px 12px;
5579
5581
  display: flex;
@@ -33572,31 +33574,52 @@ var fmtPt = (n) => new Intl.NumberFormat("pt-BR", { minimumFractionDigits: 2, ma
33572
33574
 
33573
33575
  // src/components/telemetry-grid-shopping/export.ts
33574
33576
  var import_jspdf = require("jspdf");
33577
+ function extras(d) {
33578
+ return d;
33579
+ }
33575
33580
  function makeCols(unit, colOpts) {
33576
33581
  const cols = [
33577
33582
  { key: "idx", label: "#", pdfW: 10 },
33578
33583
  { key: "nome", label: colOpts?.nameLabel || "Nome", pdfW: 100 },
33579
33584
  { key: "identificador", label: "Identificador", pdfW: 60 },
33580
33585
  { key: "consumo", label: colOpts?.valueLabel || (unit ? `Consumo (${unit})` : "Consumo"), pdfW: 50 },
33586
+ ...(colOpts?.extraColumns || []).map((c, i) => ({ key: `x${i}`, label: c.label, pdfW: c.pdfW ?? 40 })),
33581
33587
  { key: "perc", label: "%", pdfW: 20 }
33582
33588
  ];
33583
33589
  return cols.filter(
33584
33590
  (c) => !(colOpts?.hideIdentifier && c.key === "identificador") && !(colOpts?.hidePerc && c.key === "perc")
33585
33591
  );
33586
33592
  }
33587
- function buildRow(d, idx) {
33593
+ function buildRow(d, idx, colOpts) {
33594
+ const x = extras(d);
33588
33595
  const fmtVal = () => {
33589
- if (d.val === null || d.val === void 0) return "\u2014";
33596
+ if (d.val === null || d.val === void 0) return colOpts?.emptyValueText || "\u2014";
33590
33597
  return Number(d.val).toLocaleString("pt-BR", { maximumFractionDigits: 3, useGrouping: false });
33591
33598
  };
33592
33599
  const pd = resolvePercentDecimals();
33593
- return {
33600
+ const name = d.labelOrName || d.name || "\u2014";
33601
+ const row = {
33594
33602
  idx: String(idx + 1),
33595
- nome: d.labelOrName || d.name || "\u2014",
33603
+ nome: x.nameNote ? `${name} (${x.nameNote})` : name,
33596
33604
  identificador: d.deviceIdentifier || "\u2014",
33597
33605
  consumo: fmtVal(),
33598
33606
  perc: d.perc !== void 0 ? `${d.perc.toFixed(pd).replace(".", ",")}%` : "\u2014"
33599
33607
  };
33608
+ (colOpts?.extraColumns || []).forEach((_, i) => {
33609
+ row[`x${i}`] = x.extraCells?.[i] ?? "\u2014";
33610
+ });
33611
+ return row;
33612
+ }
33613
+ function forEachRow(devices, onGroup, onRow) {
33614
+ let n = 0;
33615
+ devices.forEach((d) => {
33616
+ const g = extras(d).groupHeader;
33617
+ if (g) onGroup(g);
33618
+ else onRow(d, n++);
33619
+ });
33620
+ }
33621
+ function countDataRows(devices) {
33622
+ return devices.filter((d) => !extras(d).groupHeader).length;
33600
33623
  }
33601
33624
  function fmtPeriod(period) {
33602
33625
  if (!period?.startISO) return "";
@@ -33644,10 +33667,15 @@ function exportGridCsv(devices, label, unit, period, customerName) {
33644
33667
  ].filter((v) => v !== null);
33645
33668
  const cols = makeCols(unit);
33646
33669
  const header = cols.map((c) => `"${c.label}"`).join(";");
33647
- const rows = devices.map((d, i) => {
33648
- const r = buildRow(d, i);
33649
- return cols.map((c) => `"${String(r[c.key]).replace(/"/g, '""')}"`).join(";");
33650
- });
33670
+ const rows = [];
33671
+ forEachRow(
33672
+ devices,
33673
+ (g) => rows.push(`"${g.replace(/"/g, '""')}"`),
33674
+ (d, i) => {
33675
+ const r = buildRow(d, i);
33676
+ rows.push(cols.map((c) => `"${String(r[c.key]).replace(/"/g, '""')}"`).join(";"));
33677
+ }
33678
+ );
33651
33679
  const csv = "\uFEFF" + [...metaRows, header, ...rows].join("\r\n");
33652
33680
  triggerDownload(
33653
33681
  new Blob([csv], { type: "text/csv;charset=utf-8;" }),
@@ -33663,14 +33691,23 @@ function exportGridXls(devices, label, unit, period, customerName, options) {
33663
33691
  const headerCells = cols.map(
33664
33692
  (c) => `<Cell ss:StyleID="h"><Data ss:Type="String">${escXml(c.label)}</Data></Cell>`
33665
33693
  ).join("");
33666
- const dataRows = devices.map((d, i) => {
33667
- const r = buildRow(d, i);
33668
- const cells = cols.map((c) => {
33669
- const v = escXml(String(r[c.key]));
33670
- return `<Cell><Data ss:Type="String">${v}</Data></Cell>`;
33671
- }).join("");
33672
- return `<Row>${cells}</Row>`;
33673
- }).join("\n");
33694
+ const dataRowsArr = [];
33695
+ forEachRow(
33696
+ devices,
33697
+ (g) => dataRowsArr.push(
33698
+ `<Row><Cell ss:StyleID="g" ss:MergeAcross="${cols.length - 1}"><Data ss:Type="String">${escXml(g)}</Data></Cell></Row>`
33699
+ ),
33700
+ (d, i) => {
33701
+ const r = buildRow(d, i, options?.columns);
33702
+ const cells = cols.map((c) => {
33703
+ const v = escXml(String(r[c.key]));
33704
+ return `<Cell><Data ss:Type="String">${v}</Data></Cell>`;
33705
+ }).join("");
33706
+ dataRowsArr.push(`<Row>${cells}</Row>`);
33707
+ }
33708
+ );
33709
+ const dataRows = dataRowsArr.join("\n");
33710
+ const kpiRows = (options?.kpis || []).map((k) => metaRow(k.label, k.sub ? `${k.value} (${k.sub})` : k.value)).join("\n");
33674
33711
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
33675
33712
  <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
33676
33713
  xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
@@ -33683,6 +33720,10 @@ function exportGridXls(devices, label, unit, period, customerName, options) {
33683
33720
  <Font ss:Bold="1"/>
33684
33721
  <Interior ss:Color="#F0EDF9" ss:Pattern="Solid"/>
33685
33722
  </Style>
33723
+ <Style ss:ID="g">
33724
+ <Font ss:Bold="1" ss:Color="#555555"/>
33725
+ <Interior ss:Color="#EEEEEE" ss:Pattern="Solid"/>
33726
+ </Style>
33686
33727
  </Styles>
33687
33728
  <Worksheet ss:Name="${escXml(label.slice(0, 31))}">
33688
33729
  <Table>
@@ -33690,6 +33731,8 @@ function exportGridXls(devices, label, unit, period, customerName, options) {
33690
33731
  ${customerName ? metaRow("Cliente", customerName) : ""}
33691
33732
  ${periodLabel2 ? metaRow("Per\xEDodo", periodLabel2) : ""}
33692
33733
  ${metaRow("Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR"))}
33734
+ ${kpiRows ? `<Row/>
33735
+ ${kpiRows}` : ""}
33693
33736
  <Row/>
33694
33737
  <Row>${headerCells}</Row>
33695
33738
  ${dataRows}
@@ -33744,7 +33787,8 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33744
33787
  doc.setFontSize(8);
33745
33788
  const periodLabel2 = fmtPeriod(period);
33746
33789
  const periodPart = periodLabel2 ? `Per\xEDodo: ${periodLabel2} \u2022 ` : "";
33747
- const info = `${periodPart}Gerado em: ${generatedAt} \u2022 ${devices.length} dispositivo(s) \u2022 Unidade: ${unit} \u2022 P\xE1g. ${pageNo2}`;
33790
+ const countLabel = options?.columns?.countLabel || "dispositivo(s)";
33791
+ const info = `${periodPart}Gerado em: ${generatedAt} \u2022 ${countDataRows(devices)} ${countLabel} \u2022 Unidade: ${unit} \u2022 P\xE1g. ${pageNo2}`;
33748
33792
  doc.text(info, PW - MARGIN, HDR_H / 2 + 1.5, { align: "right" });
33749
33793
  }
33750
33794
  function drawColumnHeaders(y) {
@@ -33779,6 +33823,14 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33779
33823
  doc.setLineWidth(0.1);
33780
33824
  doc.line(MARGIN, y + ROW_H, MARGIN + TABLE_W, y + ROW_H);
33781
33825
  }
33826
+ function drawGroupRow(text2, y) {
33827
+ doc.setFillColor(...ACCENT_SOFT);
33828
+ doc.rect(MARGIN, y, TABLE_W, ROW_H, "F");
33829
+ doc.setTextColor(...ACCENT2);
33830
+ doc.setFont("helvetica", "bold");
33831
+ doc.setFontSize(6.5);
33832
+ doc.text(truncate(text2, 160), MARGIN + 1.5, y + ROW_H / 2 + 2.2);
33833
+ }
33782
33834
  function drawFooter2() {
33783
33835
  doc.setFillColor(250, 249, 255);
33784
33836
  doc.rect(0, PH - FTR_H, PW, FTR_H, "F");
@@ -33825,7 +33877,7 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33825
33877
  }
33826
33878
  drawColumnHeaders(currentY);
33827
33879
  currentY += HEAD_H;
33828
- devices.forEach((d, i) => {
33880
+ const ensureSpace = () => {
33829
33881
  if (currentY + ROW_H > MAX_Y) {
33830
33882
  drawFooter2();
33831
33883
  doc.addPage();
@@ -33835,9 +33887,20 @@ function exportGridPdf(devices, label, unit, period, customerName, options) {
33835
33887
  drawColumnHeaders(currentY);
33836
33888
  currentY += HEAD_H;
33837
33889
  }
33838
- drawDataRow(buildRow(d, i), currentY, i % 2 === 0);
33839
- currentY += ROW_H;
33840
- });
33890
+ };
33891
+ forEachRow(
33892
+ devices,
33893
+ (g) => {
33894
+ ensureSpace();
33895
+ drawGroupRow(g, currentY);
33896
+ currentY += ROW_H;
33897
+ },
33898
+ (d, i) => {
33899
+ ensureSpace();
33900
+ drawDataRow(buildRow(d, i, options?.columns), currentY, i % 2 === 0);
33901
+ currentY += ROW_H;
33902
+ }
33903
+ );
33841
33904
  drawFooter2();
33842
33905
  const chart = options?.chartImage;
33843
33906
  if (chart?.dataUrl) {
@@ -35088,6 +35151,81 @@ function createModalFooter(params = {}) {
35088
35151
  };
35089
35152
  }
35090
35153
 
35154
+ // src/components/premium-modals/internal/temperatureBarsPng.ts
35155
+ function renderTemperatureBarsPng(opts) {
35156
+ if (typeof document === "undefined" || !opts.items.length) return null;
35157
+ const W = 1400;
35158
+ const ROW = 34;
35159
+ const TOP = 56;
35160
+ const H = TOP + opts.items.length * ROW + 20;
35161
+ const canvas = document.createElement("canvas");
35162
+ canvas.width = W;
35163
+ canvas.height = H;
35164
+ let ctx = null;
35165
+ try {
35166
+ ctx = canvas.getContext("2d");
35167
+ } catch {
35168
+ ctx = null;
35169
+ }
35170
+ if (!ctx) return null;
35171
+ const fmt2 = (v) => v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
35172
+ const ideal = opts.ideal && Number.isFinite(opts.ideal.min) && Number.isFinite(opts.ideal.max) ? opts.ideal : null;
35173
+ const values = opts.items.map((i) => i.value).filter((v) => v !== null && Number.isFinite(v));
35174
+ const base = values.length ? values : [20, 30];
35175
+ const lo = Math.floor(Math.min(...base, ideal ? ideal.min : Infinity) - 1);
35176
+ const hi = Math.ceil(Math.max(...base, ideal ? ideal.max : -Infinity) + 1);
35177
+ const span = Math.max(1, hi - lo);
35178
+ const LABEL_W = 460;
35179
+ const VALUE_W = 120;
35180
+ const trackX = LABEL_W + 20;
35181
+ const trackW = W - trackX - VALUE_W - 20;
35182
+ const xOf = (v) => trackX + (v - lo) / span * trackW;
35183
+ ctx.fillStyle = "#ffffff";
35184
+ ctx.fillRect(0, 0, W, H);
35185
+ ctx.fillStyle = "#6b7280";
35186
+ ctx.font = "20px Helvetica, Arial, sans-serif";
35187
+ ctx.textBaseline = "middle";
35188
+ const scale = `Escala ${fmt2(lo)}\u2013${fmt2(hi)} \xB0C`;
35189
+ ctx.fillText(ideal ? `${scale} \xB7 faixa ideal ${fmt2(ideal.min)}\u2013${fmt2(ideal.max)} \xB0C` : scale, 16, 26);
35190
+ opts.items.forEach((item, i) => {
35191
+ const y = TOP + i * ROW;
35192
+ const cy = y + ROW / 2;
35193
+ ctx.fillStyle = "#1f2937";
35194
+ ctx.font = "20px Helvetica, Arial, sans-serif";
35195
+ ctx.textAlign = "left";
35196
+ let label = item.label;
35197
+ while (ctx.measureText(label).width > LABEL_W - 10 && label.length > 4) label = label.slice(0, -2);
35198
+ if (label !== item.label) label = label.slice(0, -1) + "\u2026";
35199
+ ctx.fillText(label, 16, cy);
35200
+ if (item.value === null || !Number.isFinite(item.value)) {
35201
+ ctx.fillStyle = "#9ca3af";
35202
+ ctx.font = "italic 18px Helvetica, Arial, sans-serif";
35203
+ ctx.fillText("Sem leitura", trackX, cy);
35204
+ ctx.textAlign = "right";
35205
+ ctx.fillText("\u2014", W - 16, cy);
35206
+ return;
35207
+ }
35208
+ ctx.fillStyle = "#e5e7eb";
35209
+ ctx.fillRect(trackX, cy - 9, trackW, 18);
35210
+ if (ideal) {
35211
+ ctx.fillStyle = "rgba(34,197,94,0.18)";
35212
+ ctx.fillRect(xOf(ideal.min), cy - 9, xOf(ideal.max) - xOf(ideal.min), 18);
35213
+ }
35214
+ const v = item.value;
35215
+ ctx.fillStyle = ideal && v > ideal.max ? "#ef4444" : ideal && v < ideal.min ? "#3b82f6" : opts.accent || "#3e1a7d";
35216
+ ctx.fillRect(trackX, cy - 6, Math.max(2, xOf(v) - trackX), 12);
35217
+ ctx.fillStyle = "#111827";
35218
+ ctx.font = "bold 20px Helvetica, Arial, sans-serif";
35219
+ ctx.textAlign = "right";
35220
+ ctx.fillText(fmt2(v), W - 16, cy);
35221
+ });
35222
+ try {
35223
+ return { dataUrl: canvas.toDataURL("image/png"), width: W, height: H };
35224
+ } catch {
35225
+ return null;
35226
+ }
35227
+ }
35228
+
35091
35229
  // src/components/premium-modals/report-device/DeviceReportModal.ts
35092
35230
  var DOMAIN_CONFIG = {
35093
35231
  energy: {
@@ -35659,11 +35797,26 @@ var DeviceReportModal = class {
35659
35797
  );
35660
35798
  const zeroCount = this.data.filter((r) => r.consumption <= 0).length;
35661
35799
  const rowLabel = isHourly ? "Hora" : "Dia";
35800
+ let perDay;
35801
+ if (isTemperature) {
35802
+ const byDay = /* @__PURE__ */ new Map();
35803
+ for (const r of rows) {
35804
+ const k = r.date.slice(0, 10);
35805
+ const acc = byDay.get(k) || { sum: 0, n: 0 };
35806
+ acc.sum += r.consumption;
35807
+ acc.n += 1;
35808
+ byDay.set(k, acc);
35809
+ }
35810
+ const dayAvgs = [...byDay.values()].map((d) => d.sum / d.n);
35811
+ perDay = dayAvgs.length ? dayAvgs.reduce((x, y) => x + y, 0) / dayAvgs.length : 0;
35812
+ } else {
35813
+ perDay = total / dayCount;
35814
+ }
35662
35815
  const kpis = [
35663
35816
  { value: `${fmt2(summaryValue)} ${unit}`, label: `${this.domainConfig.summaryLabel} (${unit})` },
35664
- { value: fmt2(total / dayCount), label: `M\xE9dia por Dia (${unit})` }
35817
+ { value: fmt2(perDay), label: `M\xE9dia por Dia (${unit})` }
35665
35818
  ];
35666
- if (isHourly) {
35819
+ if (isHourly && !isTemperature) {
35667
35820
  kpis.push({ value: fmt2(rows.length ? total / rows.length : 0), label: `M\xE9dia por Hora (${unit})` });
35668
35821
  }
35669
35822
  kpis.push(
@@ -35866,10 +36019,22 @@ var DeviceReportModal = class {
35866
36019
  const summaryValue = this.domainConfig.summaryType === "average" ? valid > 0 ? total / valid : 0 : total;
35867
36020
  const now = /* @__PURE__ */ new Date();
35868
36021
  const timestamp = now.toLocaleDateString("pt-BR") + " - " + now.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
36022
+ const p = this.exportPeriod;
36023
+ const fmtD = (iso) => iso ? new Date(iso).toLocaleDateString("pt-BR") : "";
36024
+ const off = Number(this.params.temperatureOffset) || 0;
36025
+ const isTemp = this.domainConfig.summaryType === "average";
36026
+ void summaryValue;
35869
36027
  const csvData = [
35870
36028
  ["Dispositivo/Loja", this.params.identifier || "N/A", this.params.label || ""],
36029
+ ...this.params.deviceName ? [["Dispositivo (TB)", this.params.deviceName, ""]] : [],
36030
+ ...isTemp && off !== 0 ? [["Offset aplicado (\xB0C)", this.domainConfig.formatter(off), ""]] : [],
36031
+ ...this.resolveCustomerName() ? [["Cliente", this.resolveCustomerName(), ""]] : [],
36032
+ ...p?.startISO ? [["Per\xEDodo", `${fmtD(p.startISO)} \u2014 ${fmtD(p.endISO)}`, ""]] : [],
36033
+ ["Granularidade", this.granularity === "1h" ? "Hora" : "Dia", ""],
35871
36034
  ["DATA EMISS\xC3O", timestamp, ""],
35872
- [this.domainConfig.summaryLabel, this.domainConfig.formatter(summaryValue), this.domainConfig.unit],
36035
+ [],
36036
+ ...this.computeKpis().map((k) => [k.label, k.value, k.sub || ""]),
36037
+ [],
35873
36038
  [this.granularity === "1h" ? "Data/Hora" : "Data", this.domainConfig.label, ""],
35874
36039
  ...this.data.map((row) => [this.formatDate(row.date), row.noData ? "Sem leitura" : this.domainConfig.formatter(row.consumption)])
35875
36040
  ];
@@ -35890,7 +36055,11 @@ var DeviceReportModal = class {
35890
36055
  }
35891
36056
  // Título dos exports — mesmo da modal (identificador + etiqueta do device).
35892
36057
  resolveExportTitle() {
35893
- return `Relat\xF3rio - ${this.params.identifier || "SEM IDENTIFICADOR"} - ${this.params.label || "SEM ETIQUETA"}`;
36058
+ const base = `Relat\xF3rio - ${this.params.identifier || "SEM IDENTIFICADOR"} - ${this.params.label || "SEM ETIQUETA"}`;
36059
+ const dev = this.params.deviceName ? ` (${this.params.deviceName})` : "";
36060
+ const off = Number(this.params.temperatureOffset) || 0;
36061
+ const offTxt = this.domainConfig.summaryType === "average" && off !== 0 ? ` \xB7 offset ${off > 0 ? "+" : "\u2212"}${Math.abs(off).toLocaleString("pt-BR", { maximumFractionDigits: 2 })} \xB0C` : "";
36062
+ return `${base}${dev}${offTxt}`;
35894
36063
  }
35895
36064
  // Mapeia as linhas do relatório (dia/hora × consumo) para o shape TelemetryDevice
35896
36065
  // dos exporters compartilhados do grid: labelOrName = data formatada, val = consumo,
@@ -35909,16 +36078,41 @@ var DeviceReportModal = class {
35909
36078
  // Opções de coluna dos exporters: relatório single-device → Data | Consumo | %
35910
36079
  // (Identificador redundante — o device já está no título/filename).
35911
36080
  exportColumnOptions() {
36081
+ const isTemp = this.domainConfig.summaryType === "average";
35912
36082
  return {
35913
36083
  nameLabel: this.granularity === "1h" ? "Data/Hora" : "Data",
35914
- hideIdentifier: true
36084
+ hideIdentifier: true,
36085
+ // Mesmo rótulo da coluna da tela (antes saía "Consumo (°C)" em temperatura)
36086
+ valueLabel: this.domainConfig.label,
36087
+ countLabel: this.granularity === "1h" ? "hora(s)" : "dia(s)",
36088
+ ...isTemp ? { hidePerc: true, emptyValueText: "Sem leitura" } : {}
35915
36089
  };
35916
36090
  }
36091
+ // Gráfico da página final do PDF = painel da direita da tela
36092
+ async buildExportChart() {
36093
+ if (this.domainConfig.summaryType === "average") {
36094
+ const isHourly = this.granularity === "1h";
36095
+ const ranked = [...this.validRows].sort((a, b) => b.consumption - a.consumption);
36096
+ const shown = isHourly ? ranked.slice(0, 24) : ranked;
36097
+ const noData = isHourly ? [] : this.data.filter((r) => r.noData);
36098
+ const png = renderTemperatureBarsPng({
36099
+ items: [
36100
+ ...shown.map((r) => ({ label: this.formatDate(r.date), value: r.consumption })),
36101
+ ...noData.map((r) => ({ label: this.formatDate(r.date), value: null }))
36102
+ ],
36103
+ ideal: this.params.temperatureIdealRange ?? null,
36104
+ accent: this.resolveAccentHex()
36105
+ });
36106
+ const title = isHourly ? `Horas mais quentes${ranked.length > 24 ? " (top 24)" : ""}` : "Dias mais quentes (m\xE9dia do dia)";
36107
+ return png ? { ...png, title } : null;
36108
+ }
36109
+ const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
36110
+ return chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dia" } : null;
36111
+ }
35917
36112
  // PDF export — layout premium do grid + paleta do dashboard + faixa de KPIs +
35918
36113
  // página dedicada com o gráfico "Participação por Dia" da modal.
35919
36114
  async exportPDF() {
35920
36115
  if (!this.data.length) return;
35921
- const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
35922
36116
  exportGridPdf(
35923
36117
  this.buildExportDevices(),
35924
36118
  this.resolveExportTitle(),
@@ -35928,7 +36122,7 @@ var DeviceReportModal = class {
35928
36122
  {
35929
36123
  accentColor: this.resolveAccentHex(),
35930
36124
  kpis: this.computeKpis(),
35931
- chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dia" } : null,
36125
+ chartImage: await this.buildExportChart(),
35932
36126
  columns: this.exportColumnOptions()
35933
36127
  }
35934
36128
  );
@@ -35944,7 +36138,8 @@ var DeviceReportModal = class {
35944
36138
  this.resolveCustomerName() || null,
35945
36139
  {
35946
36140
  accentColor: this.resolveAccentHex(),
35947
- columns: this.exportColumnOptions()
36141
+ columns: this.exportColumnOptions(),
36142
+ kpis: this.computeKpis()
35948
36143
  }
35949
36144
  );
35950
36145
  }
@@ -38935,21 +39130,36 @@ var AllReportModal = class {
38935
39130
  }
38936
39131
  // Temperatura: valores já com offset e sem leituras inválidas (mesma base da tabela).
38937
39132
  // 1d → uma linha por sensor por DIA; 1h → uma linha por sensor por HORA.
38938
- exportTemperatureCSV(rows) {
39133
+ exportTemperatureCSV(_rows) {
39134
+ const rows = this.getFilteredData();
38939
39135
  const showId = this.showIdentifierColumn(rows);
38940
39136
  const idCol = (r) => showId ? [r.identifier] : [];
38941
39137
  const idHead = showId ? ["Identificador"] : [];
38942
39138
  const n2 = (v) => v === null || v === void 0 || !Number.isFinite(v) ? "" : v.toFixed(2);
38943
39139
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
39140
+ const gran = this.granularity;
38944
39141
  const fmtStamp = (ts) => {
38945
39142
  const { date: d, time } = this.formatSpDateTime(ts);
38946
39143
  return `${d} ${time}`;
38947
39144
  };
39145
+ const p = this.exportPeriod;
39146
+ const fmtD = (iso) => iso ? new Date(iso).toLocaleDateString("pt-BR") : "";
39147
+ const meta = [
39148
+ ["Relat\xF3rio", this.resolveTitle()],
39149
+ ...this.params.customerName ? [["Cliente", this.params.customerName]] : [],
39150
+ ...p?.startISO ? [["Per\xEDodo", `${fmtD(p.startISO)} \u2014 ${fmtD(p.endISO)}`]] : [],
39151
+ ["Granularidade", gran === "1h" ? "Hora" : "Dia"],
39152
+ ["Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR")],
39153
+ [],
39154
+ ...this.computeKpis().map((k) => [k.label, k.value, k.sub || ""]),
39155
+ []
39156
+ ];
38948
39157
  const summary = [
38949
- [...idHead, "Sensor", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "Data/Hora M\xEDn", "M\xE1x (\xB0C)", "Data/Hora M\xE1x", "Leituras", "Leituras descartadas"],
39158
+ [...idHead, "Sensor", "Offset (\xB0C)", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "Data/Hora M\xEDn", "M\xE1x (\xB0C)", "Data/Hora M\xE1x", "Leituras", "Leituras descartadas"],
38950
39159
  ...rows.map((r) => [
38951
39160
  ...idCol(r),
38952
39161
  r.name,
39162
+ r.id ? n2(this.temperatureOffsetFor(r.id)) : "",
38953
39163
  r.noData ? "Sem leitura" : n2(r.consumption),
38954
39164
  r.noData ? "" : n2(r.min),
38955
39165
  r.noData || !r.minTs ? "" : fmtStamp(r.minTs),
@@ -38959,7 +39169,6 @@ var AllReportModal = class {
38959
39169
  String(r.discarded ?? "")
38960
39170
  ])
38961
39171
  ];
38962
- const gran = this.granularity;
38963
39172
  const series = [[...idHead, "Sensor", gran === "1h" ? "Data/Hora" : "Dia", "Temperatura (\xB0C)"]];
38964
39173
  const fmtTs = (ts) => gran === "1h" ? new Date(ts).toLocaleString("pt-BR", {
38965
39174
  day: "2-digit",
@@ -38972,9 +39181,9 @@ var AllReportModal = class {
38972
39181
  for (const r of rows) {
38973
39182
  const raw = r.id && this.temperatureStats.get(r.id)?.series || [];
38974
39183
  const pts = gran === "1h" ? this.aggregateHourly(raw, "avg") : this.aggregateDaily(raw, "avg");
38975
- for (const p of pts) series.push([...idCol(r), r.name, fmtTs(p.timestamp), p.value.toFixed(2)]);
39184
+ for (const p2 of pts) series.push([...idCol(r), r.name, fmtTs(p2.timestamp), p2.value.toFixed(2)]);
38976
39185
  }
38977
- const csvContent = toCsv([...summary, [], ...series]);
39186
+ const csvContent = toCsv([...meta, ...summary, [], ...series]);
38978
39187
  this.downloadCSV(csvContent, `relatorio-temperatura-${gran}-${date}.csv`);
38979
39188
  }
38980
39189
  // 1h CSV: one row per device × hour. The series comes from per-device requests
@@ -39063,28 +39272,73 @@ var AllReportModal = class {
39063
39272
  }
39064
39273
  // Maps the report rows to the TelemetryDevice shape consumed by the shared
39065
39274
  // TELEMETRY grid exporters (only labelOrName/name/deviceIdentifier/val/perc are read).
39275
+ // Linhas dos exports = a TABELA DA TELA: mesma ordem/filtros (getFilteredData), cabeçalhos
39276
+ // de grupo com o mesmo resumo, e — em temperatura — Mín/Máx, offset e "Sem leitura".
39066
39277
  buildExportDevices() {
39067
- const sorted = [...this.data].sort(
39068
- (a, b) => !!a.noData !== !!b.noData ? a.noData ? 1 : -1 : b.consumption - a.consumption
39069
- );
39070
- const total = sorted.reduce((s, r) => s + (r.consumption || 0), 0);
39071
- return sorted.map((r) => ({
39072
- labelOrName: r.name,
39073
- name: r.name,
39074
- deviceIdentifier: r.identifier,
39075
- // Temperatura: média do sensor (sem leitura → "—"); sem % de participação
39076
- val: this.isTemperature && r.noData ? null : r.consumption,
39077
- perc: this.isTemperature ? void 0 : total > 0 ? r.consumption / total * 100 : 0
39078
- }));
39278
+ const rows = this.getFilteredData();
39279
+ const grandTotal = this.calculateTotalConsumption();
39280
+ const toDevice = (r) => {
39281
+ const base = {
39282
+ labelOrName: r.name,
39283
+ name: r.name,
39284
+ deviceIdentifier: r.identifier,
39285
+ val: this.isTemperature && r.noData ? null : r.consumption,
39286
+ perc: this.isTemperature ? void 0 : grandTotal > 0 ? r.consumption / grandTotal * 100 : 0
39287
+ };
39288
+ if (!this.isTemperature) return base;
39289
+ const off = r.id && !r.noData ? this.temperatureOffsetFor(r.id) : 0;
39290
+ return {
39291
+ ...base,
39292
+ extraCells: [this.fmtTemp(r.noData ? null : r.min), this.fmtTemp(r.noData ? null : r.max)],
39293
+ ...off ? { nameNote: `offset ${off > 0 ? "+" : "\u2212"}${fmtPt(Math.abs(off))} \xB0C` } : {}
39294
+ };
39295
+ };
39296
+ const out = [];
39297
+ if (rows.some((r) => r.groupLabel)) {
39298
+ const order = [];
39299
+ const byGroup = /* @__PURE__ */ new Map();
39300
+ for (const r of rows) {
39301
+ const g = r.groupLabel || "\u2014";
39302
+ if (!byGroup.has(g)) {
39303
+ byGroup.set(g, []);
39304
+ order.push(g);
39305
+ }
39306
+ byGroup.get(g).push(r);
39307
+ }
39308
+ for (const g of order) {
39309
+ const items = byGroup.get(g);
39310
+ out.push({ groupHeader: `${g.toUpperCase()} \xB7 ${this.groupSummaryText(items)}` });
39311
+ items.forEach((r) => out.push(toDevice(r)));
39312
+ }
39313
+ } else {
39314
+ rows.forEach((r) => out.push(toDevice(r)));
39315
+ }
39316
+ return out;
39079
39317
  }
39080
- // Colunas do PDF/XLS: temperatura = Sensor | Temperatura média (sem %, Identificador só se útil)
39318
+ // Mesmo texto do cabeçalho de grupo da tabela
39319
+ groupSummaryText(items) {
39320
+ if (this.isTemperature) {
39321
+ const withData = items.filter((r) => !r.noData);
39322
+ const avg = withData.length ? withData.reduce((s, r) => s + r.consumption, 0) / withData.length : null;
39323
+ return `${items.length} sensores \xB7 m\xE9dia ${this.fmtTemp(avg)} \xB0C`;
39324
+ }
39325
+ const total = items.reduce((s, r) => s + r.consumption, 0);
39326
+ return `${items.length} dispositivos \xB7 ${fmtPt(total)} ${this.domainConfig.unit}`;
39327
+ }
39328
+ // Colunas do PDF/XLS = colunas da tela. Temperatura: Sensor | Média | Mín | Máx (sem %).
39081
39329
  exportColumns() {
39082
39330
  if (!this.isTemperature) return null;
39083
39331
  return {
39084
39332
  nameLabel: "Sensor",
39085
- valueLabel: "Temperatura m\xE9dia (\xB0C)",
39333
+ valueLabel: "M\xE9dia (\xB0C)",
39086
39334
  hidePerc: true,
39087
- hideIdentifier: !this.showIdentifierColumn()
39335
+ hideIdentifier: !this.showIdentifierColumn(),
39336
+ extraColumns: [
39337
+ { label: "M\xEDn (\xB0C)", pdfW: 35 },
39338
+ { label: "M\xE1x (\xB0C)", pdfW: 35 }
39339
+ ],
39340
+ emptyValueText: "Sem leitura",
39341
+ countLabel: "sensor(es)"
39088
39342
  };
39089
39343
  }
39090
39344
  // Accent hex da paleta do dashboard (tema efetivo) para o PDF/XLS — cai no
@@ -39098,26 +39352,38 @@ var AllReportModal = class {
39098
39352
  const vars = typeof theme.cssVars === "function" ? theme.cssVars() : theme;
39099
39353
  return vars?.["--myio-brand-700"];
39100
39354
  }
39355
+ // Gráfico da página final do PDF = o painel da direita da tela
39356
+ async buildExportChart() {
39357
+ if (this.isTemperature) {
39358
+ const png = renderTemperatureBarsPng({
39359
+ items: this.getFilteredData().map((r) => ({ label: r.name, value: r.noData ? null : r.consumption })),
39360
+ ideal: this.temperatureIdealRange,
39361
+ accent: this.resolveAccentHex()
39362
+ });
39363
+ return png ? { ...png, title: "Temperatura m\xE9dia por sensor" } : null;
39364
+ }
39365
+ const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
39366
+ return chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null;
39367
+ }
39101
39368
  // PDF export — layout premium do grid + paleta do dashboard + faixa de KPIs +
39102
- // página dedicada com o gráfico de participação da modal.
39369
+ // página dedicada com o mesmo gráfico do painel da modal.
39103
39370
  async exportPDF() {
39104
39371
  if (!this.data.length) return;
39105
- const chartPng = await this.participationChart?.toPngDataUrl?.().catch(() => null);
39106
39372
  exportGridPdf(
39107
39373
  this.buildExportDevices(),
39108
39374
  this.resolveTitle(),
39109
39375
  this.domainConfig.unit,
39110
39376
  this.exportPeriod,
39111
- null,
39377
+ this.params.customerName || null,
39112
39378
  {
39113
39379
  accentColor: this.resolveAccentHex(),
39114
39380
  kpis: this.computeKpis(),
39115
39381
  columns: this.exportColumns(),
39116
- chartImage: chartPng ? { ...chartPng, title: "Participa\xE7\xE3o por Dispositivo" } : null
39382
+ chartImage: await this.buildExportChart()
39117
39383
  }
39118
39384
  );
39119
39385
  }
39120
- // XLS export (XML Spreadsheet) — same as the TELEMETRY grid export.
39386
+ // XLS export (XML Spreadsheet) — mesma tabela + KPIs da tela.
39121
39387
  exportXLS() {
39122
39388
  if (!this.data.length) return;
39123
39389
  exportGridXls(
@@ -39125,8 +39391,8 @@ var AllReportModal = class {
39125
39391
  this.resolveTitle(),
39126
39392
  this.domainConfig.unit,
39127
39393
  this.exportPeriod,
39128
- null,
39129
- { accentColor: this.resolveAccentHex(), columns: this.exportColumns() }
39394
+ this.params.customerName || null,
39395
+ { accentColor: this.resolveAccentHex(), columns: this.exportColumns(), kpis: this.computeKpis() }
39130
39396
  );
39131
39397
  }
39132
39398
  async fetchCustomerTotals(startISO, endISO) {
@@ -79599,7 +79865,10 @@ async function openTemperatureModal(params) {
79599
79865
  document.body.appendChild(modalContainer);
79600
79866
  renderModal3(modalContainer, state6, modalId);
79601
79867
  try {
79602
- state6.data = await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs));
79868
+ state6.data = discardInvalid(
79869
+ await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs)),
79870
+ state6
79871
+ );
79603
79872
  state6.stats = calculateStats(state6.data, state6.clampRange, state6.temperatureOffset);
79604
79873
  state6.isLoading = false;
79605
79874
  renderModal3(modalContainer, state6, modalId);
@@ -79624,7 +79893,10 @@ async function openTemperatureModal(params) {
79624
79893
  state6.isLoading = true;
79625
79894
  renderModal3(modalContainer, state6, modalId);
79626
79895
  try {
79627
- state6.data = await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs));
79896
+ state6.data = discardInvalid(
79897
+ await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs)),
79898
+ state6
79899
+ );
79628
79900
  state6.stats = calculateStats(state6.data, state6.clampRange, state6.temperatureOffset);
79629
79901
  state6.isLoading = false;
79630
79902
  renderModal3(modalContainer, state6, modalId);
@@ -79676,6 +79948,8 @@ function renderModal3(container, state6, modalId, error) {
79676
79948
  showMaximize: true,
79677
79949
  showClose: true,
79678
79950
  primaryColor: state6.accent,
79951
+ solid: true,
79952
+ // padrão dos relatórios: cor do dashboard também no tema light
79679
79953
  borderRadius: "10px 10px 0 0",
79680
79954
  draggable: false
79681
79955
  })}
@@ -80014,12 +80288,19 @@ function drawChart2(modalId, state6) {
80014
80288
  ctx.textAlign = "center";
80015
80289
  const numLabels = Math.min(8, chartData.length);
80016
80290
  const labelInterval = Math.max(1, Math.floor(chartData.length / numLabels));
80291
+ const xSpan = chartData.length > 1 ? chartData[chartData.length - 1].x - chartData[0].x : 0;
80292
+ const multiDay = state6.granularity === "hour" && xSpan > 24 * 60 * 60 * 1e3;
80017
80293
  for (let i = 0; i < chartData.length; i += labelInterval) {
80018
80294
  const point = chartData[i];
80019
80295
  const date = new Date(point.x);
80020
80296
  let label;
80297
+ let subLabel = "";
80021
80298
  if (state6.granularity === "hour") {
80022
80299
  label = date.toLocaleTimeString(state6.locale, { hour: "2-digit", minute: "2-digit" });
80300
+ if (multiDay) {
80301
+ subLabel = label;
80302
+ label = date.toLocaleDateString(state6.locale, { day: "2-digit", month: "2-digit" });
80303
+ }
80023
80304
  } else {
80024
80305
  label = date.toLocaleDateString(state6.locale, { day: "2-digit", month: "2-digit" });
80025
80306
  }
@@ -80031,6 +80312,7 @@ function drawChart2(modalId, state6) {
80031
80312
  ctx.stroke();
80032
80313
  ctx.fillStyle = colors2.textMuted;
80033
80314
  ctx.fillText(label, point.screenX, height - paddingBottom + 18);
80315
+ if (subLabel) ctx.fillText(subLabel, point.screenX, height - paddingBottom + 31);
80034
80316
  }
80035
80317
  ctx.strokeStyle = colors2.border;
80036
80318
  ctx.lineWidth = 1;
@@ -80242,7 +80524,10 @@ async function setupEventListeners3(container, state6, modalId, onClose) {
80242
80524
  state6.dateRangePicker = null;
80243
80525
  renderModal3(container, state6, modalId);
80244
80526
  try {
80245
- state6.data = await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs));
80527
+ state6.data = discardInvalid(
80528
+ await (state6.dataFetcher ? state6.dataFetcher(state6.startTs, state6.endTs) : fetchTemperatureData(state6.token, state6.deviceId, state6.startTs, state6.endTs)),
80529
+ state6
80530
+ );
80246
80531
  state6.stats = calculateStats(state6.data, state6.clampRange, state6.temperatureOffset);
80247
80532
  state6.isLoading = false;
80248
80533
  renderModal3(container, state6, modalId);
@@ -80256,19 +80541,64 @@ async function setupEventListeners3(container, state6, modalId, onClose) {
80256
80541
  }
80257
80542
  });
80258
80543
  }
80544
+ function discardInvalid(data, state6) {
80545
+ const off = Number(state6.temperatureOffset) || 0;
80546
+ return data.filter((p) => {
80547
+ const v = Number(p.value) + off;
80548
+ return Number.isFinite(v) && v >= state6.clampRange.min && v <= state6.clampRange.max;
80549
+ });
80550
+ }
80259
80551
  function exportCsv(state6) {
80260
80552
  if (state6.data.length === 0) return;
80261
- const startDateStr = new Date(state6.startTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
80262
- const endDateStr = new Date(state6.endTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
80263
- exportTemperatureCSV(
80264
- state6.data,
80265
- state6.label,
80266
- state6.stats,
80267
- startDateStr,
80268
- endDateStr,
80269
- state6.clampRange,
80270
- state6.temperatureOffset
80271
- );
80553
+ const off = Number(state6.temperatureOffset) || 0;
80554
+ const n2 = (v) => v.toFixed(2).replace(".", ",");
80555
+ const q = (v) => `"${String(v).replace(/"/g, '""')}"`;
80556
+ const dt = (ts) => new Date(ts).toLocaleString("pt-BR", {
80557
+ day: "2-digit",
80558
+ month: "2-digit",
80559
+ year: "numeric",
80560
+ hour: "2-digit",
80561
+ minute: "2-digit",
80562
+ timeZone: state6.timezone || "America/Sao_Paulo"
80563
+ });
80564
+ const fmtD = (ts) => new Date(ts).toLocaleDateString("pt-BR", { timeZone: state6.timezone || "America/Sao_Paulo" });
80565
+ const filtered = filterByDayPeriods(state6.data, state6.selectedPeriods);
80566
+ const lines = [
80567
+ ["Relat\xF3rio", `${state6.label} - Hist\xF3rico de Temperatura`],
80568
+ ...state6.deviceName ? [["Dispositivo (TB)", state6.deviceName]] : [],
80569
+ ...state6.customerName ? [["Cliente", state6.customerName]] : [],
80570
+ ["Per\xEDodo", `${fmtD(state6.startTs)} \u2014 ${fmtD(state6.endTs)}`],
80571
+ ["Granularidade", state6.granularity === "hour" ? "Hora (m\xE9dia da hora fechada)" : "Dia (m\xE9dia do dia)"],
80572
+ ["Per\xEDodos do dia", getSelectedPeriodsLabel(state6.selectedPeriods)],
80573
+ ...off !== 0 ? [["Offset aplicado (\xB0C)", n2(off)]] : [],
80574
+ ["Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR")],
80575
+ [],
80576
+ ["Temperatura atual (\xB0C)", state6.currentTemperature !== null ? n2(state6.currentTemperature) : "N/A"],
80577
+ ["M\xE9dia do per\xEDodo (\xB0C)", state6.stats.count > 0 ? n2(state6.stats.avg) : "N/A"],
80578
+ ["M\xEDnima (\xB0C)", state6.stats.count > 0 ? n2(state6.stats.min) : "N/A"],
80579
+ ["M\xE1xima (\xB0C)", state6.stats.count > 0 ? n2(state6.stats.max) : "N/A"],
80580
+ ["Leituras", String(state6.stats.count)],
80581
+ []
80582
+ ];
80583
+ if (state6.granularity === "hour") {
80584
+ lines.push(["Data/Hora", "Temperatura (\xB0C)"]);
80585
+ aggregateByHour(filtered, state6.clampRange, off).forEach((p) => lines.push([dt(p.ts), n2(Number(p.value))]));
80586
+ } else {
80587
+ lines.push(["Dia", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "M\xE1x (\xB0C)", "Leituras"]);
80588
+ aggregateByDay(filtered, state6.clampRange, off).forEach(
80589
+ (d) => lines.push([fmtD(d.dateTs), n2(d.avg), n2(d.min), n2(d.max), String(d.count)])
80590
+ );
80591
+ }
80592
+ const csv = "\uFEFF" + lines.map((r) => r.map(q).join(";")).join("\r\n");
80593
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
80594
+ const url = URL.createObjectURL(blob);
80595
+ const a = document.createElement("a");
80596
+ a.href = url;
80597
+ a.download = `temperatura_${state6.label.replace(/\s+/g, "_")}_${state6.granularity === "hour" ? "1h" : "1d"}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
80598
+ document.body.appendChild(a);
80599
+ a.click();
80600
+ document.body.removeChild(a);
80601
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
80272
80602
  }
80273
80603
  function buildHeaderTitle(state6) {
80274
80604
  const base = `${escHtml3(state6.label)} - Hist\xF3rico de Temperatura`;
@@ -80321,6 +80651,21 @@ function mountChrome(container, state6, modalId) {
80321
80651
  }
80322
80652
 
80323
80653
  // src/components/temperature/TemperatureComparisonModal.ts
80654
+ var _cmpFooters = /* @__PURE__ */ new Map();
80655
+ function resolveAccent2(palette) {
80656
+ const fallback = "#3e1a7d";
80657
+ const src = palette ?? (typeof window !== "undefined" ? window.MyIOUtils?.theme : void 0);
80658
+ if (!src) return fallback;
80659
+ const vars = typeof src.cssVars === "function" ? src.cssVars() : src;
80660
+ const accent = src.accent || vars?.["--myio-brand-700"];
80661
+ return typeof accent === "string" && accent ? accent : fallback;
80662
+ }
80663
+ function resolveCustomerName2(explicit) {
80664
+ if (explicit) return explicit;
80665
+ if (typeof window === "undefined") return "";
80666
+ const w = window;
80667
+ return w.MyIOOrchestrator?.customerName || w.MyIOUtils?.customerName || "";
80668
+ }
80324
80669
  async function openTemperatureComparisonModal(params) {
80325
80670
  const modalId = `myio-temp-comparison-modal-${Date.now()}`;
80326
80671
  const defaultDateRange = getTodaySoFar();
@@ -80342,7 +80687,9 @@ async function openTemperatureComparisonModal(params) {
80342
80687
  // All periods selected by default
80343
80688
  temperatureMin: params.temperatureMin ?? null,
80344
80689
  temperatureMax: params.temperatureMax ?? null,
80345
- dataFetcher: params.dataFetcher ?? null
80690
+ dataFetcher: params.dataFetcher ?? null,
80691
+ customerName: resolveCustomerName2(params.customerName),
80692
+ accent: resolveAccent2(params.palette)
80346
80693
  };
80347
80694
  const savedGranularity = localStorage.getItem("myio-temp-comparison-granularity");
80348
80695
  const savedTheme = localStorage.getItem("myio-temp-comparison-theme");
@@ -80358,6 +80705,8 @@ async function openTemperatureComparisonModal(params) {
80358
80705
  await setupEventListeners4(modalContainer, state6, modalId, params.onClose);
80359
80706
  return {
80360
80707
  destroy: () => {
80708
+ _cmpFooters.get(modalId)?.destroy();
80709
+ _cmpFooters.delete(modalId);
80361
80710
  modalContainer.remove();
80362
80711
  params.onClose?.();
80363
80712
  },
@@ -80384,7 +80733,9 @@ async function fetchAllDevicesData(state6) {
80384
80733
  try {
80385
80734
  const raw = state6.dataFetcher ? await state6.dataFetcher(device, state6.startTs, state6.endTs) : await fetchTemperatureData(state6.token, deviceId, state6.startTs, state6.endTs);
80386
80735
  const offset = Number(device.temperatureOffset) || 0;
80387
- const data = offset ? raw.map((p) => ({ ...p, value: Number(p.value) + offset })) : raw;
80736
+ const data = raw.map((p) => ({ ...p, value: Number(p.value) + offset })).filter(
80737
+ (p) => Number.isFinite(p.value) && p.value >= state6.clampRange.min && p.value <= state6.clampRange.max
80738
+ );
80388
80739
  const stats = calculateStats(data, state6.clampRange);
80389
80740
  return {
80390
80741
  device,
@@ -80481,7 +80832,7 @@ function renderModal4(container, state6, modalId) {
80481
80832
  <!-- Header - MyIO Premium Style -->
80482
80833
  <div style="
80483
80834
  padding: 4px 8px; display: flex; align-items: center; justify-content: space-between;
80484
- background: #3e1a7d; color: white; border-radius: ${isMaximized ? "0" : "10px 10px 0 0"};
80835
+ background: ${state6.accent}; color: white; border-radius: ${isMaximized ? "0" : "10px 10px 0 0"};
80485
80836
  min-height: 20px;
80486
80837
  ">
80487
80838
  <h2 style="margin: 6px; font-size: 18px; font-weight: 600; color: white; line-height: 2;">
@@ -80563,7 +80914,7 @@ function renderModal4(container, state6, modalId) {
80563
80914
  name="${modalId}-period"
80564
80915
  value="${period.id}"
80565
80916
  ${state6.selectedPeriods.includes(period.id) ? "checked" : ""}
80566
- style="width: 16px; height: 16px; cursor: pointer; accent-color: #3e1a7d;">
80917
+ style="width: 16px; height: 16px; cursor: pointer; accent-color: ${state6.accent};">
80567
80918
  ${period.label}
80568
80919
  </label>
80569
80920
  `).join("")}
@@ -80596,7 +80947,7 @@ function renderModal4(container, state6, modalId) {
80596
80947
  </div>
80597
80948
  <!-- Query Button -->
80598
80949
  <button id="${modalId}-query" style="
80599
- background: #3e1a7d; color: white; border: none;
80950
+ background: ${state6.accent}; color: white; border: none;
80600
80951
  padding: 8px 16px; border-radius: 6px; cursor: pointer;
80601
80952
  font-size: 14px; font-weight: 500; height: 38px;
80602
80953
  display: flex; align-items: center; gap: 8px;
@@ -80640,33 +80991,15 @@ function renderModal4(container, state6, modalId) {
80640
80991
  ${statsHTML}
80641
80992
  </div>
80642
80993
 
80643
- <!-- Actions -->
80644
- <div style="display: flex; justify-content: flex-end; gap: 12px;">
80645
- <button id="${modalId}-export" style="
80646
- background: ${state6.theme === "dark" ? "rgba(255,255,255,0.1)" : "#f7f7f7"};
80647
- color: ${colors2.text}; border: 1px solid ${colors2.border};
80648
- padding: 8px 16px; border-radius: 6px; cursor: pointer;
80649
- font-size: 14px; display: flex; align-items: center; gap: 8px;
80650
- font-family: 'Nunito', system-ui, sans-serif;
80651
- " ${state6.deviceData.every((dd) => dd.data.length === 0) ? "disabled" : ""}>
80652
- \u{1F4E5} Exportar CSV
80653
- </button>
80654
- <button id="${modalId}-close-btn" style="
80655
- background: #3e1a7d; color: white; border: none;
80656
- padding: 8px 16px; border-radius: 6px; cursor: pointer;
80657
- font-size: 14px; font-weight: 500;
80658
- font-family: 'Nunito', system-ui, sans-serif;
80659
- ">
80660
- Fechar
80661
- </button>
80662
- </div>
80663
80994
  </div><!-- End Body -->
80995
+ <!-- Footer premium (customer \xB7 rel\xF3gio \xB7 vers\xE3o | Powered by MYIO | CSV) \u2014 padr\xE3o dos relat\xF3rios -->
80996
+ <div id="${modalId}-footer-slot"></div>
80664
80997
  </div>
80665
80998
  </div>
80666
80999
  <style>
80667
81000
  @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
80668
81001
  #${modalId} select:focus, #${modalId} input:focus {
80669
- outline: 2px solid #3e1a7d;
81002
+ outline: 2px solid ${state6.accent};
80670
81003
  outline-offset: 2px;
80671
81004
  }
80672
81005
  #${modalId} button:hover:not(:disabled) {
@@ -80682,6 +81015,7 @@ function renderModal4(container, state6, modalId) {
80682
81015
  }
80683
81016
  </style>
80684
81017
  `;
81018
+ mountCmpFooter(container, state6, modalId);
80685
81019
  }
80686
81020
  function drawComparisonChart2(modalId, state6) {
80687
81021
  const chartContainer = document.getElementById(`${modalId}-chart`);
@@ -80888,12 +81222,19 @@ function drawComparisonChart2(modalId, state6) {
80888
81222
  const xAxisPoints = processedData[0]?.points || [];
80889
81223
  const numLabels = Math.min(8, xAxisPoints.length);
80890
81224
  const labelInterval = Math.max(1, Math.floor(xAxisPoints.length / numLabels));
81225
+ const xSpan = xAxisPoints.length > 1 ? xAxisPoints[xAxisPoints.length - 1].x - xAxisPoints[0].x : 0;
81226
+ const multiDay = state6.granularity === "hour" && xSpan > 24 * 60 * 60 * 1e3;
80891
81227
  for (let i = 0; i < xAxisPoints.length; i += labelInterval) {
80892
81228
  const point = xAxisPoints[i];
80893
81229
  const date = new Date(point.x);
80894
81230
  let label;
81231
+ let subLabel = "";
80895
81232
  if (state6.granularity === "hour") {
80896
81233
  label = date.toLocaleTimeString(state6.locale, { hour: "2-digit", minute: "2-digit" });
81234
+ if (multiDay) {
81235
+ subLabel = label;
81236
+ label = date.toLocaleDateString(state6.locale, { day: "2-digit", month: "2-digit" });
81237
+ }
80897
81238
  } else {
80898
81239
  label = date.toLocaleDateString(state6.locale, { day: "2-digit", month: "2-digit" });
80899
81240
  }
@@ -80905,6 +81246,7 @@ function drawComparisonChart2(modalId, state6) {
80905
81246
  ctx.stroke();
80906
81247
  ctx.fillStyle = colors2.textMuted;
80907
81248
  ctx.fillText(label, point.screenX, height - paddingBottom + 18);
81249
+ if (subLabel) ctx.fillText(subLabel, point.screenX, height - paddingBottom + 31);
80908
81250
  }
80909
81251
  ctx.strokeStyle = colors2.border;
80910
81252
  ctx.lineWidth = 1;
@@ -81014,6 +81356,8 @@ function setupComparisonChartTooltip2(canvas, container, chartData, state6, colo
81014
81356
  }
81015
81357
  async function setupEventListeners4(container, state6, modalId, onClose) {
81016
81358
  const closeModal2 = () => {
81359
+ _cmpFooters.get(modalId)?.destroy();
81360
+ _cmpFooters.delete(modalId);
81017
81361
  container.remove();
81018
81362
  onClose?.();
81019
81363
  };
@@ -81135,49 +81479,89 @@ async function setupEventListeners4(container, state6, modalId, onClose) {
81135
81479
  drawComparisonChart2(modalId, state6);
81136
81480
  await setupEventListeners4(container, state6, modalId, onClose);
81137
81481
  });
81138
- document.getElementById(`${modalId}-export`)?.addEventListener("click", () => {
81139
- if (state6.deviceData.every((dd) => dd.data.length === 0)) return;
81140
- exportComparisonCSV2(state6);
81482
+ }
81483
+ function mountCmpFooter(container, state6, modalId) {
81484
+ _cmpFooters.get(modalId)?.destroy();
81485
+ _cmpFooters.delete(modalId);
81486
+ const slot = container.querySelector(`#${modalId}-footer-slot`);
81487
+ if (!slot) return;
81488
+ const lib = typeof window !== "undefined" ? window.MyIOLibrary : void 0;
81489
+ const footer = createModalFooter({
81490
+ customerName: state6.customerName,
81491
+ libVersion: lib?.version ? { current: lib.version } : false,
81492
+ themeMode: state6.theme === "dark" ? "dark" : "light",
81493
+ exports: {
81494
+ csv: {
81495
+ onClick: () => {
81496
+ if (state6.deviceData.every((dd) => dd.data.length === 0)) return;
81497
+ exportComparisonCSV2(state6);
81498
+ },
81499
+ disabled: state6.deviceData.every((dd) => dd.data.length === 0),
81500
+ tooltipText: "Exporta em CSV as leituras de cada sensor (offset aplicado)"
81501
+ }
81502
+ }
81141
81503
  });
81504
+ slot.appendChild(footer.element);
81505
+ _cmpFooters.set(modalId, footer);
81142
81506
  }
81143
81507
  function exportComparisonCSV2(state6) {
81144
- const startDateStr = new Date(state6.startTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
81145
- const endDateStr = new Date(state6.endTs).toLocaleDateString(state6.locale).replace(/\//g, "-");
81146
- const BOM = "\uFEFF";
81147
- let csvContent = BOM;
81148
- csvContent += `Compara\xE7\xE3o de Temperatura
81149
- `;
81150
- csvContent += `Per\xEDodo: ${startDateStr} at\xE9 ${endDateStr}
81151
- `;
81152
- csvContent += `Sensores: ${state6.devices.map((d) => d.label).join(", ")}
81153
- `;
81154
- csvContent += "\n";
81155
- csvContent += "Estat\xEDsticas por Sensor:\n";
81156
- csvContent += "Sensor,M\xE9dia (\xB0C),Min (\xB0C),Max (\xB0C),Leituras\n";
81157
- state6.deviceData.forEach((dd) => {
81158
- csvContent += `"${dd.device.label}",${dd.stats.avg.toFixed(2)},${dd.stats.min.toFixed(2)},${dd.stats.max.toFixed(2)},${dd.stats.count}
81159
- `;
81508
+ const n2 = (v) => v.toFixed(2).replace(".", ",");
81509
+ const q = (v) => `"${String(v).replace(/"/g, '""')}"`;
81510
+ const fmtD = (ts) => new Date(ts).toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });
81511
+ const dt = (ts) => new Date(ts).toLocaleString("pt-BR", {
81512
+ day: "2-digit",
81513
+ month: "2-digit",
81514
+ year: "numeric",
81515
+ hour: "2-digit",
81516
+ minute: "2-digit",
81517
+ timeZone: "America/Sao_Paulo"
81160
81518
  });
81161
- csvContent += "\n";
81162
- csvContent += "Dados Detalhados:\n";
81163
- csvContent += "Data/Hora,Sensor,Temperatura (\xB0C)\n";
81164
- state6.deviceData.forEach((dd) => {
81165
- dd.data.forEach((item) => {
81166
- const date = new Date(item.ts).toLocaleString(state6.locale);
81167
- const temp = Number(item.value).toFixed(2);
81168
- csvContent += `"${date}","${dd.device.label}",${temp}
81169
- `;
81170
- });
81519
+ const offTxt = (d) => {
81520
+ const o = Number(d.temperatureOffset) || 0;
81521
+ return o !== 0 ? n2(o) : "";
81522
+ };
81523
+ const lines = [
81524
+ ["Relat\xF3rio", `Compara\xE7\xE3o de Temperatura - ${state6.devices.length} sensores`],
81525
+ ...state6.customerName ? [["Cliente", state6.customerName]] : [],
81526
+ ["Per\xEDodo", `${fmtD(state6.startTs)} \u2014 ${fmtD(state6.endTs)}`],
81527
+ ["Granularidade", state6.granularity === "hour" ? "Hora (m\xE9dia da hora fechada)" : "Dia (m\xE9dia do dia)"],
81528
+ ["Per\xEDodos do dia", getSelectedPeriodsLabel(state6.selectedPeriods)],
81529
+ ["Gerado em", (/* @__PURE__ */ new Date()).toLocaleString("pt-BR")],
81530
+ [],
81531
+ ["Sensor", "Offset (\xB0C)", "M\xE9dia (\xB0C)", "M\xEDn (\xB0C)", "M\xE1x (\xB0C)", "Leituras"],
81532
+ ...state6.deviceData.map((dd) => [
81533
+ dd.device.label,
81534
+ offTxt(dd.device),
81535
+ dd.stats.count > 0 ? n2(dd.stats.avg) : "N/A",
81536
+ dd.stats.count > 0 ? n2(dd.stats.min) : "N/A",
81537
+ dd.stats.count > 0 ? n2(dd.stats.max) : "N/A",
81538
+ String(dd.stats.count)
81539
+ ]),
81540
+ []
81541
+ ];
81542
+ const perDevice = state6.deviceData.map((dd) => {
81543
+ const filtered = filterByDayPeriods(dd.data, state6.selectedPeriods);
81544
+ 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]);
81545
+ return new Map(pts);
81171
81546
  });
81172
- const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
81547
+ const allTs = [...new Set(perDevice.flatMap((m) => [...m.keys()]))].sort((a, b) => a - b);
81548
+ lines.push([state6.granularity === "hour" ? "Data/Hora" : "Dia", ...state6.deviceData.map((dd) => `${dd.device.label} (\xB0C)`)]);
81549
+ allTs.forEach(
81550
+ (ts) => lines.push([
81551
+ state6.granularity === "hour" ? dt(ts) : fmtD(ts),
81552
+ ...perDevice.map((m) => m.has(ts) ? n2(m.get(ts)) : "")
81553
+ ])
81554
+ );
81555
+ const csv = "\uFEFF" + lines.map((r) => r.map(q).join(";")).join("\r\n");
81556
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
81173
81557
  const url = URL.createObjectURL(blob);
81174
81558
  const link = document.createElement("a");
81175
81559
  link.href = url;
81176
- link.download = `comparacao_temperatura_${startDateStr}_${endDateStr}.csv`;
81560
+ link.download = `comparacao_temperatura_${state6.granularity === "hour" ? "1h" : "1d"}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
81177
81561
  document.body.appendChild(link);
81178
81562
  link.click();
81179
81563
  document.body.removeChild(link);
81180
- URL.revokeObjectURL(url);
81564
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
81181
81565
  }
81182
81566
 
81183
81567
  // src/components/temperature/TemperatureSettingsModal.ts
@@ -93944,7 +94328,7 @@ function fmtPct3(pct) {
93944
94328
  if (pct == null || !isFinite(pct)) return "";
93945
94329
  return `${pct.toLocaleString("pt-BR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`;
93946
94330
  }
93947
- function resolveAccent2(explicit, doc) {
94331
+ function resolveAccent3(explicit, doc) {
93948
94332
  if (explicit) return explicit;
93949
94333
  try {
93950
94334
  const v = getComputedStyle(doc.documentElement).getPropertyValue("--myio-brand-700").trim();
@@ -93990,7 +94374,7 @@ function createGoalsBarTooltip(opts) {
93990
94374
  injectStyles12(doc);
93991
94375
  const el2 = doc.createElement("div");
93992
94376
  el2.className = "myio-gbt";
93993
- el2.style.setProperty("--myio-gbt-accent", resolveAccent2(opts?.accentColor, doc));
94377
+ el2.style.setProperty("--myio-gbt-accent", resolveAccent3(opts?.accentColor, doc));
93994
94378
  doc.body.appendChild(el2);
93995
94379
  let pinned = false;
93996
94380
  let hideTimer = null;
@@ -94048,7 +94432,7 @@ function createGoalsBarTooltip(opts) {
94048
94432
  });
94049
94433
  }
94050
94434
  function render3(data) {
94051
- const accent = resolveAccent2(data.accentColor ?? opts?.accentColor, doc);
94435
+ const accent = resolveAccent3(data.accentColor ?? opts?.accentColor, doc);
94052
94436
  el2.style.setProperty("--myio-gbt-accent", accent);
94053
94437
  const rowsHtml = data.rows && data.rows.length > 0 ? data.rows.map((r) => renderRow(r, 0)).join("") : `<div class="myio-gbt__empty">Sem dados para este per\xEDodo</div>`;
94054
94438
  el2.innerHTML = `
@@ -132804,19 +133188,19 @@ var TelemetryGridShoppingView = class {
132804
133188
  this.maximizeBtnEl?.addEventListener("click", this.toggleMaximize.bind(this));
132805
133189
  const label = this.params.labelWidget || CONTEXT_CONFIG2[this.params.context]?.headerLabel || "Dispositivos";
132806
133190
  const unit = DOMAIN_CONFIG8[this.params.domain]?.unit || "";
132807
- const resolveCustomerName2 = () => {
133191
+ const resolveCustomerName3 = () => {
132808
133192
  const devs = this.controller.getFilteredDevices();
132809
133193
  const name = devs.find((d) => d.customerName)?.customerName;
132810
133194
  return name ? String(name).trim() : null;
132811
133195
  };
132812
133196
  this.root.querySelector("#btnExportPdf")?.addEventListener("click", () => {
132813
- exportGridPdf(this.controller.getFilteredDevices(), label, unit, null, resolveCustomerName2());
133197
+ exportGridPdf(this.controller.getFilteredDevices(), label, unit, null, resolveCustomerName3());
132814
133198
  });
132815
133199
  this.root.querySelector("#btnExportXls")?.addEventListener("click", () => {
132816
- exportGridXls(this.controller.getFilteredDevices(), label, unit, null, resolveCustomerName2());
133200
+ exportGridXls(this.controller.getFilteredDevices(), label, unit, null, resolveCustomerName3());
132817
133201
  });
132818
133202
  this.root.querySelector("#btnExportCsv")?.addEventListener("click", () => {
132819
- exportGridCsv(this.controller.getFilteredDevices(), label, unit, null, resolveCustomerName2());
133203
+ exportGridCsv(this.controller.getFilteredDevices(), label, unit, null, resolveCustomerName3());
132820
133204
  });
132821
133205
  this.bindFilterModalEvents();
132822
133206
  }