
## client/custom/modules/hyb-report/src/views/fields/data-matrix.js
71: this.formulas = parsed.formulas || []; // [PHASE 2]: Khởi tạo mảng formulas toàn cục
239: formulas: this.formulas || [] // [PHASE 2]: Nhét Formula vào Payload chung

## client/custom/modules/hyb-report/src/views/fields/template-input.js
128: const noDataLabel = this.translate('No Data') || 'Không có dữ liệu';
129: const searchPlaceholder = this.translate('Search') || 'Tìm kiếm...';
179: <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Chèn biến từ ${entityName}">

## client/custom/modules/hyb-report/src/views/fields/dynamic-enum-array.js
73: this.render(); // Render lại ngay để Dropdown thứ 2 được làm mới
82: this.syncUpToParent(); // Thằng này chỉ lưu template, không cần render lại
96: const label = this.translate('addCriteria', 'labels', 'HybReport') || 'Thêm Phân Nhóm';
116: item.timeConfigHtml = ''; // Trạm chứa UI Thời gian
137: console.error('🚨 [HybReport] LỖI CACHE: File matrix-util.js chưa được cập nhật. Vui lòng Rebuild EspoCRM và ấn Ctrl+F5.');

## client/custom/modules/hyb-report/src/views/report-viewer/widget.js
325: sortParams: this.sortParams  // [#10] Bug1 fix: truyền sortParams
491: this.currentView = 'detail'; // fallback an toàn

## client/custom/modules/hyb-report/src/views/report-viewer/helpers/filter-manager.js
2: const VERSION = "v8.0.2"; // UPDATE: Tối ưu hóa xử lý DateRange
158: filterArray = [from, '>']; // Filter dạng lớn hơn nếu chỉ có Từ Ngày
160: filterArray = [to, '<'];   // Filter dạng nhỏ hơn nếu chỉ có Đến Ngày

## client/custom/modules/hyb-report/src/views/report-viewer/helpers/renderers/tables/pivot-table.js
182: dataPoints: { 'Total': row } // Trong mảng phẳng, metric nằm trực tiếp trong row
233: const headerRowHeight = 42; // Chiều cao cố định cho mỗi hàng header để tính top offset

## client/custom/modules/hyb-report/src/views/report-viewer/helpers/renderers/echarts/basic-charts.js
62: xAxisDimension: '', legendDimension: '', // [MỚI - EPIC 6]: Ánh xạ trục động
170: let dimensionKeys = []; // Danh sách các key cột thực tế
171: let actualMetricKeys = []; // [BUG 2 FIX]: Khai báo toàn cục để Number Metric dùng được
395: topPad = 40; // Dành chỗ cho Title
409: topPad += 25; // Legend cao khoảng 25px
419: leftPad = '15%'; // Bóp lề trái để hiện legend dọc
424: rightPad = '15%'; // Bóp lề phải để hiện legend dọc
541: let lDim = (dConfig.legendDimensions || [])[0] || dConfig.legendDimension; // [#16] Pie chỉ dùng 1 legend dim
816: position: 'top', // Trục giá trị nằm trên đầu

## client/custom/modules/hyb-report/src/views/report-viewer/helpers/renderers/echarts/base-chart.js
19: type: 'slider', // Thanh trượt vật lý
22: bottom: 5, // Cấu hình của ông
23: height: 25, // Cấu hình của ông
35: type: 'inside', // Cuộn bằng chuột
44: return { dataZoom: dataZoom, bottomPadding: '15%' }; // bottomPadding của ông

## client/custom/modules/hyb-report/src/views/report-studio/index.js
16: return model.fetch(); // Lấy data cũ nếu là Edit

## client/custom/modules/hyb-report/src/views/report-studio/studio-workspace.js
279: $sb.show(); // Hiá»‡n láº¡i sidebar khi chuyá»ƒn tá»« detail
303: $sb.show(); // Hiá»‡n láº¡i sidebar khi chuyá»ƒn tá»« detail
417: const msg = this.translate('metricAlreadyExists', 'messages', 'HybReport') || 'Chá»‰ sá»‘ nÃ y Ä‘Ã£ tá»“n táº¡i!';
635: if (relevantSet === null) return; // filter/metric â€” skip
642: $wrapper.addClass('already-selected').hide(); // hide háº³n trong cÃ¹ng zone
698: const msg = view.translate('fieldAlreadyExists', 'messages', 'HybReport') || 'TrÆ°á»ng nÃ y Ä‘Ã£ cÃ³ trong danh sÃ¡ch!';
901: const label = data.sort === 'ASC' ? 'TÄƒng dáº§n' : 'Giáº£m dáº§n';
902: badges += `<i class="fas ${icon} text-danger" title="Sáº¯p xáº¿p: ${label}" style="font-size: 10px; margin-left: 6px;"></i>`;
917: const modeTitle = isContinuous ? 'Mode: Continuous (dÃ²ng thá»i gian)' : 'Mode: Extract (bá»™ pháº­n thá»i gian)';
945: badges += `<span class="badge badge-success" title="Äá»‹nh dáº¡ng: ${data.displayFormat}" style="font-size: 8px; padding: 1px 3px; margin-left: 6px; background: #10b981; color: #f

## client/custom/modules/hyb-report/src/views/report-studio/studio-page.js
179: <span class="hyb-sp-ds-meta">với Root: ${_.escape(rootLabel)}</span>

## client/custom/modules/hyb-report/src/views/report-studio/popovers/field-settings.js
15: 'change [data-name="timeMode"]': 'onGrainOrModeChange'  // [#22C] rebuild format khi đổi mode
64: isGroupZone: isGroupZone, // [#30]: ẩn NONE option ở zone group
88: {v:'default', l:'2026 (mặc định)'},
89: {v:'short',   l:'26 (rút gọn)'},
90: {v:'numeric', l:'2026 (số)'}
93: {v:'default', l:'2026-Q1 (mặc định)'},
94: {v:'short',   l:'Q1 (rút gọn)'},
95: {v:'numeric', l:'1 (số)'},
96: {v:'full',    l:'2026-Q1 (đầy đủ)'}
98: {v:'default', l:'Q1 (mặc định)'},
99: {v:'short',   l:'Q1 (rút gọn)'},
100: {v:'numeric', l:'1 (số)'}
103: {v:'default', l:'2026-01 (mặc định)'},
104: {v:'short',   l:'Jan (tên ngắn)'},
105: {v:'numeric', l:'01 (số)'},
106: {v:'full',    l:'January 2026 (đầy đủ)'}
108: {v:'default', l:'Jan (mặc định)'},
109: {v:'short',   l:'Jan (tên ngắn)'},
110: {v:'numeric', l:'01 (số)'},
111: {v:'full',    l:'January (tên đầy đủ)'}
114: {v:'default', l:'2026-W05 (mặc định)'},
115: {v:'short',   l:'W05 (rút gọn)'},
116: {v:'numeric', l:'5 (số)'}
118: {v:'default', l:'W05 (mặc định)'},
119: {v:'short',   l:'W05 (rút gọn)'},
120: {v:'numeric', l:'5 (số)'}
123: {v:'default', l:'2026-01-30 (mặc định)'},
124: {v:'short',   l:'30/01 (ngày/tháng)'},
125: {v:'numeric', l:'30 (số ngày)'},
126: {v:'full',    l:'Thursday, 30/01/2026 (đầy đủ)'}
128: {v:'default', l:'Mon (mặc định)'},
129: {v:'short',   l:'Mon (tên ngắn)'},
130: {v:'numeric', l:'2 (số thứ)'},
131: {v:'full',    l:'Monday (tên đầy đủ)'}
134: {v:'default', l:'2026-01-30 08:00 (mặc định)'},
135: {v:'short',   l:'08:00 (giờ)'},
136: {v:'numeric', l:'8 (số)'},
137: {v:'full',    l:'2026-01-30 08:00 (đầy đủ)'}
139: {v:'default', l:'08:00-09:00 (mặc định)'},
140: {v:'short',   l:'08h (rút gọn)'},
... + 2

## client/custom/modules/hyb-report/src/views/report-studio/popovers/base.js
45: console.log('HybReport Debug: Bấm vùng ngoài Popover. Đóng:', e.target);

## client/custom/modules/hyb-report/src/views/report-studio/popovers/field-picker-palette.js
238: <button type="button" class="hyb-fpp-basket-remove" data-key="${item.key}" data-state="${item.state}">×</button>

## client/custom/modules/hyb-report/src/views/report-studio/popovers/metric-settings.js
64: const placeholderL = this.translate('selectCalc', 'labels', 'HybReport') || 'Thiết lập phép tính...';

## client/custom/modules/hyb-report/src/views/report-studio/fields/metrics-array-studio.js
43: const label = this.translate('metricAdd', 'labels', 'HybReport') || 'Thêm Phép tính';
117: Espo.Ui.error('Sếp phải đặt Tên (Label) trước khi khóa Tag nhé!');
124: Espo.Ui.error('Tên Tag này bị trùng rồi sếp ơi!');
150: Espo.Ui.warning(`Đã đạt giới hạn ${MatrixUtil.CONFIG.MAX_METRICS} phép tính!`);

## client/custom/modules/hyb-report/src/views/report-studio/fields/data-matrix-studio.js
149: sourceEntityType: ds.entityType, // Quan trọng để BE phân giải Macro ACL
156: isCore:           true // Đánh dấu bộ lọc "Mỏ neo" không thể xóa
204: Helper.renderAllZones(this); // [MỚI]: Vẽ đồng bộ cả Rows, Columns và Detail Columns
283: alert("Lỗi: Không tìm thấy trình xử lý công thức!");
600: Espo.Ui.error('Trường này đã tồn tại ở Trục khác!');
636: cancelReport(e) { e.preventDefault(); if (confirm('Đóng báo cáo?')) { const router = this.getRouter(); if (router) router.navigate('#CReportDashboard', {trigger: true}); else windo
638: toggleDataGridPanel(e) { e.preventDefault(); const $panel = this.$el.find('#data-grid-panel'); const $btn = $(e.currentTarget); const $icon = $btn.find('i.fa-chevron-down, i.fa-che
775: finalType = 'BarChart'; // Mặc định là Cột khi có dữ liệu

## client/custom/modules/hyb-report/src/views/report-studio/modals/formula-editor.js
151: const ds = dataSources.find(p => p.sourceId === srcId) || dataSources[0]; // Dự phòng
167: html = `<div style="padding: 10px; text-align:center; color:#94a3b8; font-size:11px;"><i>Chưa có Metric hoặc Field nào được đưa ra lưới. Vui lòng thiết lập cột trước.</i></div>`;
215: Espo.Ui.error(this.translate('errEmptyFormula', 'messages', 'HybReport') || 'Công thức không được để trống.');

## client/custom/modules/hyb-report/src/views/report-studio/panels/datasource-selector.js
273: isReverse: $el.attr('data-is-reverse') === 'true',  // [#19] .attr() thay vì .data() — tránh jQuery camelCase trap
465: <span>${this.translate('dragToJoin', 'labels', 'HybReport') || 'Kéo thực thể vào đây để thực hiện JOIN'}</span>

## client/custom/modules/hyb-report/src/views/report-studio/panels/chart-settings.js
227: if (confirm('Khôi phục biểu đồ về bảng Pivot mặc định?')) {

## client/custom/modules/hyb-report/src/views/report-studio/helpers/basic-charts.js
29: <div style="font-size: 16px; font-weight: bold; color: #475569;">Chưa chọn Loại Biểu đồ</div>
30: <div style="font-size: 13px; margin-top: 5px;">Vui lòng mở Khối 2 (Cấu hình Nâng cao) để chọn định dạng hiển thị.</div>
62: xAxisDimension: '', legendDimension: '', // [MỚI - EPIC 6]: Ánh xạ trục động
170: let dimensionKeys = []; // Danh sách các key cột thực tế
171: let actualMetricKeys = []; // [BUG 2 FIX]: Khai báo toàn cục để Number Metric dùng được
217: if (currentVal && currentVal !== 'Tổng hợp') currentPath.push(currentVal);
222: let fullRowKey = currentPath.join(' | ') || 'Tổng';
257: if (rowGroups['Tổng']) delete rowGroups['Tổng'];
258: if (rowGroups['Tổng hợp']) delete rowGroups['Tổng hợp'];
295: let metricLabel = 'Giá trị';
311: metricLabel = legacy.seriesNames[0] || 'Giá trị';
396: topPad = 40; // Dành chỗ cho Title
410: topPad += 25; // Legend cao khoảng 25px
420: leftPad = '15%'; // Bóp lề trái để hiện legend dọc
425: rightPad = '15%'; // Bóp lề phải để hiện legend dọc
434: dataView: { readOnly: true, title: 'Xem Dữ liệu', lang: ['Dữ liệu Biểu đồ', 'Đóng', 'Làm mới'] },
435: saveAsImage: { title: 'Lưu Ảnh', name: widget.name || 'chart' }
650: position: 'top', // Trục giá trị nằm trên đầu

## client/custom/modules/hyb-report/src/views/report-studio/helpers/popover-helper.js
274: html += `</select><div class="text-muted" style="font-size: 10px; margin-top: 3px;">Giữ Ctrl/Cmd để chọn nhiều</div>`;

## client/custom/modules/hyb-report/src/views/report-studio/helpers/drag-drop-helper.js
211: const msg = view.translate('fieldAlreadyExists', 'messages', 'HybReport') || 'Trường này đã có trong danh sách!';

## client/custom/modules/hyb-report/src/views/report-studio/helpers/chart-panel-helps.js
205: view._currentCanvasView = 'pivot'; // Đổi state sang Pivot
206: this.updateChartUIState(view);     // Update UI mà ko render lại toàn bộ
212: view._currentCanvasView = 'detail'; // Đổi state sang Detail

## client/custom/modules/hyb-report/src/views/report-studio/helpers/studio-renderer.js
19: let axisContext = meta.axisContext || {}; // [V10]: Context map từ BE
64: const field = parts.slice(1).join('.'); // hỗ trợ field có dấu chấm
115: let _hourlyAxisIdx = -1; // vị trí axis hour trong rowAxisKeys gốc
141: break; // chỉ hỗ trợ 1 axis hour

## client/custom/modules/hyb-report/src/views/report-studio/helpers/dropdown-helper.js
319: const msg = view.translate('fieldAlreadyExists', 'messages', 'HybReport') || 'Trường này đã có trong danh sách!';

## client/custom/modules/hyb-report/src/helpers/matrix-util.js
290: const seenReverse = {};  // tránh duplicate
516: <input type="text" class="form-control input-sm template-search-input" placeholder="Tìm kiếm trường..." style="border-radius: 4px; border-color: #cbd5e1; font-size: 12px; box-shado
518: <li class="no-results-msg" style="display: none; padding: 12px; text-align: center; color: #94a3b8; font-style: italic; font-size: 12px;">Không có dữ liệu</li>
548: listHtml += `<li class="empty-wand-msg" style="padding: 15px; text-align: center; color: #94a3b8; font-style: italic; font-size: 12px;">Không có dữ liệu hợp lệ</li>`;
622: const showSectionHeader = isMetric || !isSingleSection || true; // [FIX]: Luôn hiện Header để đảm bảo tính "đa lớp" như Admin yêu cầu

## client/custom/modules/hyb-report/src/helpers/report-name-builder.js
12: let dimensions = []; // Chứa các Hàng
13: let metrics = [];    // Chứa các Cột
20: metrics.push(m.columnLabel); // Ưu tiên lấy Nhãn tự đặt
23: metrics.push(`${func} (${m.field})`); // Ví dụ: SUM (amount)
25: metrics.push(model.view ? model.view.translate('countMetric', 'labels', 'HybReport') : 'Count'); // Nếu field rỗng -> Là đếm tất cả bản ghi

## custom/Espo/Modules/HybReport/Resources/metadata/clientDefs/CReportDashboard.json
26: "label": "Xem Báo Cáo",
42: "label": "Xem Báo Cáo",

## custom/Espo/Modules/HybReport/Hooks/CReportDashboard/SyncToWidgets.php
55: 'name' => $reportName // Đồng bộ cả tên nếu cần

## custom/Espo/Modules/HybReport/HybReport/Resources/metadata/clientDefs/CReportDashboard.json
26: "label": "Xem Báo Cáo",
42: "label": "Xem Báo Cáo",

## custom/Espo/Modules/HybReport/HybReport/Hooks/CReportDashboard/SyncToWidgets.php
55: 'name' => $reportName // Đồng bộ cả tên nếu cần

## custom/Espo/Modules/HybReport/HybReport/Services/CReportWidget.php
27: $this->config  // [TIMEZONE FIX] Truyền Config để PayloadParser đọc đúng timezone
77: $enableChart = true; // default safe: trả về để backward compat

## custom/Espo/Modules/HybReport/HybReport/Tools/Report/MatrixQueryBuilder.php
41: $tagToMetricIdMap = []; // Bản đồ lưu trữ Tên Tag -> ID thực tế
328: if ($valA === '[Chưa phân bổ]' || $valA === '') return 1;
329: if ($valB === '[Chưa phân bổ]' || $valB === '') return -1;
487: $field = 'Global'; $vStr = 'Tổng hợp';
543: error_log("[HybReport Pivot] Lỗi SQL Unified Tree: " . $e->getMessage());

## custom/Espo/Modules/HybReport/HybReport/Tools/Report/PayloadParser.php
267: $this->applyFiltersToBuilder($selectBuilder, $filters, $aliases, $dataSources); // [#23 Gap 0]: Apply filters vào Pivot/Chart
403: 'timeMode' => $timeMode, // [#30]: cần biết extract vs continuous để sort đúng
798: $this->applyFiltersToBuilder($selectBuilder, $filters, $aliases, $prunedDataSources); // [#23 Gap 0-Detail]: Apply filters vào Detail Grid
945: 'continuous', // detail không dùng timeMode
1044: $entity = $revMatch[1]; // Override entity từ pattern
1154: $single   = null; // không dùng single cho range
1176: continue; // Skip: operator cần input nhưng chưa có value

## custom/Espo/Modules/HybReport/Services/CReportWidget.php
27: $this->config  // [TIMEZONE FIX] Truyền Config để PayloadParser đọc đúng timezone
77: $enableChart = true; // default safe: trả về để backward compat

## custom/Espo/Modules/HybReport/Tools/Report/MatrixQueryBuilder.php
41: $tagToMetricIdMap = []; // Bản đồ lưu trữ Tên Tag -> ID thực tế
328: if ($valA === '[Chưa phân bổ]' || $valA === '') return 1;
329: if ($valB === '[Chưa phân bổ]' || $valB === '') return -1;
505: $field = 'Global'; $vStr = 'Tổng hợp';
561: error_log("[HybReport Pivot] Lỗi SQL Unified Tree: " . $e->getMessage());

## custom/Espo/Modules/HybReport/Tools/Report/PayloadParser.php
352: $this->applyFiltersToBuilder($selectBuilder, $filters, $aliases, $dataSources); // [#23 Gap 0]: Apply filters vào Pivot/Chart
488: 'timeMode' => $timeMode, // [#30]: cần biết extract vs continuous để sort đúng
890: $this->applyFiltersToBuilder($selectBuilder, $filters, $aliases, $prunedDataSources); // [#23 Gap 0-Detail]: Apply filters vào Detail Grid
1037: 'continuous', // detail không dùng timeMode
1140: $entity = $revMatch[1]; // Override entity từ pattern
1277: $single   = null; // không dùng single cho range
1299: continue; // Skip: operator cần input nhưng chưa có value
