
## client/custom/modules/hyb-report/src/helpers/field-group.js
24: 'TextAndString': ['varchar', 'url', 'urlMultiple', 'email', 'phone', 'personName', 'colorpicker', 'address', 'id'],
25: 'Numbers': ['int', 'float', 'decimal', 'currency', 'currencyConverted', 'number', 'autoincrement', 'duration'],
26: 'DateTime': ['date', 'datetime', 'datetimeOptional', 'date-time'],
27: 'Dropdowns': ['enum', 'multiEnum', 'enumInt', 'checklist'],
28: 'Boolean': ['bool'],
29: 'Relationships': ['link', 'linkOne', 'hasOne']
34: if (groupKey === 'Relationships' && !this.canDiveDeeper(currentPath, maxDepth)) return 'REJECTED_DEPTH_LIMIT';
38: return 'Others';

## client/custom/modules/hyb-report/src/helpers/matrix-util.js
62: { v: 'startsWith', l: 'Starts With', requireInput: 'text' },
63: { v: 'contains', l: 'Contains', requireInput: 'text' },
64: { v: 'equals', l: 'Equals', requireInput: 'text' },
65: { v: 'endsWith', l: 'Ends With', requireInput: 'text' },
66: { v: 'isLike', l: 'Is Like (%)', requireInput: 'text' },
67: { v: 'notContains', l: 'Not Contains', requireInput: 'text' },
68: { v: 'notEquals', l: 'Not Equals', requireInput: 'text' },
69: { v: 'isNotLike', l: 'Is Not Like (%)', requireInput: 'text' },
70: { v: 'anyOf', l: 'Any Of', requireInput: 'textArray' },
71: { v: 'noneOf', l: 'None Of', requireInput: 'textArray' },
72: { v: 'isEmpty', l: 'Is Empty', requireInput: null },
73: { v: 'isNotEmpty', l: 'Is Not Empty', requireInput: null }
81: { v: 'isNot', l: 'Is Not', requireInput: 'enum' },
82: { v: 'anyOf', l: 'Any Of', requireInput: 'enumArray' },
83: { v: 'noneOf', l: 'None Of', requireInput: 'enumArray' },
84: { v: 'isEmpty', l: 'Is Empty', requireInput: null },
85: { v: 'isNotEmpty', l: 'Is Not Empty', requireInput: null }
89: { v: 'isNot', l: 'Is Not', requireInput: 'link' },
90: { v: 'isEmpty', l: 'Is Empty', requireInput: null },
91: { v: 'isNotEmpty', l: 'Is Not Empty', requireInput: null }
98: { v: 'today', l: 'Today', requireInput: null },
99: { v: 'thisWeek', l: 'This Week', requireInput: null },
100: { v: 'lastWeek', l: 'Last Week', requireInput: null },
101: { v: 'past', l: 'Past', requireInput: null },
102: { v: 'future', l: 'Future', requireInput: null },
103: { v: 'last7Days', l: 'Last 7 Days', requireInput: null },
104: { v: 'thisMonth', l: 'This Month', requireInput: null },
105: { v: 'lastMonth', l: 'Last Month', requireInput: null },
107: { v: 'nextMonth', l: 'Next Month', requireInput: null },
108: { v: 'thisQuarter', l: 'This Quarter', requireInput: null },
109: { v: 'lastQuarter', l: 'Last Quarter', requireInput: null },
110: { v: 'thisYear', l: 'This Year', requireInput: null },
111: { v: 'lastYear', l: 'Last Year', requireInput: null },
112: { v: 'ever', l: 'Ever', requireInput: null },
115: { v: 'lastXDays', l: 'Last X Days', requireInput: 'number' },
116: { v: 'nextXDays', l: 'Next X Days', requireInput: 'number' },
117: { v: 'olderThanXDays', l: 'Older Than X Days', requireInput: 'number' },
118: { v: 'afterXDays', l: 'After X Days', requireInput: 'number' },
122: { v: 'after', l: 'After', requireInput: 'datePicker' },
123: { v: 'before', l: 'Before', requireInput: 'datePicker' },
124: { v: 'custom', l: 'Custom... (Between)', requireInput: 'dateRangePicker' },
126: { v: 'isEmpty', l: 'Is Empty', requireInput: null },
127: { v: 'isNotEmpty', l: 'Is Not Empty', requireInput: null }
140: { v: 'between', l: 'Between', requireInput: 'numberRange' },
141: { v: 'isEmpty', l: 'Is Empty', requireInput: null },
142: { v: 'isNotEmpty', l: 'Is Not Empty', requireInput: null }
150: { v: 'isEmpty', l: 'Is Empty', requireInput: null },
151: { v: 'isNotEmpty', l: 'Is Not Empty', requireInput: null }
161: BANNED_PROPS: ['disabled', 'utility', 'notStorable', 'directAccessDisabled', 'fields', 'layoutDetailDisabled', 'layoutFiltersDisabled', 'layoutListDisabled']
181: default: 'HH:00 - HH:59',
182: options: ['HH:00 - HH:59']
185: default: 'DD/MM/YYYY',
186: options: ['DD/MM/YYYY', 'YYYY-MM-DD', 'DD-MM-YYYY']
193: default: 'MM/YYYY',
194: options: ['MM/YYYY', 'YYYY-MM', 'M/YYYY']
201: default: 'YYYY',
202: options: ['YYYY']
290: const seenReverse = {};  // tránh duplicate
347: if (['linkParent', 'belongsToParent'].includes(fieldDef.type)) targetEntity = 'POLYMORPHIC';
348: if (!targetEntity && ['assignedUser', 'createdBy', 'modifiedBy'].includes(f)) targetEntity = 'User';
... + 16

## 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
22: let func = m.func || 'COUNT';
23: metrics.push(`${func} (${m.field})`); // Ví dụ: SUM (amount)
25: metrics.push('Số lượng'); // Nếu field rỗng -> Là đếm tất cả bản ghi
43: let metricStr = metrics.length > 0 ? metrics.join(', ') : 'Báo cáo';
44: let dimStr = dimensions.length > 0 ? ' theo ' + dimensions.join(' và ') : '';

## 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
127: if (val === 'None' || val === '' || val === null || val === undefined) return '-';
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';
244: if (nodeHasMetrics || colKey) rowGroups[fullRowKey].pieDataRaw.push({ colKey: colKey || 'Chung', metrics: cItem.metrics });
257: if (rowGroups['Tổng']) delete rowGroups['Tổng'];
258: if (rowGroups['Tổng hợp']) delete rowGroups['Tổng hợp'];
274: if (type === 'NumberMetric' || type === 'List') {
275: if (type === 'NumberMetric') {
295: let metricLabel = 'Giá trị';
311: metricLabel = legacy.seriesNames[0] || 'Giá trị';
343: trigger: type === 'PieChart' || type === 'Sunburst' ? 'item' : 'axis',
384: if (dConfig.barOrientation === 'horizontal' && type !== 'LineChart' && type !== 'PieChart' && type !== 'Sunburst') {
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' }
441: if (type === 'Sunburst') {
509: else if (type === 'PieChart') {
629: type: type === 'LineChart' ? 'line' : 'bar',
647: if (dConfig.barOrientation === 'horizontal' && type !== 'LineChart') {
650: position: 'top', // Trục giá trị nằm trên đầu
772: type: type === 'LineChart' ? 'line' : 'bar',
783: if (type === 'BarChart') {
790: if (type === 'LineChart') {
803: markLineData.push({ yAxis: dConfig.markLineTarget, name: 'Target', lineStyle: { type: 'solid', color: '#10b981', width: 2 } });
829: if (type === 'BarChart' && dConfig.barOrientation === 'horizontal') {

## client/custom/modules/hyb-report/src/views/report-studio/helpers/chart-panel-helps.js
190: view.displayConfig.configs.chart.type = 'BarChart';
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/drag-drop-helper.js
36: const typeClass = join.joinType === 'INNER' ? 'label-warning' : 'label-success';
87: if (item.sort && item.sort !== 'NONE' && !isGridZone) {
157: data-sort="${item.sort || 'NONE'}" data-time-grain="${item.timeGrain || ''}"

## client/custom/modules/hyb-report/src/views/report-studio/helpers/field-palette-helper.js
3: row: { zone: '#groupby-rows-dropzone', zoneKey: 'rows', titleKey: 'addFieldsToRows', subtitleKey: 'chooseRowDimensions' },
4: col: { zone: '#groupby-cols-dropzone', zoneKey: 'cols', titleKey: 'addFieldsToColumns', subtitleKey: 'chooseColumnDimensions' },
6: filter: { zone: null, zoneKey: 'filter', titleKey: 'addFilters', subtitleKey: 'chooseFieldsToFilter' }

## client/custom/modules/hyb-report/src/views/report-studio/helpers/matrix-studio-helps.js
41: console.log("HybReport Debug: Bridge renderPivotTable reached", { pivotResult, StudioRendererAvailable: !!StudioRenderer });
43: console.warn("HybReport Debug: Missing data or container", { pivotResult, containerLen: $container.length });

## client/custom/modules/hyb-report/src/views/report-studio/helpers/metric-panel-helps.js
68: console.log("MetricHelper: Dangling tokens removed from formulas.");
177: func: 'COUNT',
309: console.log("MetricHelper: syncOrder completed & Model updated", columnOrder);

## client/custom/modules/hyb-report/src/views/report-studio/helpers/popover-helper.js
13: const currentSort = view.$activeFieldTag.attr('data-sort') || 'NONE';
78: if (actualFieldType === 'date' || actualFieldType === 'datetime') {
155: if (sortVal !== 'NONE') {
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/studio-renderer.js
10: console.log("HybReport Debug: StudioRenderer.render reached", { pivotResult });
12: console.warn("HybReport Debug: Renderer - Missing data or container", { pivotResult, containerLen: $container.length });
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
99: if (rowAxisKeys.length === 0) rKeyArr = ['Grand Total'];
105: if (colAxisKeys.length === 0) cKeyArr = ['Total'];
115: let _hourlyAxisIdx = -1; // vị trí axis hour trong rowAxisKeys gốc
141: break; // chỉ hỗ trợ 1 axis hour
182: if (colAxisKeys.length === 0) cKeyArr = ['Total'];
222: if (_originalRowAxisKeys.length === 0) rKeyArr = ['Grand Total'];
228: if (rowAxisKeys.length === 0) rKeyArr = ['Grand Total'];
233: if (colAxisKeys.length === 0) cKeyArr = ['Total'];
253: if (_originalRowAxisKeys.length === 0) rKeyArr = ['Grand Total'];
260: if (rowAxisKeys.length === 0) rKeyArr = ['Grand Total'];
269: if (colAxisKeys.length === 0) cKeyArr = ['Total'];
312: rowHeaderLabel = rowAxisKeys.map(k => _resolveLabel(k)).join(' / ') || 'Dimensions';

## client/custom/modules/hyb-report/src/views/report-studio/panels/chart-settings.js
33: const axisCharts = ['BarChart', 'LineChart', 'ScatterChart'];
36: const echartsCharts = ['BarChart', 'LineChart', 'PieChart', 'ScatterChart', 'FunnelChart', 'RadarChart', 'GaugeChart', 'Sunburst'];
37: const nonMappingCharts = ['GaugeChart', 'NumberMetric', 'List'];
41: isBarChart:        wType === 'BarChart',
42: isLineChart:       wType === 'LineChart',
43: isPieChart:        wType === 'PieChart',
44: isNumberMetric:    wType === 'NumberMetric',
46: isGaugeChart:      wType === 'GaugeChart',
63: showFunnelLegend:  wType !== 'FunnelChart',
65: showXRotation:     wType !== 'ScatterChart',
67: showDataZoom:      ['BarChart', 'LineChart'].includes(wType),
69: showNumberFormatOnly: ['NumberMetric', 'List'].includes(wType),
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/panels/datasource-selector.js
22: 'input #search-entity-input':        'onSearchInput',
28: 'drop #join-dropzone':                'onDropJoin',
29: 'click #btn-save-data-model':         'onApply',
99: 'belongsTo':       { icon: 'fas fa-arrow-up',       color: '#16a34a', risk: 'safe',    badgeKey: 'safeJoin',          card: 'N:1',  borderColor: '#16a34a' },
100: 'hasOne':          { icon: 'fas fa-circle',         color: '#16a34a', risk: 'safe',    badgeKey: 'safeJoin',          card: '1:1',  borderColor: '#16a34a' },
101: 'linkOne':         { icon: 'fas fa-circle',         color: '#16a34a', risk: 'safe',    badgeKey: 'safeJoin',          card: '1:1',  borderColor: '#16a34a' },
215: <div class="dropped-block data-card-block data-card-root" data-source-id="${rootSource.sourceId}" data-entity="${rootSource.entityType}" style="border-left: 4px solid #3b82f6; display: flex; align-items: center; padding:
224: const rootEntityLinks = this.getMetadata().get(['entityDefs', rootSource.entityType, 'links']) || {};
273: isReverse: $el.attr('data-is-reverse') === 'true',  // [#19] .attr() thay vì .data() — tránh jQuery camelCase trap
319: <div class="dropped-block data-card-block data-card-root" data-source-id="${rootSourceId}" data-entity="${dragData.entity}" style="border-left: 4px solid #3b82f6; display: flex; align-items: center; padding: 6px 12px; ba
365: joinType: 'LEFT',
389: const foreignScope = revMatch[1];  // e.g. 'Call'
391: const scopeLabel = this._safeTranslate(foreignScope, 'scopeNames', 'HybReport') || foreignScope;
396: const entName = this._safeTranslate(join.entityType, 'scopeNames', 'HybReport') || join.entityType;
409: const safeLeftL = this._safeTranslate('leftJoinLabel', 'labels', 'HybReport') || 'LEFT JOIN';
410: const safeInnerL = this._safeTranslate('innerJoinLabel', 'labels', 'HybReport') || 'INNER JOIN';
423: <option value="LEFT" ${join.joinType === 'LEFT' ? 'selected' : ''}>${safeLeftL}</option>
424: <option value="INNER" ${join.joinType === 'INNER' ? 'selected' : ''}>${safeInnerL}</option>
512: deps.push({ type: 'Metric', label: c.columnLabel || c.func });
542: joinLink: '', joinType: 'LEFT', calculations: []
607: const personEntities = ['Lead', 'Contact', 'User', 'Account'];
613: sourceId: rootSourceId, field: 'firstName', fieldType: 'varchar', key: `${rootSourceId}.firstName`,
617: sourceId: rootSourceId, field: 'lastName', fieldType: 'varchar', key: `${rootSourceId}.lastName`,
622: sourceId: rootSourceId, field: 'name', fieldType: 'varchar', key: `${rootSourceId}.name`,
628: sourceId: rootSourceId, field: 'assignedUser', fieldType: 'link', key: `${rootSourceId}.assignedUser`,
629: foreignEntity: 'User',
633: sourceId: rootSourceId, field: 'createdAt', fieldType: 'datetime', key: `${rootSourceId}.createdAt`,

## client/custom/modules/hyb-report/src/views/report-studio/panels/filter-panel.js
28: 'click .filter-item-block': '_openFilterSettings',
29: 'click .btn-remove-filter': '_removeFilter'

## client/custom/modules/hyb-report/src/views/report-studio/panels/pivot-settings.js
27: 'change .sf-metric-sort-select':   'onMetricSortChange',   // [#41]
28: 'change .sf-metric-sort-dir':      'onMetricSortDirChange', // [#41]
42: || `${calc.func || 'COUNT'}(${calc.field || 'id'})`;
89: const dir = $(e.currentTarget).val() || 'DESC';
103: layout.pivot.metricSort = { calcId: '', dir: 'DESC' };

## 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
11: 'input #hyb-fpp-search': 'onSearch',
12: 'click .hyb-fpp-field:not(.is-disabled)': 'onFieldToggle',
29: if (e.key === 'Escape') this.close();
30: if (e.key === 'Enter' && this._getTotalSelectedCount() > 0) this.apply();
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/field-settings.js
12: 'click .insert-field-btn': 'insertWandField',
15: 'change [data-name="timeMode"]': 'onGrainOrModeChange'  // [#22C] rebuild format khi đổi mode
27: this.currentSort = $tag.attr('data-sort') || 'NONE';
49: const effectiveSort = isGroupZone && (!this.currentSort || this.currentSort === 'NONE')
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)'},
141: {v:'numeric', l:'8 (số)'},
142: {v:'full',    l:'08:00-09:00 (khoảng)'}

## client/custom/modules/hyb-report/src/views/report-studio/popovers/filter-settings.js
12: 'change .macro-field-select': 'onMacroFieldChange',
14: 'input .search-tag-input': 'onSearchInput',
15: 'click .result-item': 'onSearchResultClick',
35: this.hasMacroSupport = this.isCore || ['User', 'Team'].includes(this.fDraft.foreignEntity);
45: this.fDraft.valueMode = (this.fDraft.macroScope === 'Team') ? 'SPECIFIC_TEAM' : 'SPECIFIC';
50: this.fDraft.valueMode = 'SPECIFIC';
148: <option value="SPECIFIC" ${valueMode === 'SPECIFIC' ? 'selected' : ''}>${mSpecU}</option>
151: <div class="filter-popover-tagbox-container" style="min-height: 34px; padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; background: #fff; margin-bottom: 8px; position: relative; ${(valueMode === 'SPECIFIC' || 
276: this.clearView('linkField');
277: this.createView('linkField', 'views/fields/link', {
282: foreignEntity: this.fDraft.foreignEntity || 'User',
347: if (val === 'SPECIFIC' || val === 'SPECIFIC_TEAM') {
351: this.fDraft.macroScope = (val === 'SPECIFIC_TEAM') ? 'Team' : 'User';
356: this.fDraft.macroScope = 'User';
361: if (val === 'SPECIFIC' || val === 'SPECIFIC_TEAM') {
382: const scope = mode === 'SPECIFIC_TEAM' ? 'Team' : 'User';
401: style="padding: 8px 12px; cursor: ${isSelected ? 'default' : 'pointer'}; font-size: 11px; color: ${isSelected ? '#94a3b8' : '#334155'}; background: #fff; transition: background 0.2s; display: flex; align-items: center; j
458: const linkView = this.getView('linkField');

## client/custom/modules/hyb-report/src/views/report-studio/popovers/metric-settings.js
14: 'click .unified-field-search': 'onSearchClick',
15: 'keyup .unified-field-search': 'onLiveSearch',
18: 'click .metric-item': 'onItemSelect',
96: if (fn.name === 'COUNT') {
128: if (fn.name === 'COUNT') {

## client/custom/modules/hyb-report/src/views/report-studio/popovers/unified-field-list.js
8: 'click .combobox-item': 'onItemSelect',
