myio-js-library 0.1.500 → 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.500",
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",
@@ -15633,6 +15633,57 @@ function renderCardComponentV5({
15633
15633
  `;
15634
15634
  document.head.appendChild(layoutStyle);
15635
15635
  }
15636
+ if (!document.getElementById("myio-card-alert-styles")) {
15637
+ const alertStyle = document.createElement("style");
15638
+ alertStyle.id = "myio-card-alert-styles";
15639
+ alertStyle.textContent = `
15640
+ .myio-alert-overlay {
15641
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100000;
15642
+ display: flex; align-items: center; justify-content: center;
15643
+ background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
15644
+ -webkit-backdrop-filter: blur(4px);
15645
+ animation: myio-fadeIn 0.2s ease-out;
15646
+ }
15647
+ @keyframes myio-fadeIn { from { opacity: 0; } to { opacity: 1; } }
15648
+ .myio-alert-box {
15649
+ position: relative; max-width: 480px; width: 90%; padding: 32px;
15650
+ background: #ffffff; border: 1px solid rgba(0,0,0,0.1); border-radius: 20px;
15651
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
15652
+ animation: myio-slideUp 0.3s cubic-bezier(0.4,0,0.2,1);
15653
+ }
15654
+ @keyframes myio-slideUp {
15655
+ from { opacity: 0; transform: translateY(40px) scale(0.95); }
15656
+ to { opacity: 1; transform: translateY(0) scale(1); }
15657
+ }
15658
+ .myio-alert-icon {
15659
+ width: 64px; height: 64px; margin: 0 auto 20px;
15660
+ display: flex; align-items: center; justify-content: center;
15661
+ background: linear-gradient(135deg, #3E1A7D 0%, #2D1359 100%);
15662
+ border: 2px solid #3E1A7D; border-radius: 50%; color: #ffffff; font-size: 32px;
15663
+ }
15664
+ .myio-alert-title {
15665
+ margin: 0 0 12px; font-size: 24px; font-weight: 700; color: #000000;
15666
+ text-align: center; letter-spacing: -0.02em;
15667
+ }
15668
+ .myio-alert-message {
15669
+ margin: 0 0 28px; font-size: 16px; font-weight: 500; color: #000000;
15670
+ text-align: center; line-height: 1.6;
15671
+ }
15672
+ .myio-alert-button {
15673
+ width: 100%; height: 48px; font-size: 15px; font-weight: 700;
15674
+ text-transform: uppercase;
15675
+ background: linear-gradient(135deg, #3E1A7D 0%, #2D1359 100%);
15676
+ border: none; border-radius: 12px; color: #ffffff; cursor: pointer;
15677
+ box-shadow: 0 4px 16px rgba(62,26,125,0.4);
15678
+ transition: all 0.2s cubic-bezier(0.4,0,0.2,1);
15679
+ }
15680
+ .myio-alert-button:hover {
15681
+ background: linear-gradient(135deg, #4E2A9D 0%, #3E1A7D 100%);
15682
+ box-shadow: 0 6px 24px rgba(62,26,125,0.5); transform: translateY(-2px);
15683
+ }
15684
+ `;
15685
+ document.head.appendChild(alertStyle);
15686
+ }
15636
15687
  const actionsContainer = document.createElement("div");
15637
15688
  actionsContainer.className = "card-actions";
15638
15689
  if (typeof handleActionDashboard === "function") {
@@ -15681,6 +15732,44 @@ function renderCardComponentV5({
15681
15732
  if (enhancedCardElement && actionsContainer.children.length > 0) {
15682
15733
  enhancedCardElement.insertBefore(actionsContainer, enhancedCardElement.firstChild);
15683
15734
  }
15735
+ let _cardAlertOverlay = null;
15736
+ function hideCardAlert() {
15737
+ if (_cardAlertOverlay && _cardAlertOverlay.parentNode) {
15738
+ _cardAlertOverlay.remove();
15739
+ _cardAlertOverlay = null;
15740
+ }
15741
+ }
15742
+ function showCardLimitAlert() {
15743
+ if (_cardAlertOverlay) hideCardAlert();
15744
+ const maxAllowed = MyIOSelectionStore?.MAX_SELECTION ?? 20;
15745
+ const overlay = document.createElement("div");
15746
+ overlay.className = "myio-alert-overlay";
15747
+ overlay.innerHTML = `
15748
+ <div class="myio-alert-box">
15749
+ <div class="myio-alert-icon">\u26A0</div>
15750
+ <h2 class="myio-alert-title">Limite Atingido</h2>
15751
+ <p class="myio-alert-message">
15752
+ Voc\xEA pode selecionar no m\xE1ximo <strong>${maxAllowed} dispositivos</strong> para compara\xE7\xE3o.
15753
+ Remova um dispositivo antes de adicionar outro.
15754
+ </p>
15755
+ <button class="myio-alert-button">FECHAR</button>
15756
+ </div>`;
15757
+ document.body.appendChild(overlay);
15758
+ _cardAlertOverlay = overlay;
15759
+ const closeBtn = overlay.querySelector(".myio-alert-button");
15760
+ const close = () => {
15761
+ document.removeEventListener("keydown", handleEscape);
15762
+ hideCardAlert();
15763
+ };
15764
+ const handleEscape = (e) => {
15765
+ if (e.key === "Escape") close();
15766
+ };
15767
+ closeBtn.addEventListener("click", close);
15768
+ overlay.addEventListener("click", (e) => {
15769
+ if (e.target === overlay) close();
15770
+ });
15771
+ document.addEventListener("keydown", handleEscape);
15772
+ }
15684
15773
  if (enableSelection && MyIOSelectionStore) {
15685
15774
  const checkbox = enhancedCardElement.querySelector(".card-checkbox");
15686
15775
  if (checkbox) {
@@ -15691,10 +15780,10 @@ function renderCardComponentV5({
15691
15780
  const selectedEntities = MyIOSelectionStore.getSelectedEntities();
15692
15781
  console.log("selectedEntities", selectedEntities);
15693
15782
  const isTryingToAdd = e.target.checked;
15694
- if (isTryingToAdd && currentCount >= 6) {
15783
+ if (isTryingToAdd && currentCount >= (MyIOSelectionStore.MAX_SELECTION ?? 20)) {
15695
15784
  e.preventDefault();
15696
15785
  e.target.checked = false;
15697
- MyIOToast2.show("N\xE3o \xE9 poss\xEDvel selecionar mais de 6 itens.", "warning");
15786
+ showCardLimitAlert();
15698
15787
  return;
15699
15788
  }
15700
15789
  MyIOSelectionStore.add(entityId);
@@ -17936,6 +18025,95 @@ function renderCardComponentV6({
17936
18025
  if (enhancedCardElement && actionsContainer.children.length > 0) {
17937
18026
  enhancedCardElement.insertBefore(actionsContainer, enhancedCardElement.firstChild);
17938
18027
  }
18028
+ let _cardAlertOverlay = null;
18029
+ function hideCardAlert() {
18030
+ if (_cardAlertOverlay && _cardAlertOverlay.parentNode) {
18031
+ _cardAlertOverlay.remove();
18032
+ _cardAlertOverlay = null;
18033
+ }
18034
+ }
18035
+ function showCardLimitAlert() {
18036
+ if (_cardAlertOverlay) hideCardAlert();
18037
+ if (!document.getElementById("myio-card-alert-styles")) {
18038
+ const alertStyle = document.createElement("style");
18039
+ alertStyle.id = "myio-card-alert-styles";
18040
+ alertStyle.textContent = `
18041
+ .myio-alert-overlay {
18042
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100000;
18043
+ display: flex; align-items: center; justify-content: center;
18044
+ background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
18045
+ -webkit-backdrop-filter: blur(4px);
18046
+ animation: myio-fadeIn 0.2s ease-out;
18047
+ }
18048
+ @keyframes myio-fadeIn { from { opacity: 0; } to { opacity: 1; } }
18049
+ .myio-alert-box {
18050
+ position: relative; max-width: 480px; width: 90%; padding: 32px;
18051
+ background: #ffffff; border: 1px solid rgba(0,0,0,0.1); border-radius: 20px;
18052
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
18053
+ animation: myio-slideUp 0.3s cubic-bezier(0.4,0,0.2,1);
18054
+ }
18055
+ @keyframes myio-slideUp {
18056
+ from { opacity: 0; transform: translateY(40px) scale(0.95); }
18057
+ to { opacity: 1; transform: translateY(0) scale(1); }
18058
+ }
18059
+ .myio-alert-icon {
18060
+ width: 64px; height: 64px; margin: 0 auto 20px;
18061
+ display: flex; align-items: center; justify-content: center;
18062
+ background: linear-gradient(135deg, #3E1A7D 0%, #2D1359 100%);
18063
+ border: 2px solid #3E1A7D; border-radius: 50%; color: #ffffff; font-size: 32px;
18064
+ }
18065
+ .myio-alert-title {
18066
+ margin: 0 0 12px; font-size: 24px; font-weight: 700; color: #000000;
18067
+ text-align: center; letter-spacing: -0.02em;
18068
+ }
18069
+ .myio-alert-message {
18070
+ margin: 0 0 28px; font-size: 16px; font-weight: 500; color: #000000;
18071
+ text-align: center; line-height: 1.6;
18072
+ }
18073
+ .myio-alert-button {
18074
+ width: 100%; height: 48px; font-size: 15px; font-weight: 700;
18075
+ text-transform: uppercase;
18076
+ background: linear-gradient(135deg, #3E1A7D 0%, #2D1359 100%);
18077
+ border: none; border-radius: 12px; color: #ffffff; cursor: pointer;
18078
+ box-shadow: 0 4px 16px rgba(62,26,125,0.4);
18079
+ transition: all 0.2s cubic-bezier(0.4,0,0.2,1);
18080
+ }
18081
+ .myio-alert-button:hover {
18082
+ background: linear-gradient(135deg, #4E2A9D 0%, #3E1A7D 100%);
18083
+ box-shadow: 0 6px 24px rgba(62,26,125,0.5); transform: translateY(-2px);
18084
+ }
18085
+ `;
18086
+ document.head.appendChild(alertStyle);
18087
+ }
18088
+ const maxAllowed = MyIOSelectionStore?.MAX_SELECTION ?? 20;
18089
+ const overlay = document.createElement("div");
18090
+ overlay.className = "myio-alert-overlay";
18091
+ overlay.innerHTML = `
18092
+ <div class="myio-alert-box">
18093
+ <div class="myio-alert-icon">\u26A0</div>
18094
+ <h2 class="myio-alert-title">Limite Atingido</h2>
18095
+ <p class="myio-alert-message">
18096
+ Voc\xEA pode selecionar no m\xE1ximo <strong>${maxAllowed} dispositivos</strong> para compara\xE7\xE3o.
18097
+ Remova um dispositivo antes de adicionar outro.
18098
+ </p>
18099
+ <button class="myio-alert-button">FECHAR</button>
18100
+ </div>`;
18101
+ document.body.appendChild(overlay);
18102
+ _cardAlertOverlay = overlay;
18103
+ const closeBtn = overlay.querySelector(".myio-alert-button");
18104
+ const close = () => {
18105
+ document.removeEventListener("keydown", handleEscape);
18106
+ hideCardAlert();
18107
+ };
18108
+ const handleEscape = (e) => {
18109
+ if (e.key === "Escape") close();
18110
+ };
18111
+ closeBtn.addEventListener("click", close);
18112
+ overlay.addEventListener("click", (e) => {
18113
+ if (e.target === overlay) close();
18114
+ });
18115
+ document.addEventListener("keydown", handleEscape);
18116
+ }
17939
18117
  if (enableSelection && MyIOSelectionStore) {
17940
18118
  const checkbox = enhancedCardElement.querySelector(".card-checkbox");
17941
18119
  if (checkbox) {
@@ -17943,10 +18121,10 @@ function renderCardComponentV6({
17943
18121
  e.stopPropagation();
17944
18122
  if (e.target.checked) {
17945
18123
  const currentCount = MyIOSelectionStore.getSelectedEntities().length;
17946
- if (currentCount >= 6) {
18124
+ if (currentCount >= (MyIOSelectionStore.MAX_SELECTION ?? 20)) {
17947
18125
  e.preventDefault();
17948
18126
  e.target.checked = false;
17949
- MyIOToast2.show("N\xE3o \xE9 poss\xEDvel selecionar mais de 6 itens.", "warning");
18127
+ showCardLimitAlert();
17950
18128
  return;
17951
18129
  }
17952
18130
  MyIOSelectionStore.add(entityId);
@@ -32814,6 +32992,13 @@ var AllReportModal = class {
32814
32992
  domainConfig;
32815
32993
  // Granularity: '1d' (daily) | '1h' (hourly)
32816
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;
32817
33002
  // Debug logging helper
32818
33003
  debugLog(message, data) {
32819
33004
  if (this.debugEnabled) {
@@ -32822,7 +33007,7 @@ var AllReportModal = class {
32822
33007
  }
32823
33008
  // Helper: normalize identifiers (upper, strip spaces and non-alphanum)
32824
33009
  normalizeId(v) {
32825
- 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, "");
32826
33011
  }
32827
33012
  // Helper: extract store identifier from API item
32828
33013
  // Priority: assetName -> parse from name (last token or token after space) -> null
@@ -32891,6 +33076,10 @@ var AllReportModal = class {
32891
33076
  this.dateRangePicker.destroy();
32892
33077
  this.dateRangePicker = null;
32893
33078
  }
33079
+ if (this.exclusionTooltipCleanup) {
33080
+ this.exclusionTooltipCleanup();
33081
+ this.exclusionTooltipCleanup = null;
33082
+ }
32894
33083
  if (this.filterModal) {
32895
33084
  this.filterModal.destroy();
32896
33085
  this.filterModal = null;
@@ -32929,6 +33118,20 @@ var AllReportModal = class {
32929
33118
  <button id="filter-btn" class="myio-btn myio-btn-secondary" style="background: var(--myio-brand-700); color: white;">
32930
33119
  \u{1F50D} Filtros & Ordena\xE7\xE3o
32931
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>
32932
33135
  <div class="myio-form-group" style="margin-bottom: 0; margin-left: auto;">
32933
33136
  <label class="myio-label" for="search-input">Busca r\xE1pida</label>
32934
33137
  <input type="text" id="search-input" class="myio-input" placeholder="Digite para filtrar..." style="width: 200px;">
@@ -32987,6 +33190,20 @@ var AllReportModal = class {
32987
33190
  this.renderTable();
32988
33191
  });
32989
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
+ }
32990
33207
  try {
32991
33208
  this.dateRangePicker = await attach(dateRangeInput, {
32992
33209
  presetStart: this.getDefaultStartDate(),
@@ -33029,6 +33246,7 @@ var AllReportModal = class {
33029
33246
  const customerTotalsData = await this.fetchCustomerTotals(startISO, endISO);
33030
33247
  this.debugLog("\u2705 API response received", customerTotalsData);
33031
33248
  this.debugLog("\u{1F504} Processing API response...");
33249
+ this.lastApiResponse = customerTotalsData;
33032
33250
  this.data = this.mapCustomerTotalsResponse(customerTotalsData);
33033
33251
  this.debugLog("\u2705 Data mapping completed", {
33034
33252
  mappedDataLength: this.data.length,
@@ -33248,48 +33466,6 @@ var AllReportModal = class {
33248
33466
  });
33249
33467
  }
33250
33468
  renderPagination() {
33251
- return;
33252
- const container = document.getElementById("pagination-container");
33253
- if (!container) return;
33254
- const filteredData = this.getFilteredData();
33255
- const totalPages = Math.ceil(filteredData.length / this.itemsPerPage);
33256
- if (totalPages <= 1) {
33257
- container.style.display = "none";
33258
- return;
33259
- }
33260
- const startItem = (this.currentPage - 1) * this.itemsPerPage + 1;
33261
- const endItem = Math.min(this.currentPage * this.itemsPerPage, filteredData.length);
33262
- container.innerHTML = `
33263
- <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
33264
- <div style="color: var(--myio-text-muted);">
33265
- Mostrando ${startItem}-${endItem} de ${filteredData.length} lojas
33266
- </div>
33267
- <div style="display: flex; gap: 8px; align-items: center;">
33268
- <button id="prev-page" class="myio-btn myio-btn-outline" ${this.currentPage === 1 ? "disabled" : ""}>
33269
- Anterior
33270
- </button>
33271
- <span style="padding: 0 12px; font-weight: bold;">
33272
- ${this.currentPage} / ${totalPages}
33273
- </span>
33274
- <button id="next-page" class="myio-btn myio-btn-outline" ${this.currentPage === totalPages ? "disabled" : ""}>
33275
- Pr\xF3ximo
33276
- </button>
33277
- </div>
33278
- </div>
33279
- `;
33280
- document.getElementById("prev-page")?.addEventListener("click", () => {
33281
- if (this.currentPage > 1) {
33282
- this.currentPage--;
33283
- this.renderTable();
33284
- }
33285
- });
33286
- document.getElementById("next-page")?.addEventListener("click", () => {
33287
- if (this.currentPage < totalPages) {
33288
- this.currentPage++;
33289
- this.renderTable();
33290
- }
33291
- });
33292
- container.style.display = "block";
33293
33469
  }
33294
33470
  calculateTotalConsumption() {
33295
33471
  return this.data.reduce((sum, row) => sum + row.consumption, 0);
@@ -33333,7 +33509,7 @@ var AllReportModal = class {
33333
33509
  }
33334
33510
  generateStoreId(storeName) {
33335
33511
  const name = (storeName || "SEM-ID").toString();
33336
- return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
33512
+ return name.toLowerCase().replace(/\s+/g, "-").replace(/[0300-036f]/g, "");
33337
33513
  }
33338
33514
  applyFiltersAndSort(selectedIds, sortMode) {
33339
33515
  this.selectedStoreIds = new Set(selectedIds);
@@ -33382,8 +33558,10 @@ var AllReportModal = class {
33382
33558
  async fetchCustomerTotals(startISO, endISO) {
33383
33559
  if (this.params.fetcher) {
33384
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.");
33385
33563
  return await this.params.fetcher({
33386
- baseUrl: this.params.api.dataApiBaseUrl,
33564
+ baseUrl: baseUrl2,
33387
33565
  token: token2,
33388
33566
  customerId: this.params.customerId,
33389
33567
  startISO,
@@ -33420,6 +33598,87 @@ var AllReportModal = class {
33420
33598
  this.debugLog("[AllReportModal] Customer totals response:", data);
33421
33599
  return data;
33422
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
+ }
33423
33682
  mapCustomerTotalsResponse(apiResponse) {
33424
33683
  this.debugLog("\u{1F50D} Starting mapCustomerTotalsResponse", { apiResponse });
33425
33684
  const apiArray = Array.isArray(apiResponse?.data) ? apiResponse.data : Array.isArray(apiResponse) ? apiResponse : [];
@@ -33457,6 +33716,10 @@ var AllReportModal = class {
33457
33716
  const apiId = String(apiItem?.id || "");
33458
33717
  if (!apiId || !orchIdSet.has(apiId)) continue;
33459
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
+ }
33460
33723
  const consumption = Math.round(this.pickConsumption(apiItem) * 100) / 100;
33461
33724
  const result = {
33462
33725
  identifier: meta?.identifier || apiItem.name || apiId,
@@ -84133,7 +84396,7 @@ async function fetchIngestionDevicesAllPaged(customerId) {
84133
84396
  function findIngestionDeviceByCentralSlaveId(devices, centralId, slaveId) {
84134
84397
  const slaveIdNum = typeof slaveId === "string" ? parseInt(slaveId, 10) : slaveId;
84135
84398
  for (const device of devices) {
84136
- const deviceGatewayId = device.gatewayId || device.gateway?.id;
84399
+ const deviceGatewayId = device.gateway?.hardwareUuid || device.gatewayId || device.gateway?.id;
84137
84400
  if (deviceGatewayId === centralId && device.slaveId === slaveIdNum) {
84138
84401
  console.log(
84139
84402
  "[UpsellModal] Found matching device:",
@@ -84151,8 +84414,8 @@ function findIngestionDeviceByCentralSlaveId(devices, centralId, slaveId) {
84151
84414
  console.log(
84152
84415
  `[UpsellModal] Sample device ${i}:`,
84153
84416
  d.name,
84154
- "gatewayId:",
84155
- d.gatewayId || d.gateway?.id,
84417
+ "gateway(hwUuid|id):",
84418
+ d.gateway?.hardwareUuid || d.gatewayId || d.gateway?.id,
84156
84419
  "slaveId:",
84157
84420
  d.slaveId
84158
84421
  );
@@ -84337,7 +84600,7 @@ function openUpsellModal(params) {
84337
84600
  selectedDevices: [],
84338
84601
  bulkAttributeModal: { open: false, attribute: "deviceType", value: "", saving: false },
84339
84602
  bulkProfileModal: { open: false, selectedProfileId: "", saving: false },
84340
- bulkOwnerModal: { open: false, saving: false },
84603
+ bulkOwnerModal: { open: false, saving: false, targetCustomerId: "" },
84341
84604
  columnWidths: {
84342
84605
  name: 180,
84343
84606
  label: 120,
@@ -84374,6 +84637,7 @@ function openUpsellModal(params) {
84374
84637
  lojasDeviceData: [],
84375
84638
  lojasDataLoading: false,
84376
84639
  lojasConfig: null,
84640
+ lojasApplyRelation: true,
84377
84641
  customModeModal: { open: false },
84378
84642
  bulkRelationModal: { open: false, target: "CUSTOMER", selectedAssetId: "", selectedAssetName: "", search: "", newAssetName: "", assetsLoaded: false, overrideCustomerId: "", overrideCustomerName: "", customerSearch: "", customerPickerOpen: false },
84379
84643
  checkFixLoading: false,
@@ -84538,6 +84802,12 @@ function renderModal4(container, state6, modalId, t, error) {
84538
84802
  font-size: 14px; font-weight: 500; font-family: 'Roboto', Arial, sans-serif;
84539
84803
  display: flex; align-items: center; gap: 6px;
84540
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>
84541
84811
  ` : ""}
84542
84812
  ${state6.currentStep === 3 && state6.lojasMode ? `
84543
84813
  <button id="${modalId}-lojas-sync" style="
@@ -84776,7 +85046,18 @@ function renderModal4(container, state6, modalId, t, error) {
84776
85046
  </div>
84777
85047
  ` : ""}
84778
85048
 
84779
- ${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 `
84780
85061
  <!-- Bulk Owner Modal -->
84781
85062
  <div class="myio-bulk-owner-overlay" style="
84782
85063
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
@@ -84802,17 +85083,25 @@ function renderModal4(container, state6, modalId, t, error) {
84802
85083
  <div style="font-size: 14px; color: ${colors2.text}; font-weight: 500;">${state6.selectedDevices.length} dispositivos</div>
84803
85084
  </div>
84804
85085
 
84805
- <div style="margin-bottom: 16px; padding: 12px; background: ${colors2.success}20; border-radius: 8px; border: 1px solid ${colors2.success}40;">
84806
- <div style="font-size: 12px; color: ${colors2.textMuted}; margin-bottom: 4px;">Novo Owner (Customer):</div>
84807
- <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>
84808
85097
  <div style="font-size: 11px; color: ${colors2.textMuted}; margin-top: 4px;">
84809
- ID: ${state6.selectedCustomer?.id?.id || "N/A"}
85098
+ ID: ${effId || "N/A"}${state6.customers.length === 0 ? " \xB7 carregando lista de clientes\u2026" : ""}
84810
85099
  </div>
84811
85100
  </div>
84812
85101
 
84813
85102
  <div style="margin-bottom: 16px; padding: 12px; background: ${colors2.warning}20; border-radius: 8px; border: 1px solid ${colors2.warning}40;">
84814
85103
  <div style="font-size: 12px; color: ${colors2.warning}; font-weight: 500;">
84815
- \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}".
84816
85105
  </div>
84817
85106
  </div>
84818
85107
 
@@ -84826,13 +85115,14 @@ function renderModal4(container, state6, modalId, t, error) {
84826
85115
  background: #10b981; color: white; border: none;
84827
85116
  padding: 10px 20px; border-radius: 6px; cursor: pointer;
84828
85117
  font-size: 14px; font-weight: 500;
84829
- " ${state6.bulkOwnerModal.saving || !state6.selectedCustomer ? "disabled" : ""}>
85118
+ " ${state6.bulkOwnerModal.saving || !effId ? "disabled" : ""}>
84830
85119
  ${state6.bulkOwnerModal.saving ? "Salvando..." : "Atribuir Owner para " + state6.selectedDevices.length + " devices"}
84831
85120
  </button>
84832
85121
  </div>
84833
85122
  </div>
84834
85123
  </div>
84835
- ` : ""}
85124
+ `;
85125
+ })() : ""}
84836
85126
 
84837
85127
  ${state6.customModeModal.open ? `
84838
85128
  <!-- CUSTOM Mode Picker Modal -->
@@ -85052,6 +85342,7 @@ function renderModal4(container, state6, modalId, t, error) {
85052
85342
  `;
85053
85343
  })() : ""}
85054
85344
  `;
85345
+ delete container.dataset.upsellListenersBound;
85055
85346
  setupEventListeners3(container, state6, modalId, t);
85056
85347
  }
85057
85348
  function renderStepIndicator(step, label, currentStep, colors2) {
@@ -85255,6 +85546,8 @@ function renderCheckFixRow(r, state6, modalId, colors2, dupPairIds, dupIngestion
85255
85546
  };
85256
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";
85257
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 ?? ""))}"`;
85258
85551
  return `
85259
85552
  <tr class="myio-list-item ${isSelected ? "selected" : ""}" data-device-id="${r.deviceId}"
85260
85553
  style="border-bottom:1px solid ${colors2.border}; cursor:pointer;">
@@ -85263,21 +85556,21 @@ function renderCheckFixRow(r, state6, modalId, colors2, dupPairIds, dupIngestion
85263
85556
  <input type="checkbox" class="myio-device-checkbox" data-device-id="${r.deviceId}"
85264
85557
  ${isSelectedMulti ? "checked" : ""} style="width:14px;height:14px;cursor:pointer;accent-color:${MYIO_PURPLE};"/>
85265
85558
  </td>` : ""}
85266
- <td style="${cell()} max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${r.deviceName}">${r.deviceName}</td>
85267
- <td style="${cell()} max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${colors2.textMuted};" title="${r.deviceLabel}">${r.deviceLabel || dash}</td>
85268
- <td style="${cell(typeActWrong ? "ok" : "none", true)}">${r.inferred.deviceType}</td>
85269
- <td style="${cell(typeActWrong ? r.typeEqualsProfile ? "warn" : "bad" : "none", true)}" title="${typeActWrong ? `esperado: ${r.inferred.deviceType}` : ""}">${r.actual.type || dash}</td>
85270
- <td style="${cell(devTypeWrong ? "ok" : "none", true)}">${r.inferred.deviceType}</td>
85271
- <td style="${cell(devTypeWrong ? "bad" : "none", true)}" title="${devTypeWrong ? `esperado: ${r.inferred.deviceType}` : ""}">${r.actual.deviceType || dash}</td>
85272
- <td style="${cell(devProfWrong ? "ok" : "none", true)}">${r.inferred.deviceProfile}</td>
85273
- <td style="${cell(devProfWrong ? "bad" : "none", true)}" title="${devProfWrong ? `esperado: ${r.inferred.deviceProfile}` : ""}">${r.actual.deviceProfile || dash}</td>
85274
- <td style="${cell()} white-space:nowrap; color:${colors2.textMuted}; font-size:9px;">${tsStr}</td>
85275
- <td style="${cell()} font-size:9px;">${valStr}</td>
85276
- <td style="${cell()} color:${connColor}; font-weight:600; white-space:nowrap;">${r.connStatus || dash}</td>
85277
- <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>
85278
- <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>
85279
- <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>
85280
- <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>
85281
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"};"
85282
85575
  ${r.status !== "ok" ? `data-cf-status="${r.status}" data-cf-device="${encodeURIComponent(r.deviceName)}" data-cf-detail="${encodeURIComponent(_buildCfStatusDetail(r))}"` : ""}>
85283
85576
  ${STATUS_ICON[r.status]} ${r.status}
@@ -85323,16 +85616,7 @@ function renderStep2(state6, modalId, colors2, t) {
85323
85616
  }
85324
85617
  return true;
85325
85618
  });
85326
- const searchFilteredDevices = searchTerm ? filteredDevices.filter((d) => {
85327
- const name = (d.name || "").toLowerCase();
85328
- const label = (d.label || "").toLowerCase();
85329
- const type = (d.type || "").toLowerCase();
85330
- const deviceType = (d.serverAttrs?.deviceType || "").toLowerCase();
85331
- const deviceProfile = (d.serverAttrs?.deviceProfile || "").toLowerCase();
85332
- const slaveId = String(d.serverAttrs?.slaveId ?? "").toLowerCase();
85333
- const status = (d.latestTelemetry?.connectionStatus?.value || "").toLowerCase();
85334
- return name.includes(searchTerm) || label.includes(searchTerm) || type.includes(searchTerm) || deviceType.includes(searchTerm) || deviceProfile.includes(searchTerm) || slaveId.includes(searchTerm) || status.includes(searchTerm);
85335
- }) : filteredDevices;
85619
+ const searchFilteredDevices = searchTerm ? filteredDevices.filter((d) => buildDeviceSearchHaystack(d, state6).includes(searchTerm)) : filteredDevices;
85336
85620
  const sortedDevices = sortDevices2(searchFilteredDevices, sortField, sortOrder);
85337
85621
  const gridHeight = state6.isMaximized ? "calc(100vh - 340px)" : "360px";
85338
85622
  const hasActiveFilters = filterTypes.length > 0 || filterDeviceTypes.length > 0 || filterDeviceProfiles.length > 0 || filterStatuses.length > 0 || filterTelemetryKeys.length > 0;
@@ -85800,6 +86084,95 @@ function renderStep2(state6, modalId, colors2, t) {
85800
86084
  })()}
85801
86085
  `;
85802
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
+ }
85803
86176
  function renderDeviceRow(device, state6, modalId, colors2) {
85804
86177
  const deviceId = getEntityId(device);
85805
86178
  const isSelectedSingle = state6.deviceSelectionMode === "single" && getEntityId(state6.selectedDevice) === deviceId;
@@ -85914,6 +86287,10 @@ function renderDeviceRow(device, state6, modalId, colors2) {
85914
86287
  " title="${statusTs}">(+)</span>` : ""}
85915
86288
  `;
85916
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"`;
85917
86294
  return `
85918
86295
  <div class="myio-list-item ${isSelected ? "selected" : ""}"
85919
86296
  data-device-id="${deviceId}" style="
@@ -85929,8 +86306,8 @@ function renderDeviceRow(device, state6, modalId, colors2) {
85929
86306
  " />
85930
86307
  </div>
85931
86308
  ` : ""}
85932
- <div style="width: 28px; font-size: 16px; flex-shrink: 0;">${getDeviceIcon3(device.type)}</div>
85933
- <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;">
85934
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}">
85935
86312
  ${device.name}
85936
86313
  </div>
@@ -85940,22 +86317,22 @@ function renderDeviceRow(device, state6, modalId, colors2) {
85940
86317
  display: flex; align-items: center; justify-content: center; border: 1px solid ${colors2.border};
85941
86318
  " title="Ver detalhes">\u24D8</span>
85942
86319
  </div>
85943
- <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;">
85944
86321
  <div style="font-size: 10px; color: ${colors2.textMuted}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${device.label ?? ""}">
85945
86322
  ${device.label ?? ""}
85946
86323
  </div>
85947
86324
  </div>
85948
- <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;">
85949
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%;
85950
86327
  background: ${device.type?.includes("HIDRO") ? "#dbeafe" : "#fef3c7"};
85951
86328
  color: ${device.type?.includes("HIDRO") ? "#1e40af" : "#92400e"};" title="${device.type || ""}">
85952
86329
  ${device.type || "\u2014"}
85953
86330
  </div>
85954
86331
  </div>
85955
- <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;">
85956
86333
  <span style="font-size: 9px; color: ${colors2.textMuted};">${createdTimeStr}</span>
85957
86334
  </div>
85958
- <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;">
85959
86336
  ${(() => {
85960
86337
  if (!state6.relationsLoaded) return `<span style="font-size: 8px; color: ${colors2.textMuted}; font-style: italic;">\u2014</span>`;
85961
86338
  const rels = state6.deviceRelToMap.get(deviceId) || [];
@@ -85965,7 +86342,7 @@ function renderDeviceRow(device, state6, modalId, colors2) {
85965
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>` : ""}`;
85966
86343
  })()}
85967
86344
  </div>
85968
- <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;">
85969
86346
  ${(() => {
85970
86347
  if (!state6.relationsLoaded) return `<span style="font-size: 8px; color: ${colors2.textMuted}; font-style: italic;">\u2014</span>`;
85971
86348
  const rels = state6.deviceRelFromMap.get(deviceId) || [];
@@ -85975,25 +86352,25 @@ function renderDeviceRow(device, state6, modalId, colors2) {
85975
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>` : ""}`;
85976
86353
  })()}
85977
86354
  </div>
85978
- <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;">
85979
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>`}
85980
86357
  </div>
85981
- <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;">
85982
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>`}
85983
86360
  </div>
85984
- <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;">
85985
86362
  ${renderDeviceTypeValue()}
85986
86363
  </div>
85987
- <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;">
85988
86365
  ${renderDeviceProfileValue()}
85989
86366
  </div>
85990
- <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;">
85991
86368
  ${renderTelemetryValue()}
85992
86369
  </div>
85993
- <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;">
85994
86371
  ${renderStatusValue()}
85995
86372
  </div>
85996
- <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">
85997
86374
  ${isSelected ? `<span style="color: ${colors2.success}; font-size: 14px;">\u2713</span>` : ""}
85998
86375
  </div>
85999
86376
  </div>
@@ -86375,7 +86752,11 @@ function renderLojasStep3(state6, modalId, colors2, t) {
86375
86752
  <span>Profile alvo: <strong style="color: ${colors2.text};">3F_MEDIDOR</strong></span>
86376
86753
  <span>deviceType: <strong style="color: ${colors2.text};">3F_MEDIDOR</strong></span>
86377
86754
  <span>deviceProfile: <strong style="color: ${colors2.text};">3F_MEDIDOR</strong></span>
86378
- <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>
86379
86760
  </div>
86380
86761
  </div>
86381
86762
  `;
@@ -86939,6 +87320,8 @@ async function openClearGcdrIdsModal(state6) {
86939
87320
  }
86940
87321
  }
86941
87322
  function setupEventListeners3(container, state6, modalId, t, onClose) {
87323
+ if (container.dataset.upsellListenersBound === "true") return;
87324
+ container.dataset.upsellListenersBound = "true";
86942
87325
  const closeHandler = () => closeModal(container, onClose);
86943
87326
  const overlay = container.querySelector(".myio-upsell-modal-overlay");
86944
87327
  if (overlay) {
@@ -87054,10 +87437,18 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87054
87437
  });
87055
87438
  });
87056
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
+ });
87057
87448
  document.getElementById(`${modalId}-device-search`)?.addEventListener("input", (e) => {
87058
87449
  const search = e.target.value.toLowerCase();
87059
87450
  state6.deviceSearchTerm = e.target.value;
87060
- filterDeviceListVisual(container, state6.devices, search, state6.deviceFilters, state6.deviceSort);
87451
+ filterDeviceListVisual(container, state6.devices, search, state6.deviceFilters, state6.deviceSort, state6);
87061
87452
  });
87062
87453
  document.getElementById(`${modalId}-device-type-filter`)?.addEventListener("change", (e) => {
87063
87454
  const select = e.target;
@@ -87279,15 +87670,17 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87279
87670
  renderModal4(container, state6, modalId, t);
87280
87671
  setupEventListeners3(container, state6, modalId, t, onClose);
87281
87672
  });
87282
- document.getElementById(`${modalId}-check-fix`)?.addEventListener("click", async () => {
87673
+ document.getElementById(`${modalId}-check-fix`)?.addEventListener("click", () => {
87283
87674
  if (state6.checkFixLoading) return;
87284
- state6.checkFixLoading = true;
87285
- renderModal4(container, state6, modalId, t);
87286
- setupEventListeners3(container, state6, modalId, t, onClose);
87287
- await runCheckFixRoutine(state6, container, modalId, t, onClose);
87288
- state6.checkFixLoading = false;
87289
- renderModal4(container, state6, modalId, t);
87290
- 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
+ });
87291
87684
  });
87292
87685
  document.getElementById(`${modalId}-checkfix-filter`)?.addEventListener("change", (e) => {
87293
87686
  state6.checkFixFilter = e.target.value;
@@ -87385,35 +87778,7 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87385
87778
  }
87386
87779
  });
87387
87780
  document.getElementById(`${modalId}-select-all`)?.addEventListener("click", () => {
87388
- const {
87389
- types: filterTypes,
87390
- deviceTypes: filterDeviceTypes,
87391
- deviceProfiles: filterDeviceProfiles,
87392
- statuses: filterStatuses,
87393
- telemetryKeys: filterTelemetryKeys
87394
- } = state6.deviceFilters;
87395
- let filteredDevices = state6.devices.filter((d) => {
87396
- if (filterTypes.length > 0 && !filterTypes.includes(d.type || "")) return false;
87397
- if (filterDeviceTypes.length > 0 && !filterDeviceTypes.includes(d.serverAttrs?.deviceType || ""))
87398
- return false;
87399
- if (filterDeviceProfiles.length > 0 && !filterDeviceProfiles.includes(d.serverAttrs?.deviceProfile || ""))
87400
- return false;
87401
- if (filterStatuses.length > 0) {
87402
- const status = d.latestTelemetry?.connectionStatus?.value || "offline";
87403
- if (!filterStatuses.includes(status)) return false;
87404
- }
87405
- if (filterTelemetryKeys.length > 0) {
87406
- const telem = d.latestTelemetry;
87407
- const hasMatch = filterTelemetryKeys.some((k) => {
87408
- if (k === "pulses") return telem?.pulses != null;
87409
- if (k === "consumption") return telem?.consumption != null;
87410
- return false;
87411
- });
87412
- if (!hasMatch) return false;
87413
- }
87414
- return true;
87415
- });
87416
- state6.selectedDevices = [...filteredDevices];
87781
+ state6.selectedDevices = [...getGridVisibleDevices(state6)];
87417
87782
  const listEl = document.getElementById(`${modalId}-device-list`);
87418
87783
  const savedScroll = listEl ? listEl.scrollTop : 0;
87419
87784
  renderModal4(container, state6, modalId, t);
@@ -87529,12 +87894,27 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87529
87894
  document.getElementById(`${modalId}-bulk-profile-save`)?.addEventListener("click", async () => {
87530
87895
  await saveBulkProfile(state6, container, modalId, t, onClose);
87531
87896
  });
87532
- document.getElementById(`${modalId}-bulk-owner`)?.addEventListener("click", () => {
87897
+ document.getElementById(`${modalId}-bulk-owner`)?.addEventListener("click", async () => {
87533
87898
  if (!state6.selectedCustomer) {
87534
87899
  alert("Selecione um Customer primeiro no Step 1");
87535
87900
  return;
87536
87901
  }
87537
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;
87538
87918
  renderModal4(container, state6, modalId, t);
87539
87919
  setupEventListeners3(container, state6, modalId, t, onClose);
87540
87920
  });
@@ -87579,6 +87959,10 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87579
87959
  if (!state6.selectedCustomer || state6.selectedDevices.length === 0) return;
87580
87960
  await handleBulkSyncIngestionId(state6, container, modalId, t, onClose);
87581
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
+ });
87582
87966
  document.getElementById(`${modalId}-clear-gcdr-ids`)?.addEventListener("click", () => {
87583
87967
  if (!state6.selectedCustomer) {
87584
87968
  alert("Selecione um Customer primeiro no Step 1");
@@ -87708,6 +88092,14 @@ function setupEventListeners3(container, state6, modalId, t, onClose) {
87708
88092
  });
87709
88093
  }
87710
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
+ }
87711
88103
  }
87712
88104
  document.getElementById(`${modalId}-change-owner`)?.addEventListener("click", () => {
87713
88105
  const form = document.getElementById(`${modalId}-change-owner-form`);
@@ -87929,7 +88321,8 @@ Total de devices no customer: ${ingestionDevices.length}`
87929
88321
  state6.devices,
87930
88322
  state6.deviceSearchTerm.toLowerCase(),
87931
88323
  state6.deviceFilters,
87932
- state6.deviceSort
88324
+ state6.deviceSort,
88325
+ state6
87933
88326
  );
87934
88327
  }
87935
88328
  }
@@ -88069,7 +88462,7 @@ function filterCustomerList(container, customers, search, selected, sort) {
88069
88462
  item.style.display = matches ? "flex" : "none";
88070
88463
  });
88071
88464
  }
88072
- function filterDeviceListVisual(container, devices, search, filters, sort) {
88465
+ function filterDeviceListVisual(container, devices, search, filters, sort, state6) {
88073
88466
  const listContainer = container.querySelector('[id$="-device-list"]');
88074
88467
  if (!listContainer) return;
88075
88468
  let filtered = devices.filter((d) => {
@@ -88089,7 +88482,7 @@ function filterDeviceListVisual(container, devices, search, filters, sort) {
88089
88482
  item.style.display = "none";
88090
88483
  return;
88091
88484
  }
88092
- 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);
88093
88486
  const el2 = item;
88094
88487
  const isTableRow = el2.tagName === "TR";
88095
88488
  el2.style.display = matchesSearch ? isTableRow ? "" : "flex" : "none";
@@ -88440,6 +88833,56 @@ async function loadLojasData(state6, container, modalId, t, onClose) {
88440
88833
  setupEventListeners3(container, state6, modalId, t, onClose);
88441
88834
  }
88442
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
+ }
88443
88886
  async function handleBulkSyncIngestionId(state6, container, modalId, t, onClose) {
88444
88887
  if (!state6.selectedCustomer || state6.selectedDevices.length === 0) return;
88445
88888
  const customerId = getEntityId(state6.selectedCustomer);
@@ -88586,15 +89029,17 @@ async function handleLojasApply(state6, container, modalId, t, onClose) {
88586
89029
  if (identifierInput) data[i].identifier = identifierInput.value;
88587
89030
  }
88588
89031
  const activeConfig = state6.lojasConfig ?? CUSTOM_MODES[0];
89032
+ const applyRelation = state6.lojasApplyRelation;
88589
89033
  const confirmMsg = `Aplicar configura\xE7\xE3o "${activeConfig.label}" para ${data.length} dispositivos?
88590
89034
 
88591
89035
  Cada device receber\xE1:
88592
89036
  - Label atualizado (etiqueta)
88593
89037
  - Profile: ${activeConfig.deviceProfile}
88594
89038
  - deviceType/deviceProfile: ${activeConfig.deviceType} / ${activeConfig.deviceProfile}
88595
- - Rela\xE7\xF5es existentes removidas
89039
+ ` + (applyRelation ? `- Rela\xE7\xF5es existentes removidas
88596
89040
  - Nova rela\xE7\xE3o: Customer \u2192 Device (Contains)
88597
-
89041
+ ` : `- Rela\xE7\xF5es N\xC3O ser\xE3o alteradas (checkbox de rela\xE7\xE3o desmarcado)
89042
+ `) + `
88598
89043
  Deseja continuar?`;
88599
89044
  if (!confirm(confirmMsg)) return;
88600
89045
  showBusyProgress(`Aplicando ${activeConfig.label}...`, data.length);
@@ -88622,31 +89067,33 @@ Deseja continuar?`;
88622
89067
  attrs.ingestionId = d.ingestionId;
88623
89068
  }
88624
89069
  await tbPost(state6, `/api/plugins/telemetry/DEVICE/${d.deviceId}/attributes/SERVER_SCOPE`, attrs);
88625
- if (d.currentRelations.length > 0) {
88626
- updateBusyProgress(i + 1, `[${i + 1}/${data.length}] ${d.name}: Removendo rela\xE7\xF5es...`);
88627
- for (const rel of d.currentRelations) {
88628
- try {
88629
- const params = new URLSearchParams({
88630
- fromId: rel.from.id,
88631
- fromType: rel.from.entityType,
88632
- toId: d.deviceId,
88633
- toType: "DEVICE",
88634
- relationType: rel.type || "Contains",
88635
- relationTypeGroup: rel.typeGroup || "COMMON"
88636
- });
88637
- await tbDelete(state6, `/api/relation?${params.toString()}`);
88638
- } catch (e) {
88639
- 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
+ }
88640
89087
  }
88641
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
+ });
88642
89096
  }
88643
- updateBusyProgress(i + 1, `[${i + 1}/${data.length}] ${d.name}: Criando rela\xE7\xE3o...`);
88644
- await tbPost(state6, "/api/relation", {
88645
- from: { entityType: "CUSTOMER", id: customerId },
88646
- to: { entityType: "DEVICE", id: d.deviceId },
88647
- type: "Contains",
88648
- typeGroup: "COMMON"
88649
- });
88650
89097
  successCount++;
88651
89098
  } catch (error) {
88652
89099
  errorCount++;
@@ -88934,8 +89381,111 @@ async function loadDeviceTelemetryInBatch(state6, container, modalId, t, onClose
88934
89381
  hideBusyProgress();
88935
89382
  }
88936
89383
  }
88937
- async function runCheckFixRoutine(state6, container, modalId, t, onClose) {
88938
- 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;
88939
89489
  if (devices.length === 0) return;
88940
89490
  const BATCH_SIZE = 5;
88941
89491
  const BATCH_DELAY_MS = 1500;
@@ -89218,10 +89768,11 @@ ${errors.slice(0, 5).join("\n")}` + (errors.length > 5 ? `
89218
89768
  }
89219
89769
  async function saveBulkOwner(state6, container, modalId, t, onClose) {
89220
89770
  const devices = state6.selectedDevices;
89221
- const newCustomerId = state6.selectedCustomer?.id?.id;
89222
- 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";
89223
89774
  if (!newCustomerId) {
89224
- alert("Por favor, selecione um Customer no Step 1 primeiro.");
89775
+ alert("Por favor, selecione um Customer de destino.");
89225
89776
  return;
89226
89777
  }
89227
89778
  if (devices.length === 0) {