myio-js-library 0.1.508 → 0.1.510

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
@@ -563,9 +563,12 @@ __export(index_exports, {
563
563
  ALARM_STATE_CONFIG: () => STATE_CONFIG,
564
564
  AMBIENTE_GROUP_CSS_PREFIX: () => AMBIENTE_GROUP_CSS_PREFIX,
565
565
  AMBIENTE_MODAL_CSS_PREFIX: () => AMBIENTE_MODAL_CSS_PREFIX,
566
+ ANNOTATION_CSV_COLUMNS: () => CSV_COLUMNS,
567
+ ANNOTATION_SORT_OPTIONS: () => SORT_OPTIONS,
566
568
  ANNOTATION_TYPE_COLORS: () => ANNOTATION_TYPE_COLORS,
567
569
  ANNOTATION_TYPE_LABELS: () => ANNOTATION_TYPE_LABELS,
568
570
  ANNOTATION_TYPE_LABELS_EN: () => ANNOTATION_TYPE_LABELS_EN,
571
+ ANNOTATION_VIRTUAL_THRESHOLD: () => VIRTUAL_SCROLL_THRESHOLD,
569
572
  ActionButtonController: () => ActionButtonController,
570
573
  ActionButtonView: () => ActionButtonView,
571
574
  AlarmService: () => AlarmService,
@@ -588,6 +591,7 @@ __export(index_exports, {
588
591
  ContractSummaryTooltip: () => ContractSummaryTooltip,
589
592
  CustomerCardV1: () => CustomerCardV1,
590
593
  CustomerCardV2: () => CustomerCardV2,
594
+ CustomerDeviceService: () => CustomerDeviceService,
591
595
  DAY_LABELS: () => DAY_LABELS,
592
596
  DAY_LABELS_FULL: () => DAY_LABELS_FULL,
593
597
  DECIMAL_OPTIONS: () => DECIMAL_OPTIONS,
@@ -595,6 +599,7 @@ __export(index_exports, {
595
599
  DEFAULT_ALARM_FILTERS: () => DEFAULT_ALARM_FILTERS,
596
600
  DEFAULT_ALARM_FILTER_TABS: () => DEFAULT_ALARM_FILTER_TABS,
597
601
  DEFAULT_ALARM_STATS: () => DEFAULT_ALARM_STATS,
602
+ DEFAULT_ANNOTATION_SORT: () => DEFAULT_SORT,
598
603
  DEFAULT_BAS_SETTINGS: () => DEFAULT_BAS_SETTINGS,
599
604
  DEFAULT_CLAMP_RANGE: () => DEFAULT_CLAMP_RANGE,
600
605
  DEFAULT_DASHBOARD_KPIS: () => DEFAULT_DASHBOARD_KPIS,
@@ -692,6 +697,7 @@ __export(index_exports, {
692
697
  HEADER_STYLE_DEFAULT: () => HEADER_STYLE_DEFAULT,
693
698
  HEADER_STYLE_PREMIUM_GREEN: () => HEADER_STYLE_PREMIUM_GREEN,
694
699
  HEADER_STYLE_SLIM: () => HEADER_STYLE_SLIM,
700
+ HeaderAnnotationsPanel: () => HeaderAnnotationsPanel,
695
701
  HeaderDevicesGridController: () => HeaderDevicesGridController,
696
702
  HeaderDevicesGridView: () => HeaderDevicesGridView,
697
703
  HeaderFilterModal: () => HeaderFilterModal,
@@ -790,6 +796,7 @@ __export(index_exports, {
790
796
  TempRangeTooltip: () => TempRangeTooltip,
791
797
  TempSensorSummaryTooltip: () => TempSensorSummaryTooltip,
792
798
  UsersSummaryTooltip: () => UsersSummaryTooltip,
799
+ VirtualList: () => VirtualList,
793
800
  WAITING_STATUSES: () => WAITING_STATUSES,
794
801
  WATER_DEVICE_CATEGORIES: () => WATER_DEVICE_CATEGORIES,
795
802
  WATER_SORT_OPTIONS: () => WATER_SORT_OPTIONS,
@@ -806,6 +813,9 @@ __export(index_exports, {
806
813
  assignShoppingColors: () => assignShoppingColors,
807
814
  averageByDay: () => averageByDay,
808
815
  buildAmbienteGroupData: () => buildAmbienteGroupData,
816
+ buildAnnotationServiceOrchestrator: () => buildAnnotationServiceOrchestrator,
817
+ buildAnnotationsCsv: () => buildAnnotationsCsv,
818
+ buildAnnotationsExportFilename: () => buildExportFilename,
809
819
  buildDeviceGridEntityObject: () => buildEntityObject,
810
820
  buildEquipmentCategoryDataForTooltip: () => buildEquipmentCategoryDataForTooltip,
811
821
  buildEquipmentCategorySummary: () => buildEquipmentCategorySummary,
@@ -840,6 +850,7 @@ __export(index_exports, {
840
850
  classifyWaterLabels: () => classifyWaterLabels,
841
851
  clearAllAuthCaches: () => clearAllAuthCaches,
842
852
  clearFreshdeskTicketsOnTB: () => clearFreshdeskTicketsOnTB,
853
+ closeAnnotationsExportModal: () => closeExportModal,
843
854
  connectionStatusIcons: () => connectionStatusIcons,
844
855
  createActionButton: () => createActionButton,
845
856
  createAlarmCardElement: () => createAlarmCardElement,
@@ -856,6 +867,7 @@ __export(index_exports, {
856
867
  createCustomerCardV2: () => createCustomerCardV2,
857
868
  createDateRangePicker: () => createDateRangePicker2,
858
869
  createDaysGrid: () => createDaysGrid,
870
+ createDefaultAnnotationFilter: () => createDefaultFilter,
859
871
  createDeviceGridBusyModal: () => createBusyModal,
860
872
  createDeviceGridState: () => createState,
861
873
  createDeviceGridV6: () => createDeviceGridV6,
@@ -904,6 +916,7 @@ __export(index_exports, {
904
916
  createToggleSwitch: () => createToggleSwitch,
905
917
  createWaterPanelComponent: () => createWaterPanelComponent,
906
918
  createWidgetController: () => createWidgetController,
919
+ csvEscapeAnnotation: () => csvEscape,
907
920
  decodePayload: () => decodePayload,
908
921
  decodePayloadBase64Xor: () => decodePayloadBase64Xor,
909
922
  deleteFreshdeskTicket: () => deleteTicket,
@@ -915,6 +928,10 @@ __export(index_exports, {
915
928
  determineInterval: () => determineInterval,
916
929
  deviceStatusIcons: () => deviceStatusIcons,
917
930
  doSchedulesOverlap: () => doSchedulesOverlap,
931
+ downloadAnnotationsTextFile: () => downloadTextFile,
932
+ escapeAnnotationHtml: () => escapeHtml6,
933
+ exportAnnotationsCsv: () => exportAnnotationsCsv,
934
+ exportAnnotationsPdf: () => exportAnnotationsPdf,
918
935
  exportGridCsv: () => exportGridCsv,
919
936
  exportGridPdf: () => exportGridPdf,
920
937
  exportGridXls: () => exportGridXls,
@@ -939,6 +956,7 @@ __export(index_exports, {
939
956
  formatAlarmRelativeTime: () => formatAlarmRelativeTime,
940
957
  formatAllInSameUnit: () => formatAllInSameUnit,
941
958
  formatAllInSameWaterUnit: () => formatAllInSameWaterUnit,
959
+ formatAnnotationRelativeTime: () => formatRelative,
942
960
  formatDashboardPercentage: () => formatPercentage4,
943
961
  formatDateForInput: () => formatDateForInput,
944
962
  formatDateToYMD: () => formatDateToYMD,
@@ -987,6 +1005,7 @@ __export(index_exports, {
987
1005
  getFirstDayOfMonthFor: () => getFirstDayOfMonthFor,
988
1006
  getGroupColor: () => getGroupColor,
989
1007
  getHashColor: () => getHashColor,
1008
+ getHeaderAnnotationsPanel: () => getHeaderAnnotationsPanel,
990
1009
  getImageByConsumption: () => getImageByConsumption,
991
1010
  getLastDayOfMonth: () => getLastDayOfMonth,
992
1011
  getModalHeaderStyles: () => getModalHeaderStyles,
@@ -1010,6 +1029,7 @@ __export(index_exports, {
1010
1029
  groupByDay: () => groupByDay,
1011
1030
  handleDeviceType: () => handleDeviceType,
1012
1031
  hasSelectedDays: () => hasSelectedDays,
1032
+ highlightAnnotationMatches: () => highlightMatches,
1013
1033
  initMyIOAuthContext: () => initMyIOAuthContext,
1014
1034
  initOnOffTimelineTooltips: () => initOnOffTimelineTooltips,
1015
1035
  injectActionButtonStyles: () => injectActionButtonStyles,
@@ -1023,6 +1043,7 @@ __export(index_exports, {
1023
1043
  injectDeviceOperationalCardGridStyles: () => injectDeviceOperationalCardGridStyles,
1024
1044
  injectDeviceOperationalCardStyles: () => injectDeviceOperationalCardStyles,
1025
1045
  injectFancoilRemoteStyles: () => injectFancoilRemoteStyles,
1046
+ injectHeaderAnnotationsStyles: () => injectStylesOnce,
1026
1047
  injectHeaderDevicesGridStyles: () => injectHeaderDevicesGridStyles,
1027
1048
  injectHeaderShoppingStyles: () => injectHeaderShoppingStyles,
1028
1049
  injectMenuShoppingStyles: () => injectMenuShoppingStyles,
@@ -1041,6 +1062,7 @@ __export(index_exports, {
1041
1062
  injectSwitchControlStyles: () => injectSwitchControlStyles,
1042
1063
  interpolateTemperature: () => interpolateTemperature,
1043
1064
  isAlarmActive: () => isAlarmActive,
1065
+ isAnnotationOverdue: () => isOverdue,
1044
1066
  isConnectionStale: () => isConnectionStale,
1045
1067
  isDeviceOffline: () => isDeviceOffline,
1046
1068
  isEndAfterStart: () => isEndAfterStart,
@@ -1063,6 +1085,7 @@ __export(index_exports, {
1063
1085
  mapDeviceStatusToCardStatus: () => mapDeviceStatusToCardStatus,
1064
1086
  mapDeviceToConnectionStatus: () => mapDeviceToConnectionStatus,
1065
1087
  myioExportData: () => myioExportData,
1088
+ nfdNormalizeAnnotationSearch: () => nfdNormalize,
1066
1089
  normalizeConnectionStatus: () => normalizeConnectionStatus,
1067
1090
  normalizeRecipients: () => normalizeRecipients,
1068
1091
  numbers: () => numbers_exports,
@@ -1071,6 +1094,7 @@ __export(index_exports, {
1071
1094
  openAlarmDetailsModal: () => openAlarmDetailsModal,
1072
1095
  openAmbienteDetailModal: () => openAmbienteDetailModal,
1073
1096
  openAmbienteGroupModal: () => openAmbienteGroupModal,
1097
+ openAnnotationsExportModal: () => openExportModal,
1074
1098
  openContractDevicesModal: () => openContractDevicesModal,
1075
1099
  openDashboardPopup: () => openDashboardPopup,
1076
1100
  openDashboardPopupAllReport: () => openDashboardPopupAllReport,
@@ -1097,6 +1121,7 @@ __export(index_exports, {
1097
1121
  openUserManagementModal: () => openUserManagementModal,
1098
1122
  openWelcomeModal: () => openWelcomeModal,
1099
1123
  parseInputDateToDate: () => parseInputDateToDate,
1124
+ parseLogAnnotations: () => parseLogAnnotations,
1100
1125
  periodKey: () => periodKey,
1101
1126
  readFreshdeskTicketsFromTB: () => readFreshdeskTicketsFromTB,
1102
1127
  recalculateDeviceStatus: () => recalculateDeviceStatus,
@@ -1110,6 +1135,7 @@ __export(index_exports, {
1110
1135
  removeOperationalHeaderDevicesGridStyles: () => removeOperationalHeaderDevicesGridStyles,
1111
1136
  removeSchedulingSharedStyles: () => removeSchedulingSharedStyles,
1112
1137
  renderAlarmCard: () => renderAlarmCard,
1138
+ renderAnnotationItemCard: () => renderAnnotationItemCard,
1113
1139
  renderCardAmbienteV6: () => renderCardAmbienteV6,
1114
1140
  renderCardComponent: () => renderCardComponent,
1115
1141
  renderCardComponentEnhanced: () => renderCardComponent2,
@@ -1140,6 +1166,8 @@ __export(index_exports, {
1140
1166
  schedShowConfirmModal: () => showConfirmModal,
1141
1167
  schedShowNotificationModal: () => showNotificationModal,
1142
1168
  shouldFlashIcon: () => shouldFlashIcon,
1169
+ shouldVirtualizeAnnotationList: () => shouldVirtualize,
1170
+ sortAnnotationGroups: () => sortGroups,
1143
1171
  sortDeviceGridDevices: () => sortDevices,
1144
1172
  strings: () => strings_exports,
1145
1173
  telemetryInfoFormatEnergy: () => formatEnergy2,
@@ -1151,6 +1179,7 @@ __export(index_exports, {
1151
1179
  toCSV: () => toCSV,
1152
1180
  toFixedSafe: () => toFixedSafe,
1153
1181
  toFreshdeskTicketSummary: () => toSummary,
1182
+ truncateAnnotationText: () => truncate2,
1154
1183
  updateDeviceGridStats: () => updateStats,
1155
1184
  updateFreshdeskTicket: () => updateTicket,
1156
1185
  upsertAlarmAnnotation: () => upsertAlarmAnnotation,
@@ -1164,7 +1193,7 @@ module.exports = __toCommonJS(index_exports);
1164
1193
  // package.json
1165
1194
  var package_default = {
1166
1195
  name: "myio-js-library",
1167
- version: "0.1.508",
1196
+ version: "0.1.510",
1168
1197
  description: "A clean, standalone JS SDK for MYIO projects",
1169
1198
  license: "MIT",
1170
1199
  repository: "github:gh-myio/myio-js-library",
@@ -16574,16 +16603,18 @@ function hideWithAnimation3() {
16574
16603
  }
16575
16604
  function positionTooltip3(container, triggerElement) {
16576
16605
  const rect = triggerElement.getBoundingClientRect();
16606
+ const margin = 12;
16607
+ const vw = window.innerWidth;
16608
+ const vh = window.innerHeight;
16609
+ const cw = container.offsetWidth || 450;
16610
+ const ch = container.offsetHeight || 420;
16577
16611
  let left = rect.left;
16612
+ if (left + cw > vw - margin) left = vw - cw - margin;
16613
+ if (left < margin) left = margin;
16578
16614
  let top = rect.bottom + 8;
16579
- const tooltipWidth = 380;
16580
- if (left + tooltipWidth > window.innerWidth - 20) {
16581
- left = window.innerWidth - tooltipWidth - 20;
16582
- }
16583
- if (left < 10) left = 10;
16584
- if (top + 400 > window.innerHeight) {
16585
- top = rect.top - 8 - 400;
16586
- if (top < 10) top = 10;
16615
+ if (top + ch > vh - margin) {
16616
+ const above = rect.top - 8 - ch;
16617
+ top = above >= margin ? above : Math.max(margin, vh - ch - margin);
16587
16618
  }
16588
16619
  container.style.left = left + "px";
16589
16620
  container.style.top = top + "px";
@@ -49868,6 +49899,11 @@ var AnnotationsTab = class {
49868
49899
  return false;
49869
49900
  }
49870
49901
  this.onAnnotationChange?.(this.annotations);
49902
+ window.dispatchEvent(
49903
+ new CustomEvent("myio:annotation-changed", {
49904
+ detail: { deviceId: this.deviceId, action: "save", timestamp: Date.now() }
49905
+ })
49906
+ );
49871
49907
  return true;
49872
49908
  } catch (error) {
49873
49909
  console.error("[AnnotationsTab] Error saving annotations:", error);
@@ -66462,7 +66498,7 @@ async function createInputDateRangePickerInsideDIV(params) {
66462
66498
  placeholder = "Clique para selecionar per\xEDodo",
66463
66499
  pickerOptions = {},
66464
66500
  classNames = {},
66465
- injectStyles: injectStyles15 = true,
66501
+ injectStyles: injectStyles16 = true,
66466
66502
  showHelper = true
66467
66503
  } = params;
66468
66504
  validateId(containerId, "containerId");
@@ -66471,7 +66507,7 @@ async function createInputDateRangePickerInsideDIV(params) {
66471
66507
  if (!container) {
66472
66508
  throw new Error(`[createInputDateRangePickerInsideDIV] Container '#${containerId}' not found`);
66473
66509
  }
66474
- if (injectStyles15) {
66510
+ if (injectStyles16) {
66475
66511
  injectPremiumStyles();
66476
66512
  }
66477
66513
  let inputEl = document.getElementById(inputId);
@@ -66670,7 +66706,7 @@ function openGoalsPanel(params) {
66670
66706
  modal.setAttribute("aria-labelledby", "goals-modal-title");
66671
66707
  modal.innerHTML = generateModalHTML();
66672
66708
  document.body.appendChild(modal);
66673
- injectStyles15();
66709
+ injectStyles16();
66674
66710
  attachEventListeners();
66675
66711
  trapFocus(modal);
66676
66712
  renderTabContent();
@@ -67432,7 +67468,7 @@ function openGoalsPanel(params) {
67432
67468
  maximumFractionDigits: 2
67433
67469
  }).format(value);
67434
67470
  }
67435
- function injectStyles15() {
67471
+ function injectStyles16() {
67436
67472
  const styleId = "myio-goals-panel-styles";
67437
67473
  if (document.getElementById(styleId)) return;
67438
67474
  const style = document.createElement("style");
@@ -80626,7 +80662,7 @@ function createConsumptionChartWidget(config) {
80626
80662
  </div>
80627
80663
  `;
80628
80664
  }
80629
- function injectStyles15() {
80665
+ function injectStyles16() {
80630
80666
  if (styleElement) return;
80631
80667
  styleElement = document.createElement("style");
80632
80668
  styleElement.id = `${widgetId}-styles`;
@@ -81120,7 +81156,7 @@ function createConsumptionChartWidget(config) {
81120
81156
  console.error(`[ConsumptionWidget] Container #${config.containerId} not found`);
81121
81157
  return;
81122
81158
  }
81123
- injectStyles15();
81159
+ injectStyles16();
81124
81160
  containerElement.innerHTML = renderHTML();
81125
81161
  setupListeners();
81126
81162
  setLoading(true);
@@ -120215,7 +120251,7 @@ var AlarmsNotificationsPanelView = class {
120215
120251
  const weightSum = colWeights.reduce((s, w) => s + w, 0);
120216
120252
  const colWidths = colWeights.map((w) => w / weightSum * TABLE_W);
120217
120253
  const colX = (ci) => MARGIN + colWidths.slice(0, ci).reduce((s, w) => s + w, 0);
120218
- const truncate2 = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
120254
+ const truncate3 = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
120219
120255
  const drawHeader2 = (pageNo2) => {
120220
120256
  doc.setFillColor(62, 26, 125);
120221
120257
  doc.rect(0, 0, PW, HDR_H, "F");
@@ -120257,7 +120293,7 @@ var AlarmsNotificationsPanelView = class {
120257
120293
  cells.forEach((cell, ci) => {
120258
120294
  const x = colX(ci) + 1.5;
120259
120295
  const maxChars = Math.floor(colWidths[ci] / 1.8);
120260
- doc.text(truncate2(String(cell ?? ""), maxChars), x, y + ROW_H / 2 + 2.2);
120296
+ doc.text(truncate3(String(cell ?? ""), maxChars), x, y + ROW_H / 2 + 2.2);
120261
120297
  });
120262
120298
  doc.setDrawColor(230, 228, 240);
120263
120299
  doc.setLineWidth(0.1);
@@ -141943,6 +141979,2543 @@ function _buildMap(tickets) {
141943
141979
  return map;
141944
141980
  }
141945
141981
 
141982
+ // src/services/annotations/CustomerDeviceService.ts
141983
+ var CustomerDeviceService = class {
141984
+ customerId;
141985
+ tbHost;
141986
+ jwt;
141987
+ pageSize;
141988
+ concurrency;
141989
+ chunkDelayMs;
141990
+ maxRetries;
141991
+ logger;
141992
+ constructor(cfg) {
141993
+ this.customerId = cfg.customerId;
141994
+ this.tbHost = cfg.tbHost.replace(/\/+$/, "");
141995
+ this.jwt = cfg.jwt;
141996
+ this.pageSize = cfg.pageSize ?? 1e3;
141997
+ this.concurrency = cfg.concurrency ?? 5;
141998
+ this.chunkDelayMs = cfg.chunkDelayMs ?? 50;
141999
+ this.maxRetries = cfg.maxRetries ?? 3;
142000
+ const raw = cfg.logger ?? console;
142001
+ const noop = () => {
142002
+ };
142003
+ const log = raw.log || noop;
142004
+ this.logger = {
142005
+ debug: raw.debug || log,
142006
+ info: raw.info || log,
142007
+ warn: raw.warn || log,
142008
+ error: raw.error || log
142009
+ };
142010
+ }
142011
+ get headers() {
142012
+ return {
142013
+ "Content-Type": "application/json",
142014
+ "X-Authorization": `Bearer ${this.jwt}`
142015
+ };
142016
+ }
142017
+ /**
142018
+ * Fetches ALL devices of the customer using TB's paginated `deviceInfos`
142019
+ * endpoint. Hard limit: 10 pages (i.e. 10× pageSize devices) — beyond that
142020
+ * we log a warning and stop, to protect the dashboard from runaway fetches.
142021
+ *
142022
+ * AC-9
142023
+ */
142024
+ async fetchAllCustomerDevices() {
142025
+ const HARD_PAGE_CAP = 10;
142026
+ const out = [];
142027
+ for (let page = 0; page < HARD_PAGE_CAP; page++) {
142028
+ const url = `${this.tbHost}/api/customer/${encodeURIComponent(this.customerId)}/deviceInfos?pageSize=${this.pageSize}&page=${page}`;
142029
+ const data = await this._requestJson(url);
142030
+ const flat = (data?.data ?? []).map(this._flatten);
142031
+ out.push(...flat);
142032
+ if (!data?.hasNext) break;
142033
+ if (page === HARD_PAGE_CAP - 1) {
142034
+ this.logger.warn(
142035
+ `[CustomerDeviceService] hit hard page cap of ${HARD_PAGE_CAP} for customer ${this.customerId}; some devices may be missing.`
142036
+ );
142037
+ }
142038
+ }
142039
+ this.logger.debug(
142040
+ `[CustomerDeviceService] fetched ${out.length} devices for customer ${this.customerId}`
142041
+ );
142042
+ return out;
142043
+ }
142044
+ /**
142045
+ * Fetches SERVER_SCOPE attributes for a batch of devices. Splits into chunks
142046
+ * of `concurrency` (default 5), sequenced with a 50ms delay between chunks.
142047
+ * Returns a Map<deviceId, Record<key, value>> of attribute values.
142048
+ *
142049
+ * Failures per device are logged and skipped — the rest still returns.
142050
+ *
142051
+ * AC-10
142052
+ */
142053
+ async fetchAttributesBatch(deviceIds, keys) {
142054
+ const result = /* @__PURE__ */ new Map();
142055
+ if (deviceIds.length === 0 || keys.length === 0) return result;
142056
+ const keysParam = keys.map(encodeURIComponent).join(",");
142057
+ for (let i = 0; i < deviceIds.length; i += this.concurrency) {
142058
+ const slice = deviceIds.slice(i, i + this.concurrency);
142059
+ const settled = await Promise.allSettled(
142060
+ slice.map(
142061
+ (deviceId) => this._fetchAttributesForDevice(deviceId, keysParam).then(
142062
+ (attrs) => ({ deviceId, attrs })
142063
+ )
142064
+ )
142065
+ );
142066
+ for (const r of settled) {
142067
+ if (r.status === "fulfilled") {
142068
+ result.set(r.value.deviceId, r.value.attrs);
142069
+ } else {
142070
+ this.logger.warn(
142071
+ "[CustomerDeviceService] attribute fetch failed for one device:",
142072
+ r.reason
142073
+ );
142074
+ }
142075
+ }
142076
+ if (i + this.concurrency < deviceIds.length) {
142077
+ await _sleep(this.chunkDelayMs);
142078
+ }
142079
+ }
142080
+ return result;
142081
+ }
142082
+ // ── Internals ────────────────────────────────────────────────────────────
142083
+ _flatten(d) {
142084
+ const id = typeof d.id === "object" && d.id?.id ? d.id.id : String(d.id ?? "");
142085
+ return {
142086
+ id,
142087
+ name: d.name ?? "",
142088
+ label: d.label ?? d.name ?? "",
142089
+ deviceType: d.type ?? d.deviceProfileName ?? ""
142090
+ };
142091
+ }
142092
+ async _fetchAttributesForDevice(deviceId, keysParam) {
142093
+ const url = `${this.tbHost}/api/plugins/telemetry/DEVICE/${encodeURIComponent(deviceId)}/values/attributes/SERVER_SCOPE?keys=${keysParam}`;
142094
+ const list = await this._requestJson(url);
142095
+ const flat = {};
142096
+ for (const kv of list ?? []) {
142097
+ if (kv && typeof kv.key === "string") flat[kv.key] = kv.value;
142098
+ }
142099
+ return flat;
142100
+ }
142101
+ async _requestJson(url) {
142102
+ let attempt = 0;
142103
+ while (true) {
142104
+ try {
142105
+ const res = await fetch(url, { method: "GET", headers: this.headers });
142106
+ if (res.status === 429 || res.status >= 500) {
142107
+ if (attempt < this.maxRetries) {
142108
+ const backoffMs = Math.pow(2, attempt) * 1e3;
142109
+ this.logger.warn(
142110
+ `[CustomerDeviceService] ${res.status} on ${url} \u2014 retry ${attempt + 1}/${this.maxRetries} in ${backoffMs}ms`
142111
+ );
142112
+ attempt++;
142113
+ await _sleep(backoffMs);
142114
+ continue;
142115
+ }
142116
+ }
142117
+ if (!res.ok) {
142118
+ throw new Error(`HTTP ${res.status} on ${url}`);
142119
+ }
142120
+ return await res.json();
142121
+ } catch (err) {
142122
+ if (attempt < this.maxRetries && _isNetworkError(err)) {
142123
+ const backoffMs = Math.pow(2, attempt) * 1e3;
142124
+ this.logger.warn(
142125
+ `[CustomerDeviceService] network error on ${url} \u2014 retry ${attempt + 1}/${this.maxRetries} in ${backoffMs}ms`
142126
+ );
142127
+ attempt++;
142128
+ await _sleep(backoffMs);
142129
+ continue;
142130
+ }
142131
+ throw err;
142132
+ }
142133
+ }
142134
+ }
142135
+ };
142136
+ function _sleep(ms) {
142137
+ return new Promise((resolve) => setTimeout(resolve, ms));
142138
+ }
142139
+ function _isNetworkError(err) {
142140
+ if (!err || typeof err !== "object") return false;
142141
+ const name = err.name ?? "";
142142
+ const message = String(err.message ?? "").toLowerCase();
142143
+ return name === "TypeError" || message.includes("network") || message.includes("failed to fetch");
142144
+ }
142145
+
142146
+ // src/services/annotations/parseLogAnnotations.ts
142147
+ function parseLogAnnotations(raw, ctx, logger = console) {
142148
+ if (raw == null || raw === "") return [];
142149
+ let value = raw;
142150
+ if (typeof value === "string") {
142151
+ try {
142152
+ value = JSON.parse(value);
142153
+ } catch (err) {
142154
+ logger.warn(
142155
+ `[parseLogAnnotations] JSON parse failed${ctx ? ` (${ctx})` : ""}:`,
142156
+ err?.message ?? err
142157
+ );
142158
+ return [];
142159
+ }
142160
+ }
142161
+ if (Array.isArray(value)) {
142162
+ return _filterArray(value, ctx, logger);
142163
+ }
142164
+ if (value !== null && typeof value === "object") {
142165
+ const obj = value;
142166
+ if (Array.isArray(obj.annotations)) {
142167
+ return _filterArray(obj.annotations, ctx, logger);
142168
+ }
142169
+ }
142170
+ logger.warn(
142171
+ `[parseLogAnnotations] Unknown shape${ctx ? ` (${ctx})` : ""}; expected array, object{annotations}, or JSON string. Got:`,
142172
+ typeof value
142173
+ );
142174
+ return [];
142175
+ }
142176
+ function _filterArray(arr, ctx, logger) {
142177
+ const kept = [];
142178
+ let dropped = 0;
142179
+ for (const item of arr) {
142180
+ if (item !== null && typeof item === "object" && typeof item.id === "string" && typeof item.text === "string" && typeof item.type === "string" && typeof item.status === "string") {
142181
+ kept.push(item);
142182
+ } else {
142183
+ dropped++;
142184
+ }
142185
+ }
142186
+ if (dropped > 0) {
142187
+ logger.warn(
142188
+ `[parseLogAnnotations] dropped ${dropped} malformed entries${ctx ? ` (${ctx})` : ""}`
142189
+ );
142190
+ }
142191
+ return kept;
142192
+ }
142193
+
142194
+ // src/services/annotations/AnnotationServiceOrchestrator.ts
142195
+ var DOMAIN_ICONS2 = {
142196
+ energy: "\u26A1",
142197
+ water: "\u{1F4A7}",
142198
+ temperature: "\u{1F321}\uFE0F",
142199
+ unknown: "\xB7"
142200
+ };
142201
+ var DOMAIN_LABELS2 = {
142202
+ energy: "Energia",
142203
+ water: "\xC1gua",
142204
+ temperature: "Temperatura",
142205
+ unknown: "Indeterminado"
142206
+ };
142207
+ var BUCKET_NO_IDENTIFIER = "Sem Identificador";
142208
+ function _normalizeLogger(raw) {
142209
+ const noop = () => {
142210
+ };
142211
+ const log = raw && raw.log || noop;
142212
+ return {
142213
+ debug: raw?.debug || log,
142214
+ info: raw?.info || log,
142215
+ warn: raw?.warn || log,
142216
+ error: raw?.error || log
142217
+ };
142218
+ }
142219
+ async function buildAnnotationServiceOrchestrator(params) {
142220
+ const logger = _normalizeLogger(params.logger ?? console);
142221
+ const cacheTtlMs = params.cacheTtlMs ?? 6e4;
142222
+ const client = new CustomerDeviceService({
142223
+ customerId: params.customerId,
142224
+ tbHost: params.tbHost,
142225
+ jwt: params.jwt,
142226
+ logger
142227
+ });
142228
+ const state6 = {
142229
+ devices: [],
142230
+ byIdentifier: /* @__PURE__ */ new Map(),
142231
+ byDeviceId: /* @__PURE__ */ new Map(),
142232
+ byDomain: /* @__PURE__ */ new Map(),
142233
+ fetchedAt: 0,
142234
+ stale: false
142235
+ };
142236
+ async function _fetchAndIndex() {
142237
+ const t0 = Date.now();
142238
+ const flatDevices = await client.fetchAllCustomerDevices();
142239
+ const deviceIds = flatDevices.map((d) => d.id);
142240
+ const attrs = await client.fetchAttributesBatch(deviceIds, [
142241
+ "log_annotations",
142242
+ "identifier"
142243
+ ]);
142244
+ const annotated = flatDevices.map((d) => {
142245
+ const a = attrs.get(d.id) ?? {};
142246
+ const rawAnnotations = a["log_annotations"];
142247
+ const identifierRaw = a["identifier"];
142248
+ return {
142249
+ deviceId: d.id,
142250
+ name: d.name,
142251
+ label: d.label,
142252
+ identifier: typeof identifierRaw === "string" && identifierRaw.length > 0 ? identifierRaw : null,
142253
+ domain: _classifyDomain(d.deviceType),
142254
+ deviceType: d.deviceType,
142255
+ annotations: parseLogAnnotations(rawAnnotations, d.id, logger)
142256
+ };
142257
+ });
142258
+ state6.devices = annotated;
142259
+ state6.byIdentifier = _indexByIdentifier(annotated);
142260
+ state6.byDeviceId = _indexByDeviceId(annotated);
142261
+ state6.byDomain = _indexByDomain(annotated);
142262
+ state6.fetchedAt = Date.now();
142263
+ state6.stale = false;
142264
+ const durationMs = state6.fetchedAt - t0;
142265
+ logger.debug(
142266
+ `[AnnotationServiceOrchestrator] built \u2014 ${annotated.length} devices, ${_totalAnnotations(annotated)} annotations, ${durationMs}ms`
142267
+ );
142268
+ }
142269
+ await _fetchAndIndex();
142270
+ if (typeof window !== "undefined") {
142271
+ window.addEventListener("myio:annotation-changed", (ev) => {
142272
+ logger.debug(
142273
+ "[AnnotationServiceOrchestrator] myio:annotation-changed received:",
142274
+ ev.detail
142275
+ );
142276
+ state6.stale = true;
142277
+ orchestrator.refresh().catch((err) => {
142278
+ logger.warn("[AnnotationServiceOrchestrator] refresh after annotation-changed failed:", err);
142279
+ });
142280
+ });
142281
+ }
142282
+ const orchestrator = {
142283
+ get devices() {
142284
+ return state6.devices;
142285
+ },
142286
+ get byIdentifier() {
142287
+ return state6.byIdentifier;
142288
+ },
142289
+ get byDeviceId() {
142290
+ return state6.byDeviceId;
142291
+ },
142292
+ get byDomain() {
142293
+ return state6.byDomain;
142294
+ },
142295
+ get fetchedAt() {
142296
+ return state6.fetchedAt;
142297
+ },
142298
+ // Queries
142299
+ getAll: () => state6.devices.slice(),
142300
+ getByIdentifier: (identifier) => state6.byIdentifier.get(identifier)?.slice() ?? [],
142301
+ getByDevice: (deviceId) => state6.byDeviceId.get(deviceId) ?? null,
142302
+ getByDomain: (domain) => state6.byDomain.get(domain)?.slice() ?? [],
142303
+ getGroups: (groupBy, filter) => _buildGroups(state6, groupBy, filter),
142304
+ // Counts
142305
+ getTotalCount: () => _countNonArchived(state6.devices),
142306
+ getPendingCount: () => _countWhere(state6.devices, (a) => a.status !== "archived" && a.type === "pending"),
142307
+ getOverdueCount: () => _countWhere(state6.devices, (a) => a.status !== "archived" && _isOverdue(a)),
142308
+ // Lifecycle — AC-13
142309
+ refresh: async () => {
142310
+ await _fetchAndIndex();
142311
+ if (typeof window !== "undefined") {
142312
+ window.dispatchEvent(
142313
+ new CustomEvent("myio:annotations-refreshed", {
142314
+ detail: {
142315
+ totalCount: _countNonArchived(state6.devices),
142316
+ durationMs: 0
142317
+ // already logged inside _fetchAndIndex
142318
+ }
142319
+ })
142320
+ );
142321
+ }
142322
+ },
142323
+ // AC-12 helper — TTL-based staleness
142324
+ invalidate: () => {
142325
+ state6.stale = true;
142326
+ state6.fetchedAt = Math.min(state6.fetchedAt, Date.now() - cacheTtlMs - 1);
142327
+ }
142328
+ };
142329
+ return orchestrator;
142330
+ }
142331
+ function _indexByIdentifier(devices) {
142332
+ const map = /* @__PURE__ */ new Map();
142333
+ for (const d of devices) {
142334
+ const key = d.identifier;
142335
+ const arr = map.get(key);
142336
+ if (arr) arr.push(d);
142337
+ else map.set(key, [d]);
142338
+ }
142339
+ return map;
142340
+ }
142341
+ function _indexByDeviceId(devices) {
142342
+ const map = /* @__PURE__ */ new Map();
142343
+ for (const d of devices) map.set(d.deviceId, d);
142344
+ return map;
142345
+ }
142346
+ function _indexByDomain(devices) {
142347
+ const map = /* @__PURE__ */ new Map();
142348
+ for (const d of devices) {
142349
+ const arr = map.get(d.domain);
142350
+ if (arr) arr.push(d);
142351
+ else map.set(d.domain, [d]);
142352
+ }
142353
+ return map;
142354
+ }
142355
+ function _classifyDomain(deviceType) {
142356
+ const t = (deviceType || "").toUpperCase();
142357
+ if (!t) return "unknown";
142358
+ if (t.includes("HIDROMETRO") || t.includes("CAIXA_DAGUA") || t === "TANK") return "water";
142359
+ if (t.includes("TERMOSTATO") || t.includes("TEMP")) return "temperature";
142360
+ if (t.includes("3F_MEDIDOR") || t.includes("ENTRADA") || t.includes("RELOGIO") || t.includes("TRAFO") || t.includes("SUBESTACAO") || t.includes("MEDIDOR") || t.includes("CHILLER") || t.includes("FANCOIL") || t.includes("HVAC") || t.includes("AR_CONDICIONADO") || t.includes("BOMBA") || t.includes("ELEVADOR") || t.includes("ESCADA")) {
142361
+ return "energy";
142362
+ }
142363
+ return "unknown";
142364
+ }
142365
+ function _buildGroups(state6, groupBy, filter) {
142366
+ const filtered = state6.devices.map((d) => _applyFilterToDevice(d, filter)).filter(
142367
+ (d) => d !== null
142368
+ );
142369
+ const buckets = /* @__PURE__ */ new Map();
142370
+ for (const d of filtered) {
142371
+ let key;
142372
+ if (groupBy === "identifier") key = d.identifier ?? BUCKET_NO_IDENTIFIER;
142373
+ else if (groupBy === "device") key = d.deviceId;
142374
+ else key = d.domain;
142375
+ const arr = buckets.get(key);
142376
+ if (arr) arr.push(d);
142377
+ else buckets.set(key, [d]);
142378
+ }
142379
+ const groups = [];
142380
+ for (const [key, devs] of buckets) {
142381
+ const annotations = devs.flatMap((d) => d.annotations);
142382
+ groups.push({
142383
+ key,
142384
+ label: _labelForGroup(key, groupBy, devs[0]),
142385
+ icon: _iconForGroup(key, groupBy),
142386
+ devices: devs,
142387
+ totalAnnotations: annotations.length,
142388
+ maxImportance: annotations.reduce((acc, a) => Math.max(acc, a.importance || 0), 0),
142389
+ mostRecentAt: _maxCreatedAt(annotations)
142390
+ });
142391
+ }
142392
+ return groups;
142393
+ }
142394
+ function _applyFilterToDevice(device, filter) {
142395
+ if (!filter) {
142396
+ const visible2 = device.annotations.filter((a) => a.status !== "archived");
142397
+ if (visible2.length === 0) return null;
142398
+ return { ...device, annotations: visible2 };
142399
+ }
142400
+ const now = Date.now();
142401
+ const sevenDaysMs = 7 * 24 * 60 * 60 * 1e3;
142402
+ const visible = device.annotations.filter((a) => {
142403
+ if (filter.statuses.size > 0) {
142404
+ if (!filter.statuses.has(a.status)) return false;
142405
+ } else if (a.status === "archived") {
142406
+ return false;
142407
+ }
142408
+ if (filter.types.size > 0 && !filter.types.has(a.type)) return false;
142409
+ if (filter.importance.size > 0 && !filter.importance.has(a.importance)) return false;
142410
+ if (filter.actionableOnly) {
142411
+ const isPending = a.type === "pending" && a.status !== "archived";
142412
+ if (!isPending) return false;
142413
+ const hasDue = !!a.dueDate;
142414
+ if (hasDue) {
142415
+ const dueMs = new Date(a.dueDate).getTime();
142416
+ if (!(dueMs <= now + sevenDaysMs)) return false;
142417
+ }
142418
+ }
142419
+ if (filter.searchTerm) {
142420
+ const needle = _nfd(filter.searchTerm);
142421
+ const haystack = _nfd(
142422
+ [a.text, device.identifier ?? "", device.name, device.label].join(" ")
142423
+ );
142424
+ if (!haystack.includes(needle)) return false;
142425
+ }
142426
+ return true;
142427
+ });
142428
+ if (visible.length === 0) return null;
142429
+ return { ...device, annotations: visible };
142430
+ }
142431
+ function _labelForGroup(key, groupBy, sample) {
142432
+ if (groupBy === "identifier") return key;
142433
+ if (groupBy === "device") return sample?.label || sample?.name || key;
142434
+ return DOMAIN_LABELS2[key] ?? key;
142435
+ }
142436
+ function _iconForGroup(key, groupBy) {
142437
+ if (groupBy === "domain") return DOMAIN_ICONS2[key];
142438
+ return void 0;
142439
+ }
142440
+ function _maxCreatedAt(annotations) {
142441
+ if (annotations.length === 0) return null;
142442
+ let max = annotations[0].createdAt;
142443
+ for (let i = 1; i < annotations.length; i++) {
142444
+ if (annotations[i].createdAt > max) max = annotations[i].createdAt;
142445
+ }
142446
+ return max;
142447
+ }
142448
+ function _countNonArchived(devices) {
142449
+ let n = 0;
142450
+ for (const d of devices) {
142451
+ for (const a of d.annotations) if (a.status !== "archived") n++;
142452
+ }
142453
+ return n;
142454
+ }
142455
+ function _countWhere(devices, predicate) {
142456
+ let n = 0;
142457
+ for (const d of devices) {
142458
+ for (const a of d.annotations) if (predicate(a)) n++;
142459
+ }
142460
+ return n;
142461
+ }
142462
+ function _totalAnnotations(devices) {
142463
+ let n = 0;
142464
+ for (const d of devices) n += d.annotations.length;
142465
+ return n;
142466
+ }
142467
+ function _isOverdue(a) {
142468
+ if (!a.dueDate || a.type !== "pending") return false;
142469
+ const due = new Date(a.dueDate).getTime();
142470
+ if (isNaN(due)) return false;
142471
+ return due < Date.now();
142472
+ }
142473
+ function _nfd(s) {
142474
+ return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
142475
+ }
142476
+
142477
+ // src/components/header-annotations-panel/styles.ts
142478
+ var HEADER_ANNOTATIONS_STYLES_ID = "myio-annotations-panel-styles";
142479
+ var HEADER_ANNOTATIONS_STYLES = `
142480
+ /* Container \u2014 anchored under the HEADER button by default */
142481
+ .myio-annotations-panel {
142482
+ position: fixed;
142483
+ z-index: 99998;
142484
+ width: min(720px, 90vw);
142485
+ max-height: min(80vh, 720px);
142486
+ background: #fff;
142487
+ border: 1px solid #e2e8f0;
142488
+ border-radius: 12px;
142489
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.18);
142490
+ display: flex;
142491
+ flex-direction: column;
142492
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
142493
+ color: #1e293b;
142494
+ overflow: hidden;
142495
+ animation: myio-anno-pop 0.16s ease-out;
142496
+ }
142497
+ @keyframes myio-anno-pop {
142498
+ from { opacity: 0; transform: translateY(-6px); }
142499
+ to { opacity: 1; transform: translateY(0); }
142500
+ }
142501
+
142502
+ .myio-annotations-panel.maximized {
142503
+ width: 90vw !important;
142504
+ height: 90vh !important;
142505
+ max-height: 90vh !important;
142506
+ top: 5vh !important;
142507
+ left: 5vw !important;
142508
+ }
142509
+ .myio-annotations-panel.is-dragging {
142510
+ cursor: grabbing;
142511
+ user-select: none;
142512
+ }
142513
+ .myio-annotations-panel.is-dragging * {
142514
+ pointer-events: none;
142515
+ }
142516
+ .myio-annotations-panel-header[data-drag-handle] {
142517
+ cursor: grab;
142518
+ }
142519
+ .myio-annotations-panel-header[data-drag-handle]:active {
142520
+ cursor: grabbing;
142521
+ }
142522
+ .myio-annotations-panel-action.is-active {
142523
+ background: rgba(108, 92, 231, 0.18);
142524
+ color: #4c3aac;
142525
+ }
142526
+
142527
+ /* Virtual list bookkeeping styles (RFC-0203 M6) */
142528
+ .myio-vlist-container { /* container is .myio-annotations-body in practice */ }
142529
+ .myio-vlist-viewport { will-change: transform; }
142530
+
142531
+ /* Header */
142532
+ .myio-annotations-panel-header {
142533
+ display: flex;
142534
+ align-items: center;
142535
+ gap: 8px;
142536
+ padding: 12px 16px;
142537
+ border-bottom: 1px solid #e2e8f0;
142538
+ background: linear-gradient(135deg, rgba(108, 92, 231, 0.05), rgba(108, 92, 231, 0.02));
142539
+ }
142540
+ .myio-annotations-panel-title {
142541
+ flex: 1;
142542
+ margin: 0;
142543
+ font-size: 15px;
142544
+ font-weight: 700;
142545
+ color: #4c3aac;
142546
+ letter-spacing: -0.01em;
142547
+ }
142548
+ .myio-annotations-panel-title .myio-annotations-icon {
142549
+ margin-right: 6px;
142550
+ font-size: 16px;
142551
+ }
142552
+ .myio-annotations-panel-meta {
142553
+ font-size: 11px;
142554
+ font-weight: 500;
142555
+ color: #64748b;
142556
+ }
142557
+ .myio-annotations-panel-actions {
142558
+ display: flex;
142559
+ align-items: center;
142560
+ gap: 4px;
142561
+ }
142562
+ .myio-annotations-panel-action {
142563
+ width: 28px;
142564
+ height: 28px;
142565
+ display: inline-flex;
142566
+ align-items: center;
142567
+ justify-content: center;
142568
+ border: 1px solid transparent;
142569
+ border-radius: 6px;
142570
+ background: transparent;
142571
+ color: #64748b;
142572
+ cursor: pointer;
142573
+ font-size: 14px;
142574
+ transition: background 0.15s, color 0.15s;
142575
+ }
142576
+ .myio-annotations-panel-action:hover {
142577
+ background: rgba(108, 92, 231, 0.1);
142578
+ color: #4c3aac;
142579
+ }
142580
+ .myio-annotations-panel-action:focus-visible {
142581
+ outline: 2px solid #6c5ce7;
142582
+ outline-offset: 2px;
142583
+ }
142584
+
142585
+ /* Tabs */
142586
+ .myio-annotations-tabs {
142587
+ display: flex;
142588
+ gap: 0;
142589
+ padding: 0 8px;
142590
+ border-bottom: 1px solid #e2e8f0;
142591
+ background: #fafbff;
142592
+ flex-shrink: 0;
142593
+ }
142594
+ .myio-annotations-tab {
142595
+ flex: 1;
142596
+ padding: 10px 12px;
142597
+ font: inherit;
142598
+ font-size: 13px;
142599
+ font-weight: 600;
142600
+ color: #64748b;
142601
+ background: transparent;
142602
+ border: none;
142603
+ border-bottom: 2px solid transparent;
142604
+ cursor: pointer;
142605
+ transition: color 0.15s, border-color 0.15s;
142606
+ white-space: nowrap;
142607
+ }
142608
+ .myio-annotations-tab:hover {
142609
+ color: #4c3aac;
142610
+ }
142611
+ .myio-annotations-tab[aria-selected="true"] {
142612
+ color: #4c3aac;
142613
+ border-bottom-color: #6c5ce7;
142614
+ }
142615
+ .myio-annotations-tab:focus-visible {
142616
+ outline: 2px solid #6c5ce7;
142617
+ outline-offset: -2px;
142618
+ border-radius: 4px;
142619
+ }
142620
+
142621
+ /* Toolbar (RFC-0203 M5) */
142622
+ .myio-annotations-toolbar {
142623
+ display: flex;
142624
+ flex-direction: column;
142625
+ gap: 6px;
142626
+ padding: 8px 12px 0 12px;
142627
+ background: #fff;
142628
+ border-bottom: 1px solid #f1f5f9;
142629
+ flex-shrink: 0;
142630
+ }
142631
+ .myio-annotations-toolbar-row {
142632
+ display: flex;
142633
+ align-items: center;
142634
+ gap: 6px;
142635
+ }
142636
+ .myio-annotations-toolbar-meta {
142637
+ font-size: 11px;
142638
+ color: #64748b;
142639
+ padding-bottom: 6px;
142640
+ }
142641
+ .myio-annotations-toolbar-count { font-weight: 600; }
142642
+
142643
+ .myio-annotations-toolbar-search {
142644
+ flex: 1;
142645
+ display: inline-flex;
142646
+ align-items: center;
142647
+ gap: 6px;
142648
+ height: 32px;
142649
+ padding: 0 10px;
142650
+ border: 1px solid #e2e8f0;
142651
+ border-radius: 6px;
142652
+ background: #fff;
142653
+ }
142654
+ .myio-annotations-toolbar-search:focus-within {
142655
+ border-color: #6c5ce7;
142656
+ box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.15);
142657
+ }
142658
+ .myio-annotations-toolbar-search input {
142659
+ flex: 1;
142660
+ border: none;
142661
+ outline: none;
142662
+ font: inherit;
142663
+ font-size: 13px;
142664
+ color: #1e293b;
142665
+ background: transparent;
142666
+ }
142667
+ .myio-annotations-search-icon { font-size: 13px; color: #94a3b8; }
142668
+
142669
+ .myio-annotations-toolbar-sort {
142670
+ height: 32px;
142671
+ padding: 0 8px;
142672
+ border: 1px solid #e2e8f0;
142673
+ border-radius: 6px;
142674
+ background: #fff;
142675
+ font: inherit;
142676
+ font-size: 12px;
142677
+ color: #1e293b;
142678
+ cursor: pointer;
142679
+ }
142680
+ .myio-annotations-toolbar-sort:focus-visible {
142681
+ border-color: #6c5ce7;
142682
+ outline: 2px solid rgba(108, 92, 231, 0.3);
142683
+ outline-offset: 1px;
142684
+ }
142685
+
142686
+ .myio-annotations-toolbar-filter-btn {
142687
+ height: 32px;
142688
+ padding: 0 10px;
142689
+ border: 1px solid #e2e8f0;
142690
+ border-radius: 6px;
142691
+ background: #fff;
142692
+ font: inherit;
142693
+ font-size: 12px;
142694
+ font-weight: 600;
142695
+ color: #4c3aac;
142696
+ cursor: pointer;
142697
+ display: inline-flex;
142698
+ align-items: center;
142699
+ gap: 6px;
142700
+ }
142701
+ .myio-annotations-toolbar-filter-btn:hover {
142702
+ background: rgba(108, 92, 231, 0.06);
142703
+ border-color: rgba(108, 92, 231, 0.35);
142704
+ }
142705
+ .myio-annotations-toolbar-filter-btn[aria-expanded="true"] {
142706
+ background: rgba(108, 92, 231, 0.12);
142707
+ border-color: #6c5ce7;
142708
+ }
142709
+
142710
+ /* Filter dropdown panel */
142711
+ .myio-annotations-filters {
142712
+ padding: 8px 0 12px 0;
142713
+ border-top: 1px dashed #e2e8f0;
142714
+ margin-top: 6px;
142715
+ }
142716
+ .myio-annotations-filter-section { margin-bottom: 8px; }
142717
+ .myio-annotations-filter-section-title {
142718
+ font-size: 11px;
142719
+ font-weight: 700;
142720
+ color: #64748b;
142721
+ text-transform: uppercase;
142722
+ letter-spacing: 0.04em;
142723
+ margin-bottom: 4px;
142724
+ }
142725
+ .myio-annotations-filter-chips {
142726
+ display: flex;
142727
+ flex-wrap: wrap;
142728
+ gap: 6px;
142729
+ }
142730
+ .myio-annotations-filter-chip {
142731
+ display: inline-flex;
142732
+ align-items: center;
142733
+ gap: 4px;
142734
+ padding: 4px 10px;
142735
+ border: 1px solid #e2e8f0;
142736
+ border-radius: 999px;
142737
+ background: #f8fafc;
142738
+ font-size: 12px;
142739
+ color: #334155;
142740
+ cursor: pointer;
142741
+ user-select: none;
142742
+ transition: background 0.12s, border-color 0.12s;
142743
+ }
142744
+ .myio-annotations-filter-chip:hover {
142745
+ border-color: rgba(108, 92, 231, 0.4);
142746
+ }
142747
+ .myio-annotations-filter-chip input {
142748
+ position: absolute;
142749
+ opacity: 0;
142750
+ pointer-events: none;
142751
+ }
142752
+ .myio-annotations-filter-chip.is-on {
142753
+ background: rgba(108, 92, 231, 0.12);
142754
+ border-color: #6c5ce7;
142755
+ color: #4c3aac;
142756
+ font-weight: 600;
142757
+ }
142758
+ .myio-annotations-filter-actions {
142759
+ display: flex;
142760
+ justify-content: flex-end;
142761
+ margin-top: 6px;
142762
+ }
142763
+
142764
+ /* Search highlight */
142765
+ .myio-annotations-item mark {
142766
+ background: rgba(245, 158, 11, 0.35);
142767
+ color: inherit;
142768
+ padding: 0 1px;
142769
+ border-radius: 2px;
142770
+ }
142771
+
142772
+ /* Body */
142773
+ .myio-annotations-body {
142774
+ flex: 1;
142775
+ overflow: auto;
142776
+ padding: 8px 12px 12px 12px;
142777
+ background: #fff;
142778
+ }
142779
+
142780
+ /* Group */
142781
+ .myio-annotations-group {
142782
+ margin-top: 8px;
142783
+ border: 1px solid #e2e8f0;
142784
+ border-radius: 8px;
142785
+ overflow: hidden;
142786
+ background: #fff;
142787
+ }
142788
+ .myio-annotations-group-header {
142789
+ display: flex;
142790
+ align-items: center;
142791
+ gap: 8px;
142792
+ padding: 8px 12px;
142793
+ background: #f8fafc;
142794
+ border-bottom: 1px solid #e2e8f0;
142795
+ font-size: 12px;
142796
+ font-weight: 700;
142797
+ color: #334155;
142798
+ }
142799
+ .myio-annotations-group-icon { font-size: 14px; }
142800
+ .myio-annotations-group-label { flex: 1; }
142801
+ .myio-annotations-group-count {
142802
+ font-size: 11px;
142803
+ font-weight: 600;
142804
+ color: #64748b;
142805
+ padding: 2px 8px;
142806
+ background: rgba(108, 92, 231, 0.1);
142807
+ border-radius: 10px;
142808
+ }
142809
+ .myio-annotations-group--no-id .myio-annotations-group-header {
142810
+ background: #fef3c7;
142811
+ color: #92400e;
142812
+ border-bottom-color: #fde68a;
142813
+ }
142814
+
142815
+ /* Item */
142816
+ .myio-annotations-item {
142817
+ display: grid;
142818
+ grid-template-columns: 22px 1fr auto;
142819
+ gap: 10px;
142820
+ padding: 10px 12px;
142821
+ border-bottom: 1px solid #f1f5f9;
142822
+ cursor: pointer;
142823
+ transition: background 0.12s;
142824
+ }
142825
+ .myio-annotations-item:last-child { border-bottom: none; }
142826
+ .myio-annotations-item:hover { background: rgba(108, 92, 231, 0.05); }
142827
+ .myio-annotations-item:focus-visible {
142828
+ outline: 2px solid #6c5ce7;
142829
+ outline-offset: -2px;
142830
+ }
142831
+
142832
+ .myio-annotations-item-icon {
142833
+ font-size: 16px;
142834
+ line-height: 22px;
142835
+ text-align: center;
142836
+ }
142837
+ .myio-annotations-item-body { min-width: 0; }
142838
+ .myio-annotations-item-text {
142839
+ font-size: 13px;
142840
+ font-weight: 500;
142841
+ color: #1e293b;
142842
+ margin: 0 0 4px 0;
142843
+ line-height: 1.4;
142844
+ display: -webkit-box;
142845
+ -webkit-line-clamp: 2;
142846
+ -webkit-box-orient: vertical;
142847
+ overflow: hidden;
142848
+ }
142849
+ .myio-annotations-item-meta {
142850
+ font-size: 11px;
142851
+ color: #64748b;
142852
+ display: flex;
142853
+ flex-wrap: wrap;
142854
+ gap: 6px 10px;
142855
+ }
142856
+ .myio-annotations-item-device {
142857
+ font-weight: 600;
142858
+ color: #4c3aac;
142859
+ }
142860
+
142861
+ .myio-annotations-item-side {
142862
+ display: flex;
142863
+ flex-direction: column;
142864
+ align-items: flex-end;
142865
+ gap: 4px;
142866
+ }
142867
+ .myio-annotations-importance-badge {
142868
+ display: inline-block;
142869
+ min-width: 18px;
142870
+ padding: 2px 6px;
142871
+ border-radius: 10px;
142872
+ font-size: 10px;
142873
+ font-weight: 700;
142874
+ color: #fff;
142875
+ text-align: center;
142876
+ line-height: 1.2;
142877
+ }
142878
+ .myio-annotations-importance-1 { background: #94a3b8; }
142879
+ .myio-annotations-importance-2 { background: #64748b; }
142880
+ .myio-annotations-importance-3 { background: #6c5ce7; }
142881
+ .myio-annotations-importance-4 { background: #f59e0b; }
142882
+ .myio-annotations-importance-5 { background: #dc2626; }
142883
+
142884
+ .myio-annotations-overdue {
142885
+ font-size: 10px;
142886
+ font-weight: 700;
142887
+ color: #dc2626;
142888
+ text-transform: uppercase;
142889
+ }
142890
+
142891
+ /* Empty state */
142892
+ .myio-annotations-empty {
142893
+ padding: 32px 16px;
142894
+ text-align: center;
142895
+ color: #94a3b8;
142896
+ font-size: 13px;
142897
+ }
142898
+ .myio-annotations-empty-icon {
142899
+ font-size: 28px;
142900
+ margin-bottom: 8px;
142901
+ opacity: 0.5;
142902
+ }
142903
+
142904
+ /* Loading state */
142905
+ .myio-annotations-loading {
142906
+ padding: 24px;
142907
+ text-align: center;
142908
+ color: #64748b;
142909
+ font-size: 13px;
142910
+ }
142911
+
142912
+ /* Footer */
142913
+ .myio-annotations-panel-footer {
142914
+ display: flex;
142915
+ align-items: center;
142916
+ justify-content: space-between;
142917
+ padding: 8px 16px;
142918
+ border-top: 1px solid #e2e8f0;
142919
+ background: #fafbff;
142920
+ font-size: 11px;
142921
+ color: #64748b;
142922
+ flex-shrink: 0;
142923
+ }
142924
+ .myio-annotations-panel-footer-meta {
142925
+ flex: 1;
142926
+ text-align: center;
142927
+ font-size: 11px;
142928
+ color: #94a3b8;
142929
+ }
142930
+ .myio-annotations-panel-footer-action {
142931
+ font: inherit;
142932
+ font-size: 12px;
142933
+ font-weight: 600;
142934
+ color: #4c3aac;
142935
+ background: transparent;
142936
+ border: 1px solid rgba(108, 92, 231, 0.3);
142937
+ border-radius: 6px;
142938
+ padding: 4px 10px;
142939
+ cursor: pointer;
142940
+ transition: background 0.15s, border-color 0.15s;
142941
+ }
142942
+ .myio-annotations-panel-footer-action:hover {
142943
+ background: rgba(108, 92, 231, 0.08);
142944
+ border-color: rgba(108, 92, 231, 0.5);
142945
+ }
142946
+ .myio-annotations-panel-footer-action:focus-visible {
142947
+ outline: 2px solid #6c5ce7;
142948
+ outline-offset: 1px;
142949
+ }
142950
+ `;
142951
+ function injectStylesOnce() {
142952
+ if (typeof document === "undefined") return;
142953
+ if (document.getElementById(HEADER_ANNOTATIONS_STYLES_ID)) return;
142954
+ const style = document.createElement("style");
142955
+ style.id = HEADER_ANNOTATIONS_STYLES_ID;
142956
+ style.textContent = HEADER_ANNOTATIONS_STYLES;
142957
+ document.head.appendChild(style);
142958
+ }
142959
+
142960
+ // src/components/header-annotations-panel/searchSortFilter.ts
142961
+ function nfdNormalize(s) {
142962
+ if (!s) return "";
142963
+ return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
142964
+ }
142965
+ function highlightMatches(escapedText, term) {
142966
+ if (!term) return escapedText;
142967
+ const needle = nfdNormalize(term);
142968
+ if (!needle) return escapedText;
142969
+ const lowered = nfdNormalize(escapedText);
142970
+ let result = "";
142971
+ let i = 0;
142972
+ while (i < escapedText.length) {
142973
+ const found = lowered.indexOf(needle, i);
142974
+ if (found === -1) {
142975
+ result += escapedText.slice(i);
142976
+ break;
142977
+ }
142978
+ result += escapedText.slice(i, found);
142979
+ result += "<mark>" + escapedText.slice(found, found + needle.length) + "</mark>";
142980
+ i = found + needle.length;
142981
+ }
142982
+ return result;
142983
+ }
142984
+ var SORT_OPTIONS = [
142985
+ { key: "alpha-asc", label: "Alfab\xE9tica (A \u2192 Z)" },
142986
+ { key: "alpha-desc", label: "Alfab\xE9tica (Z \u2192 A)" },
142987
+ { key: "count-desc", label: "Mais anota\xE7\xF5es" },
142988
+ { key: "count-asc", label: "Menos anota\xE7\xF5es" },
142989
+ { key: "importance-desc", label: "Maior import\xE2ncia" },
142990
+ { key: "recent-desc", label: "Mais recente" }
142991
+ ];
142992
+ var DEFAULT_SORT = "alpha-asc";
142993
+ function sortGroups(groups, key) {
142994
+ const arr = groups.slice();
142995
+ const cmp = _comparator(key);
142996
+ arr.sort((a, b) => cmp(a, b));
142997
+ return arr;
142998
+ }
142999
+ function _comparator(key) {
143000
+ switch (key) {
143001
+ case "alpha-asc":
143002
+ return (a, b) => a.label.localeCompare(b.label, "pt-BR", { sensitivity: "base" });
143003
+ case "alpha-desc":
143004
+ return (a, b) => b.label.localeCompare(a.label, "pt-BR", { sensitivity: "base" });
143005
+ case "count-desc":
143006
+ return (a, b) => b.totalAnnotations - a.totalAnnotations || a.label.localeCompare(b.label, "pt-BR");
143007
+ case "count-asc":
143008
+ return (a, b) => a.totalAnnotations - b.totalAnnotations || a.label.localeCompare(b.label, "pt-BR");
143009
+ case "importance-desc":
143010
+ return (a, b) => b.maxImportance - a.maxImportance || b.totalAnnotations - a.totalAnnotations;
143011
+ case "recent-desc":
143012
+ return (a, b) => _compareIsoDesc(a.mostRecentAt, b.mostRecentAt);
143013
+ default:
143014
+ return () => 0;
143015
+ }
143016
+ }
143017
+ function _compareIsoDesc(a, b) {
143018
+ if (a === b) return 0;
143019
+ if (!a) return 1;
143020
+ if (!b) return -1;
143021
+ return b.localeCompare(a);
143022
+ }
143023
+ function createDefaultFilter() {
143024
+ return {
143025
+ types: /* @__PURE__ */ new Set(),
143026
+ statuses: /* @__PURE__ */ new Set(),
143027
+ importance: /* @__PURE__ */ new Set(),
143028
+ actionableOnly: false,
143029
+ searchTerm: ""
143030
+ };
143031
+ }
143032
+ var FILTER_TYPE_OPTIONS = [
143033
+ { id: "observation", label: "Observa\xE7\xE3o", icon: "\u{1F4DD}" },
143034
+ { id: "pending", label: "Pend\xEAncia", icon: "\u26A0\uFE0F" },
143035
+ { id: "maintenance", label: "Manuten\xE7\xE3o", icon: "\u{1F527}" },
143036
+ { id: "activity", label: "Atividade", icon: "\u2713" }
143037
+ ];
143038
+ var FILTER_STATUS_OPTIONS = [
143039
+ { id: "created", label: "Criada" },
143040
+ { id: "modified", label: "Modificada" },
143041
+ { id: "archived", label: "Arquivada" }
143042
+ // AC-27: off by default; toggle exposes it
143043
+ ];
143044
+ function countAnnotationsInGroups(groups) {
143045
+ let n = 0;
143046
+ for (const g of groups) n += g.totalAnnotations;
143047
+ return n;
143048
+ }
143049
+ function toggleInSet(set, value) {
143050
+ const next = new Set(set);
143051
+ if (next.has(value)) next.delete(value);
143052
+ else next.add(value);
143053
+ return next;
143054
+ }
143055
+ function withSearchTerm(filter, term) {
143056
+ return { ...filter, searchTerm: term };
143057
+ }
143058
+
143059
+ // src/components/header-annotations-panel/AnnotationItemCard.ts
143060
+ var DOMAIN_ICONS3 = {
143061
+ energy: "\u26A1",
143062
+ water: "\u{1F4A7}",
143063
+ temperature: "\u{1F321}\uFE0F",
143064
+ unknown: "\xB7"
143065
+ };
143066
+ var TYPE_ICONS = {
143067
+ observation: "\u{1F4DD}",
143068
+ pending: "\u26A0\uFE0F",
143069
+ maintenance: "\u{1F527}",
143070
+ activity: "\u2713"
143071
+ };
143072
+ var ITEM_TEXT_MAX = 120;
143073
+ function escapeHtml6(input) {
143074
+ if (input == null) return "";
143075
+ return String(input).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
143076
+ }
143077
+ function truncate2(input, max) {
143078
+ const s = String(input ?? "");
143079
+ if (s.length <= max) return s;
143080
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
143081
+ }
143082
+ function formatRelative(iso, now = Date.now()) {
143083
+ if (!iso) return "";
143084
+ const t = new Date(iso).getTime();
143085
+ if (isNaN(t)) return "";
143086
+ const diffMs = now - t;
143087
+ const sec = Math.round(diffMs / 1e3);
143088
+ if (sec < 60) return "agora h\xE1 pouco";
143089
+ const min = Math.round(sec / 60);
143090
+ if (min < 60) return `h\xE1 ${min} min`;
143091
+ const hr = Math.round(min / 60);
143092
+ if (hr < 24) return `h\xE1 ${hr} h`;
143093
+ const day = Math.round(hr / 24);
143094
+ if (day < 30) return `h\xE1 ${day} d`;
143095
+ const mo = Math.round(day / 30);
143096
+ if (mo < 12) return `h\xE1 ${mo} m\xEAs${mo > 1 ? "es" : ""}`;
143097
+ const yr = Math.round(mo / 12);
143098
+ return `h\xE1 ${yr} ano${yr > 1 ? "s" : ""}`;
143099
+ }
143100
+ function isOverdue(annotation, now = Date.now()) {
143101
+ if (annotation.type !== "pending") return false;
143102
+ if (!annotation.dueDate) return false;
143103
+ const due = new Date(annotation.dueDate).getTime();
143104
+ if (isNaN(due)) return false;
143105
+ return due < now;
143106
+ }
143107
+ function renderAnnotationItemCard(device, annotation, searchTerm) {
143108
+ const domainIcon = DOMAIN_ICONS3[device.domain] ?? DOMAIN_ICONS3.unknown;
143109
+ const typeIcon = TYPE_ICONS[annotation.type] ?? "\xB7";
143110
+ const importance = Math.max(1, Math.min(5, annotation.importance || 1));
143111
+ const textEscaped = escapeHtml6(truncate2(annotation.text || "", ITEM_TEXT_MAX));
143112
+ const deviceLabelEscaped = escapeHtml6(device.label || device.name || device.deviceId);
143113
+ const author = escapeHtml6(annotation.createdBy?.name || "sem autor");
143114
+ const when = escapeHtml6(formatRelative(annotation.createdAt));
143115
+ const overdueTag = isOverdue(annotation) ? '<span class="myio-annotations-overdue">Vencida</span>' : "";
143116
+ const text = searchTerm ? highlightMatches(textEscaped, searchTerm) : textEscaped;
143117
+ const deviceLabel = searchTerm ? highlightMatches(deviceLabelEscaped, searchTerm) : deviceLabelEscaped;
143118
+ const identifierEscaped = device.identifier ? escapeHtml6(device.identifier) : "";
143119
+ const identifierHighlighted = searchTerm && identifierEscaped ? highlightMatches(identifierEscaped, searchTerm) : identifierEscaped;
143120
+ const identifierTag = device.identifier ? `<span class="myio-annotations-item-device">${identifierHighlighted}</span>` : "";
143121
+ return `
143122
+ <button
143123
+ class="myio-annotations-item"
143124
+ type="button"
143125
+ data-device-id="${escapeHtml6(device.deviceId)}"
143126
+ data-annotation-id="${escapeHtml6(annotation.id)}"
143127
+ tabindex="0"
143128
+ aria-label="${escapeHtml6(annotation.text)} \u2014 ${deviceLabel}"
143129
+ >
143130
+ <span class="myio-annotations-item-icon" aria-hidden="true">${typeIcon}</span>
143131
+ <div class="myio-annotations-item-body">
143132
+ <p class="myio-annotations-item-text">${text}</p>
143133
+ <div class="myio-annotations-item-meta">
143134
+ <span aria-hidden="true">${domainIcon}</span>
143135
+ ${identifierTag}
143136
+ <span>${deviceLabel}</span>
143137
+ <span>\xB7</span>
143138
+ <span>${author}</span>
143139
+ <span>\xB7</span>
143140
+ <span>${when}</span>
143141
+ </div>
143142
+ </div>
143143
+ <div class="myio-annotations-item-side">
143144
+ <span class="myio-annotations-importance-badge myio-annotations-importance-${importance}" title="Import\xE2ncia ${importance}">${importance}</span>
143145
+ ${overdueTag}
143146
+ </div>
143147
+ </button>
143148
+ `.trim();
143149
+ }
143150
+
143151
+ // src/components/header-annotations-panel/VirtualList.ts
143152
+ var VIRTUAL_SCROLL_THRESHOLD = 100;
143153
+ function shouldVirtualize(itemCount) {
143154
+ return itemCount > VIRTUAL_SCROLL_THRESHOLD;
143155
+ }
143156
+ var VirtualList = class {
143157
+ container;
143158
+ rows;
143159
+ overscan;
143160
+ spacer;
143161
+ viewport;
143162
+ /** Cumulative pixel offset for each row (offsets[i] = top of row i). */
143163
+ offsets;
143164
+ /** Total content height. */
143165
+ totalHeight;
143166
+ _onScroll;
143167
+ _rafScheduled = false;
143168
+ _destroyed = false;
143169
+ constructor(opts) {
143170
+ this.container = opts.container;
143171
+ this.rows = opts.rows;
143172
+ this.overscan = opts.overscan ?? 6;
143173
+ this.offsets = new Array(this.rows.length);
143174
+ let acc = 0;
143175
+ for (let i = 0; i < this.rows.length; i++) {
143176
+ this.offsets[i] = acc;
143177
+ acc += this.rows[i].height;
143178
+ }
143179
+ this.totalHeight = acc;
143180
+ this.container.classList.add("myio-vlist-container");
143181
+ this.container.style.position = "relative";
143182
+ this.container.style.overflowY = "auto";
143183
+ this.spacer = document.createElement("div");
143184
+ this.spacer.className = "myio-vlist-spacer";
143185
+ this.spacer.style.position = "relative";
143186
+ this.spacer.style.width = "100%";
143187
+ this.spacer.style.height = `${this.totalHeight}px`;
143188
+ this.viewport = document.createElement("div");
143189
+ this.viewport.className = "myio-vlist-viewport";
143190
+ this.viewport.style.position = "absolute";
143191
+ this.viewport.style.top = "0px";
143192
+ this.viewport.style.left = "0";
143193
+ this.viewport.style.right = "0";
143194
+ this.spacer.appendChild(this.viewport);
143195
+ this.container.innerHTML = "";
143196
+ this.container.appendChild(this.spacer);
143197
+ this._onScroll = () => this._scheduleRender();
143198
+ this.container.addEventListener("scroll", this._onScroll, { passive: true });
143199
+ this._renderVisible();
143200
+ }
143201
+ /** Re-render the visible window (e.g. after a DOM resize). Idempotent. */
143202
+ refresh() {
143203
+ if (this._destroyed) return;
143204
+ this._renderVisible();
143205
+ }
143206
+ /** Remove the spacer/viewport and detach listeners. */
143207
+ destroy() {
143208
+ if (this._destroyed) return;
143209
+ this._destroyed = true;
143210
+ this.container.removeEventListener("scroll", this._onScroll);
143211
+ this.container.classList.remove("myio-vlist-container");
143212
+ this.container.innerHTML = "";
143213
+ }
143214
+ // ── Internals ───────────────────────────────────────────────────────────
143215
+ _scheduleRender() {
143216
+ if (this._rafScheduled) return;
143217
+ this._rafScheduled = true;
143218
+ requestAnimationFrame(() => {
143219
+ this._rafScheduled = false;
143220
+ this._renderVisible();
143221
+ });
143222
+ }
143223
+ _renderVisible() {
143224
+ if (this._destroyed) return;
143225
+ const scrollTop = this.container.scrollTop;
143226
+ const viewportH = this.container.clientHeight || 1;
143227
+ const startIdx = Math.max(0, this._findRowAt(scrollTop) - this.overscan);
143228
+ const endIdx = Math.min(
143229
+ this.rows.length - 1,
143230
+ this._findRowAt(scrollTop + viewportH) + this.overscan
143231
+ );
143232
+ const html = [];
143233
+ for (let i = startIdx; i <= endIdx; i++) {
143234
+ html.push(this.rows[i].render());
143235
+ }
143236
+ const topOffset = this.offsets[startIdx] ?? 0;
143237
+ this.viewport.style.transform = `translateY(${topOffset}px)`;
143238
+ this.viewport.innerHTML = html.join("");
143239
+ }
143240
+ /** Binary search for the row whose offset contains `y`. */
143241
+ _findRowAt(y) {
143242
+ if (this.rows.length === 0) return 0;
143243
+ let lo = 0;
143244
+ let hi = this.rows.length - 1;
143245
+ while (lo < hi) {
143246
+ const mid = lo + hi + 1 >> 1;
143247
+ if (this.offsets[mid] <= y) lo = mid;
143248
+ else hi = mid - 1;
143249
+ }
143250
+ return lo;
143251
+ }
143252
+ };
143253
+
143254
+ // src/components/header-annotations-panel/ExportModal.ts
143255
+ var MODAL_DOM_ID = "myio-annotations-export-modal";
143256
+ var STYLES_ID3 = "myio-annotations-export-modal-styles";
143257
+ var STYLES4 = `
143258
+ .${MODAL_DOM_ID}-backdrop {
143259
+ position: fixed; inset: 0; z-index: 99999;
143260
+ background: rgba(15, 23, 42, 0.55);
143261
+ display: flex; align-items: center; justify-content: center;
143262
+ animation: myio-anno-export-fade 0.12s ease-out;
143263
+ }
143264
+ @keyframes myio-anno-export-fade {
143265
+ from { opacity: 0; } to { opacity: 1; }
143266
+ }
143267
+ .${MODAL_DOM_ID} {
143268
+ width: min(420px, 92vw);
143269
+ background: #fff;
143270
+ border-radius: 12px;
143271
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.3);
143272
+ padding: 18px 20px;
143273
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
143274
+ color: #1e293b;
143275
+ }
143276
+ .${MODAL_DOM_ID} h3 {
143277
+ margin: 0 0 12px 0;
143278
+ font-size: 16px;
143279
+ color: #4c3aac;
143280
+ }
143281
+ .${MODAL_DOM_ID} fieldset {
143282
+ border: none;
143283
+ margin: 0 0 12px 0;
143284
+ padding: 0;
143285
+ }
143286
+ .${MODAL_DOM_ID} legend {
143287
+ font-size: 12px;
143288
+ font-weight: 700;
143289
+ color: #64748b;
143290
+ text-transform: uppercase;
143291
+ letter-spacing: 0.04em;
143292
+ margin-bottom: 4px;
143293
+ }
143294
+ .${MODAL_DOM_ID} label {
143295
+ display: block;
143296
+ font-size: 13px;
143297
+ padding: 3px 0;
143298
+ cursor: pointer;
143299
+ }
143300
+ .${MODAL_DOM_ID} input[type="radio"],
143301
+ .${MODAL_DOM_ID} input[type="checkbox"] {
143302
+ margin-right: 6px;
143303
+ accent-color: #6c5ce7;
143304
+ }
143305
+ .${MODAL_DOM_ID} input[disabled] + span { color: #94a3b8; }
143306
+ .${MODAL_DOM_ID}-actions {
143307
+ display: flex; gap: 8px; justify-content: flex-end;
143308
+ margin-top: 8px;
143309
+ }
143310
+ .${MODAL_DOM_ID}-actions button {
143311
+ font: inherit; font-size: 13px; font-weight: 600;
143312
+ padding: 6px 14px;
143313
+ border-radius: 6px;
143314
+ cursor: pointer;
143315
+ }
143316
+ .${MODAL_DOM_ID}-cancel {
143317
+ background: #fff;
143318
+ border: 1px solid #cbd5e1;
143319
+ color: #475569;
143320
+ }
143321
+ .${MODAL_DOM_ID}-confirm {
143322
+ background: #6c5ce7;
143323
+ border: 1px solid #6c5ce7;
143324
+ color: #fff;
143325
+ }
143326
+ .${MODAL_DOM_ID}-confirm:disabled {
143327
+ background: #cbd5e1; border-color: #cbd5e1; cursor: not-allowed;
143328
+ }
143329
+ `;
143330
+ function injectStyles15() {
143331
+ if (typeof document === "undefined") return;
143332
+ if (document.getElementById(STYLES_ID3)) return;
143333
+ const el2 = document.createElement("style");
143334
+ el2.id = STYLES_ID3;
143335
+ el2.textContent = STYLES4;
143336
+ document.head.appendChild(el2);
143337
+ }
143338
+ var _activeBackdrop = null;
143339
+ function openExportModal(options) {
143340
+ injectStyles15();
143341
+ closeExportModal();
143342
+ const backdrop = document.createElement("div");
143343
+ backdrop.className = `${MODAL_DOM_ID}-backdrop`;
143344
+ backdrop.setAttribute("role", "presentation");
143345
+ backdrop.innerHTML = `
143346
+ <div class="${MODAL_DOM_ID}" role="dialog" aria-modal="true" aria-labelledby="${MODAL_DOM_ID}-title">
143347
+ <h3 id="${MODAL_DOM_ID}-title">Exportar anota\xE7\xF5es</h3>
143348
+
143349
+ <fieldset>
143350
+ <legend>Formato</legend>
143351
+ <label><input type="radio" name="fmt" value="pdf" checked /><span>PDF</span></label>
143352
+ <label><input type="radio" name="fmt" value="csv" /><span>CSV</span></label>
143353
+ </fieldset>
143354
+
143355
+ <fieldset data-fmt-pdf-only>
143356
+ <legend>N\xEDveis (PDF apenas)</legend>
143357
+ <label><input type="checkbox" name="lvl-summary" checked /><span>Sum\xE1rio (totais + KPIs)</span></label>
143358
+ <label><input type="checkbox" name="lvl-consolidated" checked /><span>Consolidado (por device)</span></label>
143359
+ <label><input type="checkbox" name="lvl-detailed" /><span>Detalhado (anota\xE7\xE3o por anota\xE7\xE3o)</span></label>
143360
+ </fieldset>
143361
+
143362
+ <fieldset>
143363
+ <legend>Escopo</legend>
143364
+ <label><input type="radio" name="scope" value="current-tab" checked /><span>Aba atual</span></label>
143365
+ <label><input type="radio" name="scope" value="all" /><span>Todas as anota\xE7\xF5es</span></label>
143366
+ <label><input type="radio" name="scope" value="filtered" ${options.hasActiveFilter ? "" : "disabled"} /><span>Resultado filtrado / buscado</span></label>
143367
+ </fieldset>
143368
+
143369
+ <div class="${MODAL_DOM_ID}-actions">
143370
+ <button type="button" class="${MODAL_DOM_ID}-cancel">Cancelar</button>
143371
+ <button type="button" class="${MODAL_DOM_ID}-confirm">Exportar</button>
143372
+ </div>
143373
+ </div>
143374
+ `;
143375
+ document.body.appendChild(backdrop);
143376
+ _activeBackdrop = backdrop;
143377
+ const modal = backdrop.querySelector(`.${MODAL_DOM_ID}`);
143378
+ const fmtPdfOnly = modal.querySelector("[data-fmt-pdf-only]");
143379
+ const confirmBtn = modal.querySelector(`.${MODAL_DOM_ID}-confirm`);
143380
+ const cancelBtn = modal.querySelector(`.${MODAL_DOM_ID}-cancel`);
143381
+ const fmtRadios = Array.from(modal.querySelectorAll('input[name="fmt"]'));
143382
+ const updatePdfFieldset = () => {
143383
+ const isPdf = fmtRadios.find((r) => r.checked)?.value === "pdf";
143384
+ if (fmtPdfOnly) {
143385
+ const inputs = Array.from(fmtPdfOnly.querySelectorAll("input"));
143386
+ inputs.forEach((i) => {
143387
+ i.disabled = !isPdf;
143388
+ });
143389
+ fmtPdfOnly.style.opacity = isPdf ? "1" : "0.5";
143390
+ }
143391
+ const enabled = !isPdf || modal.querySelector('input[name="lvl-summary"]')?.checked || modal.querySelector('input[name="lvl-consolidated"]')?.checked || modal.querySelector('input[name="lvl-detailed"]')?.checked;
143392
+ confirmBtn.disabled = !enabled;
143393
+ };
143394
+ fmtRadios.forEach((r) => r.addEventListener("change", updatePdfFieldset));
143395
+ modal.querySelectorAll('input[name^="lvl-"]').forEach((cb) => cb.addEventListener("change", updatePdfFieldset));
143396
+ updatePdfFieldset();
143397
+ const close = () => closeExportModal();
143398
+ cancelBtn.addEventListener("click", close);
143399
+ backdrop.addEventListener("click", (e) => {
143400
+ if (e.target === backdrop) close();
143401
+ });
143402
+ const onEsc = (e) => {
143403
+ if (e.key === "Escape") {
143404
+ close();
143405
+ window.removeEventListener("keydown", onEsc);
143406
+ }
143407
+ };
143408
+ window.addEventListener("keydown", onEsc);
143409
+ confirmBtn.addEventListener("click", () => {
143410
+ const fmt2 = fmtRadios.find((r) => r.checked)?.value;
143411
+ const scope = modal.querySelector('input[name="scope"]:checked')?.value ?? "current-tab";
143412
+ const levels = [];
143413
+ if (fmt2 === "pdf") {
143414
+ if (modal.querySelector('input[name="lvl-summary"]')?.checked) levels.push("summary");
143415
+ if (modal.querySelector('input[name="lvl-consolidated"]')?.checked) levels.push("consolidated");
143416
+ if (modal.querySelector('input[name="lvl-detailed"]')?.checked) levels.push("detailed");
143417
+ }
143418
+ const opts = {
143419
+ format: fmt2,
143420
+ levels: fmt2 === "pdf" ? levels : void 0,
143421
+ scope
143422
+ };
143423
+ try {
143424
+ options.onExport(opts);
143425
+ } catch (err) {
143426
+ (options.logger ?? console).warn("[ExportModal] onExport threw:", err);
143427
+ }
143428
+ close();
143429
+ });
143430
+ setTimeout(() => {
143431
+ const focusable = modal.querySelector('input[name="fmt"]:checked');
143432
+ focusable?.focus();
143433
+ }, 0);
143434
+ return close;
143435
+ }
143436
+ function closeExportModal() {
143437
+ if (_activeBackdrop && _activeBackdrop.parentNode) {
143438
+ _activeBackdrop.parentNode.removeChild(_activeBackdrop);
143439
+ }
143440
+ _activeBackdrop = null;
143441
+ }
143442
+
143443
+ // src/components/header-annotations-panel/ExportCSV.ts
143444
+ var CSV_COLUMNS = [
143445
+ "identifier",
143446
+ "device_name",
143447
+ "device_label",
143448
+ "domain",
143449
+ "annotation_id",
143450
+ "type",
143451
+ "importance",
143452
+ "status",
143453
+ "text",
143454
+ "created_at",
143455
+ "created_by_email",
143456
+ "due_date",
143457
+ "acknowledged"
143458
+ ];
143459
+ function csvEscape(value) {
143460
+ if (value == null) return "";
143461
+ const s = String(value);
143462
+ if (s === "") return "";
143463
+ const needsQuoting = /[",\n\r;]/.test(s);
143464
+ if (!needsQuoting) return s;
143465
+ return '"' + s.replace(/"/g, '""') + '"';
143466
+ }
143467
+ function buildRow2(device, ann) {
143468
+ return {
143469
+ identifier: device.identifier ?? "",
143470
+ device_name: device.name ?? "",
143471
+ device_label: device.label ?? "",
143472
+ domain: device.domain ?? "",
143473
+ annotation_id: ann.id ?? "",
143474
+ type: ann.type ?? "",
143475
+ importance: String(ann.importance ?? ""),
143476
+ status: ann.status ?? "",
143477
+ text: ann.text ?? "",
143478
+ created_at: ann.createdAt ?? "",
143479
+ created_by_email: ann.createdBy?.email ?? "",
143480
+ due_date: ann.dueDate ?? "",
143481
+ acknowledged: ann.acknowledged ? "true" : "false"
143482
+ };
143483
+ }
143484
+ function buildAnnotationsCsv(devices, options = {}) {
143485
+ const includeArchived = options.includeArchived ?? false;
143486
+ const lines = [];
143487
+ lines.push(CSV_COLUMNS.join(","));
143488
+ for (const device of devices) {
143489
+ for (const ann of device.annotations) {
143490
+ if (!includeArchived && ann.status === "archived") continue;
143491
+ const row = buildRow2(device, ann);
143492
+ lines.push(CSV_COLUMNS.map((c) => csvEscape(row[c])).join(","));
143493
+ }
143494
+ }
143495
+ return "\uFEFF" + lines.join("\r\n") + "\r\n";
143496
+ }
143497
+ function buildExportFilename(customerName, ext, now = /* @__PURE__ */ new Date()) {
143498
+ const safeCustomer = (customerName || "customer").normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").toLowerCase() || "customer";
143499
+ const yyyy = now.getFullYear();
143500
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
143501
+ const dd = String(now.getDate()).padStart(2, "0");
143502
+ const hh = String(now.getHours()).padStart(2, "0");
143503
+ const mi = String(now.getMinutes()).padStart(2, "0");
143504
+ return `anotacoes_${safeCustomer}_${yyyy}${mm}${dd}_${hh}${mi}.${ext}`;
143505
+ }
143506
+ function downloadTextFile(filename, content, mimeType) {
143507
+ if (typeof document === "undefined" || typeof Blob === "undefined") return;
143508
+ const blob = new Blob([content], { type: mimeType + ";charset=utf-8" });
143509
+ const url = URL.createObjectURL(blob);
143510
+ const a = document.createElement("a");
143511
+ a.href = url;
143512
+ a.download = filename;
143513
+ a.style.display = "none";
143514
+ document.body.appendChild(a);
143515
+ a.click();
143516
+ document.body.removeChild(a);
143517
+ setTimeout(() => URL.revokeObjectURL(url), 0);
143518
+ }
143519
+ function exportAnnotationsCsv(devices, options = {}) {
143520
+ const filename = buildExportFilename(options.customerName, "csv");
143521
+ const content = buildAnnotationsCsv(devices, { includeArchived: options.includeArchived });
143522
+ downloadTextFile(filename, content, "text/csv");
143523
+ return { filename, content };
143524
+ }
143525
+
143526
+ // src/components/header-annotations-panel/ExportPDF.ts
143527
+ var import_jspdf4 = require("jspdf");
143528
+ var PAGE_W = 210;
143529
+ var PAGE_H = 297;
143530
+ var MARGIN_X = 14;
143531
+ var MARGIN_TOP = 14;
143532
+ var MARGIN_BOTTOM = 18;
143533
+ var LINE_H = 5;
143534
+ function exportAnnotationsPdf(devices, options) {
143535
+ if (!options.levels || options.levels.length === 0) {
143536
+ throw new Error("exportAnnotationsPdf: at least one level required");
143537
+ }
143538
+ const includeArchived = options.includeArchived ?? false;
143539
+ const visibleDevices2 = _filterVisible(devices, includeArchived);
143540
+ const filename = buildExportFilename(options.customerName, "pdf", options.now);
143541
+ const doc = new import_jspdf4.jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
143542
+ const cursor = { y: MARGIN_TOP };
143543
+ _renderCover(doc, cursor, options, visibleDevices2);
143544
+ if (options.levels.includes("summary")) {
143545
+ _newPageIfNeeded(doc, cursor, 60);
143546
+ _renderSummary(doc, cursor, visibleDevices2);
143547
+ }
143548
+ if (options.levels.includes("consolidated")) {
143549
+ _newPage(doc, cursor);
143550
+ _renderConsolidated(doc, cursor, visibleDevices2);
143551
+ }
143552
+ if (options.levels.includes("detailed")) {
143553
+ _newPage(doc, cursor);
143554
+ _renderDetailed(doc, cursor, visibleDevices2);
143555
+ }
143556
+ _renderFooterAllPages(doc, options.customerName);
143557
+ doc.save(filename);
143558
+ return filename;
143559
+ }
143560
+ function _renderCover(doc, cursor, options, devices) {
143561
+ const total = _countAnnotations(devices);
143562
+ const customer = options.customerName || "customer";
143563
+ doc.setFontSize(20);
143564
+ doc.setTextColor(76, 58, 172);
143565
+ doc.text(options.title || "Anota\xE7\xF5es Operacionais", MARGIN_X, cursor.y + 8);
143566
+ cursor.y += 16;
143567
+ doc.setFontSize(11);
143568
+ doc.setTextColor(80);
143569
+ doc.text(`Cliente: ${customer}`, MARGIN_X, cursor.y);
143570
+ cursor.y += LINE_H;
143571
+ doc.text(
143572
+ `Gerado em: ${(options.now ?? /* @__PURE__ */ new Date()).toLocaleString("pt-BR")}`,
143573
+ MARGIN_X,
143574
+ cursor.y
143575
+ );
143576
+ cursor.y += LINE_H;
143577
+ doc.text(`Total de anota\xE7\xF5es: ${total} (em ${devices.length} devices)`, MARGIN_X, cursor.y);
143578
+ cursor.y += LINE_H + 4;
143579
+ doc.setDrawColor(108, 92, 231);
143580
+ doc.setLineWidth(0.4);
143581
+ doc.line(MARGIN_X, cursor.y, PAGE_W - MARGIN_X, cursor.y);
143582
+ cursor.y += 6;
143583
+ }
143584
+ function _renderSummary(doc, cursor, devices) {
143585
+ _sectionHeader(doc, cursor, "Sum\xE1rio");
143586
+ const byType = _countByType(devices);
143587
+ const byImportance = _countByImportance(devices);
143588
+ const byDomain = _countByDomain(devices);
143589
+ doc.setFontSize(11);
143590
+ doc.setTextColor(40);
143591
+ const lines = [
143592
+ `Por tipo:`,
143593
+ ` \u2022 Pend\xEAncia: ${byType.pending}`,
143594
+ ` \u2022 Manuten\xE7\xE3o: ${byType.maintenance}`,
143595
+ ` \u2022 Observa\xE7\xE3o: ${byType.observation}`,
143596
+ ` \u2022 Atividade: ${byType.activity}`,
143597
+ ``,
143598
+ `Por import\xE2ncia:`,
143599
+ ` \u2022 Cr\xEDtica (5): ${byImportance[5]}`,
143600
+ ` \u2022 Alta (4): ${byImportance[4]}`,
143601
+ ` \u2022 M\xE9dia (3): ${byImportance[3]}`,
143602
+ ` \u2022 Baixa (2): ${byImportance[2]}`,
143603
+ ` \u2022 Muito baixa (1): ${byImportance[1]}`,
143604
+ ``,
143605
+ `Por dom\xEDnio:`,
143606
+ ` \u2022 Energia: ${byDomain.energy}`,
143607
+ ` \u2022 \xC1gua: ${byDomain.water}`,
143608
+ ` \u2022 Temperatura: ${byDomain.temperature}`,
143609
+ ` \u2022 Indeterminado: ${byDomain.unknown}`
143610
+ ];
143611
+ for (const ln of lines) {
143612
+ _newPageIfNeeded(doc, cursor, LINE_H);
143613
+ doc.text(ln, MARGIN_X, cursor.y);
143614
+ cursor.y += LINE_H;
143615
+ }
143616
+ }
143617
+ function _renderConsolidated(doc, cursor, devices) {
143618
+ _sectionHeader(doc, cursor, "Consolidado por device");
143619
+ const sorted = devices.slice().filter((d) => d.annotations.length > 0).sort((a, b) => b.annotations.length - a.annotations.length);
143620
+ doc.setFontSize(10);
143621
+ doc.setTextColor(40);
143622
+ for (const d of sorted) {
143623
+ _newPageIfNeeded(doc, cursor, LINE_H * 2);
143624
+ doc.setFont(void 0, "bold");
143625
+ const ident = d.identifier ? `[${d.identifier}] ` : "";
143626
+ doc.text(`${ident}${d.label || d.name} (${d.annotations.length})`, MARGIN_X, cursor.y);
143627
+ cursor.y += LINE_H;
143628
+ doc.setFont(void 0, "normal");
143629
+ const last = d.annotations.reduce(
143630
+ (acc, a) => !acc || a.createdAt > acc.createdAt ? a : acc,
143631
+ null
143632
+ );
143633
+ if (last) {
143634
+ const txt = ` \xDAltima: "${_truncate(last.text, 90)}" \u2014 ${last.createdBy?.name ?? ""} \u2014 ${_formatDate(last.createdAt)}`;
143635
+ const wrapped = doc.splitTextToSize(txt, PAGE_W - MARGIN_X * 2);
143636
+ for (const line of wrapped) {
143637
+ _newPageIfNeeded(doc, cursor, LINE_H);
143638
+ doc.text(line, MARGIN_X, cursor.y);
143639
+ cursor.y += LINE_H;
143640
+ }
143641
+ }
143642
+ cursor.y += 1;
143643
+ }
143644
+ }
143645
+ function _renderDetailed(doc, cursor, devices) {
143646
+ _sectionHeader(doc, cursor, "Detalhado por anota\xE7\xE3o");
143647
+ doc.setFontSize(10);
143648
+ doc.setTextColor(40);
143649
+ for (const d of devices) {
143650
+ if (d.annotations.length === 0) continue;
143651
+ _newPageIfNeeded(doc, cursor, LINE_H * 2);
143652
+ doc.setFont(void 0, "bold");
143653
+ const ident = d.identifier ? `[${d.identifier}] ` : "";
143654
+ doc.text(`${ident}${d.label || d.name}`, MARGIN_X, cursor.y);
143655
+ cursor.y += LINE_H;
143656
+ doc.setFont(void 0, "normal");
143657
+ for (const a of d.annotations) {
143658
+ const header = ` ${a.type.toUpperCase()} \xB7 Import\xE2ncia ${a.importance} \xB7 ${a.status}`;
143659
+ _newPageIfNeeded(doc, cursor, LINE_H * 3);
143660
+ doc.text(header, MARGIN_X, cursor.y);
143661
+ cursor.y += LINE_H;
143662
+ const wrapped = doc.splitTextToSize(` "${a.text}"`, PAGE_W - MARGIN_X * 2);
143663
+ for (const line of wrapped) {
143664
+ _newPageIfNeeded(doc, cursor, LINE_H);
143665
+ doc.text(line, MARGIN_X, cursor.y);
143666
+ cursor.y += LINE_H;
143667
+ }
143668
+ const meta = ` por ${a.createdBy?.name ?? "\u2014"} (${a.createdBy?.email ?? "\u2014"}) em ${_formatDate(a.createdAt)}${a.dueDate ? " \xB7 vence " + _formatDate(a.dueDate) : ""}`;
143669
+ const metaWrapped = doc.splitTextToSize(meta, PAGE_W - MARGIN_X * 2);
143670
+ for (const line of metaWrapped) {
143671
+ _newPageIfNeeded(doc, cursor, LINE_H);
143672
+ doc.text(line, MARGIN_X, cursor.y);
143673
+ cursor.y += LINE_H;
143674
+ }
143675
+ cursor.y += 2;
143676
+ }
143677
+ cursor.y += 2;
143678
+ }
143679
+ }
143680
+ function _renderFooterAllPages(doc, customerName) {
143681
+ const pageCount = doc.internal.getNumberOfPages();
143682
+ for (let p = 1; p <= pageCount; p++) {
143683
+ doc.setPage(p);
143684
+ doc.setFontSize(8);
143685
+ doc.setTextColor(120);
143686
+ doc.text(
143687
+ `MyIO \xB7 ${customerName || ""} \xB7 Documento confidencial \u2014 cont\xE9m dados operacionais.`,
143688
+ MARGIN_X,
143689
+ PAGE_H - 8
143690
+ );
143691
+ doc.text(`P\xE1gina ${p} / ${pageCount}`, PAGE_W - MARGIN_X - 25, PAGE_H - 8);
143692
+ }
143693
+ }
143694
+ function _sectionHeader(doc, cursor, title) {
143695
+ doc.setFontSize(14);
143696
+ doc.setTextColor(76, 58, 172);
143697
+ doc.text(title, MARGIN_X, cursor.y);
143698
+ cursor.y += 7;
143699
+ doc.setDrawColor(200);
143700
+ doc.setLineWidth(0.2);
143701
+ doc.line(MARGIN_X, cursor.y - 3, PAGE_W - MARGIN_X, cursor.y - 3);
143702
+ }
143703
+ function _newPage(doc, cursor) {
143704
+ doc.addPage();
143705
+ cursor.y = MARGIN_TOP;
143706
+ }
143707
+ function _newPageIfNeeded(doc, cursor, required) {
143708
+ if (cursor.y + required > PAGE_H - MARGIN_BOTTOM) {
143709
+ _newPage(doc, cursor);
143710
+ }
143711
+ }
143712
+ function _filterVisible(devices, includeArchived) {
143713
+ if (includeArchived) return devices;
143714
+ return devices.map((d) => ({
143715
+ ...d,
143716
+ annotations: d.annotations.filter((a) => a.status !== "archived")
143717
+ })).filter((d) => d.annotations.length > 0);
143718
+ }
143719
+ function _countAnnotations(devices) {
143720
+ return devices.reduce((acc, d) => acc + d.annotations.length, 0);
143721
+ }
143722
+ function _countByType(devices) {
143723
+ const acc = { observation: 0, pending: 0, maintenance: 0, activity: 0 };
143724
+ for (const d of devices) {
143725
+ for (const a of d.annotations) {
143726
+ if (acc[a.type] !== void 0) acc[a.type]++;
143727
+ }
143728
+ }
143729
+ return acc;
143730
+ }
143731
+ function _countByImportance(devices) {
143732
+ const acc = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
143733
+ for (const d of devices) {
143734
+ for (const a of d.annotations) {
143735
+ const lv = a.importance;
143736
+ if (acc[lv] !== void 0) acc[lv]++;
143737
+ }
143738
+ }
143739
+ return acc;
143740
+ }
143741
+ function _countByDomain(devices) {
143742
+ const acc = { energy: 0, water: 0, temperature: 0, unknown: 0 };
143743
+ for (const d of devices) {
143744
+ acc[d.domain] = (acc[d.domain] ?? 0) + d.annotations.length;
143745
+ }
143746
+ return acc;
143747
+ }
143748
+ function _truncate(s, max) {
143749
+ const t = String(s ?? "");
143750
+ return t.length > max ? t.slice(0, max - 1) + "\u2026" : t;
143751
+ }
143752
+ function _formatDate(iso) {
143753
+ if (!iso) return "\u2014";
143754
+ const d = new Date(iso);
143755
+ if (isNaN(d.getTime())) return iso;
143756
+ return d.toLocaleString("pt-BR");
143757
+ }
143758
+
143759
+ // src/components/header-annotations-panel/HeaderAnnotationsPanel.ts
143760
+ var TABS = [
143761
+ { id: "identifier", label: "Por Identificador" },
143762
+ { id: "device", label: "Por Device" },
143763
+ { id: "domain", label: "Por Dom\xEDnio" }
143764
+ ];
143765
+ var TAB_STORAGE_KEY = "myio.annotations.activeTab";
143766
+ var SORT_STORAGE_KEY = "myio.annotations.sortBy";
143767
+ var SEARCH_DEBOUNCE_MS = 250;
143768
+ var PANEL_DOM_ID = "myio-annotations-panel";
143769
+ var HeaderAnnotationsPanel = class {
143770
+ root = null;
143771
+ anchorButton = null;
143772
+ activeTab = "identifier";
143773
+ sortBy = DEFAULT_SORT;
143774
+ filter = createDefaultFilter();
143775
+ isOpen = false;
143776
+ // RFC-0203 M6 — Tooltip behaviors state
143777
+ isPinned = false;
143778
+ isMaximized = false;
143779
+ isDragging = false;
143780
+ dragOffset = { x: 0, y: 0 };
143781
+ vlist = null;
143782
+ opts;
143783
+ // Bound listeners stored for removal
143784
+ _onEscKey;
143785
+ _onClickOutside;
143786
+ _onAnnotationsRefreshed;
143787
+ _onDragMove;
143788
+ _onDragEnd;
143789
+ _onFocusTrap;
143790
+ // Debounce timer for search input
143791
+ _searchDebounceTimer = null;
143792
+ constructor(options = {}) {
143793
+ this.opts = {
143794
+ getOrchestrator: options.getOrchestrator ?? (() => (typeof window !== "undefined" ? window.AnnotationServiceOrchestrator : null) ?? null),
143795
+ logger: options.logger ?? console
143796
+ };
143797
+ this.activeTab = this._loadActiveTab();
143798
+ this.sortBy = this._loadSortBy();
143799
+ this._onEscKey = (e) => {
143800
+ if (e.key === "Escape" && this.isOpen) this.hide();
143801
+ };
143802
+ this._onClickOutside = (e) => {
143803
+ if (!this.isOpen || !this.root) return;
143804
+ if (this.isPinned) return;
143805
+ const target = e.target;
143806
+ if (!target) return;
143807
+ if (this.root.contains(target)) return;
143808
+ if (this.anchorButton && this.anchorButton.contains(target)) return;
143809
+ this.hide();
143810
+ };
143811
+ this._onAnnotationsRefreshed = () => {
143812
+ if (this.isOpen) this._render();
143813
+ };
143814
+ this._onDragMove = (e) => {
143815
+ if (!this.isDragging || !this.root) return;
143816
+ e.preventDefault();
143817
+ const left = e.clientX - this.dragOffset.x;
143818
+ const top = e.clientY - this.dragOffset.y;
143819
+ const maxLeft = window.innerWidth - 80;
143820
+ const maxTop = window.innerHeight - 40;
143821
+ this.root.style.left = `${Math.max(0, Math.min(left, maxLeft))}px`;
143822
+ this.root.style.top = `${Math.max(0, Math.min(top, maxTop))}px`;
143823
+ };
143824
+ this._onDragEnd = () => {
143825
+ if (!this.isDragging) return;
143826
+ this.isDragging = false;
143827
+ if (this.root) this.root.classList.remove("is-dragging");
143828
+ window.removeEventListener("mousemove", this._onDragMove);
143829
+ window.removeEventListener("mouseup", this._onDragEnd);
143830
+ };
143831
+ this._onFocusTrap = (e) => {
143832
+ if (!this.isMaximized || !this.root || e.key !== "Tab") return;
143833
+ const focusables = Array.from(
143834
+ this.root.querySelectorAll(
143835
+ 'button:not([disabled]), [tabindex="0"], input:not([disabled]), select:not([disabled])'
143836
+ )
143837
+ ).filter((el2) => el2.offsetParent !== null);
143838
+ if (focusables.length === 0) return;
143839
+ const first = focusables[0];
143840
+ const last = focusables[focusables.length - 1];
143841
+ const active = document.activeElement;
143842
+ if (e.shiftKey && active === first) {
143843
+ e.preventDefault();
143844
+ last.focus();
143845
+ } else if (!e.shiftKey && active === last) {
143846
+ e.preventDefault();
143847
+ first.focus();
143848
+ }
143849
+ };
143850
+ }
143851
+ // ── Lifecycle ──────────────────────────────────────────────────────────
143852
+ /** Render + position + show. Idempotent. */
143853
+ show(anchorButton) {
143854
+ injectStylesOnce();
143855
+ this.anchorButton = anchorButton;
143856
+ if (!this.root) this.root = this._createRoot();
143857
+ this._render();
143858
+ this._position();
143859
+ this.root.style.display = "";
143860
+ this.root.setAttribute("aria-hidden", "false");
143861
+ this.isOpen = true;
143862
+ this._bindWindowListeners();
143863
+ const firstTab = this.root.querySelector('.myio-annotations-tab[aria-selected="true"]');
143864
+ if (firstTab) firstTab.focus();
143865
+ }
143866
+ /** Hide the panel without destroying it. */
143867
+ hide() {
143868
+ if (!this.root) return;
143869
+ this.root.style.display = "none";
143870
+ this.root.setAttribute("aria-hidden", "true");
143871
+ this.isOpen = false;
143872
+ this._unbindWindowListeners();
143873
+ if (this.anchorButton) {
143874
+ try {
143875
+ this.anchorButton.focus();
143876
+ } catch {
143877
+ }
143878
+ }
143879
+ }
143880
+ /** Toggle convenience. */
143881
+ toggle(anchorButton) {
143882
+ if (this.isOpen) this.hide();
143883
+ else this.show(anchorButton);
143884
+ }
143885
+ /** Remove the panel DOM + listeners. After this, `show()` recreates. */
143886
+ destroy() {
143887
+ this._unbindWindowListeners();
143888
+ if (this.vlist) {
143889
+ this.vlist.destroy();
143890
+ this.vlist = null;
143891
+ }
143892
+ if (this.root && this.root.parentNode) this.root.parentNode.removeChild(this.root);
143893
+ this.root = null;
143894
+ this.anchorButton = null;
143895
+ this.isOpen = false;
143896
+ this.isPinned = false;
143897
+ this.isMaximized = false;
143898
+ this.isDragging = false;
143899
+ }
143900
+ /** Test/inspection helper. */
143901
+ getActiveTab() {
143902
+ return this.activeTab;
143903
+ }
143904
+ /** Test helper — programmatic tab switch (also persists to sessionStorage). */
143905
+ setActiveTab(tab) {
143906
+ if (!TABS.some((t) => t.id === tab)) return;
143907
+ this.activeTab = tab;
143908
+ this._saveActiveTab(tab);
143909
+ if (this.isOpen) this._render();
143910
+ }
143911
+ /** Test/inspection helper. */
143912
+ getSortBy() {
143913
+ return this.sortBy;
143914
+ }
143915
+ /** Test helper — programmatic sort change (persists). */
143916
+ setSortBy(sort) {
143917
+ if (!SORT_OPTIONS.some((o) => o.key === sort)) return;
143918
+ this.sortBy = sort;
143919
+ this._saveSortBy(sort);
143920
+ if (this.isOpen) this._render();
143921
+ }
143922
+ /** Test/inspection helper. */
143923
+ getFilter() {
143924
+ return this.filter;
143925
+ }
143926
+ /** Test helper — programmatic filter merge. */
143927
+ setFilter(patch) {
143928
+ this.filter = { ...this.filter, ...patch };
143929
+ if (this.isOpen) this._render();
143930
+ }
143931
+ // ── Internals ──────────────────────────────────────────────────────────
143932
+ _loadActiveTab() {
143933
+ try {
143934
+ if (typeof sessionStorage === "undefined") return "identifier";
143935
+ const stored = sessionStorage.getItem(TAB_STORAGE_KEY);
143936
+ if (stored && TABS.some((t) => t.id === stored)) return stored;
143937
+ } catch {
143938
+ }
143939
+ return "identifier";
143940
+ }
143941
+ _saveActiveTab(tab) {
143942
+ try {
143943
+ if (typeof sessionStorage !== "undefined") {
143944
+ sessionStorage.setItem(TAB_STORAGE_KEY, tab);
143945
+ }
143946
+ } catch {
143947
+ }
143948
+ }
143949
+ _loadSortBy() {
143950
+ try {
143951
+ if (typeof sessionStorage === "undefined") return DEFAULT_SORT;
143952
+ const stored = sessionStorage.getItem(SORT_STORAGE_KEY);
143953
+ if (stored && SORT_OPTIONS.some((o) => o.key === stored)) return stored;
143954
+ } catch {
143955
+ }
143956
+ return DEFAULT_SORT;
143957
+ }
143958
+ _saveSortBy(sort) {
143959
+ try {
143960
+ if (typeof sessionStorage !== "undefined") {
143961
+ sessionStorage.setItem(SORT_STORAGE_KEY, sort);
143962
+ }
143963
+ } catch {
143964
+ }
143965
+ }
143966
+ _createRoot() {
143967
+ const el2 = document.createElement("div");
143968
+ el2.id = PANEL_DOM_ID;
143969
+ el2.className = "myio-annotations-panel";
143970
+ el2.setAttribute("role", "dialog");
143971
+ el2.setAttribute("aria-modal", "false");
143972
+ el2.setAttribute("aria-labelledby", PANEL_DOM_ID + "-title");
143973
+ el2.setAttribute("aria-hidden", "true");
143974
+ el2.style.display = "none";
143975
+ document.body.appendChild(el2);
143976
+ return el2;
143977
+ }
143978
+ _position() {
143979
+ if (!this.root || !this.anchorButton) return;
143980
+ const rect = this.anchorButton.getBoundingClientRect();
143981
+ const panelWidth = Math.min(720, window.innerWidth * 0.9);
143982
+ let left = rect.left;
143983
+ if (left + panelWidth > window.innerWidth - 8) {
143984
+ left = Math.max(8, window.innerWidth - panelWidth - 8);
143985
+ }
143986
+ const top = rect.bottom + 8;
143987
+ this.root.style.left = `${left}px`;
143988
+ this.root.style.top = `${top}px`;
143989
+ this.root.style.width = `${panelWidth}px`;
143990
+ }
143991
+ _bindWindowListeners() {
143992
+ if (typeof window === "undefined") return;
143993
+ window.addEventListener("keydown", this._onEscKey);
143994
+ setTimeout(() => {
143995
+ window.addEventListener("mousedown", this._onClickOutside, true);
143996
+ }, 0);
143997
+ window.addEventListener("myio:annotations-refreshed", this._onAnnotationsRefreshed);
143998
+ window.addEventListener("keydown", this._onFocusTrap);
143999
+ }
144000
+ _unbindWindowListeners() {
144001
+ if (typeof window === "undefined") return;
144002
+ window.removeEventListener("keydown", this._onEscKey);
144003
+ window.removeEventListener("mousedown", this._onClickOutside, true);
144004
+ window.removeEventListener("myio:annotations-refreshed", this._onAnnotationsRefreshed);
144005
+ window.removeEventListener("keydown", this._onFocusTrap);
144006
+ window.removeEventListener("mousemove", this._onDragMove);
144007
+ window.removeEventListener("mouseup", this._onDragEnd);
144008
+ }
144009
+ _render() {
144010
+ if (!this.root) return;
144011
+ if (this.vlist) {
144012
+ this.vlist.destroy();
144013
+ this.vlist = null;
144014
+ }
144015
+ const orch = this.opts.getOrchestrator();
144016
+ this.root.innerHTML = this._renderHTML(orch);
144017
+ this._bindInteractiveElements();
144018
+ this._maybeActivateVirtualScroll(orch);
144019
+ }
144020
+ /**
144021
+ * AC-28 — Replace the body's flat innerHTML with a VirtualList when total
144022
+ * item count exceeds the threshold (100). Below the threshold, the static
144023
+ * render path stays as-is for simplicity.
144024
+ */
144025
+ _maybeActivateVirtualScroll(orch) {
144026
+ if (!this.root) return;
144027
+ const body = this.root.querySelector("#myio-anno-body");
144028
+ if (!body) return;
144029
+ if (!orch) return;
144030
+ const rawGroups = orch.getGroups(this.activeTab, this.filter);
144031
+ const groups = sortGroups(rawGroups, this.sortBy);
144032
+ const totalItems = countAnnotationsInGroups(groups);
144033
+ if (!shouldVirtualize(totalItems)) return;
144034
+ const term = this.filter.searchTerm || "";
144035
+ const rows = [];
144036
+ for (const g of groups) {
144037
+ const isNoIdentifier = this.activeTab === "identifier" && g.key === "Sem Identificador";
144038
+ const groupClass = isNoIdentifier ? "myio-annotations-group myio-annotations-group--no-id" : "myio-annotations-group";
144039
+ rows.push({
144040
+ key: `g:${g.key}`,
144041
+ height: 40,
144042
+ // group header
144043
+ render: () => `
144044
+ <header class="myio-annotations-group-header ${groupClass}">
144045
+ ${g.icon ? `<span class="myio-annotations-group-icon" aria-hidden="true">${escapeHtml6(g.icon)}</span>` : ""}
144046
+ <span class="myio-annotations-group-label">${escapeHtml6(g.label)}</span>
144047
+ <span class="myio-annotations-group-count">${g.totalAnnotations}</span>
144048
+ </header>`
144049
+ });
144050
+ for (const d of g.devices) {
144051
+ for (const a of d.annotations) {
144052
+ rows.push({
144053
+ key: `i:${d.deviceId}:${a.id}`,
144054
+ height: 78,
144055
+ // item card (text + meta + badges)
144056
+ render: () => renderAnnotationItemCard(d, a, term)
144057
+ });
144058
+ }
144059
+ }
144060
+ }
144061
+ this.vlist = new VirtualList({ container: body, rows });
144062
+ body.addEventListener("click", this._onBodyClickDelegated);
144063
+ }
144064
+ /** Delegated click handler for items rendered by the VirtualList. */
144065
+ _onBodyClickDelegated = (e) => {
144066
+ const target = e.target;
144067
+ if (!target) return;
144068
+ const item = target.closest(".myio-annotations-item");
144069
+ if (item) this._handleItemClick(item);
144070
+ };
144071
+ _renderHTML(orch) {
144072
+ const totalAllUnfiltered = orch?.getTotalCount?.() ?? 0;
144073
+ const pending = orch?.getPendingCount?.() ?? 0;
144074
+ const overdue = orch?.getOverdueCount?.() ?? 0;
144075
+ const tabsHtml = TABS.map(
144076
+ (t) => `<button
144077
+ class="myio-annotations-tab"
144078
+ type="button"
144079
+ role="tab"
144080
+ id="myio-anno-tab-${t.id}"
144081
+ data-tab="${t.id}"
144082
+ aria-selected="${t.id === this.activeTab}"
144083
+ aria-controls="myio-anno-body"
144084
+ tabindex="${t.id === this.activeTab ? 0 : -1}"
144085
+ >${t.label}</button>`
144086
+ ).join("");
144087
+ const rawGroups = orch ? orch.getGroups(this.activeTab, this.filter) : [];
144088
+ const groups = sortGroups(rawGroups, this.sortBy);
144089
+ const filteredCount = countAnnotationsInGroups(groups);
144090
+ const bodyHtml = this._renderBody(groups);
144091
+ return `
144092
+ <div class="myio-annotations-panel-header" data-region="header" data-drag-handle>
144093
+ <h2 class="myio-annotations-panel-title" id="${PANEL_DOM_ID}-title">
144094
+ <span class="myio-annotations-icon" aria-hidden="true">\u{1F4CB}</span>Anota\xE7\xF5es
144095
+ </h2>
144096
+ <span class="myio-annotations-panel-meta">${totalAllUnfiltered} ativas \xB7 ${pending} pendentes \xB7 ${overdue} vencidas</span>
144097
+ <div class="myio-annotations-panel-actions">
144098
+ <button
144099
+ class="myio-annotations-panel-action ${this.isMaximized ? "is-active" : ""}"
144100
+ type="button"
144101
+ data-action="maximize"
144102
+ title="${this.isMaximized ? "Restaurar" : "Maximizar"}"
144103
+ aria-label="${this.isMaximized ? "Restaurar tamanho" : "Maximizar painel"}"
144104
+ aria-pressed="${this.isMaximized}"
144105
+ >${this.isMaximized ? "\u{1F5D7}" : "\u2922"}</button>
144106
+ <button
144107
+ class="myio-annotations-panel-action ${this.isPinned ? "is-active" : ""}"
144108
+ type="button"
144109
+ data-action="pin"
144110
+ title="${this.isPinned ? "Desafixar" : "Afixar painel"}"
144111
+ aria-label="${this.isPinned ? "Desafixar painel" : "Afixar painel"}"
144112
+ aria-pressed="${this.isPinned}"
144113
+ >\u{1F4CC}</button>
144114
+ <button class="myio-annotations-panel-action" type="button" data-action="close" title="Fechar" aria-label="Fechar painel">\u2715</button>
144115
+ </div>
144116
+ </div>
144117
+ <div class="myio-annotations-tabs" role="tablist" aria-label="Modo de agrupamento">${tabsHtml}</div>
144118
+ ${this._renderToolbar(filteredCount, totalAllUnfiltered)}
144119
+ <div class="myio-annotations-body" id="myio-anno-body" role="tabpanel" aria-labelledby="myio-anno-tab-${this.activeTab}">${bodyHtml}</div>
144120
+ <div class="myio-annotations-panel-footer">
144121
+ <button class="myio-annotations-panel-footer-action" type="button" data-action="export">\u{1F4E5} Exportar\u2026</button>
144122
+ <span class="myio-annotations-panel-footer-meta">RFC-0203 \xB7 ${groups.length} grupos \xB7 ${filteredCount} anota\xE7\xF5es</span>
144123
+ <button class="myio-annotations-panel-footer-action" type="button" data-action="refresh">Atualizar \u21BB</button>
144124
+ </div>
144125
+ `;
144126
+ }
144127
+ _renderToolbar(filteredCount, totalCount) {
144128
+ const sortOptions = SORT_OPTIONS.map(
144129
+ (o) => `<option value="${o.key}" ${o.key === this.sortBy ? "selected" : ""}>${escapeHtml6(o.label)}</option>`
144130
+ ).join("");
144131
+ const term = escapeHtml6(this.filter.searchTerm || "");
144132
+ const filtersActiveCount = this._activeFilterCount();
144133
+ const filterIndicator = filtersActiveCount > 0 ? `<span class="myio-annotations-group-count">${filtersActiveCount}</span>` : "";
144134
+ const showingHtml = filteredCount === totalCount ? `${totalCount} anota\xE7\xF5es` : `${filteredCount} de ${totalCount} anota\xE7\xF5es`;
144135
+ return `
144136
+ <div class="myio-annotations-toolbar" data-region="toolbar">
144137
+ <div class="myio-annotations-toolbar-row">
144138
+ <label class="myio-annotations-toolbar-search">
144139
+ <span class="myio-annotations-search-icon" aria-hidden="true">\u{1F50D}</span>
144140
+ <input
144141
+ type="search"
144142
+ data-input="search"
144143
+ placeholder="Buscar identificador, device, texto\u2026"
144144
+ aria-label="Buscar anota\xE7\xF5es"
144145
+ value="${term}"
144146
+ />
144147
+ </label>
144148
+ <select class="myio-annotations-toolbar-sort" data-input="sort" aria-label="Ordena\xE7\xE3o">
144149
+ ${sortOptions}
144150
+ </select>
144151
+ <button
144152
+ class="myio-annotations-toolbar-filter-btn"
144153
+ type="button"
144154
+ data-action="toggle-filters"
144155
+ aria-expanded="false"
144156
+ aria-controls="myio-anno-filters"
144157
+ >\u2699 Filtros ${filterIndicator}</button>
144158
+ </div>
144159
+ <div class="myio-annotations-toolbar-row myio-annotations-toolbar-meta">
144160
+ <span class="myio-annotations-toolbar-count">${showingHtml}</span>
144161
+ </div>
144162
+ <div id="myio-anno-filters" class="myio-annotations-filters" hidden>${this._renderFilters()}</div>
144163
+ </div>`;
144164
+ }
144165
+ _renderFilters() {
144166
+ const typesHtml = FILTER_TYPE_OPTIONS.map(
144167
+ (t) => `<label class="myio-annotations-filter-chip ${this.filter.types.has(t.id) ? "is-on" : ""}">
144168
+ <input type="checkbox" data-filter="type" value="${t.id}" ${this.filter.types.has(t.id) ? "checked" : ""} />
144169
+ <span>${t.icon} ${escapeHtml6(t.label)}</span>
144170
+ </label>`
144171
+ ).join("");
144172
+ const statusHtml = FILTER_STATUS_OPTIONS.map(
144173
+ (s) => `<label class="myio-annotations-filter-chip ${this.filter.statuses.has(s.id) ? "is-on" : ""}">
144174
+ <input type="checkbox" data-filter="status" value="${s.id}" ${this.filter.statuses.has(s.id) ? "checked" : ""} />
144175
+ <span>${escapeHtml6(s.label)}</span>
144176
+ </label>`
144177
+ ).join("");
144178
+ const impHtml = [1, 2, 3, 4, 5].map(
144179
+ (lv) => `<label class="myio-annotations-filter-chip ${this.filter.importance.has(lv) ? "is-on" : ""}">
144180
+ <input type="checkbox" data-filter="importance" value="${lv}" ${this.filter.importance.has(lv) ? "checked" : ""} />
144181
+ <span>${lv}</span>
144182
+ </label>`
144183
+ ).join("");
144184
+ return `
144185
+ <div class="myio-annotations-filter-section">
144186
+ <div class="myio-annotations-filter-section-title">Tipo</div>
144187
+ <div class="myio-annotations-filter-chips">${typesHtml}</div>
144188
+ </div>
144189
+ <div class="myio-annotations-filter-section">
144190
+ <div class="myio-annotations-filter-section-title">Status</div>
144191
+ <div class="myio-annotations-filter-chips">${statusHtml}</div>
144192
+ </div>
144193
+ <div class="myio-annotations-filter-section">
144194
+ <div class="myio-annotations-filter-section-title">Import\xE2ncia</div>
144195
+ <div class="myio-annotations-filter-chips">${impHtml}</div>
144196
+ </div>
144197
+ <div class="myio-annotations-filter-section">
144198
+ <label class="myio-annotations-filter-chip ${this.filter.actionableOnly ? "is-on" : ""}">
144199
+ <input type="checkbox" data-filter="actionable" ${this.filter.actionableOnly ? "checked" : ""} />
144200
+ <span>Acion\xE1veis apenas (pendentes n\xE3o-arquivadas, com vencimento \u2264 7 dias ou sem vencimento)</span>
144201
+ </label>
144202
+ </div>
144203
+ <div class="myio-annotations-filter-actions">
144204
+ <button type="button" class="myio-annotations-panel-footer-action" data-action="clear-filters">Limpar</button>
144205
+ </div>`;
144206
+ }
144207
+ _activeFilterCount() {
144208
+ let n = 0;
144209
+ n += this.filter.types.size;
144210
+ n += this.filter.statuses.size;
144211
+ n += this.filter.importance.size;
144212
+ if (this.filter.actionableOnly) n += 1;
144213
+ return n;
144214
+ }
144215
+ _renderBody(groups) {
144216
+ if (!groups || groups.length === 0) {
144217
+ const hasFilters = this.filter.searchTerm || this._activeFilterCount() > 0;
144218
+ return `
144219
+ <div class="myio-annotations-empty">
144220
+ <div class="myio-annotations-empty-icon" aria-hidden="true">\u{1F4CB}</div>
144221
+ <div>${hasFilters ? "Nada encontrado para o filtro / busca atual." : "Nenhuma anota\xE7\xE3o ativa."}</div>
144222
+ </div>`;
144223
+ }
144224
+ return groups.map((g) => this._renderGroup(g)).join("\n");
144225
+ }
144226
+ _renderGroup(group) {
144227
+ const isNoIdentifier = this.activeTab === "identifier" && group.key === "Sem Identificador";
144228
+ const term = this.filter.searchTerm || "";
144229
+ const items = group.devices.flatMap(
144230
+ (d) => d.annotations.map((a) => renderAnnotationItemCard(d, a, term))
144231
+ );
144232
+ const groupClass = isNoIdentifier ? "myio-annotations-group myio-annotations-group--no-id" : "myio-annotations-group";
144233
+ return `
144234
+ <section class="${groupClass}">
144235
+ <header class="myio-annotations-group-header">
144236
+ ${group.icon ? `<span class="myio-annotations-group-icon" aria-hidden="true">${escapeHtml6(group.icon)}</span>` : ""}
144237
+ <span class="myio-annotations-group-label">${escapeHtml6(group.label)}</span>
144238
+ <span class="myio-annotations-group-count">${group.totalAnnotations}</span>
144239
+ </header>
144240
+ ${items.join("\n")}
144241
+ </section>`;
144242
+ }
144243
+ _bindInteractiveElements() {
144244
+ if (!this.root) return;
144245
+ const tabBtns = Array.from(this.root.querySelectorAll(".myio-annotations-tab"));
144246
+ tabBtns.forEach((btn) => {
144247
+ btn.addEventListener("click", () => {
144248
+ const tab = btn.getAttribute("data-tab");
144249
+ if (tab) this.setActiveTab(tab);
144250
+ });
144251
+ });
144252
+ const tablist = this.root.querySelector(".myio-annotations-tabs");
144253
+ if (tablist) {
144254
+ tablist.addEventListener("keydown", (e) => this._onTabKeydown(e, tabBtns));
144255
+ }
144256
+ const items = this.root.querySelectorAll(".myio-annotations-item");
144257
+ items.forEach((it) => {
144258
+ it.addEventListener("click", () => this._handleItemClick(it));
144259
+ });
144260
+ const closeBtn = this.root.querySelector('[data-action="close"]');
144261
+ if (closeBtn) closeBtn.addEventListener("click", () => this.hide());
144262
+ const pinBtn = this.root.querySelector('[data-action="pin"]');
144263
+ if (pinBtn) {
144264
+ pinBtn.addEventListener("click", () => {
144265
+ this.isPinned = !this.isPinned;
144266
+ if (this.isOpen) this._render();
144267
+ });
144268
+ }
144269
+ const maxBtn = this.root.querySelector('[data-action="maximize"]');
144270
+ if (maxBtn) {
144271
+ maxBtn.addEventListener("click", () => {
144272
+ this.isMaximized = !this.isMaximized;
144273
+ if (this.root) {
144274
+ if (this.isMaximized) {
144275
+ this.root.classList.add("maximized");
144276
+ this.root.style.width = "";
144277
+ } else {
144278
+ this.root.classList.remove("maximized");
144279
+ this._position();
144280
+ }
144281
+ }
144282
+ if (this.isOpen) this._render();
144283
+ });
144284
+ }
144285
+ const dragHandle = this.root.querySelector("[data-drag-handle]");
144286
+ if (dragHandle) {
144287
+ dragHandle.addEventListener("mousedown", (e) => {
144288
+ const target = e.target;
144289
+ if (target.closest(".myio-annotations-panel-action")) return;
144290
+ if (this.isMaximized) return;
144291
+ if (!this.root) return;
144292
+ const rect = this.root.getBoundingClientRect();
144293
+ this.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
144294
+ this.isDragging = true;
144295
+ this.root.classList.add("is-dragging");
144296
+ window.addEventListener("mousemove", this._onDragMove);
144297
+ window.addEventListener("mouseup", this._onDragEnd);
144298
+ e.preventDefault();
144299
+ });
144300
+ }
144301
+ const refreshBtn = this.root.querySelector('[data-action="refresh"]');
144302
+ if (refreshBtn) {
144303
+ refreshBtn.addEventListener("click", () => {
144304
+ const orch = this.opts.getOrchestrator();
144305
+ if (orch && typeof orch.refresh === "function") {
144306
+ orch.refresh().catch((err) => this.opts.logger.warn("[HeaderAnnotationsPanel] refresh failed:", err));
144307
+ }
144308
+ });
144309
+ }
144310
+ const exportBtn = this.root.querySelector('[data-action="export"]');
144311
+ if (exportBtn) {
144312
+ exportBtn.addEventListener("click", () => this._openExportFlow());
144313
+ }
144314
+ const searchInput = this.root.querySelector('[data-input="search"]');
144315
+ if (searchInput) {
144316
+ searchInput.addEventListener("input", () => {
144317
+ if (this._searchDebounceTimer) clearTimeout(this._searchDebounceTimer);
144318
+ this._searchDebounceTimer = setTimeout(() => {
144319
+ this.filter = withSearchTerm(this.filter, searchInput.value);
144320
+ if (this.isOpen) this._render();
144321
+ requestAnimationFrame(() => {
144322
+ const fresh = this.root?.querySelector('[data-input="search"]');
144323
+ if (fresh) {
144324
+ fresh.focus();
144325
+ try {
144326
+ fresh.setSelectionRange(fresh.value.length, fresh.value.length);
144327
+ } catch {
144328
+ }
144329
+ }
144330
+ });
144331
+ }, SEARCH_DEBOUNCE_MS);
144332
+ });
144333
+ }
144334
+ const sortSelect = this.root.querySelector('[data-input="sort"]');
144335
+ if (sortSelect) {
144336
+ sortSelect.addEventListener("change", () => {
144337
+ this.setSortBy(sortSelect.value);
144338
+ });
144339
+ }
144340
+ const filterToggle = this.root.querySelector(
144341
+ '[data-action="toggle-filters"]'
144342
+ );
144343
+ const filterPanel = this.root.querySelector("#myio-anno-filters");
144344
+ if (filterToggle && filterPanel) {
144345
+ filterToggle.addEventListener("click", () => {
144346
+ const isHidden = filterPanel.hasAttribute("hidden");
144347
+ if (isHidden) {
144348
+ filterPanel.removeAttribute("hidden");
144349
+ filterToggle.setAttribute("aria-expanded", "true");
144350
+ } else {
144351
+ filterPanel.setAttribute("hidden", "");
144352
+ filterToggle.setAttribute("aria-expanded", "false");
144353
+ }
144354
+ });
144355
+ }
144356
+ const filterInputs = this.root.querySelectorAll("[data-filter]");
144357
+ filterInputs.forEach((cb) => {
144358
+ cb.addEventListener("change", () => {
144359
+ const kind = cb.getAttribute("data-filter");
144360
+ if (kind === "type") {
144361
+ const v = cb.value;
144362
+ this.setFilter({ types: toggleInSet(this.filter.types, v) });
144363
+ this._reopenFiltersAfterRender();
144364
+ } else if (kind === "status") {
144365
+ const v = cb.value;
144366
+ this.setFilter({ statuses: toggleInSet(this.filter.statuses, v) });
144367
+ this._reopenFiltersAfterRender();
144368
+ } else if (kind === "importance") {
144369
+ const v = Number(cb.value);
144370
+ this.setFilter({ importance: toggleInSet(this.filter.importance, v) });
144371
+ this._reopenFiltersAfterRender();
144372
+ } else if (kind === "actionable") {
144373
+ this.setFilter({ actionableOnly: cb.checked });
144374
+ this._reopenFiltersAfterRender();
144375
+ }
144376
+ });
144377
+ });
144378
+ const clearBtn = this.root.querySelector('[data-action="clear-filters"]');
144379
+ if (clearBtn) {
144380
+ clearBtn.addEventListener("click", () => {
144381
+ this.filter = createDefaultFilter();
144382
+ if (this.isOpen) this._render();
144383
+ this._reopenFiltersAfterRender();
144384
+ });
144385
+ }
144386
+ }
144387
+ /** After a re-render triggered by a filter checkbox, re-open the filter panel
144388
+ * so the user keeps the context visible. */
144389
+ _reopenFiltersAfterRender() {
144390
+ requestAnimationFrame(() => {
144391
+ const fp = this.root?.querySelector("#myio-anno-filters");
144392
+ const toggle = this.root?.querySelector(
144393
+ '[data-action="toggle-filters"]'
144394
+ );
144395
+ if (fp && toggle) {
144396
+ fp.removeAttribute("hidden");
144397
+ toggle.setAttribute("aria-expanded", "true");
144398
+ }
144399
+ });
144400
+ }
144401
+ _onTabKeydown(e, tabBtns) {
144402
+ const currentIdx = tabBtns.findIndex(
144403
+ (b) => b.getAttribute("aria-selected") === "true"
144404
+ );
144405
+ if (currentIdx < 0) return;
144406
+ let nextIdx = currentIdx;
144407
+ switch (e.key) {
144408
+ case "ArrowRight":
144409
+ nextIdx = (currentIdx + 1) % tabBtns.length;
144410
+ break;
144411
+ case "ArrowLeft":
144412
+ nextIdx = (currentIdx - 1 + tabBtns.length) % tabBtns.length;
144413
+ break;
144414
+ case "Home":
144415
+ nextIdx = 0;
144416
+ break;
144417
+ case "End":
144418
+ nextIdx = tabBtns.length - 1;
144419
+ break;
144420
+ default:
144421
+ return;
144422
+ }
144423
+ e.preventDefault();
144424
+ const tabId = tabBtns[nextIdx].getAttribute("data-tab");
144425
+ if (tabId) {
144426
+ this.setActiveTab(tabId);
144427
+ requestAnimationFrame(() => {
144428
+ const sel = this.root?.querySelector(
144429
+ '.myio-annotations-tab[aria-selected="true"]'
144430
+ );
144431
+ sel?.focus();
144432
+ });
144433
+ }
144434
+ }
144435
+ _handleItemClick(itemEl) {
144436
+ const deviceId = itemEl.getAttribute("data-device-id") || "";
144437
+ const annotationId = itemEl.getAttribute("data-annotation-id") || "";
144438
+ if (typeof window !== "undefined") {
144439
+ window.dispatchEvent(
144440
+ new CustomEvent("myio:annotation-clicked", {
144441
+ detail: { deviceId, annotationId, returnTo: "header-panel" }
144442
+ })
144443
+ );
144444
+ }
144445
+ }
144446
+ /**
144447
+ * RFC-0203 M7 — Opens the export modal and dispatches CSV/PDF generation.
144448
+ */
144449
+ _openExportFlow() {
144450
+ const orch = this.opts.getOrchestrator();
144451
+ if (!orch) {
144452
+ this.opts.logger.warn("[HeaderAnnotationsPanel] export: no orchestrator");
144453
+ return;
144454
+ }
144455
+ const hasActiveFilter = !!this.filter.searchTerm || this.filter.types.size > 0 || this.filter.statuses.size > 0 || this.filter.importance.size > 0 || this.filter.actionableOnly;
144456
+ const customerName = typeof window !== "undefined" && window.MyIOOrchestrator?.customerName || "";
144457
+ openExportModal({
144458
+ hasActiveFilter,
144459
+ logger: this.opts.logger,
144460
+ onExport: (opts) => {
144461
+ try {
144462
+ const devices = this._devicesForScope(opts.scope, orch);
144463
+ if (opts.format === "csv") {
144464
+ exportAnnotationsCsv(devices, {
144465
+ customerName,
144466
+ includeArchived: this.filter.statuses.has("archived")
144467
+ });
144468
+ this.opts.logger.debug("[HeaderAnnotationsPanel] CSV exported");
144469
+ } else if (opts.format === "pdf") {
144470
+ const fallback = ["summary"];
144471
+ const levels = opts.levels && opts.levels.length > 0 ? opts.levels : fallback;
144472
+ exportAnnotationsPdf(devices, {
144473
+ customerName,
144474
+ levels,
144475
+ includeArchived: this.filter.statuses.has("archived")
144476
+ });
144477
+ this.opts.logger.debug("[HeaderAnnotationsPanel] PDF exported (levels=" + levels.join(",") + ")");
144478
+ }
144479
+ } catch (err) {
144480
+ this.opts.logger.warn("[HeaderAnnotationsPanel] export failed:", err);
144481
+ if (typeof window !== "undefined") {
144482
+ try {
144483
+ alert("Falha ao exportar: " + err?.message);
144484
+ } catch {
144485
+ }
144486
+ }
144487
+ }
144488
+ }
144489
+ });
144490
+ }
144491
+ /**
144492
+ * Resolves the device subset used by the export based on the chosen scope:
144493
+ * - 'current-tab': groups visible in the active tab (respecting filter)
144494
+ * - 'filtered' : the same as current-tab when a filter is active
144495
+ * - 'all' : every device known to the orchestrator (unfiltered)
144496
+ */
144497
+ _devicesForScope(scope, orch) {
144498
+ if (scope === "all") return orch.getAll();
144499
+ const groups = orch.getGroups(this.activeTab, this.filter);
144500
+ const seen = /* @__PURE__ */ new Set();
144501
+ const out = [];
144502
+ for (const g of groups) {
144503
+ for (const d of g.devices) {
144504
+ if (!seen.has(d.deviceId)) {
144505
+ seen.add(d.deviceId);
144506
+ out.push(d);
144507
+ }
144508
+ }
144509
+ }
144510
+ return out;
144511
+ }
144512
+ };
144513
+ var _singleton = null;
144514
+ function getHeaderAnnotationsPanel() {
144515
+ if (!_singleton) _singleton = new HeaderAnnotationsPanel();
144516
+ return _singleton;
144517
+ }
144518
+
141946
144519
  // src/index.ts
141947
144520
  var version = package_default.version || "0.0.0";
141948
144521
  // Annotate the CommonJS export names for ESM import in node:
@@ -141954,9 +144527,12 @@ var version = package_default.version || "0.0.0";
141954
144527
  ALARM_STATE_CONFIG,
141955
144528
  AMBIENTE_GROUP_CSS_PREFIX,
141956
144529
  AMBIENTE_MODAL_CSS_PREFIX,
144530
+ ANNOTATION_CSV_COLUMNS,
144531
+ ANNOTATION_SORT_OPTIONS,
141957
144532
  ANNOTATION_TYPE_COLORS,
141958
144533
  ANNOTATION_TYPE_LABELS,
141959
144534
  ANNOTATION_TYPE_LABELS_EN,
144535
+ ANNOTATION_VIRTUAL_THRESHOLD,
141960
144536
  ActionButtonController,
141961
144537
  ActionButtonView,
141962
144538
  AlarmService,
@@ -141979,6 +144555,7 @@ var version = package_default.version || "0.0.0";
141979
144555
  ContractSummaryTooltip,
141980
144556
  CustomerCardV1,
141981
144557
  CustomerCardV2,
144558
+ CustomerDeviceService,
141982
144559
  DAY_LABELS,
141983
144560
  DAY_LABELS_FULL,
141984
144561
  DECIMAL_OPTIONS,
@@ -141986,6 +144563,7 @@ var version = package_default.version || "0.0.0";
141986
144563
  DEFAULT_ALARM_FILTERS,
141987
144564
  DEFAULT_ALARM_FILTER_TABS,
141988
144565
  DEFAULT_ALARM_STATS,
144566
+ DEFAULT_ANNOTATION_SORT,
141989
144567
  DEFAULT_BAS_SETTINGS,
141990
144568
  DEFAULT_CLAMP_RANGE,
141991
144569
  DEFAULT_DASHBOARD_KPIS,
@@ -142083,6 +144661,7 @@ var version = package_default.version || "0.0.0";
142083
144661
  HEADER_STYLE_DEFAULT,
142084
144662
  HEADER_STYLE_PREMIUM_GREEN,
142085
144663
  HEADER_STYLE_SLIM,
144664
+ HeaderAnnotationsPanel,
142086
144665
  HeaderDevicesGridController,
142087
144666
  HeaderDevicesGridView,
142088
144667
  HeaderFilterModal,
@@ -142181,6 +144760,7 @@ var version = package_default.version || "0.0.0";
142181
144760
  TempRangeTooltip,
142182
144761
  TempSensorSummaryTooltip,
142183
144762
  UsersSummaryTooltip,
144763
+ VirtualList,
142184
144764
  WAITING_STATUSES,
142185
144765
  WATER_DEVICE_CATEGORIES,
142186
144766
  WATER_SORT_OPTIONS,
@@ -142197,6 +144777,9 @@ var version = package_default.version || "0.0.0";
142197
144777
  assignShoppingColors,
142198
144778
  averageByDay,
142199
144779
  buildAmbienteGroupData,
144780
+ buildAnnotationServiceOrchestrator,
144781
+ buildAnnotationsCsv,
144782
+ buildAnnotationsExportFilename,
142200
144783
  buildDeviceGridEntityObject,
142201
144784
  buildEquipmentCategoryDataForTooltip,
142202
144785
  buildEquipmentCategorySummary,
@@ -142231,6 +144814,7 @@ var version = package_default.version || "0.0.0";
142231
144814
  classifyWaterLabels,
142232
144815
  clearAllAuthCaches,
142233
144816
  clearFreshdeskTicketsOnTB,
144817
+ closeAnnotationsExportModal,
142234
144818
  connectionStatusIcons,
142235
144819
  createActionButton,
142236
144820
  createAlarmCardElement,
@@ -142247,6 +144831,7 @@ var version = package_default.version || "0.0.0";
142247
144831
  createCustomerCardV2,
142248
144832
  createDateRangePicker,
142249
144833
  createDaysGrid,
144834
+ createDefaultAnnotationFilter,
142250
144835
  createDeviceGridBusyModal,
142251
144836
  createDeviceGridState,
142252
144837
  createDeviceGridV6,
@@ -142295,6 +144880,7 @@ var version = package_default.version || "0.0.0";
142295
144880
  createToggleSwitch,
142296
144881
  createWaterPanelComponent,
142297
144882
  createWidgetController,
144883
+ csvEscapeAnnotation,
142298
144884
  decodePayload,
142299
144885
  decodePayloadBase64Xor,
142300
144886
  deleteFreshdeskTicket,
@@ -142306,6 +144892,10 @@ var version = package_default.version || "0.0.0";
142306
144892
  determineInterval,
142307
144893
  deviceStatusIcons,
142308
144894
  doSchedulesOverlap,
144895
+ downloadAnnotationsTextFile,
144896
+ escapeAnnotationHtml,
144897
+ exportAnnotationsCsv,
144898
+ exportAnnotationsPdf,
142309
144899
  exportGridCsv,
142310
144900
  exportGridPdf,
142311
144901
  exportGridXls,
@@ -142330,6 +144920,7 @@ var version = package_default.version || "0.0.0";
142330
144920
  formatAlarmRelativeTime,
142331
144921
  formatAllInSameUnit,
142332
144922
  formatAllInSameWaterUnit,
144923
+ formatAnnotationRelativeTime,
142333
144924
  formatDashboardPercentage,
142334
144925
  formatDateForInput,
142335
144926
  formatDateToYMD,
@@ -142378,6 +144969,7 @@ var version = package_default.version || "0.0.0";
142378
144969
  getFirstDayOfMonthFor,
142379
144970
  getGroupColor,
142380
144971
  getHashColor,
144972
+ getHeaderAnnotationsPanel,
142381
144973
  getImageByConsumption,
142382
144974
  getLastDayOfMonth,
142383
144975
  getModalHeaderStyles,
@@ -142401,6 +144993,7 @@ var version = package_default.version || "0.0.0";
142401
144993
  groupByDay,
142402
144994
  handleDeviceType,
142403
144995
  hasSelectedDays,
144996
+ highlightAnnotationMatches,
142404
144997
  initMyIOAuthContext,
142405
144998
  initOnOffTimelineTooltips,
142406
144999
  injectActionButtonStyles,
@@ -142414,6 +145007,7 @@ var version = package_default.version || "0.0.0";
142414
145007
  injectDeviceOperationalCardGridStyles,
142415
145008
  injectDeviceOperationalCardStyles,
142416
145009
  injectFancoilRemoteStyles,
145010
+ injectHeaderAnnotationsStyles,
142417
145011
  injectHeaderDevicesGridStyles,
142418
145012
  injectHeaderShoppingStyles,
142419
145013
  injectMenuShoppingStyles,
@@ -142432,6 +145026,7 @@ var version = package_default.version || "0.0.0";
142432
145026
  injectSwitchControlStyles,
142433
145027
  interpolateTemperature,
142434
145028
  isAlarmActive,
145029
+ isAnnotationOverdue,
142435
145030
  isConnectionStale,
142436
145031
  isDeviceOffline,
142437
145032
  isEndAfterStart,
@@ -142454,6 +145049,7 @@ var version = package_default.version || "0.0.0";
142454
145049
  mapDeviceStatusToCardStatus,
142455
145050
  mapDeviceToConnectionStatus,
142456
145051
  myioExportData,
145052
+ nfdNormalizeAnnotationSearch,
142457
145053
  normalizeConnectionStatus,
142458
145054
  normalizeRecipients,
142459
145055
  numbers,
@@ -142462,6 +145058,7 @@ var version = package_default.version || "0.0.0";
142462
145058
  openAlarmDetailsModal,
142463
145059
  openAmbienteDetailModal,
142464
145060
  openAmbienteGroupModal,
145061
+ openAnnotationsExportModal,
142465
145062
  openContractDevicesModal,
142466
145063
  openDashboardPopup,
142467
145064
  openDashboardPopupAllReport,
@@ -142488,6 +145085,7 @@ var version = package_default.version || "0.0.0";
142488
145085
  openUserManagementModal,
142489
145086
  openWelcomeModal,
142490
145087
  parseInputDateToDate,
145088
+ parseLogAnnotations,
142491
145089
  periodKey,
142492
145090
  readFreshdeskTicketsFromTB,
142493
145091
  recalculateDeviceStatus,
@@ -142501,6 +145099,7 @@ var version = package_default.version || "0.0.0";
142501
145099
  removeOperationalHeaderDevicesGridStyles,
142502
145100
  removeSchedulingSharedStyles,
142503
145101
  renderAlarmCard,
145102
+ renderAnnotationItemCard,
142504
145103
  renderCardAmbienteV6,
142505
145104
  renderCardComponent,
142506
145105
  renderCardComponentEnhanced,
@@ -142531,6 +145130,8 @@ var version = package_default.version || "0.0.0";
142531
145130
  schedShowConfirmModal,
142532
145131
  schedShowNotificationModal,
142533
145132
  shouldFlashIcon,
145133
+ shouldVirtualizeAnnotationList,
145134
+ sortAnnotationGroups,
142534
145135
  sortDeviceGridDevices,
142535
145136
  strings,
142536
145137
  telemetryInfoFormatEnergy,
@@ -142542,6 +145143,7 @@ var version = package_default.version || "0.0.0";
142542
145143
  toCSV,
142543
145144
  toFixedSafe,
142544
145145
  toFreshdeskTicketSummary,
145146
+ truncateAnnotationText,
142545
145147
  updateDeviceGridStats,
142546
145148
  updateFreshdeskTicket,
142547
145149
  upsertAlarmAnnotation,