myio-js-library 0.1.501 → 0.1.502

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
@@ -1162,7 +1162,7 @@ module.exports = __toCommonJS(index_exports);
1162
1162
  // package.json
1163
1163
  var package_default = {
1164
1164
  name: "myio-js-library",
1165
- version: "0.1.501",
1165
+ version: "0.1.502",
1166
1166
  description: "A clean, standalone JS SDK for MYIO projects",
1167
1167
  license: "MIT",
1168
1168
  repository: "github:gh-myio/myio-js-library",
@@ -32992,6 +32992,13 @@ var AllReportModal = class {
32992
32992
  domainConfig;
32993
32993
  // Granularity: '1d' (daily) | '1h' (hourly)
32994
32994
  granularity = "1d";
32995
+ // When true, devices flagged via `exclude_groups_totals` for this group are dropped
32996
+ // from the report so its total reconciles with the dashboard KPIs. Toggleable in the UI.
32997
+ considerExclusion = true;
32998
+ // Raw API response kept so the exclusion toggle can re-map without a new fetch.
32999
+ lastApiResponse = null;
33000
+ // Cleanup for the InfoTooltip attached to the exclusion-flag info icon.
33001
+ exclusionTooltipCleanup = null;
32995
33002
  // Debug logging helper
32996
33003
  debugLog(message, data) {
32997
33004
  if (this.debugEnabled) {
@@ -33000,7 +33007,7 @@ var AllReportModal = class {
33000
33007
  }
33001
33008
  // Helper: normalize identifiers (upper, strip spaces and non-alphanum)
33002
33009
  normalizeId(v) {
33003
- return (v || "").toString().normalize("NFKC").toUpperCase().replace(/\s+/g, "").replace(/[^A-Z0-9]/g, "");
33010
+ return (v || "").toString().normalize("NFKC").toUpperCase().replace(/\s+/g, "").replace(/[0300-036f]/g, "");
33004
33011
  }
33005
33012
  // Helper: extract store identifier from API item
33006
33013
  // Priority: assetName -> parse from name (last token or token after space) -> null
@@ -33069,6 +33076,10 @@ var AllReportModal = class {
33069
33076
  this.dateRangePicker.destroy();
33070
33077
  this.dateRangePicker = null;
33071
33078
  }
33079
+ if (this.exclusionTooltipCleanup) {
33080
+ this.exclusionTooltipCleanup();
33081
+ this.exclusionTooltipCleanup = null;
33082
+ }
33072
33083
  if (this.filterModal) {
33073
33084
  this.filterModal.destroy();
33074
33085
  this.filterModal = null;
@@ -33107,6 +33118,20 @@ var AllReportModal = class {
33107
33118
  <button id="filter-btn" class="myio-btn myio-btn-secondary" style="background: var(--myio-brand-700); color: white;">
33108
33119
  \u{1F50D} Filtros & Ordena\xE7\xE3o
33109
33120
  </button>
33121
+ <div class="myio-form-group" style="margin-bottom: 0; display: flex; align-items: center; gap: 6px; align-self: flex-end; padding-bottom: 8px;">
33122
+ <label for="consider-exclusion" style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: var(--myio-text, #374151); white-space: nowrap;">
33123
+ <input type="checkbox" id="consider-exclusion" checked style="cursor: pointer; width: 15px; height: 15px; accent-color: var(--myio-brand-700, #5b2c9d);">
33124
+ Considerar exclus\xE3o de totais
33125
+ </label>
33126
+ <span id="exclusion-info" aria-label="Sobre a exclus\xE3o de totais" style="
33127
+ display: inline-flex; align-items: center; justify-content: center;
33128
+ width: 16px; height: 16px; border-radius: 50%;
33129
+ background: var(--myio-brand-700, #5b2c9d); color: #fff;
33130
+ font-size: 11px; font-weight: 700; font-style: italic;
33131
+ font-family: Georgia, 'Times New Roman', serif; cursor: help;
33132
+ user-select: none;
33133
+ ">i</span>
33134
+ </div>
33110
33135
  <div class="myio-form-group" style="margin-bottom: 0; margin-left: auto;">
33111
33136
  <label class="myio-label" for="search-input">Busca r\xE1pida</label>
33112
33137
  <input type="text" id="search-input" class="myio-input" placeholder="Digite para filtrar..." style="width: 200px;">
@@ -33165,6 +33190,20 @@ var AllReportModal = class {
33165
33190
  this.renderTable();
33166
33191
  });
33167
33192
  }
33193
+ const exclusionCheckbox = document.getElementById("consider-exclusion");
33194
+ exclusionCheckbox?.addEventListener("change", () => {
33195
+ this.considerExclusion = exclusionCheckbox.checked;
33196
+ this.remapAndRender();
33197
+ });
33198
+ const exclusionInfo = document.getElementById("exclusion-info");
33199
+ if (exclusionInfo) {
33200
+ this.exclusionTooltipCleanup?.();
33201
+ this.exclusionTooltipCleanup = InfoTooltip.attach(exclusionInfo, () => ({
33202
+ icon: "\u2139\uFE0F",
33203
+ title: "Exclus\xE3o de totais",
33204
+ content: this.buildExclusionTooltipContent()
33205
+ }));
33206
+ }
33168
33207
  try {
33169
33208
  this.dateRangePicker = await attach(dateRangeInput, {
33170
33209
  presetStart: this.getDefaultStartDate(),
@@ -33207,6 +33246,7 @@ var AllReportModal = class {
33207
33246
  const customerTotalsData = await this.fetchCustomerTotals(startISO, endISO);
33208
33247
  this.debugLog("\u2705 API response received", customerTotalsData);
33209
33248
  this.debugLog("\u{1F504} Processing API response...");
33249
+ this.lastApiResponse = customerTotalsData;
33210
33250
  this.data = this.mapCustomerTotalsResponse(customerTotalsData);
33211
33251
  this.debugLog("\u2705 Data mapping completed", {
33212
33252
  mappedDataLength: this.data.length,
@@ -33426,48 +33466,6 @@ var AllReportModal = class {
33426
33466
  });
33427
33467
  }
33428
33468
  renderPagination() {
33429
- return;
33430
- const container = document.getElementById("pagination-container");
33431
- if (!container) return;
33432
- const filteredData = this.getFilteredData();
33433
- const totalPages = Math.ceil(filteredData.length / this.itemsPerPage);
33434
- if (totalPages <= 1) {
33435
- container.style.display = "none";
33436
- return;
33437
- }
33438
- const startItem = (this.currentPage - 1) * this.itemsPerPage + 1;
33439
- const endItem = Math.min(this.currentPage * this.itemsPerPage, filteredData.length);
33440
- container.innerHTML = `
33441
- <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
33442
- <div style="color: var(--myio-text-muted);">
33443
- Mostrando ${startItem}-${endItem} de ${filteredData.length} lojas
33444
- </div>
33445
- <div style="display: flex; gap: 8px; align-items: center;">
33446
- <button id="prev-page" class="myio-btn myio-btn-outline" ${this.currentPage === 1 ? "disabled" : ""}>
33447
- Anterior
33448
- </button>
33449
- <span style="padding: 0 12px; font-weight: bold;">
33450
- ${this.currentPage} / ${totalPages}
33451
- </span>
33452
- <button id="next-page" class="myio-btn myio-btn-outline" ${this.currentPage === totalPages ? "disabled" : ""}>
33453
- Pr\xF3ximo
33454
- </button>
33455
- </div>
33456
- </div>
33457
- `;
33458
- document.getElementById("prev-page")?.addEventListener("click", () => {
33459
- if (this.currentPage > 1) {
33460
- this.currentPage--;
33461
- this.renderTable();
33462
- }
33463
- });
33464
- document.getElementById("next-page")?.addEventListener("click", () => {
33465
- if (this.currentPage < totalPages) {
33466
- this.currentPage++;
33467
- this.renderTable();
33468
- }
33469
- });
33470
- container.style.display = "block";
33471
33469
  }
33472
33470
  calculateTotalConsumption() {
33473
33471
  return this.data.reduce((sum, row) => sum + row.consumption, 0);
@@ -33511,7 +33509,7 @@ var AllReportModal = class {
33511
33509
  }
33512
33510
  generateStoreId(storeName) {
33513
33511
  const name = (storeName || "SEM-ID").toString();
33514
- return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
33512
+ return name.toLowerCase().replace(/\s+/g, "-").replace(/[0300-036f]/g, "");
33515
33513
  }
33516
33514
  applyFiltersAndSort(selectedIds, sortMode) {
33517
33515
  this.selectedStoreIds = new Set(selectedIds);
@@ -33560,8 +33558,10 @@ var AllReportModal = class {
33560
33558
  async fetchCustomerTotals(startISO, endISO) {
33561
33559
  if (this.params.fetcher) {
33562
33560
  const token2 = this.params.api.ingestionToken || await this.authClient.getBearer();
33561
+ const baseUrl2 = this.params.api.dataApiBaseUrl;
33562
+ if (!baseUrl2) throw new Error("dataApiBaseUrl n\xE3o configurado.");
33563
33563
  return await this.params.fetcher({
33564
- baseUrl: this.params.api.dataApiBaseUrl,
33564
+ baseUrl: baseUrl2,
33565
33565
  token: token2,
33566
33566
  customerId: this.params.customerId,
33567
33567
  startISO,
@@ -33598,6 +33598,87 @@ var AllReportModal = class {
33598
33598
  this.debugLog("[AllReportModal] Customer totals response:", data);
33599
33599
  return data;
33600
33600
  }
33601
+ // Re-map the cached API response under the current exclusion flag and refresh the UI.
33602
+ // No-op until data has been loaded at least once.
33603
+ remapAndRender() {
33604
+ if (!this.lastApiResponse) return;
33605
+ this.data = this.mapCustomerTotalsResponse(this.lastApiResponse);
33606
+ this.selectedStoreIds = new Set(this.data.map((s) => this.generateStoreId(s.identifier)));
33607
+ this.currentPage = 1;
33608
+ this.renderSummary();
33609
+ this.renderTable();
33610
+ }
33611
+ // Premium tooltip content for the exclusion-flag info icon. Uses the library
33612
+ // InfoTooltip CSS classes (myio-info-tooltip__*) — injected by InfoTooltip itself.
33613
+ buildExclusionTooltipContent() {
33614
+ return `
33615
+ <div class="myio-info-tooltip__section" style="max-width:280px;">
33616
+ <div class="myio-info-tooltip__row" style="align-items:flex-start;padding:3px 0;">
33617
+ <span class="myio-info-tooltip__label" style="font-size:11px;line-height:1.5;white-space:normal;">
33618
+ Alguns dispositivos t\xEAm o atributo <strong>exclude_groups_totals</strong> e s\xE3o
33619
+ propositalmente removidos dos totais do dashboard (ex.: medidor de locat\xE1rio que
33620
+ n\xE3o \xE9 consumo operacional do shopping).
33621
+ </span>
33622
+ </div>
33623
+ <div class="myio-info-tooltip__row" style="align-items:flex-start;padding:3px 0;">
33624
+ <span class="myio-info-tooltip__label" style="font-size:11px;line-height:1.5;white-space:normal;">
33625
+ <strong>Ligado</strong> (padr\xE3o): esses dispositivos s\xE3o omitidos do relat\xF3rio \u2014
33626
+ o total bate com os cards do dashboard.
33627
+ </span>
33628
+ </div>
33629
+ <div class="myio-info-tooltip__row" style="align-items:flex-start;padding:3px 0;">
33630
+ <span class="myio-info-tooltip__label" style="font-size:11px;line-height:1.5;white-space:normal;">
33631
+ <strong>Desligado</strong>: todos os dispositivos do grupo entram \u2014 mostra o
33632
+ consumo bruto, inclusive os exclu\xEDdos.
33633
+ </span>
33634
+ </div>
33635
+ </div>
33636
+ <div class="myio-info-tooltip__notice">
33637
+ <span class="myio-info-tooltip__notice-icon">\u{1F4A1}</span>
33638
+ <span>N\xE3o altera nenhum dado \u2014 apenas o que o relat\xF3rio soma e lista.</span>
33639
+ </div>
33640
+ `;
33641
+ }
33642
+ // Resolve the canonical `exclude_groups_totals.groups` key for the report's current group.
33643
+ // Returns null for groupings that don't map to a single exclusion key (climatizavel, etc.).
33644
+ resolveExclusionGroupKey(item) {
33645
+ const g = String(this.params.group || "").toLowerCase();
33646
+ if (g === "entrada" || g === "lojas" || g === "area_comum") return g;
33647
+ if (g === "todos") {
33648
+ const gl = String(item.groupLabel || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
33649
+ if (gl === "entrada") return "entrada";
33650
+ if (gl === "lojas") return "lojas";
33651
+ if (gl === "area comum" || gl === "areacomum") return "area_comum";
33652
+ if (gl === "climatizacao") return "climatizacao";
33653
+ if (gl === "elevadores") return "elevadores";
33654
+ if (gl === "escadas rolantes" || gl === "esc. rolantes") return "escadas_rolantes";
33655
+ if (gl === "outros" || gl === "outros equipamentos") return "outros";
33656
+ }
33657
+ return null;
33658
+ }
33659
+ // Mirrors getValorEfetivo (MAIN_VIEW): a device flagged in exclude_groups_totals for the
33660
+ // report's group is dropped, so the report total reconciles with the dashboard KPI card.
33661
+ isExcludedFromTotals(item) {
33662
+ const raw = item.excludeGroupsTotals;
33663
+ if (!raw) return false;
33664
+ let parsed;
33665
+ try {
33666
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
33667
+ } catch {
33668
+ return false;
33669
+ }
33670
+ if (!parsed || parsed.enabled !== true) return false;
33671
+ const key = this.resolveExclusionGroupKey(item);
33672
+ if (!key) return false;
33673
+ if (parsed.groups && typeof parsed.groups === "object") {
33674
+ return parsed.groups[key] === true;
33675
+ }
33676
+ if (Array.isArray(parsed.excludedGroups)) {
33677
+ const ex = parsed.excludedGroups.map((x) => String(x).toLowerCase());
33678
+ return ex.includes(key) || ex.includes("all");
33679
+ }
33680
+ return false;
33681
+ }
33601
33682
  mapCustomerTotalsResponse(apiResponse) {
33602
33683
  this.debugLog("\u{1F50D} Starting mapCustomerTotalsResponse", { apiResponse });
33603
33684
  const apiArray = Array.isArray(apiResponse?.data) ? apiResponse.data : Array.isArray(apiResponse) ? apiResponse : [];
@@ -33635,6 +33716,10 @@ var AllReportModal = class {
33635
33716
  const apiId = String(apiItem?.id || "");
33636
33717
  if (!apiId || !orchIdSet.has(apiId)) continue;
33637
33718
  const meta = orchMeta.get(apiId);
33719
+ if (this.considerExclusion && meta && this.isExcludedFromTotals(meta)) {
33720
+ this.debugLog("[AllReportModal] device excluded via exclude_groups_totals:", meta.label);
33721
+ continue;
33722
+ }
33638
33723
  const consumption = Math.round(this.pickConsumption(apiItem) * 100) / 100;
33639
33724
  const result = {
33640
33725
  identifier: meta?.identifier || apiItem.name || apiId,
@@ -84311,7 +84396,7 @@ async function fetchIngestionDevicesAllPaged(customerId) {
84311
84396
  function findIngestionDeviceByCentralSlaveId(devices, centralId, slaveId) {
84312
84397
  const slaveIdNum = typeof slaveId === "string" ? parseInt(slaveId, 10) : slaveId;
84313
84398
  for (const device of devices) {
84314
- const deviceGatewayId = device.gatewayId || device.gateway?.id;
84399
+ const deviceGatewayId = device.gateway?.hardwareUuid || device.gatewayId || device.gateway?.id;
84315
84400
  if (deviceGatewayId === centralId && device.slaveId === slaveIdNum) {
84316
84401
  console.log(
84317
84402
  "[UpsellModal] Found matching device:",
@@ -84329,8 +84414,8 @@ function findIngestionDeviceByCentralSlaveId(devices, centralId, slaveId) {
84329
84414
  console.log(
84330
84415
  `[UpsellModal] Sample device ${i}:`,
84331
84416
  d.name,
84332
- "gatewayId:",
84333
- d.gatewayId || d.gateway?.id,
84417
+ "gateway(hwUuid|id):",
84418
+ d.gateway?.hardwareUuid || d.gatewayId || d.gateway?.id,
84334
84419
  "slaveId:",
84335
84420
  d.slaveId
84336
84421
  );
@@ -84515,7 +84600,7 @@ function openUpsellModal(params) {
84515
84600
  selectedDevices: [],
84516
84601
  bulkAttributeModal: { open: false, attribute: "deviceType", value: "", saving: false },
84517
84602
  bulkProfileModal: { open: false, selectedProfileId: "", saving: false },
84518
- bulkOwnerModal: { open: false, saving: false },
84603
+ bulkOwnerModal: { open: false, saving: false, targetCustomerId: "" },
84519
84604
  columnWidths: {
84520
84605
  name: 180,
84521
84606
  label: 120,
@@ -84552,6 +84637,7 @@ function openUpsellModal(params) {
84552
84637
  lojasDeviceData: [],
84553
84638
  lojasDataLoading: false,
84554
84639
  lojasConfig: null,
84640
+ lojasApplyRelation: true,
84555
84641
  customModeModal: { open: false },
84556
84642
  bulkRelationModal: { open: false, target: "CUSTOMER", selectedAssetId: "", selectedAssetName: "", search: "", newAssetName: "", assetsLoaded: false, overrideCustomerId: "", overrideCustomerName: "", customerSearch: "", customerPickerOpen: false },
84557
84643
  checkFixLoading: false,
@@ -84716,6 +84802,12 @@ function renderModal4(container, state6, modalId, t, error) {
84716
84802
  font-size: 14px; font-weight: 500; font-family: 'Roboto', Arial, sans-serif;
84717
84803
  display: flex; align-items: center; gap: 6px;
84718
84804
  " ${!state6.selectedCustomer ? 'disabled title="Selecione um Customer primeiro"' : ""}>\u{1F504} Sync Ingestion ID (${state6.selectedDevices.length})</button>
84805
+ <button id="${modalId}-bulk-delete" style="
84806
+ background: #b91c1c; color: white; border: 1px solid #7f1d1d;
84807
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
84808
+ font-size: 14px; font-weight: 600; font-family: 'Roboto', Arial, sans-serif;
84809
+ display: flex; align-items: center; gap: 6px;
84810
+ " title="Deletar permanentemente os dispositivos selecionados (irrevers\xEDvel)">\u{1F5D1}\uFE0F Deletar (${state6.selectedDevices.length})</button>
84719
84811
  ` : ""}
84720
84812
  ${state6.currentStep === 3 && state6.lojasMode ? `
84721
84813
  <button id="${modalId}-lojas-sync" style="
@@ -84954,7 +85046,18 @@ function renderModal4(container, state6, modalId, t, error) {
84954
85046
  </div>
84955
85047
  ` : ""}
84956
85048
 
84957
- ${state6.bulkOwnerModal.open ? `
85049
+ ${state6.bulkOwnerModal.open ? (() => {
85050
+ const effId = state6.bulkOwnerModal.targetCustomerId || state6.selectedCustomer?.id?.id || "";
85051
+ const effCustomer = state6.customers.find((c) => c.id?.id === effId) || state6.selectedCustomer;
85052
+ const effName = effCustomer?.name || effCustomer?.title || "N\xE3o selecionado";
85053
+ const customerOptions = state6.customers.length === 0 ? `<option value="${effId}">${effName}</option>` : [...state6.customers].sort(
85054
+ (a, b) => (a.name || a.title || "").localeCompare(b.name || b.title || "", "pt-BR")
85055
+ ).map((c) => {
85056
+ const cid = c.id?.id || "";
85057
+ const cname = c.name || c.title || cid;
85058
+ return `<option value="${cid}" ${cid === effId ? "selected" : ""}>${cname}</option>`;
85059
+ }).join("");
85060
+ return `
84958
85061
  <!-- Bulk Owner Modal -->
84959
85062
  <div class="myio-bulk-owner-overlay" style="
84960
85063
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
@@ -84980,17 +85083,25 @@ function renderModal4(container, state6, modalId, t, error) {
84980
85083
  <div style="font-size: 14px; color: ${colors2.text}; font-weight: 500;">${state6.selectedDevices.length} dispositivos</div>
84981
85084
  </div>
84982
85085
 
84983
- <div style="margin-bottom: 16px; padding: 12px; background: ${colors2.success}20; border-radius: 8px; border: 1px solid ${colors2.success}40;">
84984
- <div style="font-size: 12px; color: ${colors2.textMuted}; margin-bottom: 4px;">Novo Owner (Customer):</div>
84985
- <div style="font-size: 14px; color: ${colors2.success}; font-weight: 600;">${state6.selectedCustomer?.name || state6.selectedCustomer?.title || "N\xE3o selecionado"}</div>
85086
+ <div style="margin-bottom: 16px;">
85087
+ <div style="font-size: 12px; color: ${colors2.textMuted}; margin-bottom: 6px;">
85088
+ Novo Owner (Customer):
85089
+ </div>
85090
+ <select id="${modalId}-bulk-owner-customer" style="
85091
+ width: 100%; padding: 9px 10px; border-radius: 8px;
85092
+ border: 1px solid ${colors2.border}; background: ${colors2.inputBg};
85093
+ color: ${colors2.text}; font-size: 13px; cursor: pointer;
85094
+ ">
85095
+ ${customerOptions}
85096
+ </select>
84986
85097
  <div style="font-size: 11px; color: ${colors2.textMuted}; margin-top: 4px;">
84987
- ID: ${state6.selectedCustomer?.id?.id || "N/A"}
85098
+ ID: ${effId || "N/A"}${state6.customers.length === 0 ? " \xB7 carregando lista de clientes\u2026" : ""}
84988
85099
  </div>
84989
85100
  </div>
84990
85101
 
84991
85102
  <div style="margin-bottom: 16px; padding: 12px; background: ${colors2.warning}20; border-radius: 8px; border: 1px solid ${colors2.warning}40;">
84992
85103
  <div style="font-size: 12px; color: ${colors2.warning}; font-weight: 500;">
84993
- \u26A0\uFE0F Aten\xE7\xE3o: Esta a\xE7\xE3o ir\xE1 atribuir todos os ${state6.selectedDevices.length} devices selecionados ao customer "${state6.selectedCustomer?.name || state6.selectedCustomer?.title}".
85104
+ \u26A0\uFE0F Aten\xE7\xE3o: Esta a\xE7\xE3o ir\xE1 atribuir todos os ${state6.selectedDevices.length} devices selecionados ao customer "${effName}".
84994
85105
  </div>
84995
85106
  </div>
84996
85107
 
@@ -85004,13 +85115,14 @@ function renderModal4(container, state6, modalId, t, error) {
85004
85115
  background: #10b981; color: white; border: none;
85005
85116
  padding: 10px 20px; border-radius: 6px; cursor: pointer;
85006
85117
  font-size: 14px; font-weight: 500;
85007
- " ${state6.bulkOwnerModal.saving || !state6.selectedCustomer ? "disabled" : ""}>
85118
+ " ${state6.bulkOwnerModal.saving || !effId ? "disabled" : ""}>
85008
85119
  ${state6.bulkOwnerModal.saving ? "Salvando..." : "Atribuir Owner para " + state6.selectedDevices.length + " devices"}
85009
85120
  </button>
85010
85121
  </div>
85011
85122
  </div>
85012
85123
  </div>
85013
- ` : ""}
85124
+ `;
85125
+ })() : ""}
85014
85126
 
85015
85127
  ${state6.customModeModal.open ? `
85016
85128
  <!-- CUSTOM Mode Picker Modal -->
@@ -85230,6 +85342,7 @@ function renderModal4(container, state6, modalId, t, error) {
85230
85342
  `;
85231
85343
  })() : ""}
85232
85344
  `;
85345
+ delete container.dataset.upsellListenersBound;
85233
85346
  setupEventListeners3(container, state6, modalId, t);
85234
85347
  }
85235
85348
  function renderStepIndicator(step, label, currentStep, colors2) {
@@ -85433,6 +85546,8 @@ function renderCheckFixRow(r, state6, modalId, colors2, dupPairIds, dupIngestion
85433
85546
  };
85434
85547
  const valStr = Object.entries(r.telemetry.values).map(([k, v]) => v != null ? `${k}:<b>${v}${unit[k] ?? ""}</b>` : null).filter(Boolean).join(" \xB7 ") || "\u2014";
85435
85548
  const connColor = r.connStatus ? CONN_COLOR[r.connStatus] || colors2.textMuted : colors2.textMuted;
85549
+ const valCopy = Object.entries(r.telemetry.values).filter(([, v]) => v != null).map(([k, v]) => `${k}: ${v}${unit[k] ?? ""}`).join(" \xB7 ");
85550
+ const copyAttr = (v) => `class="myio-copy-cell" data-copy="${encodeURIComponent(String(v ?? ""))}"`;
85436
85551
  return `
85437
85552
  <tr class="myio-list-item ${isSelected ? "selected" : ""}" data-device-id="${r.deviceId}"
85438
85553
  style="border-bottom:1px solid ${colors2.border}; cursor:pointer;">
@@ -85441,21 +85556,21 @@ function renderCheckFixRow(r, state6, modalId, colors2, dupPairIds, dupIngestion
85441
85556
  <input type="checkbox" class="myio-device-checkbox" data-device-id="${r.deviceId}"
85442
85557
  ${isSelectedMulti ? "checked" : ""} style="width:14px;height:14px;cursor:pointer;accent-color:${MYIO_PURPLE};"/>
85443
85558
  </td>` : ""}
85444
- <td style="${cell()} max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${r.deviceName}">${r.deviceName}</td>
85445
- <td style="${cell()} max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${colors2.textMuted};" title="${r.deviceLabel}">${r.deviceLabel || dash}</td>
85446
- <td style="${cell(typeActWrong ? "ok" : "none", true)}">${r.inferred.deviceType}</td>
85447
- <td style="${cell(typeActWrong ? r.typeEqualsProfile ? "warn" : "bad" : "none", true)}" title="${typeActWrong ? `esperado: ${r.inferred.deviceType}` : ""}">${r.actual.type || dash}</td>
85448
- <td style="${cell(devTypeWrong ? "ok" : "none", true)}">${r.inferred.deviceType}</td>
85449
- <td style="${cell(devTypeWrong ? "bad" : "none", true)}" title="${devTypeWrong ? `esperado: ${r.inferred.deviceType}` : ""}">${r.actual.deviceType || dash}</td>
85450
- <td style="${cell(devProfWrong ? "ok" : "none", true)}">${r.inferred.deviceProfile}</td>
85451
- <td style="${cell(devProfWrong ? "bad" : "none", true)}" title="${devProfWrong ? `esperado: ${r.inferred.deviceProfile}` : ""}">${r.actual.deviceProfile || dash}</td>
85452
- <td style="${cell()} white-space:nowrap; color:${colors2.textMuted}; font-size:9px;">${tsStr}</td>
85453
- <td style="${cell()} font-size:9px;">${valStr}</td>
85454
- <td style="${cell()} color:${connColor}; font-weight:600; white-space:nowrap;">${r.connStatus || dash}</td>
85455
- <td style="${cell()} font-size:9px; font-family:monospace; color:${colors2.textMuted}; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${r.gcdrDeviceId || ""}">${r.gcdrDeviceId || dash}</td>
85456
- <td style="${cell(dupIngestionIds.has(r.deviceId) ? "bad" : "none")} font-size:9px; font-family:monospace; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${r.ingestionId || ""}">${r.ingestionId || dash}</td>
85457
- <td style="${cell(dupPairIds.has(r.deviceId) ? "bad" : "none")} font-size:9px; font-family:monospace; max-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${r.centralId || ""}">${r.centralId || dash}</td>
85458
- <td style="${cell(dupPairIds.has(r.deviceId) ? "bad" : "none")} font-size:9px; font-family:monospace; max-width:50px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${r.slaveId || dash}</td>
85559
+ <td ${copyAttr(r.deviceName)} style="${cell()} max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${r.deviceName}">${r.deviceName}</td>
85560
+ <td ${copyAttr(r.deviceLabel)} style="${cell()} max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${colors2.textMuted};" title="${r.deviceLabel}">${r.deviceLabel || dash}</td>
85561
+ <td ${copyAttr(r.inferred.deviceType)} style="${cell(typeActWrong ? "ok" : "none", true)}">${r.inferred.deviceType}</td>
85562
+ <td ${copyAttr(r.actual.type ?? "")} style="${cell(typeActWrong ? r.typeEqualsProfile ? "warn" : "bad" : "none", true)}" title="${typeActWrong ? `esperado: ${r.inferred.deviceType}` : ""}">${r.actual.type || dash}</td>
85563
+ <td ${copyAttr(r.inferred.deviceType)} style="${cell(devTypeWrong ? "ok" : "none", true)}">${r.inferred.deviceType}</td>
85564
+ <td ${copyAttr(r.actual.deviceType ?? "")} style="${cell(devTypeWrong ? "bad" : "none", true)}" title="${devTypeWrong ? `esperado: ${r.inferred.deviceType}` : ""}">${r.actual.deviceType || dash}</td>
85565
+ <td ${copyAttr(r.inferred.deviceProfile)} style="${cell(devProfWrong ? "ok" : "none", true)}">${r.inferred.deviceProfile}</td>
85566
+ <td ${copyAttr(r.actual.deviceProfile ?? "")} style="${cell(devProfWrong ? "bad" : "none", true)}" title="${devProfWrong ? `esperado: ${r.inferred.deviceProfile}` : ""}">${r.actual.deviceProfile || dash}</td>
85567
+ <td ${copyAttr(r.telemetry.ts ? tsStr : "")} style="${cell()} white-space:nowrap; color:${colors2.textMuted}; font-size:9px;">${tsStr}</td>
85568
+ <td ${copyAttr(valCopy)} style="${cell()} font-size:9px;">${valStr}</td>
85569
+ <td ${copyAttr(r.connStatus ?? "")} style="${cell()} color:${connColor}; font-weight:600; white-space:nowrap;">${r.connStatus || dash}</td>
85570
+ <td ${copyAttr(r.gcdrDeviceId ?? "")} style="${cell()} font-size:9px; font-family:monospace; color:${colors2.textMuted}; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${r.gcdrDeviceId || ""}">${r.gcdrDeviceId || dash}</td>
85571
+ <td ${copyAttr(r.ingestionId ?? "")} style="${cell(dupIngestionIds.has(r.deviceId) ? "bad" : "none")} font-size:9px; font-family:monospace; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${r.ingestionId || ""}">${r.ingestionId || dash}</td>
85572
+ <td ${copyAttr(r.centralId ?? "")} style="${cell(dupPairIds.has(r.deviceId) ? "bad" : "none")} font-size:9px; font-family:monospace; max-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${r.centralId || ""}">${r.centralId || dash}</td>
85573
+ <td ${copyAttr(r.slaveId ?? "")} style="${cell(dupPairIds.has(r.deviceId) ? "bad" : "none")} font-size:9px; font-family:monospace; max-width:50px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${r.slaveId || dash}</td>
85459
85574
  <td style="padding:4px 6px; font-size:10px; color:${STATUS_COLOR[r.status]}; font-weight:700; white-space:nowrap; cursor:${r.status !== "ok" ? "help" : "default"};"
85460
85575
  ${r.status !== "ok" ? `data-cf-status="${r.status}" data-cf-device="${encodeURIComponent(r.deviceName)}" data-cf-detail="${encodeURIComponent(_buildCfStatusDetail(r))}"` : ""}>
85461
85576
  ${STATUS_ICON[r.status]} ${r.status}
@@ -85501,16 +85616,7 @@ function renderStep2(state6, modalId, colors2, t) {
85501
85616
  }
85502
85617
  return true;
85503
85618
  });
85504
- const searchFilteredDevices = searchTerm ? filteredDevices.filter((d) => {
85505
- const name = (d.name || "").toLowerCase();
85506
- const label = (d.label || "").toLowerCase();
85507
- const type = (d.type || "").toLowerCase();
85508
- const deviceType = (d.serverAttrs?.deviceType || "").toLowerCase();
85509
- const deviceProfile = (d.serverAttrs?.deviceProfile || "").toLowerCase();
85510
- const slaveId = String(d.serverAttrs?.slaveId ?? "").toLowerCase();
85511
- const status = (d.latestTelemetry?.connectionStatus?.value || "").toLowerCase();
85512
- return name.includes(searchTerm) || label.includes(searchTerm) || type.includes(searchTerm) || deviceType.includes(searchTerm) || deviceProfile.includes(searchTerm) || slaveId.includes(searchTerm) || status.includes(searchTerm);
85513
- }) : filteredDevices;
85619
+ const searchFilteredDevices = searchTerm ? filteredDevices.filter((d) => buildDeviceSearchHaystack(d, state6).includes(searchTerm)) : filteredDevices;
85514
85620
  const sortedDevices = sortDevices2(searchFilteredDevices, sortField, sortOrder);
85515
85621
  const gridHeight = state6.isMaximized ? "calc(100vh - 340px)" : "360px";
85516
85622
  const hasActiveFilters = filterTypes.length > 0 || filterDeviceTypes.length > 0 || filterDeviceProfiles.length > 0 || filterStatuses.length > 0 || filterTelemetryKeys.length > 0;
@@ -85978,6 +86084,95 @@ function renderStep2(state6, modalId, colors2, t) {
85978
86084
  })()}
85979
86085
  `;
85980
86086
  }
86087
+ function buildDeviceSearchHaystack(d, state6) {
86088
+ const id = getEntityId(d);
86089
+ const relTo = (state6.deviceRelToMap.get(id) || []).map((r) => r.name || "").join(" ");
86090
+ const relFrom = (state6.deviceRelFromMap.get(id) || []).map((r) => r.name || "").join(" ");
86091
+ const tel = d.latestTelemetry || {};
86092
+ const a = d.serverAttrs || {};
86093
+ return [
86094
+ d.name,
86095
+ d.label,
86096
+ d.type,
86097
+ d.createdTime ? formatDate6(d.createdTime, state6.locale) : "",
86098
+ relTo,
86099
+ relFrom,
86100
+ a.centralId,
86101
+ a.slaveId,
86102
+ a.deviceType,
86103
+ a.deviceProfile,
86104
+ tel.pulses?.value,
86105
+ tel.consumption?.value,
86106
+ tel.temperature?.value,
86107
+ tel.connectionStatus?.value
86108
+ ].map((v) => String(v ?? "").toLowerCase()).join("");
86109
+ }
86110
+ function getGridVisibleDevices(state6) {
86111
+ const {
86112
+ types: filterTypes,
86113
+ deviceTypes: filterDeviceTypes,
86114
+ deviceProfiles: filterDeviceProfiles,
86115
+ statuses: filterStatuses,
86116
+ telemetryKeys: filterTelemetryKeys
86117
+ } = state6.deviceFilters;
86118
+ const searchTerm = state6.deviceSearchTerm.toLowerCase();
86119
+ let result = state6.devices.filter((d) => {
86120
+ if (filterTypes.length > 0 && !filterTypes.includes(d.type || "")) return false;
86121
+ if (filterDeviceTypes.length > 0 && !filterDeviceTypes.includes(d.serverAttrs?.deviceType || ""))
86122
+ return false;
86123
+ if (filterDeviceProfiles.length > 0 && !filterDeviceProfiles.includes(d.serverAttrs?.deviceProfile || ""))
86124
+ return false;
86125
+ if (filterStatuses.length > 0) {
86126
+ const status = d.latestTelemetry?.connectionStatus?.value || "offline";
86127
+ if (!filterStatuses.includes(status)) return false;
86128
+ }
86129
+ if (filterTelemetryKeys.length > 0) {
86130
+ const telem = d.latestTelemetry;
86131
+ const hasMatch = filterTelemetryKeys.some((k) => {
86132
+ if (k === "pulses") return telem?.pulses != null;
86133
+ if (k === "consumption") return telem?.consumption != null;
86134
+ return false;
86135
+ });
86136
+ if (!hasMatch) return false;
86137
+ }
86138
+ return true;
86139
+ });
86140
+ if (searchTerm) {
86141
+ result = result.filter((d) => buildDeviceSearchHaystack(d, state6).includes(searchTerm));
86142
+ }
86143
+ if (state6.checkFixReport) {
86144
+ const idToRecord = new Map(state6.checkFixReport.records.map((r) => [r.deviceId, r]));
86145
+ const af = state6.checkFixAdvancedFilter;
86146
+ result = result.filter((d) => {
86147
+ const r = idToRecord.get(getEntityId(d));
86148
+ if (!r) return false;
86149
+ if (!(state6.checkFixFilter === "all" || r.status === state6.checkFixFilter)) return false;
86150
+ if (af.statuses.length > 0 && !af.statuses.includes(r.status)) return false;
86151
+ if (af.connStatuses.length > 0 && !af.connStatuses.includes(r.connStatus || "null")) return false;
86152
+ if (af.domains.length > 0 && !af.domains.includes(r.domain || "null")) return false;
86153
+ if (af.missingIngestionId && r.ingestionId) return false;
86154
+ if (af.missingCentralSlave && r.centralId && r.slaveId != null) return false;
86155
+ return true;
86156
+ });
86157
+ }
86158
+ return result;
86159
+ }
86160
+ function copyCellValue(text, el2) {
86161
+ if (!text) return;
86162
+ const flash = (ok) => {
86163
+ const prev = el2.style.backgroundColor;
86164
+ el2.style.transition = "background-color 0.15s ease";
86165
+ el2.style.backgroundColor = ok ? "rgba(34,197,94,0.35)" : "rgba(239,68,68,0.35)";
86166
+ setTimeout(() => {
86167
+ el2.style.backgroundColor = prev;
86168
+ }, 350);
86169
+ };
86170
+ if (navigator.clipboard?.writeText) {
86171
+ navigator.clipboard.writeText(text).then(() => flash(true), () => flash(false));
86172
+ } else {
86173
+ flash(false);
86174
+ }
86175
+ }
85981
86176
  function renderDeviceRow(device, state6, modalId, colors2) {
85982
86177
  const deviceId = getEntityId(device);
85983
86178
  const isSelectedSingle = state6.deviceSelectionMode === "single" && getEntityId(state6.selectedDevice) === deviceId;
@@ -86092,6 +86287,10 @@ function renderDeviceRow(device, state6, modalId, colors2) {
86092
86287
  " title="${statusTs}">(+)</span>` : ""}
86093
86288
  `;
86094
86289
  };
86290
+ const relToNames = state6.relationsLoaded ? (state6.deviceRelToMap.get(deviceId) || []).map((r) => r.name || "").filter(Boolean).join(", ") : "";
86291
+ const relFromNames = state6.relationsLoaded ? (state6.deviceRelFromMap.get(deviceId) || []).map((r) => r.name || "").filter(Boolean).join(", ") : "";
86292
+ const telemetryCopy = telemetryItems.map((it) => `${it.label}: ${it.value}${it.unit}`).join("; ");
86293
+ const copyAttr = (v) => `class="myio-copy-cell" data-copy="${encodeURIComponent(String(v ?? ""))}" title="Clique para copiar valor"`;
86095
86294
  return `
86096
86295
  <div class="myio-list-item ${isSelected ? "selected" : ""}"
86097
86296
  data-device-id="${deviceId}" style="
@@ -86107,8 +86306,8 @@ function renderDeviceRow(device, state6, modalId, colors2) {
86107
86306
  " />
86108
86307
  </div>
86109
86308
  ` : ""}
86110
- <div style="width: 28px; font-size: 16px; flex-shrink: 0;">${getDeviceIcon3(device.type)}</div>
86111
- <div style="width: ${state6.columnWidths.name}px; padding: 0 6px; overflow: hidden; display: flex; align-items: center; gap: 4px;">
86309
+ <div style="width: 28px; font-size: 16px; flex-shrink: 0; cursor: pointer;" title="Clique para selecionar o dispositivo">${getDeviceIcon3(device.type)}</div>
86310
+ <div ${copyAttr(device.name)} style="width: ${state6.columnWidths.name}px; padding: 0 6px; overflow: hidden; display: flex; align-items: center; gap: 4px; cursor: pointer;">
86112
86311
  <div style="font-weight: 600; color: ${colors2.text}; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;" title="${device.name}">
86113
86312
  ${device.name}
86114
86313
  </div>
@@ -86118,22 +86317,22 @@ function renderDeviceRow(device, state6, modalId, colors2) {
86118
86317
  display: flex; align-items: center; justify-content: center; border: 1px solid ${colors2.border};
86119
86318
  " title="Ver detalhes">\u24D8</span>
86120
86319
  </div>
86121
- <div style="width: ${state6.columnWidths.label}px; padding: 0 6px; overflow: hidden; flex-shrink: 0;">
86320
+ <div ${copyAttr(device.label ?? "")} style="width: ${state6.columnWidths.label}px; padding: 0 6px; overflow: hidden; flex-shrink: 0; cursor: pointer;">
86122
86321
  <div style="font-size: 10px; color: ${colors2.textMuted}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${device.label ?? ""}">
86123
86322
  ${device.label ?? ""}
86124
86323
  </div>
86125
86324
  </div>
86126
- <div style="width: ${state6.columnWidths.type}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden;">
86325
+ <div ${copyAttr(device.type ?? "")} style="width: ${state6.columnWidths.type}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; cursor: pointer;">
86127
86326
  <div style="font-size: 9px; padding: 2px 4px; border-radius: 3px; display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
86128
86327
  background: ${device.type?.includes("HIDRO") ? "#dbeafe" : "#fef3c7"};
86129
86328
  color: ${device.type?.includes("HIDRO") ? "#1e40af" : "#92400e"};" title="${device.type || ""}">
86130
86329
  ${device.type || "\u2014"}
86131
86330
  </div>
86132
86331
  </div>
86133
- <div style="width: ${state6.columnWidths.createdTime}px; padding: 0 6px; text-align: center; flex-shrink: 0;">
86332
+ <div ${copyAttr(device.createdTime ? createdTimeStr : "")} style="width: ${state6.columnWidths.createdTime}px; padding: 0 6px; text-align: center; flex-shrink: 0; cursor: pointer;">
86134
86333
  <span style="font-size: 9px; color: ${colors2.textMuted};">${createdTimeStr}</span>
86135
86334
  </div>
86136
- <div style="width: ${state6.columnWidths.relationTo}px; padding: 0 6px; flex-shrink: 0; overflow: hidden; display:flex; align-items:center; justify-content:center; gap:3px;">
86335
+ <div ${copyAttr(relToNames)} style="width: ${state6.columnWidths.relationTo}px; padding: 0 6px; flex-shrink: 0; overflow: hidden; display:flex; align-items:center; justify-content:center; gap:3px; cursor: pointer;">
86137
86336
  ${(() => {
86138
86337
  if (!state6.relationsLoaded) return `<span style="font-size: 8px; color: ${colors2.textMuted}; font-style: italic;">\u2014</span>`;
86139
86338
  const rels = state6.deviceRelToMap.get(deviceId) || [];
@@ -86143,7 +86342,7 @@ function renderDeviceRow(device, state6, modalId, colors2) {
86143
86342
  return `<span style="font-size:9px;color:${colors2.text};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:${more > 0 ? 68 : 108}px" title="${first.name}">${first.name}</span>${more > 0 ? `<button data-show-relto="${deviceId}" data-device-name="${encodeURIComponent(device.name || "")}" style="flex-shrink:0;font-size:9px;background:#ede9ff;color:#4c1d95;border:none;border-radius:3px;padding:1px 4px;cursor:pointer;font-weight:700;line-height:1.4">+${more}</button>` : ""}`;
86144
86343
  })()}
86145
86344
  </div>
86146
- <div style="width: ${state6.columnWidths.relationFrom}px; padding: 0 6px; flex-shrink: 0; overflow: hidden; display:flex; align-items:center; justify-content:center; gap:3px;">
86345
+ <div ${copyAttr(relFromNames)} style="width: ${state6.columnWidths.relationFrom}px; padding: 0 6px; flex-shrink: 0; overflow: hidden; display:flex; align-items:center; justify-content:center; gap:3px; cursor: pointer;">
86147
86346
  ${(() => {
86148
86347
  if (!state6.relationsLoaded) return `<span style="font-size: 8px; color: ${colors2.textMuted}; font-style: italic;">\u2014</span>`;
86149
86348
  const rels = state6.deviceRelFromMap.get(deviceId) || [];
@@ -86153,25 +86352,25 @@ function renderDeviceRow(device, state6, modalId, colors2) {
86153
86352
  return `<span style="font-size:9px;color:${colors2.text};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:${more > 0 ? 68 : 98}px" title="${first.name}">${first.name}</span>${more > 0 ? `<button data-show-relfrom="${deviceId}" data-device-name="${encodeURIComponent(device.name || "")}" style="flex-shrink:0;font-size:9px;background:#dbeafe;color:#1e40af;border:none;border-radius:3px;padding:1px 4px;cursor:pointer;font-weight:700;line-height:1.4">+${more}</button>` : ""}`;
86154
86353
  })()}
86155
86354
  </div>
86156
- <div style="width: ${state6.columnWidths.centralId}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
86355
+ <div ${copyAttr(attrs.centralId ?? "")} style="width: ${state6.columnWidths.centralId}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer;">
86157
86356
  ${!state6.deviceAttrsLoaded ? `<span style="font-size: 8px; color: ${colors2.textMuted}; font-style: italic;">\u2014</span>` : attrs.centralId ? `<span style="font-size: 9px; color: ${colors2.text};" title="${attrs.centralId}">${attrs.centralId}</span>` : `<span style="font-size: 9px; color: ${colors2.textMuted};">\u2014</span>`}
86158
86357
  </div>
86159
- <div style="width: ${state6.columnWidths.slaveId}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
86358
+ <div ${copyAttr(attrs.slaveId ?? "")} style="width: ${state6.columnWidths.slaveId}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer;">
86160
86359
  ${!state6.deviceAttrsLoaded ? `<span style="font-size: 8px; color: ${colors2.textMuted}; font-style: italic;">\u2014</span>` : attrs.slaveId != null && attrs.slaveId !== "" ? `<span style="font-size: 9px; color: ${colors2.text};" title="${attrs.slaveId}">${attrs.slaveId}</span>` : `<span style="font-size: 9px; color: ${colors2.textMuted};">\u2014</span>`}
86161
86360
  </div>
86162
- <div style="width: ${state6.columnWidths.deviceType}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
86361
+ <div ${copyAttr(state6.deviceAttrsLoaded ? attrs.deviceType ?? "" : "")} style="width: ${state6.columnWidths.deviceType}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer;">
86163
86362
  ${renderDeviceTypeValue()}
86164
86363
  </div>
86165
- <div style="width: ${state6.columnWidths.deviceProfile}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
86364
+ <div ${copyAttr(state6.deviceAttrsLoaded ? attrs.deviceProfile ?? "" : "")} style="width: ${state6.columnWidths.deviceProfile}px; padding: 0 6px; text-align: center; flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer;">
86166
86365
  ${renderDeviceProfileValue()}
86167
86366
  </div>
86168
- <div style="width: ${state6.columnWidths.telemetry}px; padding: 0 6px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: 4px; flex-wrap: wrap;">
86367
+ <div ${copyAttr(telemetryCopy)} style="width: ${state6.columnWidths.telemetry}px; padding: 0 6px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: 4px; flex-wrap: wrap; cursor: pointer;">
86169
86368
  ${renderTelemetryValue()}
86170
86369
  </div>
86171
- <div style="width: ${state6.columnWidths.status}px; padding: 0 6px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: 2px;">
86370
+ <div ${copyAttr(connStatus ?? "")} style="width: ${state6.columnWidths.status}px; padding: 0 6px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: 2px; cursor: pointer;">
86172
86371
  ${renderStatusValue()}
86173
86372
  </div>
86174
- <div style="width: 24px; flex-shrink: 0; text-align: center;">
86373
+ <div style="width: 24px; flex-shrink: 0; text-align: center; cursor: pointer;" title="Clique para selecionar o dispositivo">
86175
86374
  ${isSelected ? `<span style="color: ${colors2.success}; font-size: 14px;">\u2713</span>` : ""}
86176
86375
  </div>
86177
86376
  </div>
@@ -86553,7 +86752,11 @@ function renderLojasStep3(state6, modalId, colors2, t) {
86553
86752
  <span>Profile alvo: <strong style="color: ${colors2.text};">3F_MEDIDOR</strong></span>
86554
86753
  <span>deviceType: <strong style="color: ${colors2.text};">3F_MEDIDOR</strong></span>
86555
86754
  <span>deviceProfile: <strong style="color: ${colors2.text};">3F_MEDIDOR</strong></span>
86556
- <span>Rela\xE7\xE3o: <strong style="color: ${colors2.text};">CUSTOMER \u2192 DEVICE (Contains)</strong></span>
86755
+ <label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"
86756
+ title="Marcado: for\xE7a a rela\xE7\xE3o Customer \u2192 Device. Desmarcado: n\xE3o altera rela\xE7\xF5es.">
86757
+ <input type="checkbox" id="${modalId}-lojas-apply-relation" ${state6.lojasApplyRelation ? "checked" : ""} style="accent-color: ${MYIO_PURPLE}; cursor: pointer;" />
86758
+ <span>Rela\xE7\xE3o: <strong style="color: ${colors2.text};">CUSTOMER \u2192 DEVICE (Contains)</strong></span>
86759
+ </label>
86557
86760
  </div>
86558
86761
  </div>
86559
86762
  `;
@@ -87117,6 +87320,8 @@ async function openClearGcdrIdsModal(state6) {
87117
87320
  }
87118
87321
  }
87119
87322
  function setupEventListeners3(container, state6, modalId, t, onClose) {
87323
+ if (container.dataset.upsellListenersBound === "true") return;
87324
+ container.dataset.upsellListenersBound = "true";
87120
87325
  const closeHandler = () => closeModal(container, onClose);
87121
87326
  const overlay = container.querySelector(".myio-upsell-modal-overlay");
87122
87327
  if (overlay) {
@@ -87232,10 +87437,18 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87232
87437
  });
87233
87438
  });
87234
87439
  });
87440
+ container.querySelectorAll(".myio-copy-cell").forEach((cell) => {
87441
+ cell.addEventListener("click", (e) => {
87442
+ e.stopPropagation();
87443
+ const target = e.target;
87444
+ if (target.closest("button, input, .myio-info-btn, .myio-ts-btn")) return;
87445
+ copyCellValue(decodeURIComponent(cell.dataset.copy || ""), cell);
87446
+ });
87447
+ });
87235
87448
  document.getElementById(`${modalId}-device-search`)?.addEventListener("input", (e) => {
87236
87449
  const search = e.target.value.toLowerCase();
87237
87450
  state6.deviceSearchTerm = e.target.value;
87238
- filterDeviceListVisual(container, state6.devices, search, state6.deviceFilters, state6.deviceSort);
87451
+ filterDeviceListVisual(container, state6.devices, search, state6.deviceFilters, state6.deviceSort, state6);
87239
87452
  });
87240
87453
  document.getElementById(`${modalId}-device-type-filter`)?.addEventListener("change", (e) => {
87241
87454
  const select = e.target;
@@ -87457,15 +87670,17 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87457
87670
  renderModal4(container, state6, modalId, t);
87458
87671
  setupEventListeners3(container, state6, modalId, t, onClose);
87459
87672
  });
87460
- document.getElementById(`${modalId}-check-fix`)?.addEventListener("click", async () => {
87673
+ document.getElementById(`${modalId}-check-fix`)?.addEventListener("click", () => {
87461
87674
  if (state6.checkFixLoading) return;
87462
- state6.checkFixLoading = true;
87463
- renderModal4(container, state6, modalId, t);
87464
- setupEventListeners3(container, state6, modalId, t, onClose);
87465
- await runCheckFixRoutine(state6, container, modalId, t, onClose);
87466
- state6.checkFixLoading = false;
87467
- renderModal4(container, state6, modalId, t);
87468
- setupEventListeners3(container, state6, modalId, t, onClose);
87675
+ openCheckFixScopeDialog(state6, async (scopeDevices) => {
87676
+ state6.checkFixLoading = true;
87677
+ renderModal4(container, state6, modalId, t);
87678
+ setupEventListeners3(container, state6, modalId, t, onClose);
87679
+ await runCheckFixRoutine(state6, container, modalId, t, onClose, scopeDevices);
87680
+ state6.checkFixLoading = false;
87681
+ renderModal4(container, state6, modalId, t);
87682
+ setupEventListeners3(container, state6, modalId, t, onClose);
87683
+ });
87469
87684
  });
87470
87685
  document.getElementById(`${modalId}-checkfix-filter`)?.addEventListener("change", (e) => {
87471
87686
  state6.checkFixFilter = e.target.value;
@@ -87563,35 +87778,7 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87563
87778
  }
87564
87779
  });
87565
87780
  document.getElementById(`${modalId}-select-all`)?.addEventListener("click", () => {
87566
- const {
87567
- types: filterTypes,
87568
- deviceTypes: filterDeviceTypes,
87569
- deviceProfiles: filterDeviceProfiles,
87570
- statuses: filterStatuses,
87571
- telemetryKeys: filterTelemetryKeys
87572
- } = state6.deviceFilters;
87573
- let filteredDevices = state6.devices.filter((d) => {
87574
- if (filterTypes.length > 0 && !filterTypes.includes(d.type || "")) return false;
87575
- if (filterDeviceTypes.length > 0 && !filterDeviceTypes.includes(d.serverAttrs?.deviceType || ""))
87576
- return false;
87577
- if (filterDeviceProfiles.length > 0 && !filterDeviceProfiles.includes(d.serverAttrs?.deviceProfile || ""))
87578
- return false;
87579
- if (filterStatuses.length > 0) {
87580
- const status = d.latestTelemetry?.connectionStatus?.value || "offline";
87581
- if (!filterStatuses.includes(status)) return false;
87582
- }
87583
- if (filterTelemetryKeys.length > 0) {
87584
- const telem = d.latestTelemetry;
87585
- const hasMatch = filterTelemetryKeys.some((k) => {
87586
- if (k === "pulses") return telem?.pulses != null;
87587
- if (k === "consumption") return telem?.consumption != null;
87588
- return false;
87589
- });
87590
- if (!hasMatch) return false;
87591
- }
87592
- return true;
87593
- });
87594
- state6.selectedDevices = [...filteredDevices];
87781
+ state6.selectedDevices = [...getGridVisibleDevices(state6)];
87595
87782
  const listEl = document.getElementById(`${modalId}-device-list`);
87596
87783
  const savedScroll = listEl ? listEl.scrollTop : 0;
87597
87784
  renderModal4(container, state6, modalId, t);
@@ -87707,12 +87894,27 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87707
87894
  document.getElementById(`${modalId}-bulk-profile-save`)?.addEventListener("click", async () => {
87708
87895
  await saveBulkProfile(state6, container, modalId, t, onClose);
87709
87896
  });
87710
- document.getElementById(`${modalId}-bulk-owner`)?.addEventListener("click", () => {
87897
+ document.getElementById(`${modalId}-bulk-owner`)?.addEventListener("click", async () => {
87711
87898
  if (!state6.selectedCustomer) {
87712
87899
  alert("Selecione um Customer primeiro no Step 1");
87713
87900
  return;
87714
87901
  }
87715
87902
  state6.bulkOwnerModal.open = true;
87903
+ if (!state6.bulkOwnerModal.targetCustomerId) {
87904
+ state6.bulkOwnerModal.targetCustomerId = state6.selectedCustomer.id?.id || "";
87905
+ }
87906
+ renderModal4(container, state6, modalId, t);
87907
+ setupEventListeners3(container, state6, modalId, t, onClose);
87908
+ if (state6.customers.length === 0) {
87909
+ await loadCustomers(state6, container, modalId, t, onClose);
87910
+ if (state6.bulkOwnerModal.open) {
87911
+ renderModal4(container, state6, modalId, t);
87912
+ setupEventListeners3(container, state6, modalId, t, onClose);
87913
+ }
87914
+ }
87915
+ });
87916
+ document.getElementById(`${modalId}-bulk-owner-customer`)?.addEventListener("change", (e) => {
87917
+ state6.bulkOwnerModal.targetCustomerId = e.target.value;
87716
87918
  renderModal4(container, state6, modalId, t);
87717
87919
  setupEventListeners3(container, state6, modalId, t, onClose);
87718
87920
  });
@@ -87757,6 +87959,10 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87757
87959
  if (!state6.selectedCustomer || state6.selectedDevices.length === 0) return;
87758
87960
  await handleBulkSyncIngestionId(state6, container, modalId, t, onClose);
87759
87961
  });
87962
+ document.getElementById(`${modalId}-bulk-delete`)?.addEventListener("click", async () => {
87963
+ if (state6.selectedDevices.length === 0) return;
87964
+ await handleBulkDeleteDevices(state6, container, modalId, t, onClose);
87965
+ });
87760
87966
  document.getElementById(`${modalId}-clear-gcdr-ids`)?.addEventListener("click", () => {
87761
87967
  if (!state6.selectedCustomer) {
87762
87968
  alert("Selecione um Customer primeiro no Step 1");
@@ -87886,6 +88092,14 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87886
88092
  });
87887
88093
  }
87888
88094
  });
88095
+ const applyRelCb = document.getElementById(
88096
+ `${modalId}-lojas-apply-relation`
88097
+ );
88098
+ if (applyRelCb) {
88099
+ applyRelCb.addEventListener("change", () => {
88100
+ state6.lojasApplyRelation = applyRelCb.checked;
88101
+ });
88102
+ }
87889
88103
  }
87890
88104
  document.getElementById(`${modalId}-change-owner`)?.addEventListener("click", () => {
87891
88105
  const form = document.getElementById(`${modalId}-change-owner-form`);
@@ -88107,7 +88321,8 @@ Total de devices no customer: ${ingestionDevices.length}`
88107
88321
  state6.devices,
88108
88322
  state6.deviceSearchTerm.toLowerCase(),
88109
88323
  state6.deviceFilters,
88110
- state6.deviceSort
88324
+ state6.deviceSort,
88325
+ state6
88111
88326
  );
88112
88327
  }
88113
88328
  }
@@ -88247,7 +88462,7 @@ function filterCustomerList(container, customers, search, selected, sort) {
88247
88462
  item.style.display = matches ? "flex" : "none";
88248
88463
  });
88249
88464
  }
88250
- function filterDeviceListVisual(container, devices, search, filters, sort) {
88465
+ function filterDeviceListVisual(container, devices, search, filters, sort, state6) {
88251
88466
  const listContainer = container.querySelector('[id$="-device-list"]');
88252
88467
  if (!listContainer) return;
88253
88468
  let filtered = devices.filter((d) => {
@@ -88267,7 +88482,7 @@ function filterDeviceListVisual(container, devices, search, filters, sort) {
88267
88482
  item.style.display = "none";
88268
88483
  return;
88269
88484
  }
88270
- const matchesSearch = !search || device.name?.toLowerCase().includes(search) || device.label?.toLowerCase().includes(search) || device.type?.toLowerCase().includes(search) || device.serverAttrs?.deviceType?.toLowerCase().includes(search) || device.serverAttrs?.deviceProfile?.toLowerCase().includes(search);
88485
+ const matchesSearch = !search || buildDeviceSearchHaystack(device, state6).includes(search);
88271
88486
  const el2 = item;
88272
88487
  const isTableRow = el2.tagName === "TR";
88273
88488
  el2.style.display = matchesSearch ? isTableRow ? "" : "flex" : "none";
@@ -88618,6 +88833,56 @@ async function loadLojasData(state6, container, modalId, t, onClose) {
88618
88833
  setupEventListeners3(container, state6, modalId, t, onClose);
88619
88834
  }
88620
88835
  }
88836
+ async function handleBulkDeleteDevices(state6, container, modalId, t, onClose) {
88837
+ const devices = [...state6.selectedDevices];
88838
+ if (devices.length === 0) return;
88839
+ const preview = devices.slice(0, 8).map((d) => `\u2022 ${d.name || d.label || getEntityId(d)}`).join("\n");
88840
+ const more = devices.length > 8 ? `
88841
+ \u2026 e mais ${devices.length - 8}` : "";
88842
+ const confirm1 = `\u26A0\uFE0F DELETAR ${devices.length} dispositivo(s) do ThingsBoard?
88843
+
88844
+ ${preview}${more}
88845
+
88846
+ Esta a\xE7\xE3o \xE9 IRREVERS\xCDVEL \u2014 os dispositivos, suas rela\xE7\xF5es e telemetria ser\xE3o removidos permanentemente.`;
88847
+ if (!confirm(confirm1)) return;
88848
+ if (!confirm(
88849
+ `Confirma\xE7\xE3o final: deletar ${devices.length} dispositivo(s)?
88850
+ Esta a\xE7\xE3o N\xC3O pode ser desfeita.`
88851
+ ))
88852
+ return;
88853
+ showBusyProgress(`Deletando ${devices.length} dispositivos...`, devices.length);
88854
+ let okCount = 0;
88855
+ let failCount = 0;
88856
+ const errors = [];
88857
+ const deletedIds = /* @__PURE__ */ new Set();
88858
+ for (let i = 0; i < devices.length; i++) {
88859
+ const d = devices[i];
88860
+ const id = getEntityId(d);
88861
+ try {
88862
+ await tbDelete(state6, `/api/device/${id}`);
88863
+ deletedIds.add(id);
88864
+ okCount++;
88865
+ } catch (err) {
88866
+ failCount++;
88867
+ errors.push(`${d.name || id}: ${err.message}`);
88868
+ }
88869
+ updateBusyProgress(i + 1);
88870
+ }
88871
+ hideBusyProgress();
88872
+ state6.devices = state6.devices.filter((d) => !deletedIds.has(getEntityId(d)));
88873
+ state6.selectedDevices = state6.selectedDevices.filter((d) => !deletedIds.has(getEntityId(d)));
88874
+ if (state6.selectedDevice && deletedIds.has(getEntityId(state6.selectedDevice))) {
88875
+ state6.selectedDevice = null;
88876
+ }
88877
+ renderModal4(container, state6, modalId, t);
88878
+ setupEventListeners3(container, state6, modalId, t, onClose);
88879
+ alert(
88880
+ `Dispositivos deletados: ${okCount}` + (failCount > 0 ? `
88881
+ Falhas: ${failCount}
88882
+ ${errors.slice(0, 5).join("\n")}` + (errors.length > 5 ? `
88883
+ \u2026 e mais ${errors.length - 5} erros` : "") : "")
88884
+ );
88885
+ }
88621
88886
  async function handleBulkSyncIngestionId(state6, container, modalId, t, onClose) {
88622
88887
  if (!state6.selectedCustomer || state6.selectedDevices.length === 0) return;
88623
88888
  const customerId = getEntityId(state6.selectedCustomer);
@@ -88764,15 +89029,17 @@ async function handleLojasApply(state6, container, modalId, t, onClose) {
88764
89029
  if (identifierInput) data[i].identifier = identifierInput.value;
88765
89030
  }
88766
89031
  const activeConfig = state6.lojasConfig ?? CUSTOM_MODES[0];
89032
+ const applyRelation = state6.lojasApplyRelation;
88767
89033
  const confirmMsg = `Aplicar configura\xE7\xE3o "${activeConfig.label}" para ${data.length} dispositivos?
88768
89034
 
88769
89035
  Cada device receber\xE1:
88770
89036
  - Label atualizado (etiqueta)
88771
89037
  - Profile: ${activeConfig.deviceProfile}
88772
89038
  - deviceType/deviceProfile: ${activeConfig.deviceType} / ${activeConfig.deviceProfile}
88773
- - Rela\xE7\xF5es existentes removidas
89039
+ ` + (applyRelation ? `- Rela\xE7\xF5es existentes removidas
88774
89040
  - Nova rela\xE7\xE3o: Customer \u2192 Device (Contains)
88775
-
89041
+ ` : `- Rela\xE7\xF5es N\xC3O ser\xE3o alteradas (checkbox de rela\xE7\xE3o desmarcado)
89042
+ `) + `
88776
89043
  Deseja continuar?`;
88777
89044
  if (!confirm(confirmMsg)) return;
88778
89045
  showBusyProgress(`Aplicando ${activeConfig.label}...`, data.length);
@@ -88800,31 +89067,33 @@ Deseja continuar?`;
88800
89067
  attrs.ingestionId = d.ingestionId;
88801
89068
  }
88802
89069
  await tbPost(state6, `/api/plugins/telemetry/DEVICE/${d.deviceId}/attributes/SERVER_SCOPE`, attrs);
88803
- if (d.currentRelations.length > 0) {
88804
- updateBusyProgress(i + 1, `[${i + 1}/${data.length}] ${d.name}: Removendo rela\xE7\xF5es...`);
88805
- for (const rel of d.currentRelations) {
88806
- try {
88807
- const params = new URLSearchParams({
88808
- fromId: rel.from.id,
88809
- fromType: rel.from.entityType,
88810
- toId: d.deviceId,
88811
- toType: "DEVICE",
88812
- relationType: rel.type || "Contains",
88813
- relationTypeGroup: rel.typeGroup || "COMMON"
88814
- });
88815
- await tbDelete(state6, `/api/relation?${params.toString()}`);
88816
- } catch (e) {
88817
- console.warn("[UpsellModal] Error deleting relation for LOJAS:", e);
89070
+ if (applyRelation) {
89071
+ if (d.currentRelations.length > 0) {
89072
+ updateBusyProgress(i + 1, `[${i + 1}/${data.length}] ${d.name}: Removendo rela\xE7\xF5es...`);
89073
+ for (const rel of d.currentRelations) {
89074
+ try {
89075
+ const params = new URLSearchParams({
89076
+ fromId: rel.from.id,
89077
+ fromType: rel.from.entityType,
89078
+ toId: d.deviceId,
89079
+ toType: "DEVICE",
89080
+ relationType: rel.type || "Contains",
89081
+ relationTypeGroup: rel.typeGroup || "COMMON"
89082
+ });
89083
+ await tbDelete(state6, `/api/relation?${params.toString()}`);
89084
+ } catch (e) {
89085
+ console.warn("[UpsellModal] Error deleting relation for LOJAS:", e);
89086
+ }
88818
89087
  }
88819
89088
  }
89089
+ updateBusyProgress(i + 1, `[${i + 1}/${data.length}] ${d.name}: Criando rela\xE7\xE3o...`);
89090
+ await tbPost(state6, "/api/relation", {
89091
+ from: { entityType: "CUSTOMER", id: customerId },
89092
+ to: { entityType: "DEVICE", id: d.deviceId },
89093
+ type: "Contains",
89094
+ typeGroup: "COMMON"
89095
+ });
88820
89096
  }
88821
- updateBusyProgress(i + 1, `[${i + 1}/${data.length}] ${d.name}: Criando rela\xE7\xE3o...`);
88822
- await tbPost(state6, "/api/relation", {
88823
- from: { entityType: "CUSTOMER", id: customerId },
88824
- to: { entityType: "DEVICE", id: d.deviceId },
88825
- type: "Contains",
88826
- typeGroup: "COMMON"
88827
- });
88828
89097
  successCount++;
88829
89098
  } catch (error) {
88830
89099
  errorCount++;
@@ -89112,8 +89381,111 @@ async function loadDeviceTelemetryInBatch(state6, container, modalId, t, onClose
89112
89381
  hideBusyProgress();
89113
89382
  }
89114
89383
  }
89115
- async function runCheckFixRoutine(state6, container, modalId, t, onClose) {
89116
- const devices = state6.devices;
89384
+ function openCheckFixScopeDialog(state6, onConfirm) {
89385
+ const DIALOG_ID = "myio-upsell-cf-scope-dialog";
89386
+ document.getElementById(DIALOG_ID)?.remove();
89387
+ const c = getThemeColors5(state6.theme);
89388
+ const allDevices = state6.devices;
89389
+ const allTypes = [...new Set(allDevices.map((d) => d.type).filter(Boolean))].sort();
89390
+ let namePattern = "";
89391
+ let caseSensitive = false;
89392
+ const selectedTypes = /* @__PURE__ */ new Set();
89393
+ function computeSubset() {
89394
+ let result = allDevices;
89395
+ if (namePattern) {
89396
+ const pat = caseSensitive ? namePattern : namePattern.toLowerCase();
89397
+ result = result.filter((d) => {
89398
+ const n = caseSensitive ? d.name || "" : (d.name || "").toLowerCase();
89399
+ return n.includes(pat);
89400
+ });
89401
+ }
89402
+ if (selectedTypes.size > 0) {
89403
+ result = result.filter((d) => selectedTypes.has(d.type || ""));
89404
+ }
89405
+ return result;
89406
+ }
89407
+ const overlay = document.createElement("div");
89408
+ overlay.id = DIALOG_ID;
89409
+ overlay.style.cssText = "position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;font-family:Roboto,Inter,system-ui,sans-serif;";
89410
+ overlay.innerHTML = `
89411
+ <div style="background:${c.surface};border-radius:12px;width:480px;max-width:92vw;max-height:88vh;
89412
+ overflow:hidden;display:flex;flex-direction:column;box-shadow:0 12px 40px rgba(0,0,0,0.4);">
89413
+ <div style="background:${MYIO_PURPLE};color:#fff;padding:12px 16px;font-size:14px;font-weight:700;">
89414
+ \u{1F52C} Escopo do CHECK &amp; FIX
89415
+ </div>
89416
+ <div style="padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:14px;">
89417
+ <div style="font-size:11px;color:${c.textMuted};">
89418
+ Os filtros abaixo se <strong>combinam (E)</strong>. Deixe ambos vazios para
89419
+ diagnosticar todos os ${allDevices.length} dispositivos.
89420
+ </div>
89421
+
89422
+ <div style="display:flex;flex-direction:column;gap:8px;">
89423
+ <span style="font-size:12px;font-weight:600;color:${c.text};">Filtro por nome</span>
89424
+ <input id="cf-scope-name-input" type="text" placeholder="nome cont\xE9m\u2026 (ex.: TEMP.)" style="
89425
+ font-size:12px;padding:7px 9px;border-radius:6px;border:1px solid ${c.border};
89426
+ background:${c.inputBg};color:${c.text};" />
89427
+ <label style="display:flex;gap:6px;align-items:center;font-size:11px;color:${c.textMuted};cursor:pointer;">
89428
+ <input id="cf-scope-case" type="checkbox" style="accent-color:${MYIO_PURPLE};" />
89429
+ Diferenciar mai\xFAsculas/min\xFAsculas (case-sensitive)
89430
+ </label>
89431
+ </div>
89432
+
89433
+ <div style="display:flex;flex-direction:column;gap:8px;">
89434
+ <span style="font-size:12px;font-weight:600;color:${c.text};">Filtro por tipo (device.type)</span>
89435
+ <div style="display:flex;flex-wrap:wrap;gap:6px;max-height:160px;overflow-y:auto;">
89436
+ ${allTypes.length === 0 ? `<span style="font-size:11px;color:${c.textMuted};">Nenhum tipo dispon\xEDvel.</span>` : allTypes.map(
89437
+ (tp) => `
89438
+ <label style="display:flex;gap:5px;align-items:center;font-size:11px;color:${c.text};
89439
+ cursor:pointer;border:1px solid ${c.border};border-radius:6px;padding:3px 8px;background:${c.cardBg};">
89440
+ <input type="checkbox" class="cf-scope-type-cb" value="${tp}" style="accent-color:${MYIO_PURPLE};" />
89441
+ ${tp}
89442
+ </label>`
89443
+ ).join("")}
89444
+ </div>
89445
+ </div>
89446
+ </div>
89447
+ <div style="padding:12px 16px;border-top:1px solid ${c.border};display:flex;justify-content:flex-end;gap:8px;">
89448
+ <button id="cf-scope-cancel" style="font-size:12px;font-weight:600;padding:8px 14px;border-radius:6px;
89449
+ border:1px solid ${c.border};background:${c.cardBg};color:${c.text};cursor:pointer;">Cancelar</button>
89450
+ <button id="cf-scope-run" style="font-size:12px;font-weight:700;padding:8px 14px;border-radius:6px;
89451
+ border:none;background:${MYIO_PURPLE};color:#fff;cursor:pointer;">Executar diagn\xF3stico (${allDevices.length})</button>
89452
+ </div>
89453
+ </div>`;
89454
+ document.body.appendChild(overlay);
89455
+ const runBtn = overlay.querySelector("#cf-scope-run");
89456
+ function refresh() {
89457
+ const n = computeSubset().length;
89458
+ runBtn.textContent = `Executar diagn\xF3stico (${n})`;
89459
+ runBtn.disabled = n === 0;
89460
+ runBtn.style.opacity = n === 0 ? "0.5" : "1";
89461
+ runBtn.style.cursor = n === 0 ? "not-allowed" : "pointer";
89462
+ }
89463
+ overlay.querySelector("#cf-scope-name-input").addEventListener("input", (e) => {
89464
+ namePattern = e.target.value;
89465
+ refresh();
89466
+ });
89467
+ overlay.querySelector("#cf-scope-case").addEventListener("change", (e) => {
89468
+ caseSensitive = e.target.checked;
89469
+ refresh();
89470
+ });
89471
+ overlay.querySelectorAll(".cf-scope-type-cb").forEach((cb) => {
89472
+ cb.addEventListener("change", () => {
89473
+ if (cb.checked) selectedTypes.add(cb.value);
89474
+ else selectedTypes.delete(cb.value);
89475
+ refresh();
89476
+ });
89477
+ });
89478
+ overlay.querySelector("#cf-scope-cancel").addEventListener("click", () => overlay.remove());
89479
+ runBtn.addEventListener("click", () => {
89480
+ const subset = computeSubset();
89481
+ if (subset.length === 0) return;
89482
+ overlay.remove();
89483
+ onConfirm(subset);
89484
+ });
89485
+ refresh();
89486
+ }
89487
+ async function runCheckFixRoutine(state6, container, modalId, t, onClose, scopeDevices) {
89488
+ const devices = scopeDevices ?? state6.devices;
89117
89489
  if (devices.length === 0) return;
89118
89490
  const BATCH_SIZE = 5;
89119
89491
  const BATCH_DELAY_MS = 1500;
@@ -89396,10 +89768,11 @@ ${errors.slice(0, 5).join("\n")}` + (errors.length > 5 ? `
89396
89768
  }
89397
89769
  async function saveBulkOwner(state6, container, modalId, t, onClose) {
89398
89770
  const devices = state6.selectedDevices;
89399
- const newCustomerId = state6.selectedCustomer?.id?.id;
89400
- const customerName = state6.selectedCustomer?.name || state6.selectedCustomer?.title || "Unknown";
89771
+ const newCustomerId = state6.bulkOwnerModal.targetCustomerId || state6.selectedCustomer?.id?.id;
89772
+ const targetCustomer = state6.customers.find((c) => c.id?.id === newCustomerId) || state6.selectedCustomer;
89773
+ const customerName = targetCustomer?.name || targetCustomer?.title || "Unknown";
89401
89774
  if (!newCustomerId) {
89402
- alert("Por favor, selecione um Customer no Step 1 primeiro.");
89775
+ alert("Por favor, selecione um Customer de destino.");
89403
89776
  return;
89404
89777
  }
89405
89778
  if (devices.length === 0) {