osl-base-extended 1.1.35 → 1.1.37
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.
|
@@ -999,7 +999,7 @@ class Oslinput {
|
|
|
999
999
|
get inputType() {
|
|
1000
1000
|
if (this.type === 'password')
|
|
1001
1001
|
return this.showPassword ? 'text' : 'password';
|
|
1002
|
-
if (this.decimalPortion !== null)
|
|
1002
|
+
if (this.decimalPortion !== null || this.type === 'int')
|
|
1003
1003
|
return 'text';
|
|
1004
1004
|
return this.type;
|
|
1005
1005
|
}
|
|
@@ -1070,6 +1070,12 @@ class Oslinput {
|
|
|
1070
1070
|
event.preventDefault();
|
|
1071
1071
|
return;
|
|
1072
1072
|
}
|
|
1073
|
+
if (this.type === 'int') {
|
|
1074
|
+
if (!/\d/.test(event.key)) {
|
|
1075
|
+
event.preventDefault();
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1073
1079
|
if (this.decimalPortion !== null) {
|
|
1074
1080
|
const key = event.key;
|
|
1075
1081
|
if (!/[\d.]/.test(key)) {
|
|
@@ -1132,6 +1138,14 @@ class Oslinput {
|
|
|
1132
1138
|
this.modelChange.emit(this.model);
|
|
1133
1139
|
return;
|
|
1134
1140
|
}
|
|
1141
|
+
if (this.type === 'int') {
|
|
1142
|
+
const dotIndex = value.indexOf('.');
|
|
1143
|
+
const truncated = dotIndex !== -1 ? value.substring(0, dotIndex) : value;
|
|
1144
|
+
const digits = truncated.replace(/[^\d]/g, '');
|
|
1145
|
+
this.model = digits === '' ? null : parseInt(digits, 10);
|
|
1146
|
+
this.modelChange.emit(this.model);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1135
1149
|
if (this.isCapitalize) {
|
|
1136
1150
|
value = typeof value == 'string' ? value?.toUpperCase() : value;
|
|
1137
1151
|
}
|
|
@@ -2987,6 +3001,8 @@ class OslReportGrid {
|
|
|
2987
3001
|
rowSelection = null;
|
|
2988
3002
|
showAggregates = false;
|
|
2989
3003
|
title = '';
|
|
3004
|
+
pdfExportFromGrid = false;
|
|
3005
|
+
pdfConfig = {};
|
|
2990
3006
|
// Outputs
|
|
2991
3007
|
pageChange = new EventEmitter();
|
|
2992
3008
|
pageSizeChange = new EventEmitter();
|
|
@@ -3116,7 +3132,7 @@ class OslReportGrid {
|
|
|
3116
3132
|
}
|
|
3117
3133
|
return rows;
|
|
3118
3134
|
}
|
|
3119
|
-
|
|
3135
|
+
getSortedFilteredRows() {
|
|
3120
3136
|
let rows = this.getFilteredRows();
|
|
3121
3137
|
if (this.autoMode && this.sortStates.size > 0) {
|
|
3122
3138
|
const sorts = [...this.sortStates.values()].sort((a, b) => a.index - b.index);
|
|
@@ -3129,6 +3145,10 @@ class OslReportGrid {
|
|
|
3129
3145
|
return 0;
|
|
3130
3146
|
});
|
|
3131
3147
|
}
|
|
3148
|
+
return rows;
|
|
3149
|
+
}
|
|
3150
|
+
processData() {
|
|
3151
|
+
const rows = this.getSortedFilteredRows();
|
|
3132
3152
|
this._filteredTotal = rows.length;
|
|
3133
3153
|
if (this.activeGroups.length > 0) {
|
|
3134
3154
|
// Group ALL filtered rows first, then paginate the flat list so every
|
|
@@ -3224,8 +3244,7 @@ class OslReportGrid {
|
|
|
3224
3244
|
this.openFilterKey = null;
|
|
3225
3245
|
return;
|
|
3226
3246
|
}
|
|
3227
|
-
|
|
3228
|
-
this.buildExcelFilterItems(key);
|
|
3247
|
+
this.buildExcelFilterItems(key);
|
|
3229
3248
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
3230
3249
|
this.filterDropdownPos = {
|
|
3231
3250
|
top: rect.bottom + 4,
|
|
@@ -3233,17 +3252,38 @@ class OslReportGrid {
|
|
|
3233
3252
|
};
|
|
3234
3253
|
this.openFilterKey = key;
|
|
3235
3254
|
}
|
|
3255
|
+
getFilteredRowsExcluding(excludeKey) {
|
|
3256
|
+
let rows = [...this.datasource];
|
|
3257
|
+
if (this.globalSearch.trim()) {
|
|
3258
|
+
const term = this.globalSearch.toLowerCase();
|
|
3259
|
+
rows = rows.filter(row => this.visibleCols.some(col => String(this.getCellDisplay(row, col)).toLowerCase().includes(term)));
|
|
3260
|
+
}
|
|
3261
|
+
for (const col of this._cols) {
|
|
3262
|
+
const term = (this.columnSearch[col.key] ?? '').trim().toLowerCase();
|
|
3263
|
+
if (term) {
|
|
3264
|
+
rows = rows.filter(row => String(this.getCellDisplay(row, col)).toLowerCase().includes(term));
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
for (const [key, activeSet] of Object.entries(this.excelFilters)) {
|
|
3268
|
+
if (key === excludeKey)
|
|
3269
|
+
continue;
|
|
3270
|
+
if (activeSet.size > 0) {
|
|
3271
|
+
rows = rows.filter(row => activeSet.has(row[key]));
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
return rows;
|
|
3275
|
+
}
|
|
3236
3276
|
buildExcelFilterItems(key) {
|
|
3237
3277
|
const activeSet = this.excelFilters[key] ?? new Set();
|
|
3238
3278
|
const col = this._cols.find(c => c.key === key);
|
|
3239
3279
|
const seen = new Map();
|
|
3240
|
-
for (const row of this.
|
|
3280
|
+
for (const row of this.getFilteredRowsExcluding(key)) {
|
|
3241
3281
|
const val = row[key];
|
|
3242
3282
|
if (!seen.has(val))
|
|
3243
3283
|
seen.set(val, this.getCellDisplayByKey(val, col, undefined));
|
|
3244
3284
|
}
|
|
3245
3285
|
this.excelFilterState[key] = {
|
|
3246
|
-
search: '',
|
|
3286
|
+
search: this.excelFilterState[key]?.search ?? '',
|
|
3247
3287
|
allItems: [...seen.entries()].map(([value, label]) => ({
|
|
3248
3288
|
value, label,
|
|
3249
3289
|
checked: activeSet.size === 0 || activeSet.has(value),
|
|
@@ -3522,19 +3562,262 @@ class OslReportGrid {
|
|
|
3522
3562
|
this.processData();
|
|
3523
3563
|
this.pageSizeChange.emit({ page: 1, pageSize: this.pageSize });
|
|
3524
3564
|
}
|
|
3525
|
-
// ─── Export CSV
|
|
3565
|
+
// ─── Export CSV / Excel ───────────────────────────────────────────────────────
|
|
3526
3566
|
exportCsv() {
|
|
3527
3567
|
const cols = this.visibleCols;
|
|
3568
|
+
const filteredRows = this.getSortedFilteredRows();
|
|
3528
3569
|
const lines = [];
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3570
|
+
const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`;
|
|
3571
|
+
// Header row
|
|
3572
|
+
lines.push(cols.map(c => esc(c.headerGroup ? `${c.label} (${c.headerGroup})` : c.label)).join(','));
|
|
3573
|
+
// Build rows respecting grouping + subtotals
|
|
3574
|
+
const buildCsvRows = (rows, groupKeys, level) => {
|
|
3575
|
+
if (groupKeys.length === 0) {
|
|
3576
|
+
rows.forEach(r => lines.push(cols.map(col => esc(this.getCellDisplay(r, col))).join(',')));
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
const key = groupKeys[0];
|
|
3580
|
+
const remaining = groupKeys.slice(1);
|
|
3581
|
+
const groups = new Map();
|
|
3582
|
+
for (const row of rows) {
|
|
3583
|
+
const val = row[key];
|
|
3584
|
+
if (!groups.has(val))
|
|
3585
|
+
groups.set(val, []);
|
|
3586
|
+
groups.get(val).push(row);
|
|
3587
|
+
}
|
|
3588
|
+
const colDef = this._cols.find(c => c.key === key);
|
|
3589
|
+
const sumCols = cols.filter(c => c.sumOnPdf);
|
|
3590
|
+
for (const [val, gr] of groups) {
|
|
3591
|
+
const label = this.getCellDisplayByKey(val, colDef, undefined);
|
|
3592
|
+
const indent = ' '.repeat(level);
|
|
3593
|
+
lines.push([esc(`${indent}${colDef?.label ?? key}: ${label} (${gr.length} rows)`), ...Array(cols.length - 1).fill('""')].join(','));
|
|
3594
|
+
buildCsvRows(gr, remaining, level + 1);
|
|
3595
|
+
if (sumCols.length > 0) {
|
|
3596
|
+
lines.push(cols.map((col, i) => {
|
|
3597
|
+
if (col.sumOnPdf) {
|
|
3598
|
+
const s = gr.map(r => Number(r[col.key])).filter(v => !isNaN(v)).reduce((a, b) => a + b, 0);
|
|
3599
|
+
return esc(this.decimalPipe.transform(s, '1.0-2') ?? s);
|
|
3600
|
+
}
|
|
3601
|
+
return i === 0 ? esc(`${indent}Subtotal: ${label}`) : '""';
|
|
3602
|
+
}).join(','));
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
if (this.activeGroups.length > 0) {
|
|
3607
|
+
buildCsvRows(filteredRows, this.activeGroups, 0);
|
|
3608
|
+
}
|
|
3609
|
+
else {
|
|
3610
|
+
filteredRows.forEach(r => lines.push(cols.map(col => esc(this.getCellDisplay(r, col))).join(',')));
|
|
3611
|
+
}
|
|
3612
|
+
// Grand total row
|
|
3613
|
+
const sumCols = cols.filter(c => c.sumOnPdf);
|
|
3614
|
+
if (sumCols.length > 0) {
|
|
3615
|
+
lines.push(cols.map((col, i) => {
|
|
3616
|
+
if (col.sumOnPdf) {
|
|
3617
|
+
const s = filteredRows.map(r => Number(r[col.key])).filter(v => !isNaN(v)).reduce((a, b) => a + b, 0);
|
|
3618
|
+
return esc(this.decimalPipe.transform(s, '1.0-2') ?? s);
|
|
3619
|
+
}
|
|
3620
|
+
return i === 0 ? esc('Grand Total') : '""';
|
|
3621
|
+
}).join(','));
|
|
3622
|
+
}
|
|
3623
|
+
const blob = new Blob(['' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
|
3532
3624
|
const a = Object.assign(document.createElement('a'), {
|
|
3533
3625
|
href: URL.createObjectURL(blob), download: `${this.title || 'report'}.csv`,
|
|
3534
3626
|
});
|
|
3535
3627
|
a.click();
|
|
3536
3628
|
URL.revokeObjectURL(a.href);
|
|
3537
3629
|
}
|
|
3630
|
+
// ─── Export PDF ───────────────────────────────────────────────────────────────
|
|
3631
|
+
async exportPdf() {
|
|
3632
|
+
const [{ jsPDF }, { default: autoTable }] = await Promise.all([
|
|
3633
|
+
import('jspdf'),
|
|
3634
|
+
import('jspdf-autotable'),
|
|
3635
|
+
]);
|
|
3636
|
+
const cfg = this.pdfConfig ?? {};
|
|
3637
|
+
const orientation = cfg.orientation ?? 'landscape';
|
|
3638
|
+
const doc = new jsPDF({ orientation, unit: 'pt', format: 'a4' });
|
|
3639
|
+
const pw = doc.internal.pageSize.getWidth();
|
|
3640
|
+
const ph = doc.internal.pageSize.getHeight();
|
|
3641
|
+
const margin = 36;
|
|
3642
|
+
const usableW = pw - margin * 2;
|
|
3643
|
+
let yPos = margin;
|
|
3644
|
+
// Logo (top-left) + Company name/address (center)
|
|
3645
|
+
const logoH = 50;
|
|
3646
|
+
if (cfg.logo) {
|
|
3647
|
+
try {
|
|
3648
|
+
const img = await this._loadImageForPdf(cfg.logo);
|
|
3649
|
+
const logoW = (img.naturalWidth / img.naturalHeight) * logoH;
|
|
3650
|
+
doc.addImage(img.dataUrl, img.imgType, margin, yPos, logoW, logoH);
|
|
3651
|
+
}
|
|
3652
|
+
catch { /* ignore broken logo */ }
|
|
3653
|
+
}
|
|
3654
|
+
if (cfg.companyName) {
|
|
3655
|
+
doc.setFont('helvetica', 'bold');
|
|
3656
|
+
doc.setFontSize(16);
|
|
3657
|
+
doc.setTextColor(0, 0, 0);
|
|
3658
|
+
doc.text(cfg.companyName, pw / 2, yPos + 20, { align: 'center' });
|
|
3659
|
+
}
|
|
3660
|
+
if (cfg.companyAddress) {
|
|
3661
|
+
doc.setFont('helvetica', 'normal');
|
|
3662
|
+
doc.setFontSize(9);
|
|
3663
|
+
doc.setTextColor(60, 60, 60);
|
|
3664
|
+
doc.text(cfg.companyAddress, pw / 2, yPos + 34, { align: 'center' });
|
|
3665
|
+
}
|
|
3666
|
+
if (cfg.logo || cfg.companyName || cfg.companyAddress) {
|
|
3667
|
+
yPos += logoH + 10;
|
|
3668
|
+
}
|
|
3669
|
+
// Report name: ══════ Report Name ══════ (double lines on each side, centered)
|
|
3670
|
+
if (cfg.reportName) {
|
|
3671
|
+
doc.setFont('helvetica', 'bold');
|
|
3672
|
+
doc.setFontSize(12);
|
|
3673
|
+
doc.setTextColor(0, 0, 0);
|
|
3674
|
+
const textW = doc.getTextWidth(cfg.reportName);
|
|
3675
|
+
const cx = pw / 2;
|
|
3676
|
+
const gap = 12;
|
|
3677
|
+
const ty = yPos + 16; // text baseline
|
|
3678
|
+
const ly = ty - 5; // top line Y
|
|
3679
|
+
doc.setLineWidth(0.4);
|
|
3680
|
+
doc.setDrawColor(0, 0, 0);
|
|
3681
|
+
// left double lines — end exactly where the text begins
|
|
3682
|
+
doc.line(margin, ly, cx - textW / 2 - gap, ly);
|
|
3683
|
+
doc.line(margin, ly + 2, cx - textW / 2 - gap, ly + 2);
|
|
3684
|
+
// centered report name
|
|
3685
|
+
doc.text(cfg.reportName, cx, ty, { align: 'center' });
|
|
3686
|
+
// right double lines — start exactly where the text ends
|
|
3687
|
+
doc.line(cx + textW / 2 + gap, ly, pw - margin, ly);
|
|
3688
|
+
doc.line(cx + textW / 2 + gap, ly + 2, pw - margin, ly + 2);
|
|
3689
|
+
yPos = ty + 14;
|
|
3690
|
+
}
|
|
3691
|
+
// Build table body (recursive, supports groups + subtotals)
|
|
3692
|
+
const cols = this.visibleCols;
|
|
3693
|
+
const filteredRows = this.getSortedFilteredRows();
|
|
3694
|
+
const totalGridW = cols.reduce((s, c) => s + c._width, 0) || 1;
|
|
3695
|
+
const colWidths = cols.map(c => (c._width / totalGridW) * usableW);
|
|
3696
|
+
const sumCols = cols.filter(c => c.sumOnPdf);
|
|
3697
|
+
const body = [];
|
|
3698
|
+
// Light gray shading per group level — no blue, clean financial-report look
|
|
3699
|
+
const groupBg = [[242, 242, 242], [247, 247, 247], [252, 252, 252]];
|
|
3700
|
+
const subtotalBg = [[232, 232, 232], [237, 237, 237], [242, 242, 242]];
|
|
3701
|
+
const buildPdfRows = (rows, groupKeys, level) => {
|
|
3702
|
+
if (groupKeys.length === 0) {
|
|
3703
|
+
rows.forEach(r => body.push(cols.map(col => ({
|
|
3704
|
+
content: this.getCellDisplay(r, col),
|
|
3705
|
+
styles: { halign: col.align ?? 'left', textColor: [0, 0, 0] },
|
|
3706
|
+
}))));
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
const key = groupKeys[0];
|
|
3710
|
+
const remaining = groupKeys.slice(1);
|
|
3711
|
+
const groups = new Map();
|
|
3712
|
+
for (const row of rows) {
|
|
3713
|
+
const v = row[key];
|
|
3714
|
+
if (!groups.has(v))
|
|
3715
|
+
groups.set(v, []);
|
|
3716
|
+
groups.get(v).push(row);
|
|
3717
|
+
}
|
|
3718
|
+
const colDef = this._cols.find(c => c.key === key);
|
|
3719
|
+
const bg = groupBg[Math.min(level, groupBg.length - 1)];
|
|
3720
|
+
const sb = subtotalBg[Math.min(level, subtotalBg.length - 1)];
|
|
3721
|
+
for (const [val, gr] of groups) {
|
|
3722
|
+
const label = this.getCellDisplayByKey(val, colDef, undefined);
|
|
3723
|
+
const indent = ' '.repeat(level);
|
|
3724
|
+
body.push([{
|
|
3725
|
+
content: `${indent}${colDef?.label ?? key}: ${label} (${gr.length.toLocaleString()} rows)`,
|
|
3726
|
+
colSpan: cols.length,
|
|
3727
|
+
styles: { fontStyle: 'bold', fillColor: bg, textColor: [0, 0, 0] },
|
|
3728
|
+
}]);
|
|
3729
|
+
buildPdfRows(gr, remaining, level + 1);
|
|
3730
|
+
if (sumCols.length > 0) {
|
|
3731
|
+
body.push(cols.map((col, i) => {
|
|
3732
|
+
if (col.sumOnPdf) {
|
|
3733
|
+
const s = gr.map(r => Number(r[col.key])).filter(v => !isNaN(v)).reduce((a, b) => a + b, 0);
|
|
3734
|
+
return { content: this.decimalPipe.transform(s, '1.0-2') ?? String(s), styles: { fontStyle: 'bold', halign: 'right', fillColor: sb, textColor: [0, 0, 0] } };
|
|
3735
|
+
}
|
|
3736
|
+
return { content: i === 0 ? `${indent}Subtotal: ${label}` : '', styles: { fontStyle: 'bold', fillColor: sb, textColor: [0, 0, 0] } };
|
|
3737
|
+
}));
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
if (this.activeGroups.length > 0) {
|
|
3742
|
+
buildPdfRows(filteredRows, this.activeGroups, 0);
|
|
3743
|
+
}
|
|
3744
|
+
else {
|
|
3745
|
+
filteredRows.forEach(r => body.push(cols.map(col => ({
|
|
3746
|
+
content: this.getCellDisplay(r, col),
|
|
3747
|
+
styles: { halign: col.align ?? 'left', textColor: [0, 0, 0] },
|
|
3748
|
+
}))));
|
|
3749
|
+
}
|
|
3750
|
+
// Grand total row
|
|
3751
|
+
if (sumCols.length > 0) {
|
|
3752
|
+
body.push(cols.map((col, i) => {
|
|
3753
|
+
if (col.sumOnPdf) {
|
|
3754
|
+
const s = filteredRows.map(r => Number(r[col.key])).filter(v => !isNaN(v)).reduce((a, b) => a + b, 0);
|
|
3755
|
+
return { content: this.decimalPipe.transform(s, '1.0-2') ?? String(s), styles: { fontStyle: 'bold', halign: 'right', fillColor: [220, 220, 220], textColor: [0, 0, 0] } };
|
|
3756
|
+
}
|
|
3757
|
+
return { content: i === 0 ? 'Grand Total' : '', styles: { fontStyle: 'bold', fillColor: [220, 220, 220], textColor: [0, 0, 0] } };
|
|
3758
|
+
}));
|
|
3759
|
+
}
|
|
3760
|
+
// Render table: clean black-bordered financial-report style
|
|
3761
|
+
const colStylesMap = {};
|
|
3762
|
+
cols.forEach((col, i) => { colStylesMap[i] = { cellWidth: colWidths[i] }; });
|
|
3763
|
+
autoTable(doc, {
|
|
3764
|
+
head: [cols.map((c, i) => ({ content: c.label, styles: { halign: c.align ?? 'left', cellWidth: colWidths[i] } }))],
|
|
3765
|
+
body,
|
|
3766
|
+
startY: yPos,
|
|
3767
|
+
margin: { left: margin, right: margin, top: margin, bottom: margin + 18 },
|
|
3768
|
+
styles: {
|
|
3769
|
+
fontSize: 8,
|
|
3770
|
+
cellPadding: { top: 4, right: 6, bottom: 4, left: 6 },
|
|
3771
|
+
overflow: 'linebreak',
|
|
3772
|
+
lineColor: [0, 0, 0],
|
|
3773
|
+
lineWidth: 0.3,
|
|
3774
|
+
textColor: [0, 0, 0],
|
|
3775
|
+
fillColor: [255, 255, 255],
|
|
3776
|
+
},
|
|
3777
|
+
headStyles: {
|
|
3778
|
+
fillColor: [255, 255, 255],
|
|
3779
|
+
textColor: [0, 0, 0],
|
|
3780
|
+
fontStyle: 'bold',
|
|
3781
|
+
fontSize: 8.5,
|
|
3782
|
+
lineColor: [0, 0, 0],
|
|
3783
|
+
lineWidth: 0.4,
|
|
3784
|
+
},
|
|
3785
|
+
alternateRowStyles: cfg.alternatingRows ? { fillColor: [248, 248, 248] } : {},
|
|
3786
|
+
columnStyles: colStylesMap,
|
|
3787
|
+
showHead: 'everyPage',
|
|
3788
|
+
tableLineColor: [0, 0, 0],
|
|
3789
|
+
tableLineWidth: 0.4,
|
|
3790
|
+
});
|
|
3791
|
+
// Footer: page numbers + timestamp on every page
|
|
3792
|
+
const total = doc.internal.pages.length - 1;
|
|
3793
|
+
const ts = new Date().toLocaleString();
|
|
3794
|
+
for (let p = 1; p <= total; p++) {
|
|
3795
|
+
doc.setPage(p);
|
|
3796
|
+
doc.setFontSize(7.5);
|
|
3797
|
+
doc.setTextColor(100);
|
|
3798
|
+
doc.text(`Generated: ${ts}`, margin, ph - 14);
|
|
3799
|
+
doc.text(`Page ${p} of ${total}`, pw - margin, ph - 14, { align: 'right' });
|
|
3800
|
+
doc.setTextColor(0);
|
|
3801
|
+
}
|
|
3802
|
+
doc.save(`${this.title || 'report'}.pdf`);
|
|
3803
|
+
}
|
|
3804
|
+
async _loadImageForPdf(src) {
|
|
3805
|
+
return new Promise((resolve, reject) => {
|
|
3806
|
+
const img = new Image();
|
|
3807
|
+
img.crossOrigin = 'anonymous';
|
|
3808
|
+
img.onload = () => {
|
|
3809
|
+
const canvas = document.createElement('canvas');
|
|
3810
|
+
canvas.width = img.naturalWidth;
|
|
3811
|
+
canvas.height = img.naturalHeight;
|
|
3812
|
+
canvas.getContext('2d').drawImage(img, 0, 0);
|
|
3813
|
+
const ext = (src.split('?')[0].split('.').pop() ?? '').toLowerCase();
|
|
3814
|
+
const imgType = ext === 'png' ? 'PNG' : 'JPEG';
|
|
3815
|
+
resolve({ dataUrl: canvas.toDataURL(`image/${imgType.toLowerCase()}`), imgType, naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight });
|
|
3816
|
+
};
|
|
3817
|
+
img.onerror = reject;
|
|
3818
|
+
img.src = src;
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3538
3821
|
// ─── Aggregates ───────────────────────────────────────────────────────────────
|
|
3539
3822
|
getAggregate(col) {
|
|
3540
3823
|
if (!col.aggregate)
|
|
@@ -3607,11 +3890,11 @@ class OslReportGrid {
|
|
|
3607
3890
|
asGroupRow = (row) => row;
|
|
3608
3891
|
asDataRow = (row) => row;
|
|
3609
3892
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslReportGrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3610
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslReportGrid, isStandalone: false, selector: "osl-report-grid", inputs: { columns: "columns", datasource: "datasource", loading: "loading", totalRecords: "totalRecords", autoMode: "autoMode", isPaginated: "isPaginated", pageSize: "pageSize", tableHeight: "tableHeight", striped: "striped", exportable: "exportable", rowHeight: "rowHeight", rowSelection: "rowSelection", showAggregates: "showAggregates", title: "title" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowClick: "rowClick", selectionChange: "selectionChange" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()", "document:click": "onDocumentClick()" } }, providers: [DatePipe, DecimalPipe], viewQueries: [{ propertyName: "headerScrollRef", first: true, predicate: ["headerScrollRef"], descendants: true }, { propertyName: "bodyScrollRef", first: true, predicate: ["bodyScrollRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"osl-rg\" (click)=\"$event.stopPropagation()\">\n\n <!-- \u2550\u2550 TOOLBAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-toolbar\">\n <div class=\"osl-rg-toolbar__left\">\n @if (title) { <span class=\"osl-rg-title\">{{ title }}</span> }\n @if (!loading) {\n @if (_filteredTotal !== datasource.length) {\n <span class=\"osl-rg-filter-badge\">\n <span class=\"osl-rg-filter-badge__dot\"></span>\n {{ _filteredTotal | number }} of {{ datasource.length | number }} rows\n </span>\n } @else {\n <span class=\"osl-rg-record-count\">{{ datasource.length | number }} rows</span>\n }\n }\n </div>\n <div class=\"osl-rg-toolbar__right\">\n\n <!-- Global search -->\n <div class=\"osl-rg-global-search\">\n <svg class=\"osl-rg-global-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input class=\"osl-rg-global-search__input\" placeholder=\"Search all columns\u2026\"\n [(ngModel)]=\"globalSearch\" (ngModelChange)=\"currentPage=1; processData()\" />\n @if (globalSearch) {\n <button class=\"osl-rg-global-search__clear\" (click)=\"globalSearch=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n\n <!-- Sort active badge -->\n @if (sortStates.size > 0) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--active\" (click)=\"clearSort()\" title=\"Clear sort\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 5H4M11 9H4M11 13H4M18 4v16M15 7l3-3 3 3M15 17l3 3 3-3\"/>\n </svg>\n <span>{{ sortStates.size }} sort{{ sortStates.size > 1 ? 's' : '' }}</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Filter active badge -->\n @if (hasAnyFilter) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--danger\" (click)=\"clearAllFilters()\" title=\"Clear all filters\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n <span>Filtered</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Group toggle -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showGroupPanel\"\n (click)=\"showGroupPanel=!showGroupPanel\" title=\"Group panel\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Group</span>\n </button>\n\n <!-- Expand / collapse when grouped -->\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-toolbar-btn\" (click)=\"expandAll()\" title=\"Expand all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 20 5-5 5 5M7 4l5 5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-toolbar-btn\" (click)=\"collapseAll()\" title=\"Collapse all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5-5 5 5M7 9l5 5 5 5\"/></svg>\n </button>\n }\n\n <!-- Auto-size all -->\n <button class=\"osl-rg-toolbar-btn\" (click)=\"autoSizeAll()\" title=\"Auto-size all columns\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/>\n </svg>\n <span>Auto-size</span>\n </button>\n\n <!-- Column config -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showColumnConfig\"\n (click)=\"showColumnConfig=!showColumnConfig; $event.stopPropagation()\" title=\"Column settings\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n <span>Columns</span>\n </button>\n\n <!-- Export Excel -->\n @if (exportable) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--excel\" (click)=\"exportCsv()\" title=\"Export to Excel (CSV)\">\n <svg class=\"rg-icon rg-icon--excel\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#1D6F42\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <path d=\"M8 9.5l2.3 3.5L8 16.5h1.6l1.4-2.3 1.4 2.3H14l-2.3-3.5 2.3-3.5h-1.6l-1.4 2.3-1.4-2.3H8z\" fill=\"#fff\"/>\n </svg>\n <span>Export</span>\n </button>\n }\n </div>\n </div>\n\n <!-- \u2550\u2550 GROUP PANEL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showGroupPanel) {\n <div class=\"osl-rg-group-panel\">\n <span class=\"osl-rg-group-panel__label\">Group by:</span>\n @if (activeGroups.length === 0) {\n <span class=\"osl-rg-group-panel__hint\">Use the \u22EE column menu \u2192 \"Group by this\"</span>\n }\n <div class=\"osl-rg-group-chips\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"activeGroups\" (cdkDropListDropped)=\"onGroupPanelDrop($event)\">\n @for (key of activeGroups; track key) {\n <div class=\"osl-rg-group-chip\" cdkDrag>\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <span>{{ getGroupColLabel(key) }}</span>\n <button class=\"osl-rg-group-chip__remove\" (click)=\"removeGroup(key)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n }\n </div>\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-group-panel__clear\" (click)=\"clearGroups()\">Clear groups</button>\n }\n </div>\n }\n\n <!-- \u2550\u2550 STICKY HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-header-wrap\" #headerScrollRef>\n\n <!-- Double header \u2013 group row (only rendered when headerGroup is used on any column) -->\n @if (hasHeaderGroups) {\n <div class=\"osl-rg-header-row osl-rg-header-row--groups\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox osl-rg-th--group-span\"></div>\n }\n <div style=\"display:flex;flex:1;min-width:0;\">\n @for (group of computedHeaderGroups; track $index) {\n <div class=\"osl-rg-th osl-rg-th--group-span\"\n [class.osl-rg-th--group-span--labeled]=\"!!group.label\"\n [class.osl-rg-th--pinned]=\"group.isPinned\"\n [style.width.px]=\"group.width\"\n [style.minWidth.px]=\"group.width\"\n [style.left.px]=\"group.isPinned ? group.stickyLeft : null\"\n [style.position]=\"group.isPinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"group.isPinned ? 3 : 1\">\n @if (group.label) {\n <span class=\"osl-rg-th-group-label\">{{ group.label }}</span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"osl-rg-header-row\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Select-all checkbox -->\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"allSelected\" [indeterminate]=\"someSelected\"\n (change)=\"toggleSelectAll($any($event.target).checked)\" />\n </div>\n }\n\n <!-- Column headers (drag-to-reorder) -->\n <div class=\"osl-rg-header-cols\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"_cols\" (cdkDropListDropped)=\"onColumnReorder($event)\"\n style=\"display:flex;flex:1;min-width:0;\">\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-th\"\n [class.osl-rg-th--pinned]=\"col._pinned\"\n [class.osl-rg-th--sorted]=\"getSortState(col.key)\"\n [class.osl-rg-th--filtered]=\"hasActiveFilter(col.key)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 3 : 1\"\n cdkDrag [cdkDragDisabled]=\"col._pinned\"\n (cdkDragStarted)=\"onColumnDragStarted()\"\n (cdkDragEnded)=\"onColumnDragEnded()\"\n (click)=\"onHeaderClick(col, $event)\">\n\n <div *cdkDragPlaceholder class=\"osl-rg-th-drag-placeholder\"></div>\n\n <!-- Pin badge -->\n @if (col._pinned) {\n <span class=\"osl-rg-th-pin-badge\" title=\"Pinned\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"9\" height=\"9\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </span>\n }\n\n <!-- Label row -->\n <div class=\"osl-rg-th-content\">\n <span class=\"osl-rg-th-label\" [title]=\"col.label\">{{ col.label }}</span>\n <div class=\"osl-rg-th-actions\">\n\n <!-- Sort indicator -->\n @if (col.sortable) {\n <span class=\"osl-rg-sort-icon\"\n [class.osl-rg-sort-icon--asc]=\"getSortState(col.key)?.asc === true\"\n [class.osl-rg-sort-icon--desc]=\"getSortState(col.key)?.asc === false\">\n @if (getSortState(col.key); as s) {\n @if (s.asc) {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n }\n @if (sortStates.size > 1) { <sup class=\"osl-rg-sort-badge\">{{ s.index + 1 }}</sup> }\n } @else {\n <svg class=\"osl-rg-sort-idle\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 9l4-4 4 4M16 15l-4 4-4-4\"/></svg>\n }\n </span>\n }\n\n <!-- Excel filter button -->\n @if (col.filterable) {\n <button class=\"osl-rg-filter-btn\" [class.osl-rg-filter-btn--active]=\"hasActiveFilter(col.key)\"\n (click)=\"openExcelFilter(col.key, $event)\" title=\"Filter\">\n @if (hasActiveFilter(col.key)) {\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/></svg>\n }\n </button>\n }\n\n <!-- Column menu -->\n <button class=\"osl-rg-col-menu-btn\" (click)=\"openColumnMenu(col.key, $event)\" title=\"Column options\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"19\" r=\"2\"/></svg>\n </button>\n\n </div>\n </div>\n\n <!-- Per-column search -->\n @if (col.searchable) {\n <div class=\"osl-rg-col-search\" (click)=\"$event.stopPropagation()\">\n <svg class=\"osl-rg-col-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m18 18-3.5-3.5\"/>\n </svg>\n <input class=\"osl-rg-col-search__input\" [placeholder]=\"col.label + '\u2026'\"\n [ngModel]=\"columnSearch[col.key]\" (ngModelChange)=\"onColumnSearch(col.key, $event)\" />\n @if (columnSearch[col.key]) {\n <button class=\"osl-rg-col-search__clear\" (click)=\"columnSearch[col.key]=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n }\n\n <!-- Resize handle -->\n @if (col.resizable) {\n <div class=\"osl-rg-resize-handle\" (mousedown)=\"startResize(col, $event)\" (dblclick)=\"autoSizeColumn(col, $event)\" (click)=\"$event.stopPropagation()\"></div>\n }\n\n\n </div><!-- /osl-rg-th -->\n }\n\n </div><!-- /header-cols -->\n </div><!-- /header-row -->\n </div><!-- /header-wrap -->\n\n <!-- \u2550\u2550 SCROLLABLE BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-body\" [style.height]=\"tableHeight\" #bodyScrollRef (scroll)=\"onBodyScroll($event)\">\n <div class=\"osl-rg-inner\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Skeleton loading -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <div class=\"osl-rg-row osl-rg-row--skeleton\" [style.height.px]=\"rowHeight\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\"><div class=\"osl-rg-skel\" style=\"width:14px;height:14px;border-radius:3px;\"></div></div>\n }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\">\n <div class=\"osl-rg-skel\" [style.width]=\"$index % 3 === 0 ? '55%' : $index % 3 === 1 ? '80%' : '65%'\"></div>\n </div>\n }\n </div>\n }\n\n } @else if (isEmpty) {\n <!-- Empty state -->\n <div class=\"osl-rg-empty\">\n <div class=\"osl-rg-empty__icon\">\n <svg viewBox=\"0 0 48 48\" fill=\"none\" stroke=\"#d1d5db\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"6\" y=\"6\" width=\"36\" height=\"36\" rx=\"4\"/>\n <path d=\"M6 18h36M6 30h36M18 6v36\"/>\n </svg>\n </div>\n <p class=\"osl-rg-empty__title\">No records found</p>\n <p class=\"osl-rg-empty__sub\">\n @if (hasAnyFilter) { Try adjusting your filters or search terms }\n @else { No data to display }\n </p>\n @if (hasAnyFilter) {\n <button class=\"osl-rg-empty__action\" (click)=\"clearAllFilters()\">Clear all filters</button>\n }\n </div>\n\n } @else {\n <!-- Data rows -->\n @for (row of flatRows; track trackByRow($index, row)) {\n\n @if (row._type === 'group') {\n <div class=\"osl-rg-group-row\"\n [class.osl-rg-group-row--l0]=\"asGroupRow(row)._level === 0\"\n [class.osl-rg-group-row--l1]=\"asGroupRow(row)._level === 1\"\n [class.osl-rg-group-row--l2]=\"asGroupRow(row)._level >= 2\"\n [style.height.px]=\"rowHeight\"\n [style.paddingLeft.px]=\"asGroupRow(row)._level * 20 + 12\"\n (click)=\"toggleGroup(asGroupRow(row)._path)\">\n @if (asGroupRow(row)._expanded) {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>\n } @else {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"/></svg>\n }\n <span class=\"osl-rg-group-row__key\">{{ asGroupRow(row)._colLabel }}:</span>\n <span class=\"osl-rg-group-row__value\">{{ asGroupRow(row)._label }}</span>\n <span class=\"osl-rg-group-row__count\">{{ asGroupRow(row)._count | number }} rows</span>\n </div>\n\n } @else {\n <div class=\"osl-rg-row\"\n [class.osl-rg-row--striped]=\"striped && asDataRow(row)._rowIndex % 2 === 1\"\n [class.osl-rg-row--selected]=\"selectedRows.has(asDataRow(row)._data)\"\n [style.height.px]=\"rowHeight\"\n (click)=\"onRowClick(asDataRow(row)._data, $event)\">\n\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"selectedRows.has(asDataRow(row)._data)\"\n (change)=\"toggleRowSelect(asDataRow(row)._data, $event)\" />\n </div>\n }\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\"\n [class.osl-rg-td--pinned]=\"col._pinned\"\n [ngClass]=\"getCellClass(asDataRow(row)._data, col)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 2 : 0\"\n [style.textAlign]=\"col.align ?? 'left'\"\n (dblclick)=\"copyCell(getCellDisplay(asDataRow(row)._data, col), $event)\"\n [title]=\"getCellDisplay(asDataRow(row)._data, col)\">\n <span class=\"osl-rg-cell-text\">{{ getCellDisplay(asDataRow(row)._data, col) }}</span>\n </div>\n }\n\n </div>\n }\n\n }\n }\n\n </div><!-- /osl-rg-inner -->\n </div><!-- /osl-rg-body -->\n\n <!-- \u2550\u2550 AGGREGATE FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showAggregates && hasAnyAggregate() && !loading) {\n <div class=\"osl-rg-aggregate-row\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') { <div class=\"osl-rg-agg-cell osl-rg-agg-cell--checkbox\"></div> }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-agg-cell\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\" [style.textAlign]=\"col.align ?? 'left'\">\n @if (col.aggregate) {\n <span class=\"osl-rg-agg-label\">{{ col.aggregate | uppercase }}</span>\n <span class=\"osl-rg-agg-value\">{{ getAggregate(col) }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- \u2550\u2550 PAGINATION \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (isPaginated) {\n <div class=\"osl-rg-pagination\">\n <span class=\"osl-rg-pagination__info\">\n @if (loading) { Loading\u2026 }\n @else if (_total > 0) { Showing <strong>{{ startRecord | number }}\u2013{{ endRecord | number }}</strong> of <strong>{{ _total | number }}</strong> records }\n @else { No records }\n </span>\n <div class=\"osl-rg-pagination__controls\">\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage-1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m15 18-6-6 6-6\"/></svg>\n Prev\n </button>\n @for (page of pageNumbers; track $index) {\n @if (page === -1) { <span class=\"osl-rg-page-ellipsis\">\u2026</span> }\n @else {\n <button class=\"osl-rg-page-btn osl-rg-page-btn--num\" [class.osl-rg-page-btn--active]=\"page===currentPage\"\n [disabled]=\"loading\" (click)=\"goToPage(page)\">{{ page }}</button>\n }\n }\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage+1)\" [disabled]=\"currentPage===totalPages||loading\">\n Next\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m9 18 6-6-6-6\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage===totalPages||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-pagination__size\">\n <span class=\"osl-rg-pagination__size-label\">Rows per page</span>\n <select class=\"osl-rg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) { <option [value]=\"opt\">{{ opt }}</option> }\n </select>\n </div>\n </div>\n }\n\n</div><!-- /osl-rg -->\n\n\n<!-- \u2550\u2550 EXCEL FILTER DROPDOWN (fixed, outside overflow containers) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (openFilterKey && excelFilterState[openFilterKey]) {\n <div class=\"osl-rg-excel-filter\"\n [style.top.px]=\"filterDropdownPos.top\"\n [style.left.px]=\"filterDropdownPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-excel-filter__header\">\n <div class=\"osl-rg-excel-filter__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" style=\"flex-shrink:0;\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n Filter: {{ getColumnByKey(openFilterKey)?.label }}\n </div>\n @if (hasActiveFilter(openFilterKey)) {\n <button class=\"osl-rg-excel-filter__clear-btn\" (click)=\"clearExcelFilter(openFilterKey!)\">Clear</button>\n }\n </div>\n <div class=\"osl-rg-excel-filter__search\">\n <input class=\"osl-rg-excel-filter__search-input\" placeholder=\"Search values\u2026\"\n [(ngModel)]=\"excelFilterState[openFilterKey].search\" />\n </div>\n <div class=\"osl-rg-excel-filter__select-all\">\n <label class=\"osl-rg-excel-filter__item osl-rg-excel-filter__item--all\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"isAllExcelChecked(openFilterKey)\"\n (change)=\"toggleAllExcelFilter(openFilterKey!, $any($event.target).checked)\" />\n <span class=\"osl-rg-excel-filter__item-label\">(Select All)</span>\n </label>\n </div>\n <div class=\"osl-rg-excel-filter__list\">\n @for (item of getFilteredExcelItems(openFilterKey); track item.value) {\n <label class=\"osl-rg-excel-filter__item\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [(ngModel)]=\"item.checked\" />\n <span class=\"osl-rg-excel-filter__item-label\">{{ item.label }}</span>\n </label>\n }\n </div>\n <div class=\"osl-rg-excel-filter__footer\">\n <button class=\"osl-rg-excel-filter__apply\" (click)=\"applyExcelFilter(openFilterKey!)\">Apply Filter</button>\n <button class=\"osl-rg-excel-filter__cancel\" (click)=\"openFilterKey=null\">Cancel</button>\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONTEXT MENU (fixed) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (columnMenuKey && getColumnByKey(columnMenuKey); as menuCol) {\n <div class=\"osl-rg-col-menu\"\n [style.top.px]=\"columnMenuPos.top\" [style.left.px]=\"columnMenuPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-col-menu__header\">{{ menuCol.label }}</div>\n @if (menuCol.sortable) {\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:true,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n Sort A \u2192 Z\n </button>\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:false,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n Sort Z \u2192 A\n </button>\n }\n @if (menuCol.groupable) {\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"addGroup(menuCol.key); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/></svg>\n Group by this\n </button>\n }\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"toggleColumnPin(menuCol)\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n {{ menuCol._pinned ? 'Unpin column' : 'Pin to left' }}\n </button>\n <button class=\"osl-rg-col-menu__item\" (click)=\"autoSizeColumn(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/></svg>\n Auto-size column\n </button>\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item osl-rg-col-menu__item--danger\" (click)=\"toggleColumnVisibility(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n Hide column\n </button>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONFIG SIDE DRAWER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (showColumnConfig) {\n <div class=\"osl-rg-config-backdrop\" (click)=\"showColumnConfig=false\"></div>\n <div class=\"osl-rg-config-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-config-panel__header\">\n <div class=\"osl-rg-config-panel__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"16\" height=\"16\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n Column Settings\n </div>\n <button class=\"osl-rg-config-panel__close\" (click)=\"showColumnConfig=false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-config-panel__actions\">\n <button class=\"osl-rg-config-action\" (click)=\"autoSizeAll()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3\"/></svg>\n Auto-size all\n </button>\n <button class=\"osl-rg-config-action\" (click)=\"showAllColumns()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n Show all\n </button>\n </div>\n <div class=\"osl-rg-config-hint\">Drag to reorder \u00B7 Toggle to show/hide \u00B7 Pin to freeze</div>\n <div class=\"osl-rg-config-list\" cdkDropList (cdkDropListDropped)=\"onConfigColumnReorder($event)\">\n @for (col of _cols; track col.key) {\n <div class=\"osl-rg-config-item\" cdkDrag [class.osl-rg-config-item--hidden]=\"col._hidden\">\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"!col._hidden\" (change)=\"toggleColumnVisibility(col)\" />\n <span class=\"osl-rg-config-item__label\" [class.osl-rg-config-item__label--hidden]=\"col._hidden\">{{ col.label }}</span>\n <button class=\"osl-rg-config-item__pin-btn\" (click)=\"toggleColumnPin(col)\"\n [class.osl-rg-config-item__pin-btn--active]=\"col._pinned\"\n [title]=\"col._pinned ? 'Unpin' : 'Pin to left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </button>\n </div>\n }\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COPY TOAST \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (copiedCell !== null) {\n <div class=\"osl-rg-copy-toast\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Copied to clipboard\n </div>\n}\n", styles: ["@keyframes rg-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes rg-fade-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes rg-slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.rg-icon{width:15px;height:15px;flex-shrink:0;display:block}.rg-icon--sm{width:14px;height:14px}.rg-icon--xs{width:12px;height:12px}.rg-icon--excel{width:18px;height:18px}.rg-drag-handle{width:14px;height:14px;flex-shrink:0;color:#d1d5db;cursor:grab;display:block}.rg-drag-handle:active{cursor:grabbing}.osl-rg{position:relative;display:flex;flex-direction:column;width:100%;font-family:inherit;font-size:13px;color:#111827;background:#fff;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px #00000012,0 1px 2px #0000000a;-webkit-user-select:none;user-select:none}.osl-rg-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;background:linear-gradient(135deg,#f8faff,#f3f4f6);border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;flex-wrap:wrap}.osl-rg-toolbar__left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.osl-rg-toolbar__right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.osl-rg-title{font-size:14px;font-weight:700;color:#111827;letter-spacing:-.01em}.osl-rg-record-count{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#9ca3af;font-weight:500}.osl-rg-record-count svg{color:#d1d5db}.osl-rg-filter-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;font-size:11px;color:#1d4ed8;font-weight:600}.osl-rg-filter-badge__dot{width:6px;height:6px;border-radius:50%;background:#2563eb;animation:rg-pulse 1.5s ease-in-out infinite;flex-shrink:0}.osl-rg-toolbar-btn{display:inline-flex;align-items:center;gap:5px;height:32px;padding:0 11px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;color:#5a6478;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s ease}.osl-rg-toolbar-btn svg{flex-shrink:0}.osl-rg-toolbar-btn:hover{background:#f3f4f6;color:#1f2937;border-color:#c4c9d4;box-shadow:0 1px 3px #0000000f}.osl-rg-toolbar-btn--on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.osl-rg-toolbar-btn--active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600}.osl-rg-toolbar-btn--danger{background:#fff5f5;border-color:#fca5a5;color:#dc2626}.osl-rg-toolbar-btn--danger:hover{background:#fee2e2;border-color:#f87171}.osl-rg-toolbar-btn--excel{background:linear-gradient(135deg,#1a7a3e,#1d6f42);border-color:#166534;color:#fff;font-weight:600;box-shadow:0 1px 3px #1665344d}.osl-rg-toolbar-btn--excel:hover{background:linear-gradient(135deg,#166534,#145a2c);border-color:#14532d;color:#fff;box-shadow:0 2px 6px #16653466}.osl-rg-global-search{position:relative;display:inline-flex;align-items:center}.osl-rg-global-search__icon{position:absolute;left:9px;width:14px;height:14px;color:#9ca3af;pointer-events:none;display:block}.osl-rg-global-search__input{height:32px;padding:0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;font-size:12.5px;font-family:inherit;color:#111827;outline:none;width:210px;transition:border-color .15s,width .2s,box-shadow .15s}.osl-rg-global-search__input::placeholder{color:#b0b7c3}.osl-rg-global-search__input:focus{border-color:var(--osl-primary, #2563eb);width:260px;box-shadow:0 0 0 3px #2563eb1a}.osl-rg-global-search__clear{position:absolute;right:6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:4px;transition:all .12s}.osl-rg-global-search__clear svg{width:12px;height:12px}.osl-rg-global-search__clear:hover{color:#374151;background:#0000000d}.osl-rg-group-panel{display:flex;align-items:center;gap:8px;padding:6px 14px;background:linear-gradient(135deg,#fafbff,#f5f7ff);border-bottom:1px dashed #dde5ff;min-height:40px;flex-shrink:0;flex-wrap:wrap}.osl-rg-group-panel__label{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0}.osl-rg-group-panel__label svg{color:#9ca3af}.osl-rg-group-panel__hint{font-size:11px;color:#b0b7c3;font-style:italic}.osl-rg-group-panel__clear{margin-left:auto;font-size:11px;color:#6b7280;background:none;border:none;cursor:pointer;text-decoration:underline;flex-shrink:0;padding:0;font-family:inherit}.osl-rg-group-panel__clear:hover{color:#dc2626}.osl-rg-group-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex:1;min-height:26px}.osl-rg-group-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 6px 3px 5px;background:linear-gradient(135deg,#dbeafe,#eff6ff);border:1px solid #bfdbfe;border-radius:20px;font-size:12px;font-weight:600;color:#1e40af;cursor:grab;-webkit-user-select:none;user-select:none;box-shadow:0 1px 2px #2563eb1a;transition:box-shadow .1s}.osl-rg-group-chip:active{cursor:grabbing;box-shadow:0 2px 6px #2563eb33}.osl-rg-group-chip .rg-drag-handle{color:#93c5fd}.osl-rg-group-chip__remove{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;background:transparent;cursor:pointer;padding:0;border-radius:50%;transition:all .1s}.osl-rg-group-chip__remove svg{width:10px;height:10px;color:#93c5fd}.osl-rg-group-chip__remove:hover{background:#dbeafe}.osl-rg-group-chip__remove:hover svg{color:#1e40af}.osl-rg-table-area{display:flex;flex-direction:column;flex:1 1 0;overflow:hidden;position:relative;min-height:0}.osl-rg-header-wrap{overflow:hidden;flex-shrink:0;border-bottom:2px solid #dde5ff;background:linear-gradient(180deg,#f0f4ff,#e8efff);z-index:10;position:relative}.osl-rg-header-row{display:flex;align-items:stretch}.osl-rg-header-row--groups{border-bottom:1px solid #c7d7f0}.osl-rg-th--group-span{display:flex;align-items:center;justify-content:center;height:28px;min-height:28px;cursor:default;padding:0;border-right:1px solid #dde5ff}.osl-rg-th--group-span:hover{background:transparent!important}.osl-rg-th--group-span:last-child{border-right:none}.osl-rg-th--group-span--labeled{background:linear-gradient(135deg,#dbeafe,#eff6ff);border-bottom:2px solid #93c5fd}.osl-rg-th-group-label{font-size:11px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 10px;text-align:center;width:100%}.osl-rg-header-cols{display:flex}.osl-rg-th{position:relative;display:flex;flex-direction:column;background:transparent;border-right:1px solid #dde5ff;cursor:pointer;flex-shrink:0;transition:background .12s;overflow:visible}.osl-rg-th:last-child{border-right:none}.osl-rg-th:hover{background:#2563eb0f}.osl-rg-th--pinned{background:linear-gradient(180deg,#e8efff,#dde8ff);border-right:1px solid #bfdbfe;z-index:3}.osl-rg-th--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.14),transparent);pointer-events:none;z-index:4}.osl-rg-th--sorted{background:#6366f114}.osl-rg-th--filtered{background:#f59e0b12}.osl-rg-th-drag-placeholder{background:#e0e7ff;border:2px dashed #6366f1;border-radius:4px;opacity:.6}.osl-rg-th-pin-badge{position:absolute;top:3px;right:24px;color:var(--osl-primary, #2563eb);opacity:.6;display:flex;align-items:center}.osl-rg-th-content{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 8px 0 10px;height:36px;min-height:36px}.osl-rg-th-label{font-size:11.5px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.osl-rg-th-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.osl-rg-sort-icon{display:inline-flex;align-items:center;position:relative;color:#9ca3af;transition:color .12s;width:14px;height:14px}.osl-rg-sort-icon svg{width:13px;height:13px;display:block}.osl-rg-sort-icon--asc,.osl-rg-sort-icon--desc{color:var(--osl-primary, #2563eb)}.osl-rg-sort-idle{opacity:.3}.osl-rg-sort-badge{position:absolute;top:-4px;right:-6px;font-size:8px;font-weight:700;background:var(--osl-primary, #2563eb);color:#fff;border-radius:50%;width:11px;height:11px;display:flex;align-items:center;justify-content:center;line-height:1}.osl-rg-filter-btn,.osl-rg-col-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:#b0b7c3;cursor:pointer;padding:0;border-radius:5px;transition:all .12s}.osl-rg-filter-btn svg,.osl-rg-col-menu-btn svg{width:13px;height:13px;display:block}.osl-rg-filter-btn:hover,.osl-rg-col-menu-btn:hover{background:#00000012;color:#374151}.osl-rg-filter-btn--active{color:var(--osl-primary, #2563eb)!important;background:#2563eb1f!important}.osl-rg-col-search{position:relative;display:flex;align-items:center;padding:3px 6px;border-top:1px solid #e0e7ff;background:#ffffff80}.osl-rg-col-search__icon{position:absolute;left:10px;width:12px;height:12px;display:block;pointer-events:none;color:#c4cad6}.osl-rg-col-search__input{width:100%;height:23px;padding:0 18px 0 22px;border:1px solid #dde5ff;border-radius:5px;font-size:11px;font-family:inherit;background:#fff;color:#111827;outline:none;transition:border-color .12s,box-shadow .12s}.osl-rg-col-search__input::placeholder{color:#c4cad6}.osl-rg-col-search__input:focus{border-color:var(--osl-primary, #2563eb);box-shadow:0 0 0 2px #2563eb1a}.osl-rg-col-search__clear{position:absolute;right:8px;display:flex;align-items:center;width:14px;height:14px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:3px;transition:all .1s}.osl-rg-col-search__clear svg{width:10px;height:10px;display:block}.osl-rg-col-search__clear:hover{color:#374151;background:#0000000f}.osl-rg-resize-handle{position:absolute;right:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.osl-rg-resize-handle:after{content:\"\";position:absolute;right:2px;top:15%;bottom:15%;width:2px;border-radius:2px;background:#a5b4fc;opacity:0;transition:opacity .15s}.osl-rg-resize-handle:hover:after{opacity:1}.osl-rg-th:hover .osl-rg-resize-handle:after{opacity:.4}.osl-rg-excel-filter{position:fixed;z-index:9999;width:250px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 32px #00000026,0 2px 8px #00000012;animation:rg-fade-up .14s ease;overflow:hidden}.osl-rg-excel-filter__header{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(135deg,#f0f4ff,#e8efff);border-bottom:1px solid #dde5ff}.osl-rg-excel-filter__title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.06em}.osl-rg-excel-filter__clear-btn{font-size:11px;color:#dc2626;background:none;border:none;cursor:pointer;font-weight:600;padding:2px 6px;border-radius:4px;font-family:inherit;transition:background .1s}.osl-rg-excel-filter__clear-btn:hover{background:#fee2e2}.osl-rg-excel-filter__search{padding:7px 8px;border-bottom:1px solid #f3f4f6;position:relative}.osl-rg-excel-filter__search-input{width:100%;height:28px;padding:0 8px 0 28px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .12s;color:#111827}.osl-rg-excel-filter__search-input::placeholder{color:#b0b7c3}.osl-rg-excel-filter__search-input:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-excel-filter__select-all{padding:0 8px;border-bottom:2px solid #f3f4f6;background:#fafbff}.osl-rg-excel-filter__list{max-height:210px;overflow-y:auto;padding:3px 8px}.osl-rg-excel-filter__list::-webkit-scrollbar{width:5px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-excel-filter__item{display:flex;align-items:center;gap:8px;padding:5px 4px;font-size:12.5px;color:#374151;cursor:pointer;border-radius:5px;transition:background .1s}.osl-rg-excel-filter__item:hover{background:#f3f4f6}.osl-rg-excel-filter__item--all{font-weight:600;color:#1f2937;padding:6px 4px}.osl-rg-excel-filter__item-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.osl-rg-excel-filter__footer{display:flex;gap:6px;padding:8px 10px;border-top:1px solid #f3f4f6;background:#f9fafb}.osl-rg-excel-filter__apply{flex:1;height:30px;border:none;border-radius:6px;font-size:12px;font-family:inherit;font-weight:600;cursor:pointer;background:var(--osl-primary, #2563eb);color:#fff;transition:background .12s}.osl-rg-excel-filter__apply:hover{background:#1d4ed8}.osl-rg-excel-filter__cancel{height:30px;padding:0 14px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;background:#fff;color:#6b7280;transition:all .12s}.osl-rg-excel-filter__cancel:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-th--checkbox,.osl-rg-td--checkbox{display:flex;align-items:center;justify-content:center;width:36px;min-width:36px;flex-shrink:0;border-right:1px solid #dde5ff}.osl-rg-checkbox{width:14px;height:14px;cursor:pointer;accent-color:var(--osl-primary, #2563eb);flex-shrink:0}.osl-rg-body{overflow-x:auto;overflow-y:auto;flex-shrink:0}.osl-rg-body::-webkit-scrollbar{width:8px;height:8px}.osl-rg-body::-webkit-scrollbar-track{background:#f9fafb}.osl-rg-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-body::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-body::-webkit-scrollbar-corner{background:#f9fafb}.osl-rg-inner{display:block}.osl-rg-row{display:flex;align-items:stretch;border-bottom:1px solid #f3f4f6;transition:background .08s;cursor:default}.osl-rg-row:hover{background:#f0f5ff!important}.osl-rg-row--striped{background:#fafbff}.osl-rg-row--selected{background:#eff6ff!important;border-bottom-color:#dbeafe}.osl-rg-row--skeleton{pointer-events:none}.osl-rg-td{display:flex;align-items:center;padding:0 12px;border-right:1px solid #f0f2f7;overflow:hidden;background:inherit;flex-shrink:0}.osl-rg-td:last-child{border-right:none}.osl-rg-td--pinned{background:#fff;border-right:1px solid #e0e7ff;z-index:2}.osl-rg-td--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.07),transparent);pointer-events:none}.osl-rg-cell-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;font-size:13px;color:#1f2937;line-height:1.4}.rg-profit-pos .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-profit-neg .osl-rg-cell-text{color:#dc2626;font-weight:600}.rg-status-delivered .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-status-shipped .osl-rg-cell-text{color:#2563eb;font-weight:600}.rg-status-processing .osl-rg-cell-text{color:#d97706;font-weight:600}.rg-status-pending .osl-rg-cell-text{color:#6b7280;font-weight:500}.rg-status-cancelled .osl-rg-cell-text{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.75}.osl-rg-group-row{display:flex;align-items:center;gap:7px;border-bottom:1px solid #e5e7eb;font-weight:600;cursor:pointer;flex-shrink:0;transition:filter .1s}.osl-rg-group-row--l0{background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1e40af;border-left:4px solid #3b82f6}.osl-rg-group-row--l1{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:#5b21b6;border-left:4px solid #8b5cf6}.osl-rg-group-row--l2{background:linear-gradient(135deg,#fff7ed,#fef3c7);color:#92400e;border-left:4px solid #f59e0b}.osl-rg-group-row:hover{filter:brightness(.97)}.osl-rg-group-row__key{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;opacity:.65;flex-shrink:0}.osl-rg-group-row__value{font-size:13px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-group-row__count{font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#00000017;margin-right:12px;flex-shrink:0}.osl-rg-group-chevron{width:16px;height:16px;flex-shrink:0;opacity:.75;display:block}.osl-rg-aggregate-row{display:flex;align-items:center;border-top:2px solid #e5e7eb;background:linear-gradient(135deg,#f0fdf4,#f9fafb);flex-shrink:0;min-height:38px}.osl-rg-agg-cell{display:flex;align-items:center;gap:5px;padding:0 12px;border-right:1px solid #e5e7eb;height:38px;flex-shrink:0;overflow:hidden}.osl-rg-agg-cell--checkbox{width:36px;min-width:36px}.osl-rg-agg-label{font-size:9.5px;font-weight:800;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0;background:#e5e7eb;padding:1px 4px;border-radius:3px}.osl-rg-agg-value{font-size:12px;font-weight:700;color:#15803d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-skel{height:12px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6,#e5e7eb 75%);background-size:200% 100%;animation:rg-pulse 1.5s ease-in-out infinite}.osl-rg-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 20px;color:#9ca3af;text-align:center}.osl-rg-empty__icon{margin-bottom:18px;opacity:.7}.osl-rg-empty__title{font-size:15px;font-weight:600;color:#374151;margin:0 0 6px}.osl-rg-empty__sub{font-size:13px;color:#9ca3af;margin:0 0 18px;max-width:290px;line-height:1.5}.osl-rg-empty__action{height:34px;padding:0 18px;background:var(--osl-primary, #2563eb);color:#fff;border:none;border-radius:7px;font-size:13px;font-family:inherit;font-weight:600;cursor:pointer;transition:background .15s;box-shadow:0 2px 6px #2563eb4d}.osl-rg-empty__action:hover{background:#1d4ed8}.osl-rg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:linear-gradient(135deg,#f8faff,#f3f4f6);flex-shrink:0;flex-wrap:wrap}.osl-rg-pagination__info{font-size:12px;color:#6b7280;min-width:160px;white-space:nowrap}.osl-rg-pagination__info strong{color:#1f2937;font-weight:600}.osl-rg-pagination__controls{display:flex;align-items:center;gap:3px}.osl-rg-pagination__size{display:flex;align-items:center;gap:7px;min-width:120px;justify-content:flex-end}.osl-rg-pagination__size-label{font-size:11.5px;color:#9ca3af;white-space:nowrap}.osl-rg-page-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s;white-space:nowrap}.osl-rg-page-btn svg{display:block}.osl-rg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af;box-shadow:0 1px 2px #0000000f}.osl-rg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-rg-page-btn--num{min-width:30px;padding:0;font-size:12.5px}.osl-rg-page-btn--active{background:var(--osl-primary, #2563eb);border-color:var(--osl-primary, #2563eb);color:#fff;font-weight:700;box-shadow:0 2px 5px #2563eb59}.osl-rg-page-btn--active:hover:not(:disabled){background:#1d4ed8;border-color:#1d4ed8}.osl-rg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:26px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-rg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;font-size:12px;font-family:inherit;color:#374151;cursor:pointer;outline:none;transition:border-color .15s}.osl-rg-page-size:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu{position:fixed;z-index:9999;min-width:210px;background:#fff;border:1px solid #e5e7eb;border-radius:11px;box-shadow:0 12px 36px #00000029,0 3px 10px #00000014;overflow:hidden;animation:rg-fade-up .13s cubic-bezier(.16,1,.3,1)}.osl-rg-col-menu__header{padding:9px 14px 7px;font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f3f4f6;background:#fafbff}.osl-rg-col-menu__divider{height:1px;background:#f3f4f6;margin:2px 0}.osl-rg-col-menu__item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:none;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;transition:all .1s}.osl-rg-col-menu__item svg{color:#9ca3af;display:block}.osl-rg-col-menu__item:hover{background:#f0f4ff;color:var(--osl-primary, #2563eb);border-left-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item:hover svg{color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item--danger:hover{background:#fff5f5;color:#dc2626;border-left-color:#dc2626}.osl-rg-col-menu__item--danger:hover svg{color:#dc2626}.osl-rg-config-backdrop{position:fixed;inset:0;z-index:9000;background:#0003;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.osl-rg-config-panel{position:fixed;top:0;right:0;bottom:0;z-index:9001;width:300px;background:#fff;border-left:1px solid #e5e7eb;box-shadow:-6px 0 24px #00000021;display:flex;flex-direction:column;animation:rg-slide-in .2s cubic-bezier(.16,1,.3,1)}.osl-rg-config-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#f0f4ff,#e8efff);flex-shrink:0}.osl-rg-config-panel__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;color:#1e3a6e}.osl-rg-config-panel__title svg{color:#3b82f6}.osl-rg-config-panel__close{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#9ca3af;cursor:pointer;border-radius:7px;padding:0;transition:all .12s}.osl-rg-config-panel__close svg{width:16px;height:16px;display:block}.osl-rg-config-panel__close:hover{background:#00000012;color:#374151}.osl-rg-config-panel__actions{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#fafbff}.osl-rg-config-hint{padding:6px 16px;font-size:10.5px;color:#b0b7c3;font-style:italic;border-bottom:1px solid #f3f4f6;flex-shrink:0}.osl-rg-config-action{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s}.osl-rg-config-action svg{display:block}.osl-rg-config-action:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-config-list{flex:1;overflow-y:auto;padding:6px 0}.osl-rg-config-list::-webkit-scrollbar{width:5px}.osl-rg-config-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}.osl-rg-config-list::-webkit-scrollbar-thumb:hover{background:#d1d5db}.osl-rg-config-item{display:flex;align-items:center;gap:9px;padding:7px 14px;transition:background .1s;cursor:grab}.osl-rg-config-item:hover{background:#f9fafb}.osl-rg-config-item:active{cursor:grabbing}.osl-rg-config-item--hidden{opacity:.5}.osl-rg-config-item__label{flex:1;font-size:13px;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.osl-rg-config-item__label--hidden{color:#9ca3af;font-style:italic}.osl-rg-config-item__pin-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #e5e7eb;background:#fff;color:#d1d5db;cursor:pointer;padding:0;border-radius:5px;transition:all .12s;flex-shrink:0}.osl-rg-config-item__pin-btn svg{display:block}.osl-rg-config-item__pin-btn:hover,.osl-rg-config-item__pin-btn--active{background:#eff6ff;border-color:#93c5fd;color:var(--osl-primary, #2563eb)}.osl-rg-copy-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:99999;display:flex;align-items:center;gap:7px;background:#1f2937;color:#fff;font-size:12.5px;font-weight:500;padding:8px 18px;border-radius:22px;animation:rg-fade-up .2s ease;box-shadow:0 6px 16px #0000004d;pointer-events:none;white-space:nowrap;font-family:inherit}.osl-rg-copy-toast svg{flex-shrink:0;color:#4ade80}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i3$1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.UpperCasePipe, name: "uppercase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3893
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslReportGrid, isStandalone: false, selector: "osl-report-grid", inputs: { columns: "columns", datasource: "datasource", loading: "loading", totalRecords: "totalRecords", autoMode: "autoMode", isPaginated: "isPaginated", pageSize: "pageSize", tableHeight: "tableHeight", striped: "striped", exportable: "exportable", rowHeight: "rowHeight", rowSelection: "rowSelection", showAggregates: "showAggregates", title: "title", pdfExportFromGrid: "pdfExportFromGrid", pdfConfig: "pdfConfig" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowClick: "rowClick", selectionChange: "selectionChange" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()", "document:click": "onDocumentClick()" } }, providers: [DatePipe, DecimalPipe], viewQueries: [{ propertyName: "headerScrollRef", first: true, predicate: ["headerScrollRef"], descendants: true }, { propertyName: "bodyScrollRef", first: true, predicate: ["bodyScrollRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"osl-rg\" (click)=\"closeColumnMenu(); openFilterKey = null; $event.stopPropagation()\">\n\n <!-- \u2550\u2550 TOOLBAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-toolbar\">\n <div class=\"osl-rg-toolbar__left\">\n @if (title) { <span class=\"osl-rg-title\">{{ title }}</span> }\n @if (!loading) {\n @if (_filteredTotal !== datasource.length) {\n <span class=\"osl-rg-filter-badge\">\n <span class=\"osl-rg-filter-badge__dot\"></span>\n {{ _filteredTotal | number }} of {{ datasource.length | number }} rows\n </span>\n } @else {\n <span class=\"osl-rg-record-count\">{{ datasource.length | number }} rows</span>\n }\n }\n </div>\n <div class=\"osl-rg-toolbar__right\">\n\n <!-- Global search -->\n <div class=\"osl-rg-global-search\">\n <svg class=\"osl-rg-global-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input class=\"osl-rg-global-search__input\" placeholder=\"Search all columns\u2026\"\n [(ngModel)]=\"globalSearch\" (ngModelChange)=\"currentPage=1; processData()\" />\n @if (globalSearch) {\n <button class=\"osl-rg-global-search__clear\" (click)=\"globalSearch=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n\n <!-- Sort active badge -->\n @if (sortStates.size > 0) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--active\" (click)=\"clearSort()\" title=\"Clear sort\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 5H4M11 9H4M11 13H4M18 4v16M15 7l3-3 3 3M15 17l3 3 3-3\"/>\n </svg>\n <span>{{ sortStates.size }} sort{{ sortStates.size > 1 ? 's' : '' }}</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Filter active badge -->\n @if (hasAnyFilter) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--danger\" (click)=\"clearAllFilters()\" title=\"Clear all filters\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n <span>Filtered</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Group toggle -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showGroupPanel\"\n (click)=\"showGroupPanel=!showGroupPanel\" title=\"Group panel\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Group</span>\n </button>\n\n <!-- Expand / collapse when grouped -->\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-toolbar-btn\" (click)=\"expandAll()\" title=\"Expand all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 20 5-5 5 5M7 4l5 5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-toolbar-btn\" (click)=\"collapseAll()\" title=\"Collapse all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5-5 5 5M7 9l5 5 5 5\"/></svg>\n </button>\n }\n\n <!-- Auto-size all -->\n <button class=\"osl-rg-toolbar-btn\" (click)=\"autoSizeAll()\" title=\"Auto-size all columns\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/>\n </svg>\n <span>Auto-size</span>\n </button>\n\n <!-- Column config -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showColumnConfig\"\n (click)=\"showColumnConfig=!showColumnConfig; $event.stopPropagation()\" title=\"Column settings\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n <span>Columns</span>\n </button>\n\n <!-- Export Excel -->\n @if (exportable) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--excel\" (click)=\"exportCsv()\" title=\"Export to Excel (CSV)\">\n <svg class=\"rg-icon rg-icon--excel\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#1D6F42\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <path d=\"M8 9.5l2.3 3.5L8 16.5h1.6l1.4-2.3 1.4 2.3H14l-2.3-3.5 2.3-3.5h-1.6l-1.4 2.3-1.4-2.3H8z\" fill=\"#fff\"/>\n </svg>\n <span>Export Excel</span>\n </button>\n }\n\n <!-- Export PDF -->\n @if (exportable && pdfExportFromGrid) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--pdf\" (click)=\"exportPdf()\" title=\"Export to PDF\">\n <svg class=\"rg-icon rg-icon--pdf\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#D32F2F\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <text x=\"4.5\" y=\"17.5\" fill=\"#fff\" font-size=\"6.5\" font-family=\"sans-serif\" font-weight=\"700\">PDF</text>\n </svg>\n <span>Export PDF</span>\n </button>\n }\n </div>\n </div>\n\n <!-- \u2550\u2550 GROUP PANEL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showGroupPanel) {\n <div class=\"osl-rg-group-panel\">\n <span class=\"osl-rg-group-panel__label\">Group by:</span>\n @if (activeGroups.length === 0) {\n <span class=\"osl-rg-group-panel__hint\">Use the \u22EE column menu \u2192 \"Group by this\"</span>\n }\n <div class=\"osl-rg-group-chips\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"activeGroups\" (cdkDropListDropped)=\"onGroupPanelDrop($event)\">\n @for (key of activeGroups; track key) {\n <div class=\"osl-rg-group-chip\" cdkDrag>\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <span>{{ getGroupColLabel(key) }}</span>\n <button class=\"osl-rg-group-chip__remove\" (click)=\"removeGroup(key)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n }\n </div>\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-group-panel__clear\" (click)=\"clearGroups()\">Clear groups</button>\n }\n </div>\n }\n\n <!-- \u2550\u2550 STICKY HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-header-wrap\" #headerScrollRef>\n\n <!-- Double header \u2013 group row (only rendered when headerGroup is used on any column) -->\n @if (hasHeaderGroups) {\n <div class=\"osl-rg-header-row osl-rg-header-row--groups\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox osl-rg-th--group-span\"></div>\n }\n <div style=\"display:flex;flex:1;min-width:0;\">\n @for (group of computedHeaderGroups; track $index) {\n <div class=\"osl-rg-th osl-rg-th--group-span\"\n [class.osl-rg-th--group-span--labeled]=\"!!group.label\"\n [class.osl-rg-th--pinned]=\"group.isPinned\"\n [style.width.px]=\"group.width\"\n [style.minWidth.px]=\"group.width\"\n [style.left.px]=\"group.isPinned ? group.stickyLeft : null\"\n [style.position]=\"group.isPinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"group.isPinned ? 3 : 1\">\n @if (group.label) {\n <span class=\"osl-rg-th-group-label\">{{ group.label }}</span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"osl-rg-header-row\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Select-all checkbox -->\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"allSelected\" [indeterminate]=\"someSelected\"\n (change)=\"toggleSelectAll($any($event.target).checked)\" />\n </div>\n }\n\n <!-- Column headers (drag-to-reorder) -->\n <div class=\"osl-rg-header-cols\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"_cols\" (cdkDropListDropped)=\"onColumnReorder($event)\"\n style=\"display:flex;flex:1;min-width:0;\">\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-th\"\n [class.osl-rg-th--pinned]=\"col._pinned\"\n [class.osl-rg-th--sorted]=\"getSortState(col.key)\"\n [class.osl-rg-th--filtered]=\"hasActiveFilter(col.key)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 3 : 1\"\n cdkDrag [cdkDragDisabled]=\"col._pinned\"\n (cdkDragStarted)=\"onColumnDragStarted()\"\n (cdkDragEnded)=\"onColumnDragEnded()\"\n (click)=\"onHeaderClick(col, $event)\">\n\n <div *cdkDragPlaceholder class=\"osl-rg-th-drag-placeholder\"></div>\n\n <!-- Pin badge -->\n @if (col._pinned) {\n <span class=\"osl-rg-th-pin-badge\" title=\"Pinned\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"9\" height=\"9\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </span>\n }\n\n <!-- Label row -->\n <div class=\"osl-rg-th-content\">\n <span class=\"osl-rg-th-label\" [title]=\"col.label\">{{ col.label }}</span>\n <div class=\"osl-rg-th-actions\">\n\n <!-- Sort indicator -->\n @if (col.sortable) {\n <span class=\"osl-rg-sort-icon\"\n [class.osl-rg-sort-icon--asc]=\"getSortState(col.key)?.asc === true\"\n [class.osl-rg-sort-icon--desc]=\"getSortState(col.key)?.asc === false\">\n @if (getSortState(col.key); as s) {\n @if (s.asc) {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n }\n @if (sortStates.size > 1) { <sup class=\"osl-rg-sort-badge\">{{ s.index + 1 }}</sup> }\n } @else {\n <svg class=\"osl-rg-sort-idle\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 9l4-4 4 4M16 15l-4 4-4-4\"/></svg>\n }\n </span>\n }\n\n <!-- Excel filter button -->\n @if (col.filterable) {\n <button class=\"osl-rg-filter-btn\" [class.osl-rg-filter-btn--active]=\"hasActiveFilter(col.key)\"\n (click)=\"openExcelFilter(col.key, $event)\" title=\"Filter\">\n @if (hasActiveFilter(col.key)) {\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/></svg>\n }\n </button>\n }\n\n <!-- Column menu -->\n <button class=\"osl-rg-col-menu-btn\" (click)=\"openColumnMenu(col.key, $event)\" title=\"Column options\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"19\" r=\"2\"/></svg>\n </button>\n\n </div>\n </div>\n\n <!-- Per-column search -->\n @if (col.searchable) {\n <div class=\"osl-rg-col-search\" (click)=\"$event.stopPropagation()\">\n <svg class=\"osl-rg-col-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m18 18-3.5-3.5\"/>\n </svg>\n <input class=\"osl-rg-col-search__input\" [placeholder]=\"col.label + '\u2026'\"\n [ngModel]=\"columnSearch[col.key]\" (ngModelChange)=\"onColumnSearch(col.key, $event)\" />\n @if (columnSearch[col.key]) {\n <button class=\"osl-rg-col-search__clear\" (click)=\"columnSearch[col.key]=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n }\n\n <!-- Resize handle -->\n @if (col.resizable) {\n <div class=\"osl-rg-resize-handle\" (mousedown)=\"startResize(col, $event)\" (dblclick)=\"autoSizeColumn(col, $event)\" (click)=\"$event.stopPropagation()\"></div>\n }\n\n\n </div><!-- /osl-rg-th -->\n }\n\n </div><!-- /header-cols -->\n </div><!-- /header-row -->\n </div><!-- /header-wrap -->\n\n <!-- \u2550\u2550 SCROLLABLE BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-body\" [style.height]=\"tableHeight\" #bodyScrollRef (scroll)=\"onBodyScroll($event)\">\n <div class=\"osl-rg-inner\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Skeleton loading -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <div class=\"osl-rg-row osl-rg-row--skeleton\" [style.height.px]=\"rowHeight\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\"><div class=\"osl-rg-skel\" style=\"width:14px;height:14px;border-radius:3px;\"></div></div>\n }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\">\n <div class=\"osl-rg-skel\" [style.width]=\"$index % 3 === 0 ? '55%' : $index % 3 === 1 ? '80%' : '65%'\"></div>\n </div>\n }\n </div>\n }\n\n } @else if (isEmpty) {\n <!-- Empty state -->\n <div class=\"osl-rg-empty\">\n <div class=\"osl-rg-empty__icon\">\n <svg viewBox=\"0 0 48 48\" fill=\"none\" stroke=\"#d1d5db\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"6\" y=\"6\" width=\"36\" height=\"36\" rx=\"4\"/>\n <path d=\"M6 18h36M6 30h36M18 6v36\"/>\n </svg>\n </div>\n <p class=\"osl-rg-empty__title\">No records found</p>\n <p class=\"osl-rg-empty__sub\">\n @if (hasAnyFilter) { Try adjusting your filters or search terms }\n @else { No data to display }\n </p>\n @if (hasAnyFilter) {\n <button class=\"osl-rg-empty__action\" (click)=\"clearAllFilters()\">Clear all filters</button>\n }\n </div>\n\n } @else {\n <!-- Data rows -->\n @for (row of flatRows; track trackByRow($index, row)) {\n\n @if (row._type === 'group') {\n <div class=\"osl-rg-group-row\"\n [class.osl-rg-group-row--l0]=\"asGroupRow(row)._level === 0\"\n [class.osl-rg-group-row--l1]=\"asGroupRow(row)._level === 1\"\n [class.osl-rg-group-row--l2]=\"asGroupRow(row)._level >= 2\"\n [style.height.px]=\"rowHeight\"\n [style.paddingLeft.px]=\"asGroupRow(row)._level * 20 + 12\"\n (click)=\"toggleGroup(asGroupRow(row)._path)\">\n @if (asGroupRow(row)._expanded) {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>\n } @else {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"/></svg>\n }\n <span class=\"osl-rg-group-row__key\">{{ asGroupRow(row)._colLabel }}:</span>\n <span class=\"osl-rg-group-row__value\">{{ asGroupRow(row)._label }}</span>\n <span class=\"osl-rg-group-row__count\">{{ asGroupRow(row)._count | number }} rows</span>\n </div>\n\n } @else {\n <div class=\"osl-rg-row\"\n [class.osl-rg-row--striped]=\"striped && asDataRow(row)._rowIndex % 2 === 1\"\n [class.osl-rg-row--selected]=\"selectedRows.has(asDataRow(row)._data)\"\n [style.height.px]=\"rowHeight\"\n (click)=\"onRowClick(asDataRow(row)._data, $event)\">\n\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"selectedRows.has(asDataRow(row)._data)\"\n (change)=\"toggleRowSelect(asDataRow(row)._data, $event)\" />\n </div>\n }\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\"\n [class.osl-rg-td--pinned]=\"col._pinned\"\n [ngClass]=\"getCellClass(asDataRow(row)._data, col)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 2 : 0\"\n [style.textAlign]=\"col.align ?? 'left'\"\n (dblclick)=\"copyCell(getCellDisplay(asDataRow(row)._data, col), $event)\"\n [title]=\"getCellDisplay(asDataRow(row)._data, col)\">\n <span class=\"osl-rg-cell-text\">{{ getCellDisplay(asDataRow(row)._data, col) }}</span>\n </div>\n }\n\n </div>\n }\n\n }\n }\n\n </div><!-- /osl-rg-inner -->\n </div><!-- /osl-rg-body -->\n\n <!-- \u2550\u2550 AGGREGATE FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showAggregates && hasAnyAggregate() && !loading) {\n <div class=\"osl-rg-aggregate-row\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') { <div class=\"osl-rg-agg-cell osl-rg-agg-cell--checkbox\"></div> }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-agg-cell\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\" [style.textAlign]=\"col.align ?? 'left'\">\n @if (col.aggregate) {\n <span class=\"osl-rg-agg-label\">{{ col.aggregate | uppercase }}</span>\n <span class=\"osl-rg-agg-value\">{{ getAggregate(col) }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- \u2550\u2550 PAGINATION \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (isPaginated) {\n <div class=\"osl-rg-pagination\">\n <span class=\"osl-rg-pagination__info\">\n @if (loading) { Loading\u2026 }\n @else if (_total > 0) { Showing <strong>{{ startRecord | number }}\u2013{{ endRecord | number }}</strong> of <strong>{{ _total | number }}</strong> records }\n @else { No records }\n </span>\n <div class=\"osl-rg-pagination__controls\">\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage-1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m15 18-6-6 6-6\"/></svg>\n Prev\n </button>\n @for (page of pageNumbers; track $index) {\n @if (page === -1) { <span class=\"osl-rg-page-ellipsis\">\u2026</span> }\n @else {\n <button class=\"osl-rg-page-btn osl-rg-page-btn--num\" [class.osl-rg-page-btn--active]=\"page===currentPage\"\n [disabled]=\"loading\" (click)=\"goToPage(page)\">{{ page }}</button>\n }\n }\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage+1)\" [disabled]=\"currentPage===totalPages||loading\">\n Next\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m9 18 6-6-6-6\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage===totalPages||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-pagination__size\">\n <span class=\"osl-rg-pagination__size-label\">Rows per page</span>\n <select class=\"osl-rg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) { <option [value]=\"opt\">{{ opt }}</option> }\n </select>\n </div>\n </div>\n }\n\n</div><!-- /osl-rg -->\n\n\n<!-- \u2550\u2550 EXCEL FILTER DROPDOWN (fixed, outside overflow containers) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (openFilterKey && excelFilterState[openFilterKey]) {\n <div class=\"osl-rg-excel-filter\"\n [style.top.px]=\"filterDropdownPos.top\"\n [style.left.px]=\"filterDropdownPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-excel-filter__header\">\n <div class=\"osl-rg-excel-filter__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" style=\"flex-shrink:0;\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n Filter: {{ getColumnByKey(openFilterKey)?.label }}\n </div>\n @if (hasActiveFilter(openFilterKey)) {\n <button class=\"osl-rg-excel-filter__clear-btn\" (click)=\"clearExcelFilter(openFilterKey!)\">Clear</button>\n }\n </div>\n <div class=\"osl-rg-excel-filter__search\">\n <input class=\"osl-rg-excel-filter__search-input\" placeholder=\"Search values\u2026\"\n [(ngModel)]=\"excelFilterState[openFilterKey].search\" />\n </div>\n <div class=\"osl-rg-excel-filter__select-all\">\n <label class=\"osl-rg-excel-filter__item osl-rg-excel-filter__item--all\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"isAllExcelChecked(openFilterKey)\"\n (change)=\"toggleAllExcelFilter(openFilterKey!, $any($event.target).checked)\" />\n <span class=\"osl-rg-excel-filter__item-label\">(Select All)</span>\n </label>\n </div>\n <div class=\"osl-rg-excel-filter__list\">\n @for (item of getFilteredExcelItems(openFilterKey); track item.value) {\n <label class=\"osl-rg-excel-filter__item\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [(ngModel)]=\"item.checked\" />\n <span class=\"osl-rg-excel-filter__item-label\">{{ item.label }}</span>\n </label>\n }\n </div>\n <div class=\"osl-rg-excel-filter__footer\">\n <button class=\"osl-rg-excel-filter__apply\" (click)=\"applyExcelFilter(openFilterKey!)\">Apply Filter</button>\n <button class=\"osl-rg-excel-filter__cancel\" (click)=\"openFilterKey=null\">Cancel</button>\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONTEXT MENU (fixed) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (columnMenuKey && getColumnByKey(columnMenuKey); as menuCol) {\n <div class=\"osl-rg-col-menu\"\n [style.top.px]=\"columnMenuPos.top\" [style.left.px]=\"columnMenuPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-col-menu__header\">{{ menuCol.label }}</div>\n @if (menuCol.sortable) {\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:true,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n Sort A \u2192 Z\n </button>\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:false,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n Sort Z \u2192 A\n </button>\n }\n @if (menuCol.groupable) {\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"addGroup(menuCol.key); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/></svg>\n Group by this\n </button>\n }\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"toggleColumnPin(menuCol)\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n {{ menuCol._pinned ? 'Unpin column' : 'Pin to left' }}\n </button>\n <button class=\"osl-rg-col-menu__item\" (click)=\"autoSizeColumn(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/></svg>\n Auto-size column\n </button>\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item osl-rg-col-menu__item--danger\" (click)=\"toggleColumnVisibility(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n Hide column\n </button>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONFIG SIDE DRAWER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (showColumnConfig) {\n <div class=\"osl-rg-config-backdrop\" (click)=\"showColumnConfig=false\"></div>\n <div class=\"osl-rg-config-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-config-panel__header\">\n <div class=\"osl-rg-config-panel__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"16\" height=\"16\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n Column Settings\n </div>\n <button class=\"osl-rg-config-panel__close\" (click)=\"showColumnConfig=false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-config-panel__actions\">\n <button class=\"osl-rg-config-action\" (click)=\"autoSizeAll()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3\"/></svg>\n Auto-size all\n </button>\n <button class=\"osl-rg-config-action\" (click)=\"showAllColumns()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n Show all\n </button>\n </div>\n <div class=\"osl-rg-config-hint\">Drag to reorder \u00B7 Toggle to show/hide \u00B7 Pin to freeze</div>\n <div class=\"osl-rg-config-list\" cdkDropList (cdkDropListDropped)=\"onConfigColumnReorder($event)\">\n @for (col of _cols; track col.key) {\n <div class=\"osl-rg-config-item\" cdkDrag [class.osl-rg-config-item--hidden]=\"col._hidden\">\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"!col._hidden\" (change)=\"toggleColumnVisibility(col)\" />\n <span class=\"osl-rg-config-item__label\" [class.osl-rg-config-item__label--hidden]=\"col._hidden\">{{ col.label }}</span>\n <button class=\"osl-rg-config-item__pin-btn\" (click)=\"toggleColumnPin(col)\"\n [class.osl-rg-config-item__pin-btn--active]=\"col._pinned\"\n [title]=\"col._pinned ? 'Unpin' : 'Pin to left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </button>\n </div>\n }\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COPY TOAST \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (copiedCell !== null) {\n <div class=\"osl-rg-copy-toast\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Copied to clipboard\n </div>\n}\n", styles: ["@keyframes rg-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes rg-fade-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes rg-slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.rg-icon{width:15px;height:15px;flex-shrink:0;display:block}.rg-icon--sm{width:14px;height:14px}.rg-icon--xs{width:12px;height:12px}.rg-icon--excel{width:18px;height:18px}.rg-drag-handle{width:14px;height:14px;flex-shrink:0;color:#d1d5db;cursor:grab;display:block}.rg-drag-handle:active{cursor:grabbing}.osl-rg{position:relative;display:flex;flex-direction:column;width:100%;font-family:inherit;font-size:13px;color:#111827;background:#fff;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px #00000012,0 1px 2px #0000000a;-webkit-user-select:none;user-select:none}.osl-rg-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;background:linear-gradient(135deg,#f8faff,#f3f4f6);border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;flex-wrap:wrap}.osl-rg-toolbar__left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.osl-rg-toolbar__right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.osl-rg-title{font-size:14px;font-weight:700;color:#111827;letter-spacing:-.01em}.osl-rg-record-count{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#9ca3af;font-weight:500}.osl-rg-record-count svg{color:#d1d5db}.osl-rg-filter-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;font-size:11px;color:#1d4ed8;font-weight:600}.osl-rg-filter-badge__dot{width:6px;height:6px;border-radius:50%;background:#2563eb;animation:rg-pulse 1.5s ease-in-out infinite;flex-shrink:0}.osl-rg-toolbar-btn{display:inline-flex;align-items:center;gap:5px;height:32px;padding:0 11px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;color:#5a6478;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s ease}.osl-rg-toolbar-btn svg{flex-shrink:0}.osl-rg-toolbar-btn:hover{background:#f3f4f6;color:#1f2937;border-color:#c4c9d4;box-shadow:0 1px 3px #0000000f}.osl-rg-toolbar-btn--on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.osl-rg-toolbar-btn--active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600}.osl-rg-toolbar-btn--danger{background:#fff5f5;border-color:#fca5a5;color:#dc2626}.osl-rg-toolbar-btn--danger:hover{background:#fee2e2;border-color:#f87171}.osl-rg-toolbar-btn--excel{background:linear-gradient(135deg,#1a7a3e,#1d6f42);border-color:#166534;color:#fff;font-weight:600;box-shadow:0 1px 3px #1665344d}.osl-rg-toolbar-btn--excel:hover{background:linear-gradient(135deg,#166534,#145a2c);border-color:#14532d;color:#fff;box-shadow:0 2px 6px #16653466}.osl-rg-toolbar-btn--pdf{background:linear-gradient(135deg,#e53935,#c62828);border-color:#b71c1c;color:#fff;font-weight:600;box-shadow:0 1px 3px #b71c1c4d}.osl-rg-toolbar-btn--pdf:hover{background:linear-gradient(135deg,#c62828,#ad1515);border-color:#ad1515;color:#fff;box-shadow:0 2px 6px #b71c1c66}.rg-icon--pdf{width:18px;height:18px}.osl-rg-global-search{position:relative;display:inline-flex;align-items:center}.osl-rg-global-search__icon{position:absolute;left:9px;width:14px;height:14px;color:#9ca3af;pointer-events:none;display:block}.osl-rg-global-search__input{height:32px;padding:0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;font-size:12.5px;font-family:inherit;color:#111827;outline:none;width:210px;transition:border-color .15s,width .2s,box-shadow .15s}.osl-rg-global-search__input::placeholder{color:#b0b7c3}.osl-rg-global-search__input:focus{border-color:var(--osl-primary, #2563eb);width:260px;box-shadow:0 0 0 3px #2563eb1a}.osl-rg-global-search__clear{position:absolute;right:6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:4px;transition:all .12s}.osl-rg-global-search__clear svg{width:12px;height:12px}.osl-rg-global-search__clear:hover{color:#374151;background:#0000000d}.osl-rg-group-panel{display:flex;align-items:center;gap:8px;padding:6px 14px;background:linear-gradient(135deg,#fafbff,#f5f7ff);border-bottom:1px dashed #dde5ff;min-height:40px;flex-shrink:0;flex-wrap:wrap}.osl-rg-group-panel__label{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0}.osl-rg-group-panel__label svg{color:#9ca3af}.osl-rg-group-panel__hint{font-size:11px;color:#b0b7c3;font-style:italic}.osl-rg-group-panel__clear{margin-left:auto;font-size:11px;color:#6b7280;background:none;border:none;cursor:pointer;text-decoration:underline;flex-shrink:0;padding:0;font-family:inherit}.osl-rg-group-panel__clear:hover{color:#dc2626}.osl-rg-group-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex:1;min-height:26px}.osl-rg-group-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 6px 3px 5px;background:linear-gradient(135deg,#dbeafe,#eff6ff);border:1px solid #bfdbfe;border-radius:20px;font-size:12px;font-weight:600;color:#1e40af;cursor:grab;-webkit-user-select:none;user-select:none;box-shadow:0 1px 2px #2563eb1a;transition:box-shadow .1s}.osl-rg-group-chip:active{cursor:grabbing;box-shadow:0 2px 6px #2563eb33}.osl-rg-group-chip .rg-drag-handle{color:#93c5fd}.osl-rg-group-chip__remove{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;background:transparent;cursor:pointer;padding:0;border-radius:50%;transition:all .1s}.osl-rg-group-chip__remove svg{width:10px;height:10px;color:#93c5fd}.osl-rg-group-chip__remove:hover{background:#dbeafe}.osl-rg-group-chip__remove:hover svg{color:#1e40af}.osl-rg-table-area{display:flex;flex-direction:column;flex:1 1 0;overflow:hidden;position:relative;min-height:0}.osl-rg-header-wrap{overflow:hidden;flex-shrink:0;border-bottom:2px solid #dde5ff;background:linear-gradient(180deg,#f0f4ff,#e8efff);z-index:10;position:relative}.osl-rg-header-row{display:flex;align-items:stretch}.osl-rg-header-row--groups{border-bottom:1px solid #c7d7f0}.osl-rg-th--group-span{display:flex;align-items:center;justify-content:center;height:28px;min-height:28px;cursor:default;padding:0;border-right:1px solid #dde5ff}.osl-rg-th--group-span:hover{background:transparent!important}.osl-rg-th--group-span:last-child{border-right:none}.osl-rg-th--group-span--labeled{background:linear-gradient(135deg,#dbeafe,#eff6ff);border-bottom:2px solid #93c5fd}.osl-rg-th-group-label{font-size:11px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 10px;text-align:center;width:100%}.osl-rg-header-cols{display:flex}.osl-rg-th{position:relative;display:flex;flex-direction:column;background:transparent;border-right:1px solid #dde5ff;cursor:pointer;flex-shrink:0;transition:background .12s;overflow:visible}.osl-rg-th:last-child{border-right:none}.osl-rg-th:hover{background:#2563eb0f}.osl-rg-th--pinned{background:linear-gradient(180deg,#e8efff,#dde8ff);border-right:1px solid #bfdbfe;z-index:3}.osl-rg-th--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.14),transparent);pointer-events:none;z-index:4}.osl-rg-th--sorted{background:#6366f114}.osl-rg-th--filtered{background:#f59e0b12}.osl-rg-th-drag-placeholder{background:#e0e7ff;border:2px dashed #6366f1;border-radius:4px;opacity:.6}.osl-rg-th-pin-badge{position:absolute;top:3px;right:24px;color:var(--osl-primary, #2563eb);opacity:.6;display:flex;align-items:center}.osl-rg-th-content{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 8px 0 10px;height:36px;min-height:36px}.osl-rg-th-label{font-size:11.5px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.osl-rg-th-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.osl-rg-sort-icon{display:inline-flex;align-items:center;position:relative;color:#9ca3af;transition:color .12s;width:14px;height:14px}.osl-rg-sort-icon svg{width:13px;height:13px;display:block}.osl-rg-sort-icon--asc,.osl-rg-sort-icon--desc{color:var(--osl-primary, #2563eb)}.osl-rg-sort-idle{opacity:.3}.osl-rg-sort-badge{position:absolute;top:-4px;right:-6px;font-size:8px;font-weight:700;background:var(--osl-primary, #2563eb);color:#fff;border-radius:50%;width:11px;height:11px;display:flex;align-items:center;justify-content:center;line-height:1}.osl-rg-filter-btn,.osl-rg-col-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:#b0b7c3;cursor:pointer;padding:0;border-radius:5px;transition:all .12s}.osl-rg-filter-btn svg,.osl-rg-col-menu-btn svg{width:13px;height:13px;display:block}.osl-rg-filter-btn:hover,.osl-rg-col-menu-btn:hover{background:#00000012;color:#374151}.osl-rg-filter-btn--active{color:var(--osl-primary, #2563eb)!important;background:#2563eb1f!important}.osl-rg-col-search{position:relative;display:flex;align-items:center;padding:3px 6px;border-top:1px solid #e0e7ff;background:#ffffff80}.osl-rg-col-search__icon{position:absolute;left:10px;width:12px;height:12px;display:block;pointer-events:none;color:#c4cad6}.osl-rg-col-search__input{width:100%;height:23px;padding:0 18px 0 22px;border:1px solid #dde5ff;border-radius:5px;font-size:11px;font-family:inherit;background:#fff;color:#111827;outline:none;transition:border-color .12s,box-shadow .12s}.osl-rg-col-search__input::placeholder{color:#c4cad6}.osl-rg-col-search__input:focus{border-color:var(--osl-primary, #2563eb);box-shadow:0 0 0 2px #2563eb1a}.osl-rg-col-search__clear{position:absolute;right:8px;display:flex;align-items:center;width:14px;height:14px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:3px;transition:all .1s}.osl-rg-col-search__clear svg{width:10px;height:10px;display:block}.osl-rg-col-search__clear:hover{color:#374151;background:#0000000f}.osl-rg-resize-handle{position:absolute;right:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.osl-rg-resize-handle:after{content:\"\";position:absolute;right:2px;top:15%;bottom:15%;width:2px;border-radius:2px;background:#a5b4fc;opacity:0;transition:opacity .15s}.osl-rg-resize-handle:hover:after{opacity:1}.osl-rg-th:hover .osl-rg-resize-handle:after{opacity:.4}.osl-rg-excel-filter{position:fixed;z-index:9999;width:250px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 32px #00000026,0 2px 8px #00000012;animation:rg-fade-up .14s ease;overflow:hidden}.osl-rg-excel-filter__header{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(135deg,#f0f4ff,#e8efff);border-bottom:1px solid #dde5ff}.osl-rg-excel-filter__title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.06em}.osl-rg-excel-filter__clear-btn{font-size:11px;color:#dc2626;background:none;border:none;cursor:pointer;font-weight:600;padding:2px 6px;border-radius:4px;font-family:inherit;transition:background .1s}.osl-rg-excel-filter__clear-btn:hover{background:#fee2e2}.osl-rg-excel-filter__search{padding:7px 8px;border-bottom:1px solid #f3f4f6;position:relative}.osl-rg-excel-filter__search-input{width:100%;height:28px;padding:0 8px 0 28px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .12s;color:#111827}.osl-rg-excel-filter__search-input::placeholder{color:#b0b7c3}.osl-rg-excel-filter__search-input:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-excel-filter__select-all{padding:0 8px;border-bottom:2px solid #f3f4f6;background:#fafbff}.osl-rg-excel-filter__list{max-height:210px;overflow-y:auto;padding:3px 8px}.osl-rg-excel-filter__list::-webkit-scrollbar{width:5px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-excel-filter__item{display:flex;align-items:center;gap:8px;padding:5px 4px;font-size:12.5px;color:#374151;cursor:pointer;border-radius:5px;transition:background .1s}.osl-rg-excel-filter__item:hover{background:#f3f4f6}.osl-rg-excel-filter__item--all{font-weight:600;color:#1f2937;padding:6px 4px}.osl-rg-excel-filter__item-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.osl-rg-excel-filter__footer{display:flex;gap:6px;padding:8px 10px;border-top:1px solid #f3f4f6;background:#f9fafb}.osl-rg-excel-filter__apply{flex:1;height:30px;border:none;border-radius:6px;font-size:12px;font-family:inherit;font-weight:600;cursor:pointer;background:var(--osl-primary, #2563eb);color:#fff;transition:background .12s}.osl-rg-excel-filter__apply:hover{background:#1d4ed8}.osl-rg-excel-filter__cancel{height:30px;padding:0 14px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;background:#fff;color:#6b7280;transition:all .12s}.osl-rg-excel-filter__cancel:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-th--checkbox,.osl-rg-td--checkbox{display:flex;align-items:center;justify-content:center;width:36px;min-width:36px;flex-shrink:0;border-right:1px solid #dde5ff}.osl-rg-checkbox{width:14px;height:14px;cursor:pointer;accent-color:var(--osl-primary, #2563eb);flex-shrink:0}.osl-rg-body{overflow-x:auto;overflow-y:auto;flex-shrink:0}.osl-rg-body::-webkit-scrollbar{width:8px;height:8px}.osl-rg-body::-webkit-scrollbar-track{background:#f9fafb}.osl-rg-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-body::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-body::-webkit-scrollbar-corner{background:#f9fafb}.osl-rg-inner{display:block}.osl-rg-row{display:flex;align-items:stretch;border-bottom:1px solid #f3f4f6;transition:background .08s;cursor:default}.osl-rg-row:hover{background:#f0f5ff!important}.osl-rg-row--striped{background:#fafbff}.osl-rg-row--selected{background:#eff6ff!important;border-bottom-color:#dbeafe}.osl-rg-row--skeleton{pointer-events:none}.osl-rg-td{display:flex;align-items:center;padding:0 12px;border-right:1px solid #f0f2f7;overflow:hidden;background:inherit;flex-shrink:0}.osl-rg-td:last-child{border-right:none}.osl-rg-td--pinned{background:#fff;border-right:1px solid #e0e7ff;z-index:2}.osl-rg-td--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.07),transparent);pointer-events:none}.osl-rg-cell-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;font-size:13px;color:#1f2937;line-height:1.4}.rg-profit-pos .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-profit-neg .osl-rg-cell-text{color:#dc2626;font-weight:600}.rg-status-delivered .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-status-shipped .osl-rg-cell-text{color:#2563eb;font-weight:600}.rg-status-processing .osl-rg-cell-text{color:#d97706;font-weight:600}.rg-status-pending .osl-rg-cell-text{color:#6b7280;font-weight:500}.rg-status-cancelled .osl-rg-cell-text{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.75}.osl-rg-group-row{display:flex;align-items:center;gap:7px;border-bottom:1px solid #e5e7eb;font-weight:600;cursor:pointer;flex-shrink:0;transition:filter .1s}.osl-rg-group-row--l0{background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1e40af;border-left:4px solid #3b82f6}.osl-rg-group-row--l1{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:#5b21b6;border-left:4px solid #8b5cf6}.osl-rg-group-row--l2{background:linear-gradient(135deg,#fff7ed,#fef3c7);color:#92400e;border-left:4px solid #f59e0b}.osl-rg-group-row:hover{filter:brightness(.97)}.osl-rg-group-row__key{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;opacity:.65;flex-shrink:0}.osl-rg-group-row__value{font-size:13px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-group-row__count{font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#00000017;margin-right:12px;flex-shrink:0}.osl-rg-group-chevron{width:16px;height:16px;flex-shrink:0;opacity:.75;display:block}.osl-rg-aggregate-row{display:flex;align-items:center;border-top:2px solid #e5e7eb;background:linear-gradient(135deg,#f0fdf4,#f9fafb);flex-shrink:0;min-height:38px}.osl-rg-agg-cell{display:flex;align-items:center;gap:5px;padding:0 12px;border-right:1px solid #e5e7eb;height:38px;flex-shrink:0;overflow:hidden}.osl-rg-agg-cell--checkbox{width:36px;min-width:36px}.osl-rg-agg-label{font-size:9.5px;font-weight:800;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0;background:#e5e7eb;padding:1px 4px;border-radius:3px}.osl-rg-agg-value{font-size:12px;font-weight:700;color:#15803d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-skel{height:12px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6,#e5e7eb 75%);background-size:200% 100%;animation:rg-pulse 1.5s ease-in-out infinite}.osl-rg-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 20px;color:#9ca3af;text-align:center}.osl-rg-empty__icon{margin-bottom:18px;opacity:.7}.osl-rg-empty__title{font-size:15px;font-weight:600;color:#374151;margin:0 0 6px}.osl-rg-empty__sub{font-size:13px;color:#9ca3af;margin:0 0 18px;max-width:290px;line-height:1.5}.osl-rg-empty__action{height:34px;padding:0 18px;background:var(--osl-primary, #2563eb);color:#fff;border:none;border-radius:7px;font-size:13px;font-family:inherit;font-weight:600;cursor:pointer;transition:background .15s;box-shadow:0 2px 6px #2563eb4d}.osl-rg-empty__action:hover{background:#1d4ed8}.osl-rg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:linear-gradient(135deg,#f8faff,#f3f4f6);flex-shrink:0;flex-wrap:wrap}.osl-rg-pagination__info{font-size:12px;color:#6b7280;min-width:160px;white-space:nowrap}.osl-rg-pagination__info strong{color:#1f2937;font-weight:600}.osl-rg-pagination__controls{display:flex;align-items:center;gap:3px}.osl-rg-pagination__size{display:flex;align-items:center;gap:7px;min-width:120px;justify-content:flex-end}.osl-rg-pagination__size-label{font-size:11.5px;color:#9ca3af;white-space:nowrap}.osl-rg-page-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s;white-space:nowrap}.osl-rg-page-btn svg{display:block}.osl-rg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af;box-shadow:0 1px 2px #0000000f}.osl-rg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-rg-page-btn--num{min-width:30px;padding:0;font-size:12.5px}.osl-rg-page-btn--active{background:var(--osl-primary, #2563eb);border-color:var(--osl-primary, #2563eb);color:#fff;font-weight:700;box-shadow:0 2px 5px #2563eb59}.osl-rg-page-btn--active:hover:not(:disabled){background:#1d4ed8;border-color:#1d4ed8}.osl-rg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:26px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-rg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;font-size:12px;font-family:inherit;color:#374151;cursor:pointer;outline:none;transition:border-color .15s}.osl-rg-page-size:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu{position:fixed;z-index:9999;min-width:210px;background:#fff;border:1px solid #e5e7eb;border-radius:11px;box-shadow:0 12px 36px #00000029,0 3px 10px #00000014;overflow:hidden;animation:rg-fade-up .13s cubic-bezier(.16,1,.3,1)}.osl-rg-col-menu__header{padding:9px 14px 7px;font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f3f4f6;background:#fafbff}.osl-rg-col-menu__divider{height:1px;background:#f3f4f6;margin:2px 0}.osl-rg-col-menu__item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:none;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;transition:all .1s}.osl-rg-col-menu__item svg{color:#9ca3af;display:block}.osl-rg-col-menu__item:hover{background:#f0f4ff;color:var(--osl-primary, #2563eb);border-left-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item:hover svg{color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item--danger:hover{background:#fff5f5;color:#dc2626;border-left-color:#dc2626}.osl-rg-col-menu__item--danger:hover svg{color:#dc2626}.osl-rg-config-backdrop{position:fixed;inset:0;z-index:9000;background:#0003;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.osl-rg-config-panel{position:fixed;top:0;right:0;bottom:0;z-index:9001;width:300px;background:#fff;border-left:1px solid #e5e7eb;box-shadow:-6px 0 24px #00000021;display:flex;flex-direction:column;animation:rg-slide-in .2s cubic-bezier(.16,1,.3,1)}.osl-rg-config-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#f0f4ff,#e8efff);flex-shrink:0}.osl-rg-config-panel__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;color:#1e3a6e}.osl-rg-config-panel__title svg{color:#3b82f6}.osl-rg-config-panel__close{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#9ca3af;cursor:pointer;border-radius:7px;padding:0;transition:all .12s}.osl-rg-config-panel__close svg{width:16px;height:16px;display:block}.osl-rg-config-panel__close:hover{background:#00000012;color:#374151}.osl-rg-config-panel__actions{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#fafbff}.osl-rg-config-hint{padding:6px 16px;font-size:10.5px;color:#b0b7c3;font-style:italic;border-bottom:1px solid #f3f4f6;flex-shrink:0}.osl-rg-config-action{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s}.osl-rg-config-action svg{display:block}.osl-rg-config-action:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-config-list{flex:1;overflow-y:auto;padding:6px 0}.osl-rg-config-list::-webkit-scrollbar{width:5px}.osl-rg-config-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}.osl-rg-config-list::-webkit-scrollbar-thumb:hover{background:#d1d5db}.osl-rg-config-item{display:flex;align-items:center;gap:9px;padding:7px 14px;transition:background .1s;cursor:grab}.osl-rg-config-item:hover{background:#f9fafb}.osl-rg-config-item:active{cursor:grabbing}.osl-rg-config-item--hidden{opacity:.5}.osl-rg-config-item__label{flex:1;font-size:13px;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.osl-rg-config-item__label--hidden{color:#9ca3af;font-style:italic}.osl-rg-config-item__pin-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #e5e7eb;background:#fff;color:#d1d5db;cursor:pointer;padding:0;border-radius:5px;transition:all .12s;flex-shrink:0}.osl-rg-config-item__pin-btn svg{display:block}.osl-rg-config-item__pin-btn:hover,.osl-rg-config-item__pin-btn--active{background:#eff6ff;border-color:#93c5fd;color:var(--osl-primary, #2563eb)}.osl-rg-copy-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:99999;display:flex;align-items:center;gap:7px;background:#1f2937;color:#fff;font-size:12.5px;font-weight:500;padding:8px 18px;border-radius:22px;animation:rg-fade-up .2s ease;box-shadow:0 6px 16px #0000004d;pointer-events:none;white-space:nowrap;font-family:inherit}.osl-rg-copy-toast svg{flex-shrink:0;color:#4ade80}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i3$1.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.UpperCasePipe, name: "uppercase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3611
3894
|
}
|
|
3612
3895
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslReportGrid, decorators: [{
|
|
3613
3896
|
type: Component,
|
|
3614
|
-
args: [{ selector: 'osl-report-grid', standalone: false, providers: [DatePipe, DecimalPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"osl-rg\" (click)=\"$event.stopPropagation()\">\n\n <!-- \u2550\u2550 TOOLBAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-toolbar\">\n <div class=\"osl-rg-toolbar__left\">\n @if (title) { <span class=\"osl-rg-title\">{{ title }}</span> }\n @if (!loading) {\n @if (_filteredTotal !== datasource.length) {\n <span class=\"osl-rg-filter-badge\">\n <span class=\"osl-rg-filter-badge__dot\"></span>\n {{ _filteredTotal | number }} of {{ datasource.length | number }} rows\n </span>\n } @else {\n <span class=\"osl-rg-record-count\">{{ datasource.length | number }} rows</span>\n }\n }\n </div>\n <div class=\"osl-rg-toolbar__right\">\n\n <!-- Global search -->\n <div class=\"osl-rg-global-search\">\n <svg class=\"osl-rg-global-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input class=\"osl-rg-global-search__input\" placeholder=\"Search all columns\u2026\"\n [(ngModel)]=\"globalSearch\" (ngModelChange)=\"currentPage=1; processData()\" />\n @if (globalSearch) {\n <button class=\"osl-rg-global-search__clear\" (click)=\"globalSearch=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n\n <!-- Sort active badge -->\n @if (sortStates.size > 0) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--active\" (click)=\"clearSort()\" title=\"Clear sort\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 5H4M11 9H4M11 13H4M18 4v16M15 7l3-3 3 3M15 17l3 3 3-3\"/>\n </svg>\n <span>{{ sortStates.size }} sort{{ sortStates.size > 1 ? 's' : '' }}</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Filter active badge -->\n @if (hasAnyFilter) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--danger\" (click)=\"clearAllFilters()\" title=\"Clear all filters\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n <span>Filtered</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Group toggle -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showGroupPanel\"\n (click)=\"showGroupPanel=!showGroupPanel\" title=\"Group panel\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Group</span>\n </button>\n\n <!-- Expand / collapse when grouped -->\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-toolbar-btn\" (click)=\"expandAll()\" title=\"Expand all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 20 5-5 5 5M7 4l5 5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-toolbar-btn\" (click)=\"collapseAll()\" title=\"Collapse all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5-5 5 5M7 9l5 5 5 5\"/></svg>\n </button>\n }\n\n <!-- Auto-size all -->\n <button class=\"osl-rg-toolbar-btn\" (click)=\"autoSizeAll()\" title=\"Auto-size all columns\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/>\n </svg>\n <span>Auto-size</span>\n </button>\n\n <!-- Column config -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showColumnConfig\"\n (click)=\"showColumnConfig=!showColumnConfig; $event.stopPropagation()\" title=\"Column settings\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n <span>Columns</span>\n </button>\n\n <!-- Export Excel -->\n @if (exportable) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--excel\" (click)=\"exportCsv()\" title=\"Export to Excel (CSV)\">\n <svg class=\"rg-icon rg-icon--excel\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#1D6F42\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <path d=\"M8 9.5l2.3 3.5L8 16.5h1.6l1.4-2.3 1.4 2.3H14l-2.3-3.5 2.3-3.5h-1.6l-1.4 2.3-1.4-2.3H8z\" fill=\"#fff\"/>\n </svg>\n <span>Export</span>\n </button>\n }\n </div>\n </div>\n\n <!-- \u2550\u2550 GROUP PANEL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showGroupPanel) {\n <div class=\"osl-rg-group-panel\">\n <span class=\"osl-rg-group-panel__label\">Group by:</span>\n @if (activeGroups.length === 0) {\n <span class=\"osl-rg-group-panel__hint\">Use the \u22EE column menu \u2192 \"Group by this\"</span>\n }\n <div class=\"osl-rg-group-chips\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"activeGroups\" (cdkDropListDropped)=\"onGroupPanelDrop($event)\">\n @for (key of activeGroups; track key) {\n <div class=\"osl-rg-group-chip\" cdkDrag>\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <span>{{ getGroupColLabel(key) }}</span>\n <button class=\"osl-rg-group-chip__remove\" (click)=\"removeGroup(key)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n }\n </div>\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-group-panel__clear\" (click)=\"clearGroups()\">Clear groups</button>\n }\n </div>\n }\n\n <!-- \u2550\u2550 STICKY HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-header-wrap\" #headerScrollRef>\n\n <!-- Double header \u2013 group row (only rendered when headerGroup is used on any column) -->\n @if (hasHeaderGroups) {\n <div class=\"osl-rg-header-row osl-rg-header-row--groups\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox osl-rg-th--group-span\"></div>\n }\n <div style=\"display:flex;flex:1;min-width:0;\">\n @for (group of computedHeaderGroups; track $index) {\n <div class=\"osl-rg-th osl-rg-th--group-span\"\n [class.osl-rg-th--group-span--labeled]=\"!!group.label\"\n [class.osl-rg-th--pinned]=\"group.isPinned\"\n [style.width.px]=\"group.width\"\n [style.minWidth.px]=\"group.width\"\n [style.left.px]=\"group.isPinned ? group.stickyLeft : null\"\n [style.position]=\"group.isPinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"group.isPinned ? 3 : 1\">\n @if (group.label) {\n <span class=\"osl-rg-th-group-label\">{{ group.label }}</span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"osl-rg-header-row\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Select-all checkbox -->\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"allSelected\" [indeterminate]=\"someSelected\"\n (change)=\"toggleSelectAll($any($event.target).checked)\" />\n </div>\n }\n\n <!-- Column headers (drag-to-reorder) -->\n <div class=\"osl-rg-header-cols\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"_cols\" (cdkDropListDropped)=\"onColumnReorder($event)\"\n style=\"display:flex;flex:1;min-width:0;\">\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-th\"\n [class.osl-rg-th--pinned]=\"col._pinned\"\n [class.osl-rg-th--sorted]=\"getSortState(col.key)\"\n [class.osl-rg-th--filtered]=\"hasActiveFilter(col.key)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 3 : 1\"\n cdkDrag [cdkDragDisabled]=\"col._pinned\"\n (cdkDragStarted)=\"onColumnDragStarted()\"\n (cdkDragEnded)=\"onColumnDragEnded()\"\n (click)=\"onHeaderClick(col, $event)\">\n\n <div *cdkDragPlaceholder class=\"osl-rg-th-drag-placeholder\"></div>\n\n <!-- Pin badge -->\n @if (col._pinned) {\n <span class=\"osl-rg-th-pin-badge\" title=\"Pinned\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"9\" height=\"9\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </span>\n }\n\n <!-- Label row -->\n <div class=\"osl-rg-th-content\">\n <span class=\"osl-rg-th-label\" [title]=\"col.label\">{{ col.label }}</span>\n <div class=\"osl-rg-th-actions\">\n\n <!-- Sort indicator -->\n @if (col.sortable) {\n <span class=\"osl-rg-sort-icon\"\n [class.osl-rg-sort-icon--asc]=\"getSortState(col.key)?.asc === true\"\n [class.osl-rg-sort-icon--desc]=\"getSortState(col.key)?.asc === false\">\n @if (getSortState(col.key); as s) {\n @if (s.asc) {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n }\n @if (sortStates.size > 1) { <sup class=\"osl-rg-sort-badge\">{{ s.index + 1 }}</sup> }\n } @else {\n <svg class=\"osl-rg-sort-idle\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 9l4-4 4 4M16 15l-4 4-4-4\"/></svg>\n }\n </span>\n }\n\n <!-- Excel filter button -->\n @if (col.filterable) {\n <button class=\"osl-rg-filter-btn\" [class.osl-rg-filter-btn--active]=\"hasActiveFilter(col.key)\"\n (click)=\"openExcelFilter(col.key, $event)\" title=\"Filter\">\n @if (hasActiveFilter(col.key)) {\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/></svg>\n }\n </button>\n }\n\n <!-- Column menu -->\n <button class=\"osl-rg-col-menu-btn\" (click)=\"openColumnMenu(col.key, $event)\" title=\"Column options\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"19\" r=\"2\"/></svg>\n </button>\n\n </div>\n </div>\n\n <!-- Per-column search -->\n @if (col.searchable) {\n <div class=\"osl-rg-col-search\" (click)=\"$event.stopPropagation()\">\n <svg class=\"osl-rg-col-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m18 18-3.5-3.5\"/>\n </svg>\n <input class=\"osl-rg-col-search__input\" [placeholder]=\"col.label + '\u2026'\"\n [ngModel]=\"columnSearch[col.key]\" (ngModelChange)=\"onColumnSearch(col.key, $event)\" />\n @if (columnSearch[col.key]) {\n <button class=\"osl-rg-col-search__clear\" (click)=\"columnSearch[col.key]=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n }\n\n <!-- Resize handle -->\n @if (col.resizable) {\n <div class=\"osl-rg-resize-handle\" (mousedown)=\"startResize(col, $event)\" (dblclick)=\"autoSizeColumn(col, $event)\" (click)=\"$event.stopPropagation()\"></div>\n }\n\n\n </div><!-- /osl-rg-th -->\n }\n\n </div><!-- /header-cols -->\n </div><!-- /header-row -->\n </div><!-- /header-wrap -->\n\n <!-- \u2550\u2550 SCROLLABLE BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-body\" [style.height]=\"tableHeight\" #bodyScrollRef (scroll)=\"onBodyScroll($event)\">\n <div class=\"osl-rg-inner\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Skeleton loading -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <div class=\"osl-rg-row osl-rg-row--skeleton\" [style.height.px]=\"rowHeight\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\"><div class=\"osl-rg-skel\" style=\"width:14px;height:14px;border-radius:3px;\"></div></div>\n }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\">\n <div class=\"osl-rg-skel\" [style.width]=\"$index % 3 === 0 ? '55%' : $index % 3 === 1 ? '80%' : '65%'\"></div>\n </div>\n }\n </div>\n }\n\n } @else if (isEmpty) {\n <!-- Empty state -->\n <div class=\"osl-rg-empty\">\n <div class=\"osl-rg-empty__icon\">\n <svg viewBox=\"0 0 48 48\" fill=\"none\" stroke=\"#d1d5db\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"6\" y=\"6\" width=\"36\" height=\"36\" rx=\"4\"/>\n <path d=\"M6 18h36M6 30h36M18 6v36\"/>\n </svg>\n </div>\n <p class=\"osl-rg-empty__title\">No records found</p>\n <p class=\"osl-rg-empty__sub\">\n @if (hasAnyFilter) { Try adjusting your filters or search terms }\n @else { No data to display }\n </p>\n @if (hasAnyFilter) {\n <button class=\"osl-rg-empty__action\" (click)=\"clearAllFilters()\">Clear all filters</button>\n }\n </div>\n\n } @else {\n <!-- Data rows -->\n @for (row of flatRows; track trackByRow($index, row)) {\n\n @if (row._type === 'group') {\n <div class=\"osl-rg-group-row\"\n [class.osl-rg-group-row--l0]=\"asGroupRow(row)._level === 0\"\n [class.osl-rg-group-row--l1]=\"asGroupRow(row)._level === 1\"\n [class.osl-rg-group-row--l2]=\"asGroupRow(row)._level >= 2\"\n [style.height.px]=\"rowHeight\"\n [style.paddingLeft.px]=\"asGroupRow(row)._level * 20 + 12\"\n (click)=\"toggleGroup(asGroupRow(row)._path)\">\n @if (asGroupRow(row)._expanded) {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>\n } @else {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"/></svg>\n }\n <span class=\"osl-rg-group-row__key\">{{ asGroupRow(row)._colLabel }}:</span>\n <span class=\"osl-rg-group-row__value\">{{ asGroupRow(row)._label }}</span>\n <span class=\"osl-rg-group-row__count\">{{ asGroupRow(row)._count | number }} rows</span>\n </div>\n\n } @else {\n <div class=\"osl-rg-row\"\n [class.osl-rg-row--striped]=\"striped && asDataRow(row)._rowIndex % 2 === 1\"\n [class.osl-rg-row--selected]=\"selectedRows.has(asDataRow(row)._data)\"\n [style.height.px]=\"rowHeight\"\n (click)=\"onRowClick(asDataRow(row)._data, $event)\">\n\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"selectedRows.has(asDataRow(row)._data)\"\n (change)=\"toggleRowSelect(asDataRow(row)._data, $event)\" />\n </div>\n }\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\"\n [class.osl-rg-td--pinned]=\"col._pinned\"\n [ngClass]=\"getCellClass(asDataRow(row)._data, col)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 2 : 0\"\n [style.textAlign]=\"col.align ?? 'left'\"\n (dblclick)=\"copyCell(getCellDisplay(asDataRow(row)._data, col), $event)\"\n [title]=\"getCellDisplay(asDataRow(row)._data, col)\">\n <span class=\"osl-rg-cell-text\">{{ getCellDisplay(asDataRow(row)._data, col) }}</span>\n </div>\n }\n\n </div>\n }\n\n }\n }\n\n </div><!-- /osl-rg-inner -->\n </div><!-- /osl-rg-body -->\n\n <!-- \u2550\u2550 AGGREGATE FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showAggregates && hasAnyAggregate() && !loading) {\n <div class=\"osl-rg-aggregate-row\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') { <div class=\"osl-rg-agg-cell osl-rg-agg-cell--checkbox\"></div> }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-agg-cell\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\" [style.textAlign]=\"col.align ?? 'left'\">\n @if (col.aggregate) {\n <span class=\"osl-rg-agg-label\">{{ col.aggregate | uppercase }}</span>\n <span class=\"osl-rg-agg-value\">{{ getAggregate(col) }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- \u2550\u2550 PAGINATION \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (isPaginated) {\n <div class=\"osl-rg-pagination\">\n <span class=\"osl-rg-pagination__info\">\n @if (loading) { Loading\u2026 }\n @else if (_total > 0) { Showing <strong>{{ startRecord | number }}\u2013{{ endRecord | number }}</strong> of <strong>{{ _total | number }}</strong> records }\n @else { No records }\n </span>\n <div class=\"osl-rg-pagination__controls\">\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage-1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m15 18-6-6 6-6\"/></svg>\n Prev\n </button>\n @for (page of pageNumbers; track $index) {\n @if (page === -1) { <span class=\"osl-rg-page-ellipsis\">\u2026</span> }\n @else {\n <button class=\"osl-rg-page-btn osl-rg-page-btn--num\" [class.osl-rg-page-btn--active]=\"page===currentPage\"\n [disabled]=\"loading\" (click)=\"goToPage(page)\">{{ page }}</button>\n }\n }\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage+1)\" [disabled]=\"currentPage===totalPages||loading\">\n Next\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m9 18 6-6-6-6\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage===totalPages||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-pagination__size\">\n <span class=\"osl-rg-pagination__size-label\">Rows per page</span>\n <select class=\"osl-rg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) { <option [value]=\"opt\">{{ opt }}</option> }\n </select>\n </div>\n </div>\n }\n\n</div><!-- /osl-rg -->\n\n\n<!-- \u2550\u2550 EXCEL FILTER DROPDOWN (fixed, outside overflow containers) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (openFilterKey && excelFilterState[openFilterKey]) {\n <div class=\"osl-rg-excel-filter\"\n [style.top.px]=\"filterDropdownPos.top\"\n [style.left.px]=\"filterDropdownPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-excel-filter__header\">\n <div class=\"osl-rg-excel-filter__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" style=\"flex-shrink:0;\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n Filter: {{ getColumnByKey(openFilterKey)?.label }}\n </div>\n @if (hasActiveFilter(openFilterKey)) {\n <button class=\"osl-rg-excel-filter__clear-btn\" (click)=\"clearExcelFilter(openFilterKey!)\">Clear</button>\n }\n </div>\n <div class=\"osl-rg-excel-filter__search\">\n <input class=\"osl-rg-excel-filter__search-input\" placeholder=\"Search values\u2026\"\n [(ngModel)]=\"excelFilterState[openFilterKey].search\" />\n </div>\n <div class=\"osl-rg-excel-filter__select-all\">\n <label class=\"osl-rg-excel-filter__item osl-rg-excel-filter__item--all\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"isAllExcelChecked(openFilterKey)\"\n (change)=\"toggleAllExcelFilter(openFilterKey!, $any($event.target).checked)\" />\n <span class=\"osl-rg-excel-filter__item-label\">(Select All)</span>\n </label>\n </div>\n <div class=\"osl-rg-excel-filter__list\">\n @for (item of getFilteredExcelItems(openFilterKey); track item.value) {\n <label class=\"osl-rg-excel-filter__item\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [(ngModel)]=\"item.checked\" />\n <span class=\"osl-rg-excel-filter__item-label\">{{ item.label }}</span>\n </label>\n }\n </div>\n <div class=\"osl-rg-excel-filter__footer\">\n <button class=\"osl-rg-excel-filter__apply\" (click)=\"applyExcelFilter(openFilterKey!)\">Apply Filter</button>\n <button class=\"osl-rg-excel-filter__cancel\" (click)=\"openFilterKey=null\">Cancel</button>\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONTEXT MENU (fixed) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (columnMenuKey && getColumnByKey(columnMenuKey); as menuCol) {\n <div class=\"osl-rg-col-menu\"\n [style.top.px]=\"columnMenuPos.top\" [style.left.px]=\"columnMenuPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-col-menu__header\">{{ menuCol.label }}</div>\n @if (menuCol.sortable) {\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:true,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n Sort A \u2192 Z\n </button>\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:false,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n Sort Z \u2192 A\n </button>\n }\n @if (menuCol.groupable) {\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"addGroup(menuCol.key); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/></svg>\n Group by this\n </button>\n }\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"toggleColumnPin(menuCol)\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n {{ menuCol._pinned ? 'Unpin column' : 'Pin to left' }}\n </button>\n <button class=\"osl-rg-col-menu__item\" (click)=\"autoSizeColumn(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/></svg>\n Auto-size column\n </button>\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item osl-rg-col-menu__item--danger\" (click)=\"toggleColumnVisibility(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n Hide column\n </button>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONFIG SIDE DRAWER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (showColumnConfig) {\n <div class=\"osl-rg-config-backdrop\" (click)=\"showColumnConfig=false\"></div>\n <div class=\"osl-rg-config-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-config-panel__header\">\n <div class=\"osl-rg-config-panel__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"16\" height=\"16\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n Column Settings\n </div>\n <button class=\"osl-rg-config-panel__close\" (click)=\"showColumnConfig=false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-config-panel__actions\">\n <button class=\"osl-rg-config-action\" (click)=\"autoSizeAll()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3\"/></svg>\n Auto-size all\n </button>\n <button class=\"osl-rg-config-action\" (click)=\"showAllColumns()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n Show all\n </button>\n </div>\n <div class=\"osl-rg-config-hint\">Drag to reorder \u00B7 Toggle to show/hide \u00B7 Pin to freeze</div>\n <div class=\"osl-rg-config-list\" cdkDropList (cdkDropListDropped)=\"onConfigColumnReorder($event)\">\n @for (col of _cols; track col.key) {\n <div class=\"osl-rg-config-item\" cdkDrag [class.osl-rg-config-item--hidden]=\"col._hidden\">\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"!col._hidden\" (change)=\"toggleColumnVisibility(col)\" />\n <span class=\"osl-rg-config-item__label\" [class.osl-rg-config-item__label--hidden]=\"col._hidden\">{{ col.label }}</span>\n <button class=\"osl-rg-config-item__pin-btn\" (click)=\"toggleColumnPin(col)\"\n [class.osl-rg-config-item__pin-btn--active]=\"col._pinned\"\n [title]=\"col._pinned ? 'Unpin' : 'Pin to left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </button>\n </div>\n }\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COPY TOAST \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (copiedCell !== null) {\n <div class=\"osl-rg-copy-toast\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Copied to clipboard\n </div>\n}\n", styles: ["@keyframes rg-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes rg-fade-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes rg-slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.rg-icon{width:15px;height:15px;flex-shrink:0;display:block}.rg-icon--sm{width:14px;height:14px}.rg-icon--xs{width:12px;height:12px}.rg-icon--excel{width:18px;height:18px}.rg-drag-handle{width:14px;height:14px;flex-shrink:0;color:#d1d5db;cursor:grab;display:block}.rg-drag-handle:active{cursor:grabbing}.osl-rg{position:relative;display:flex;flex-direction:column;width:100%;font-family:inherit;font-size:13px;color:#111827;background:#fff;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px #00000012,0 1px 2px #0000000a;-webkit-user-select:none;user-select:none}.osl-rg-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;background:linear-gradient(135deg,#f8faff,#f3f4f6);border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;flex-wrap:wrap}.osl-rg-toolbar__left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.osl-rg-toolbar__right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.osl-rg-title{font-size:14px;font-weight:700;color:#111827;letter-spacing:-.01em}.osl-rg-record-count{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#9ca3af;font-weight:500}.osl-rg-record-count svg{color:#d1d5db}.osl-rg-filter-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;font-size:11px;color:#1d4ed8;font-weight:600}.osl-rg-filter-badge__dot{width:6px;height:6px;border-radius:50%;background:#2563eb;animation:rg-pulse 1.5s ease-in-out infinite;flex-shrink:0}.osl-rg-toolbar-btn{display:inline-flex;align-items:center;gap:5px;height:32px;padding:0 11px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;color:#5a6478;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s ease}.osl-rg-toolbar-btn svg{flex-shrink:0}.osl-rg-toolbar-btn:hover{background:#f3f4f6;color:#1f2937;border-color:#c4c9d4;box-shadow:0 1px 3px #0000000f}.osl-rg-toolbar-btn--on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.osl-rg-toolbar-btn--active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600}.osl-rg-toolbar-btn--danger{background:#fff5f5;border-color:#fca5a5;color:#dc2626}.osl-rg-toolbar-btn--danger:hover{background:#fee2e2;border-color:#f87171}.osl-rg-toolbar-btn--excel{background:linear-gradient(135deg,#1a7a3e,#1d6f42);border-color:#166534;color:#fff;font-weight:600;box-shadow:0 1px 3px #1665344d}.osl-rg-toolbar-btn--excel:hover{background:linear-gradient(135deg,#166534,#145a2c);border-color:#14532d;color:#fff;box-shadow:0 2px 6px #16653466}.osl-rg-global-search{position:relative;display:inline-flex;align-items:center}.osl-rg-global-search__icon{position:absolute;left:9px;width:14px;height:14px;color:#9ca3af;pointer-events:none;display:block}.osl-rg-global-search__input{height:32px;padding:0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;font-size:12.5px;font-family:inherit;color:#111827;outline:none;width:210px;transition:border-color .15s,width .2s,box-shadow .15s}.osl-rg-global-search__input::placeholder{color:#b0b7c3}.osl-rg-global-search__input:focus{border-color:var(--osl-primary, #2563eb);width:260px;box-shadow:0 0 0 3px #2563eb1a}.osl-rg-global-search__clear{position:absolute;right:6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:4px;transition:all .12s}.osl-rg-global-search__clear svg{width:12px;height:12px}.osl-rg-global-search__clear:hover{color:#374151;background:#0000000d}.osl-rg-group-panel{display:flex;align-items:center;gap:8px;padding:6px 14px;background:linear-gradient(135deg,#fafbff,#f5f7ff);border-bottom:1px dashed #dde5ff;min-height:40px;flex-shrink:0;flex-wrap:wrap}.osl-rg-group-panel__label{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0}.osl-rg-group-panel__label svg{color:#9ca3af}.osl-rg-group-panel__hint{font-size:11px;color:#b0b7c3;font-style:italic}.osl-rg-group-panel__clear{margin-left:auto;font-size:11px;color:#6b7280;background:none;border:none;cursor:pointer;text-decoration:underline;flex-shrink:0;padding:0;font-family:inherit}.osl-rg-group-panel__clear:hover{color:#dc2626}.osl-rg-group-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex:1;min-height:26px}.osl-rg-group-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 6px 3px 5px;background:linear-gradient(135deg,#dbeafe,#eff6ff);border:1px solid #bfdbfe;border-radius:20px;font-size:12px;font-weight:600;color:#1e40af;cursor:grab;-webkit-user-select:none;user-select:none;box-shadow:0 1px 2px #2563eb1a;transition:box-shadow .1s}.osl-rg-group-chip:active{cursor:grabbing;box-shadow:0 2px 6px #2563eb33}.osl-rg-group-chip .rg-drag-handle{color:#93c5fd}.osl-rg-group-chip__remove{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;background:transparent;cursor:pointer;padding:0;border-radius:50%;transition:all .1s}.osl-rg-group-chip__remove svg{width:10px;height:10px;color:#93c5fd}.osl-rg-group-chip__remove:hover{background:#dbeafe}.osl-rg-group-chip__remove:hover svg{color:#1e40af}.osl-rg-table-area{display:flex;flex-direction:column;flex:1 1 0;overflow:hidden;position:relative;min-height:0}.osl-rg-header-wrap{overflow:hidden;flex-shrink:0;border-bottom:2px solid #dde5ff;background:linear-gradient(180deg,#f0f4ff,#e8efff);z-index:10;position:relative}.osl-rg-header-row{display:flex;align-items:stretch}.osl-rg-header-row--groups{border-bottom:1px solid #c7d7f0}.osl-rg-th--group-span{display:flex;align-items:center;justify-content:center;height:28px;min-height:28px;cursor:default;padding:0;border-right:1px solid #dde5ff}.osl-rg-th--group-span:hover{background:transparent!important}.osl-rg-th--group-span:last-child{border-right:none}.osl-rg-th--group-span--labeled{background:linear-gradient(135deg,#dbeafe,#eff6ff);border-bottom:2px solid #93c5fd}.osl-rg-th-group-label{font-size:11px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 10px;text-align:center;width:100%}.osl-rg-header-cols{display:flex}.osl-rg-th{position:relative;display:flex;flex-direction:column;background:transparent;border-right:1px solid #dde5ff;cursor:pointer;flex-shrink:0;transition:background .12s;overflow:visible}.osl-rg-th:last-child{border-right:none}.osl-rg-th:hover{background:#2563eb0f}.osl-rg-th--pinned{background:linear-gradient(180deg,#e8efff,#dde8ff);border-right:1px solid #bfdbfe;z-index:3}.osl-rg-th--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.14),transparent);pointer-events:none;z-index:4}.osl-rg-th--sorted{background:#6366f114}.osl-rg-th--filtered{background:#f59e0b12}.osl-rg-th-drag-placeholder{background:#e0e7ff;border:2px dashed #6366f1;border-radius:4px;opacity:.6}.osl-rg-th-pin-badge{position:absolute;top:3px;right:24px;color:var(--osl-primary, #2563eb);opacity:.6;display:flex;align-items:center}.osl-rg-th-content{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 8px 0 10px;height:36px;min-height:36px}.osl-rg-th-label{font-size:11.5px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.osl-rg-th-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.osl-rg-sort-icon{display:inline-flex;align-items:center;position:relative;color:#9ca3af;transition:color .12s;width:14px;height:14px}.osl-rg-sort-icon svg{width:13px;height:13px;display:block}.osl-rg-sort-icon--asc,.osl-rg-sort-icon--desc{color:var(--osl-primary, #2563eb)}.osl-rg-sort-idle{opacity:.3}.osl-rg-sort-badge{position:absolute;top:-4px;right:-6px;font-size:8px;font-weight:700;background:var(--osl-primary, #2563eb);color:#fff;border-radius:50%;width:11px;height:11px;display:flex;align-items:center;justify-content:center;line-height:1}.osl-rg-filter-btn,.osl-rg-col-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:#b0b7c3;cursor:pointer;padding:0;border-radius:5px;transition:all .12s}.osl-rg-filter-btn svg,.osl-rg-col-menu-btn svg{width:13px;height:13px;display:block}.osl-rg-filter-btn:hover,.osl-rg-col-menu-btn:hover{background:#00000012;color:#374151}.osl-rg-filter-btn--active{color:var(--osl-primary, #2563eb)!important;background:#2563eb1f!important}.osl-rg-col-search{position:relative;display:flex;align-items:center;padding:3px 6px;border-top:1px solid #e0e7ff;background:#ffffff80}.osl-rg-col-search__icon{position:absolute;left:10px;width:12px;height:12px;display:block;pointer-events:none;color:#c4cad6}.osl-rg-col-search__input{width:100%;height:23px;padding:0 18px 0 22px;border:1px solid #dde5ff;border-radius:5px;font-size:11px;font-family:inherit;background:#fff;color:#111827;outline:none;transition:border-color .12s,box-shadow .12s}.osl-rg-col-search__input::placeholder{color:#c4cad6}.osl-rg-col-search__input:focus{border-color:var(--osl-primary, #2563eb);box-shadow:0 0 0 2px #2563eb1a}.osl-rg-col-search__clear{position:absolute;right:8px;display:flex;align-items:center;width:14px;height:14px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:3px;transition:all .1s}.osl-rg-col-search__clear svg{width:10px;height:10px;display:block}.osl-rg-col-search__clear:hover{color:#374151;background:#0000000f}.osl-rg-resize-handle{position:absolute;right:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.osl-rg-resize-handle:after{content:\"\";position:absolute;right:2px;top:15%;bottom:15%;width:2px;border-radius:2px;background:#a5b4fc;opacity:0;transition:opacity .15s}.osl-rg-resize-handle:hover:after{opacity:1}.osl-rg-th:hover .osl-rg-resize-handle:after{opacity:.4}.osl-rg-excel-filter{position:fixed;z-index:9999;width:250px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 32px #00000026,0 2px 8px #00000012;animation:rg-fade-up .14s ease;overflow:hidden}.osl-rg-excel-filter__header{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(135deg,#f0f4ff,#e8efff);border-bottom:1px solid #dde5ff}.osl-rg-excel-filter__title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.06em}.osl-rg-excel-filter__clear-btn{font-size:11px;color:#dc2626;background:none;border:none;cursor:pointer;font-weight:600;padding:2px 6px;border-radius:4px;font-family:inherit;transition:background .1s}.osl-rg-excel-filter__clear-btn:hover{background:#fee2e2}.osl-rg-excel-filter__search{padding:7px 8px;border-bottom:1px solid #f3f4f6;position:relative}.osl-rg-excel-filter__search-input{width:100%;height:28px;padding:0 8px 0 28px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .12s;color:#111827}.osl-rg-excel-filter__search-input::placeholder{color:#b0b7c3}.osl-rg-excel-filter__search-input:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-excel-filter__select-all{padding:0 8px;border-bottom:2px solid #f3f4f6;background:#fafbff}.osl-rg-excel-filter__list{max-height:210px;overflow-y:auto;padding:3px 8px}.osl-rg-excel-filter__list::-webkit-scrollbar{width:5px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-excel-filter__item{display:flex;align-items:center;gap:8px;padding:5px 4px;font-size:12.5px;color:#374151;cursor:pointer;border-radius:5px;transition:background .1s}.osl-rg-excel-filter__item:hover{background:#f3f4f6}.osl-rg-excel-filter__item--all{font-weight:600;color:#1f2937;padding:6px 4px}.osl-rg-excel-filter__item-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.osl-rg-excel-filter__footer{display:flex;gap:6px;padding:8px 10px;border-top:1px solid #f3f4f6;background:#f9fafb}.osl-rg-excel-filter__apply{flex:1;height:30px;border:none;border-radius:6px;font-size:12px;font-family:inherit;font-weight:600;cursor:pointer;background:var(--osl-primary, #2563eb);color:#fff;transition:background .12s}.osl-rg-excel-filter__apply:hover{background:#1d4ed8}.osl-rg-excel-filter__cancel{height:30px;padding:0 14px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;background:#fff;color:#6b7280;transition:all .12s}.osl-rg-excel-filter__cancel:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-th--checkbox,.osl-rg-td--checkbox{display:flex;align-items:center;justify-content:center;width:36px;min-width:36px;flex-shrink:0;border-right:1px solid #dde5ff}.osl-rg-checkbox{width:14px;height:14px;cursor:pointer;accent-color:var(--osl-primary, #2563eb);flex-shrink:0}.osl-rg-body{overflow-x:auto;overflow-y:auto;flex-shrink:0}.osl-rg-body::-webkit-scrollbar{width:8px;height:8px}.osl-rg-body::-webkit-scrollbar-track{background:#f9fafb}.osl-rg-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-body::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-body::-webkit-scrollbar-corner{background:#f9fafb}.osl-rg-inner{display:block}.osl-rg-row{display:flex;align-items:stretch;border-bottom:1px solid #f3f4f6;transition:background .08s;cursor:default}.osl-rg-row:hover{background:#f0f5ff!important}.osl-rg-row--striped{background:#fafbff}.osl-rg-row--selected{background:#eff6ff!important;border-bottom-color:#dbeafe}.osl-rg-row--skeleton{pointer-events:none}.osl-rg-td{display:flex;align-items:center;padding:0 12px;border-right:1px solid #f0f2f7;overflow:hidden;background:inherit;flex-shrink:0}.osl-rg-td:last-child{border-right:none}.osl-rg-td--pinned{background:#fff;border-right:1px solid #e0e7ff;z-index:2}.osl-rg-td--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.07),transparent);pointer-events:none}.osl-rg-cell-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;font-size:13px;color:#1f2937;line-height:1.4}.rg-profit-pos .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-profit-neg .osl-rg-cell-text{color:#dc2626;font-weight:600}.rg-status-delivered .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-status-shipped .osl-rg-cell-text{color:#2563eb;font-weight:600}.rg-status-processing .osl-rg-cell-text{color:#d97706;font-weight:600}.rg-status-pending .osl-rg-cell-text{color:#6b7280;font-weight:500}.rg-status-cancelled .osl-rg-cell-text{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.75}.osl-rg-group-row{display:flex;align-items:center;gap:7px;border-bottom:1px solid #e5e7eb;font-weight:600;cursor:pointer;flex-shrink:0;transition:filter .1s}.osl-rg-group-row--l0{background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1e40af;border-left:4px solid #3b82f6}.osl-rg-group-row--l1{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:#5b21b6;border-left:4px solid #8b5cf6}.osl-rg-group-row--l2{background:linear-gradient(135deg,#fff7ed,#fef3c7);color:#92400e;border-left:4px solid #f59e0b}.osl-rg-group-row:hover{filter:brightness(.97)}.osl-rg-group-row__key{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;opacity:.65;flex-shrink:0}.osl-rg-group-row__value{font-size:13px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-group-row__count{font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#00000017;margin-right:12px;flex-shrink:0}.osl-rg-group-chevron{width:16px;height:16px;flex-shrink:0;opacity:.75;display:block}.osl-rg-aggregate-row{display:flex;align-items:center;border-top:2px solid #e5e7eb;background:linear-gradient(135deg,#f0fdf4,#f9fafb);flex-shrink:0;min-height:38px}.osl-rg-agg-cell{display:flex;align-items:center;gap:5px;padding:0 12px;border-right:1px solid #e5e7eb;height:38px;flex-shrink:0;overflow:hidden}.osl-rg-agg-cell--checkbox{width:36px;min-width:36px}.osl-rg-agg-label{font-size:9.5px;font-weight:800;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0;background:#e5e7eb;padding:1px 4px;border-radius:3px}.osl-rg-agg-value{font-size:12px;font-weight:700;color:#15803d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-skel{height:12px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6,#e5e7eb 75%);background-size:200% 100%;animation:rg-pulse 1.5s ease-in-out infinite}.osl-rg-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 20px;color:#9ca3af;text-align:center}.osl-rg-empty__icon{margin-bottom:18px;opacity:.7}.osl-rg-empty__title{font-size:15px;font-weight:600;color:#374151;margin:0 0 6px}.osl-rg-empty__sub{font-size:13px;color:#9ca3af;margin:0 0 18px;max-width:290px;line-height:1.5}.osl-rg-empty__action{height:34px;padding:0 18px;background:var(--osl-primary, #2563eb);color:#fff;border:none;border-radius:7px;font-size:13px;font-family:inherit;font-weight:600;cursor:pointer;transition:background .15s;box-shadow:0 2px 6px #2563eb4d}.osl-rg-empty__action:hover{background:#1d4ed8}.osl-rg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:linear-gradient(135deg,#f8faff,#f3f4f6);flex-shrink:0;flex-wrap:wrap}.osl-rg-pagination__info{font-size:12px;color:#6b7280;min-width:160px;white-space:nowrap}.osl-rg-pagination__info strong{color:#1f2937;font-weight:600}.osl-rg-pagination__controls{display:flex;align-items:center;gap:3px}.osl-rg-pagination__size{display:flex;align-items:center;gap:7px;min-width:120px;justify-content:flex-end}.osl-rg-pagination__size-label{font-size:11.5px;color:#9ca3af;white-space:nowrap}.osl-rg-page-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s;white-space:nowrap}.osl-rg-page-btn svg{display:block}.osl-rg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af;box-shadow:0 1px 2px #0000000f}.osl-rg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-rg-page-btn--num{min-width:30px;padding:0;font-size:12.5px}.osl-rg-page-btn--active{background:var(--osl-primary, #2563eb);border-color:var(--osl-primary, #2563eb);color:#fff;font-weight:700;box-shadow:0 2px 5px #2563eb59}.osl-rg-page-btn--active:hover:not(:disabled){background:#1d4ed8;border-color:#1d4ed8}.osl-rg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:26px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-rg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;font-size:12px;font-family:inherit;color:#374151;cursor:pointer;outline:none;transition:border-color .15s}.osl-rg-page-size:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu{position:fixed;z-index:9999;min-width:210px;background:#fff;border:1px solid #e5e7eb;border-radius:11px;box-shadow:0 12px 36px #00000029,0 3px 10px #00000014;overflow:hidden;animation:rg-fade-up .13s cubic-bezier(.16,1,.3,1)}.osl-rg-col-menu__header{padding:9px 14px 7px;font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f3f4f6;background:#fafbff}.osl-rg-col-menu__divider{height:1px;background:#f3f4f6;margin:2px 0}.osl-rg-col-menu__item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:none;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;transition:all .1s}.osl-rg-col-menu__item svg{color:#9ca3af;display:block}.osl-rg-col-menu__item:hover{background:#f0f4ff;color:var(--osl-primary, #2563eb);border-left-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item:hover svg{color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item--danger:hover{background:#fff5f5;color:#dc2626;border-left-color:#dc2626}.osl-rg-col-menu__item--danger:hover svg{color:#dc2626}.osl-rg-config-backdrop{position:fixed;inset:0;z-index:9000;background:#0003;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.osl-rg-config-panel{position:fixed;top:0;right:0;bottom:0;z-index:9001;width:300px;background:#fff;border-left:1px solid #e5e7eb;box-shadow:-6px 0 24px #00000021;display:flex;flex-direction:column;animation:rg-slide-in .2s cubic-bezier(.16,1,.3,1)}.osl-rg-config-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#f0f4ff,#e8efff);flex-shrink:0}.osl-rg-config-panel__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;color:#1e3a6e}.osl-rg-config-panel__title svg{color:#3b82f6}.osl-rg-config-panel__close{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#9ca3af;cursor:pointer;border-radius:7px;padding:0;transition:all .12s}.osl-rg-config-panel__close svg{width:16px;height:16px;display:block}.osl-rg-config-panel__close:hover{background:#00000012;color:#374151}.osl-rg-config-panel__actions{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#fafbff}.osl-rg-config-hint{padding:6px 16px;font-size:10.5px;color:#b0b7c3;font-style:italic;border-bottom:1px solid #f3f4f6;flex-shrink:0}.osl-rg-config-action{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s}.osl-rg-config-action svg{display:block}.osl-rg-config-action:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-config-list{flex:1;overflow-y:auto;padding:6px 0}.osl-rg-config-list::-webkit-scrollbar{width:5px}.osl-rg-config-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}.osl-rg-config-list::-webkit-scrollbar-thumb:hover{background:#d1d5db}.osl-rg-config-item{display:flex;align-items:center;gap:9px;padding:7px 14px;transition:background .1s;cursor:grab}.osl-rg-config-item:hover{background:#f9fafb}.osl-rg-config-item:active{cursor:grabbing}.osl-rg-config-item--hidden{opacity:.5}.osl-rg-config-item__label{flex:1;font-size:13px;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.osl-rg-config-item__label--hidden{color:#9ca3af;font-style:italic}.osl-rg-config-item__pin-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #e5e7eb;background:#fff;color:#d1d5db;cursor:pointer;padding:0;border-radius:5px;transition:all .12s;flex-shrink:0}.osl-rg-config-item__pin-btn svg{display:block}.osl-rg-config-item__pin-btn:hover,.osl-rg-config-item__pin-btn--active{background:#eff6ff;border-color:#93c5fd;color:var(--osl-primary, #2563eb)}.osl-rg-copy-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:99999;display:flex;align-items:center;gap:7px;background:#1f2937;color:#fff;font-size:12.5px;font-weight:500;padding:8px 18px;border-radius:22px;animation:rg-fade-up .2s ease;box-shadow:0 6px 16px #0000004d;pointer-events:none;white-space:nowrap;font-family:inherit}.osl-rg-copy-toast svg{flex-shrink:0;color:#4ade80}\n"] }]
|
|
3897
|
+
args: [{ selector: 'osl-report-grid', standalone: false, providers: [DatePipe, DecimalPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"osl-rg\" (click)=\"closeColumnMenu(); openFilterKey = null; $event.stopPropagation()\">\n\n <!-- \u2550\u2550 TOOLBAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-toolbar\">\n <div class=\"osl-rg-toolbar__left\">\n @if (title) { <span class=\"osl-rg-title\">{{ title }}</span> }\n @if (!loading) {\n @if (_filteredTotal !== datasource.length) {\n <span class=\"osl-rg-filter-badge\">\n <span class=\"osl-rg-filter-badge__dot\"></span>\n {{ _filteredTotal | number }} of {{ datasource.length | number }} rows\n </span>\n } @else {\n <span class=\"osl-rg-record-count\">{{ datasource.length | number }} rows</span>\n }\n }\n </div>\n <div class=\"osl-rg-toolbar__right\">\n\n <!-- Global search -->\n <div class=\"osl-rg-global-search\">\n <svg class=\"osl-rg-global-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input class=\"osl-rg-global-search__input\" placeholder=\"Search all columns\u2026\"\n [(ngModel)]=\"globalSearch\" (ngModelChange)=\"currentPage=1; processData()\" />\n @if (globalSearch) {\n <button class=\"osl-rg-global-search__clear\" (click)=\"globalSearch=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n\n <!-- Sort active badge -->\n @if (sortStates.size > 0) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--active\" (click)=\"clearSort()\" title=\"Clear sort\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 5H4M11 9H4M11 13H4M18 4v16M15 7l3-3 3 3M15 17l3 3 3-3\"/>\n </svg>\n <span>{{ sortStates.size }} sort{{ sortStates.size > 1 ? 's' : '' }}</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Filter active badge -->\n @if (hasAnyFilter) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--danger\" (click)=\"clearAllFilters()\" title=\"Clear all filters\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n <span>Filtered</span>\n <svg class=\"rg-icon rg-icon--xs\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n\n <!-- Group toggle -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showGroupPanel\"\n (click)=\"showGroupPanel=!showGroupPanel\" title=\"Group panel\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Group</span>\n </button>\n\n <!-- Expand / collapse when grouped -->\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-toolbar-btn\" (click)=\"expandAll()\" title=\"Expand all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 20 5-5 5 5M7 4l5 5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-toolbar-btn\" (click)=\"collapseAll()\" title=\"Collapse all\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5-5 5 5M7 9l5 5 5 5\"/></svg>\n </button>\n }\n\n <!-- Auto-size all -->\n <button class=\"osl-rg-toolbar-btn\" (click)=\"autoSizeAll()\" title=\"Auto-size all columns\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/>\n </svg>\n <span>Auto-size</span>\n </button>\n\n <!-- Column config -->\n <button class=\"osl-rg-toolbar-btn\" [class.osl-rg-toolbar-btn--on]=\"showColumnConfig\"\n (click)=\"showColumnConfig=!showColumnConfig; $event.stopPropagation()\" title=\"Column settings\">\n <svg class=\"rg-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n <span>Columns</span>\n </button>\n\n <!-- Export Excel -->\n @if (exportable) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--excel\" (click)=\"exportCsv()\" title=\"Export to Excel (CSV)\">\n <svg class=\"rg-icon rg-icon--excel\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#1D6F42\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <path d=\"M8 9.5l2.3 3.5L8 16.5h1.6l1.4-2.3 1.4 2.3H14l-2.3-3.5 2.3-3.5h-1.6l-1.4 2.3-1.4-2.3H8z\" fill=\"#fff\"/>\n </svg>\n <span>Export Excel</span>\n </button>\n }\n\n <!-- Export PDF -->\n @if (exportable && pdfExportFromGrid) {\n <button class=\"osl-rg-toolbar-btn osl-rg-toolbar-btn--pdf\" (click)=\"exportPdf()\" title=\"Export to PDF\">\n <svg class=\"rg-icon rg-icon--pdf\" viewBox=\"0 0 24 24\">\n <rect width=\"24\" height=\"24\" rx=\"3\" fill=\"#D32F2F\"/>\n <path d=\"M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5L13.5 3z\" fill=\"#fff\" opacity=\".15\"/>\n <path d=\"M13.5 3v4.5H18L13.5 3z\" fill=\"#fff\" opacity=\".3\"/>\n <text x=\"4.5\" y=\"17.5\" fill=\"#fff\" font-size=\"6.5\" font-family=\"sans-serif\" font-weight=\"700\">PDF</text>\n </svg>\n <span>Export PDF</span>\n </button>\n }\n </div>\n </div>\n\n <!-- \u2550\u2550 GROUP PANEL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showGroupPanel) {\n <div class=\"osl-rg-group-panel\">\n <span class=\"osl-rg-group-panel__label\">Group by:</span>\n @if (activeGroups.length === 0) {\n <span class=\"osl-rg-group-panel__hint\">Use the \u22EE column menu \u2192 \"Group by this\"</span>\n }\n <div class=\"osl-rg-group-chips\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"activeGroups\" (cdkDropListDropped)=\"onGroupPanelDrop($event)\">\n @for (key of activeGroups; track key) {\n <div class=\"osl-rg-group-chip\" cdkDrag>\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <span>{{ getGroupColLabel(key) }}</span>\n <button class=\"osl-rg-group-chip__remove\" (click)=\"removeGroup(key)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n }\n </div>\n @if (activeGroups.length > 0) {\n <button class=\"osl-rg-group-panel__clear\" (click)=\"clearGroups()\">Clear groups</button>\n }\n </div>\n }\n\n <!-- \u2550\u2550 STICKY HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-header-wrap\" #headerScrollRef>\n\n <!-- Double header \u2013 group row (only rendered when headerGroup is used on any column) -->\n @if (hasHeaderGroups) {\n <div class=\"osl-rg-header-row osl-rg-header-row--groups\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox osl-rg-th--group-span\"></div>\n }\n <div style=\"display:flex;flex:1;min-width:0;\">\n @for (group of computedHeaderGroups; track $index) {\n <div class=\"osl-rg-th osl-rg-th--group-span\"\n [class.osl-rg-th--group-span--labeled]=\"!!group.label\"\n [class.osl-rg-th--pinned]=\"group.isPinned\"\n [style.width.px]=\"group.width\"\n [style.minWidth.px]=\"group.width\"\n [style.left.px]=\"group.isPinned ? group.stickyLeft : null\"\n [style.position]=\"group.isPinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"group.isPinned ? 3 : 1\">\n @if (group.label) {\n <span class=\"osl-rg-th-group-label\">{{ group.label }}</span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"osl-rg-header-row\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Select-all checkbox -->\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-th osl-rg-th--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"allSelected\" [indeterminate]=\"someSelected\"\n (change)=\"toggleSelectAll($any($event.target).checked)\" />\n </div>\n }\n\n <!-- Column headers (drag-to-reorder) -->\n <div class=\"osl-rg-header-cols\" cdkDropList cdkDropListOrientation=\"horizontal\"\n [cdkDropListData]=\"_cols\" (cdkDropListDropped)=\"onColumnReorder($event)\"\n style=\"display:flex;flex:1;min-width:0;\">\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-th\"\n [class.osl-rg-th--pinned]=\"col._pinned\"\n [class.osl-rg-th--sorted]=\"getSortState(col.key)\"\n [class.osl-rg-th--filtered]=\"hasActiveFilter(col.key)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 3 : 1\"\n cdkDrag [cdkDragDisabled]=\"col._pinned\"\n (cdkDragStarted)=\"onColumnDragStarted()\"\n (cdkDragEnded)=\"onColumnDragEnded()\"\n (click)=\"onHeaderClick(col, $event)\">\n\n <div *cdkDragPlaceholder class=\"osl-rg-th-drag-placeholder\"></div>\n\n <!-- Pin badge -->\n @if (col._pinned) {\n <span class=\"osl-rg-th-pin-badge\" title=\"Pinned\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"9\" height=\"9\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </span>\n }\n\n <!-- Label row -->\n <div class=\"osl-rg-th-content\">\n <span class=\"osl-rg-th-label\" [title]=\"col.label\">{{ col.label }}</span>\n <div class=\"osl-rg-th-actions\">\n\n <!-- Sort indicator -->\n @if (col.sortable) {\n <span class=\"osl-rg-sort-icon\"\n [class.osl-rg-sort-icon--asc]=\"getSortState(col.key)?.asc === true\"\n [class.osl-rg-sort-icon--desc]=\"getSortState(col.key)?.asc === false\">\n @if (getSortState(col.key); as s) {\n @if (s.asc) {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n }\n @if (sortStates.size > 1) { <sup class=\"osl-rg-sort-badge\">{{ s.index + 1 }}</sup> }\n } @else {\n <svg class=\"osl-rg-sort-idle\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 9l4-4 4 4M16 15l-4 4-4-4\"/></svg>\n }\n </span>\n }\n\n <!-- Excel filter button -->\n @if (col.filterable) {\n <button class=\"osl-rg-filter-btn\" [class.osl-rg-filter-btn--active]=\"hasActiveFilter(col.key)\"\n (click)=\"openExcelFilter(col.key, $event)\" title=\"Filter\">\n @if (hasActiveFilter(col.key)) {\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/></svg>\n }\n </button>\n }\n\n <!-- Column menu -->\n <button class=\"osl-rg-col-menu-btn\" (click)=\"openColumnMenu(col.key, $event)\" title=\"Column options\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><circle cx=\"12\" cy=\"19\" r=\"2\"/></svg>\n </button>\n\n </div>\n </div>\n\n <!-- Per-column search -->\n @if (col.searchable) {\n <div class=\"osl-rg-col-search\" (click)=\"$event.stopPropagation()\">\n <svg class=\"osl-rg-col-search__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m18 18-3.5-3.5\"/>\n </svg>\n <input class=\"osl-rg-col-search__input\" [placeholder]=\"col.label + '\u2026'\"\n [ngModel]=\"columnSearch[col.key]\" (ngModelChange)=\"onColumnSearch(col.key, $event)\" />\n @if (columnSearch[col.key]) {\n <button class=\"osl-rg-col-search__clear\" (click)=\"columnSearch[col.key]=''; processData()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n }\n </div>\n }\n\n <!-- Resize handle -->\n @if (col.resizable) {\n <div class=\"osl-rg-resize-handle\" (mousedown)=\"startResize(col, $event)\" (dblclick)=\"autoSizeColumn(col, $event)\" (click)=\"$event.stopPropagation()\"></div>\n }\n\n\n </div><!-- /osl-rg-th -->\n }\n\n </div><!-- /header-cols -->\n </div><!-- /header-row -->\n </div><!-- /header-wrap -->\n\n <!-- \u2550\u2550 SCROLLABLE BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"osl-rg-body\" [style.height]=\"tableHeight\" #bodyScrollRef (scroll)=\"onBodyScroll($event)\">\n <div class=\"osl-rg-inner\" [style.minWidth.px]=\"totalWidth\">\n\n <!-- Skeleton loading -->\n @if (loading) {\n @for (sk of skeletonRows; track $index) {\n <div class=\"osl-rg-row osl-rg-row--skeleton\" [style.height.px]=\"rowHeight\">\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\"><div class=\"osl-rg-skel\" style=\"width:14px;height:14px;border-radius:3px;\"></div></div>\n }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\">\n <div class=\"osl-rg-skel\" [style.width]=\"$index % 3 === 0 ? '55%' : $index % 3 === 1 ? '80%' : '65%'\"></div>\n </div>\n }\n </div>\n }\n\n } @else if (isEmpty) {\n <!-- Empty state -->\n <div class=\"osl-rg-empty\">\n <div class=\"osl-rg-empty__icon\">\n <svg viewBox=\"0 0 48 48\" fill=\"none\" stroke=\"#d1d5db\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"6\" y=\"6\" width=\"36\" height=\"36\" rx=\"4\"/>\n <path d=\"M6 18h36M6 30h36M18 6v36\"/>\n </svg>\n </div>\n <p class=\"osl-rg-empty__title\">No records found</p>\n <p class=\"osl-rg-empty__sub\">\n @if (hasAnyFilter) { Try adjusting your filters or search terms }\n @else { No data to display }\n </p>\n @if (hasAnyFilter) {\n <button class=\"osl-rg-empty__action\" (click)=\"clearAllFilters()\">Clear all filters</button>\n }\n </div>\n\n } @else {\n <!-- Data rows -->\n @for (row of flatRows; track trackByRow($index, row)) {\n\n @if (row._type === 'group') {\n <div class=\"osl-rg-group-row\"\n [class.osl-rg-group-row--l0]=\"asGroupRow(row)._level === 0\"\n [class.osl-rg-group-row--l1]=\"asGroupRow(row)._level === 1\"\n [class.osl-rg-group-row--l2]=\"asGroupRow(row)._level >= 2\"\n [style.height.px]=\"rowHeight\"\n [style.paddingLeft.px]=\"asGroupRow(row)._level * 20 + 12\"\n (click)=\"toggleGroup(asGroupRow(row)._path)\">\n @if (asGroupRow(row)._expanded) {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>\n } @else {\n <svg class=\"osl-rg-group-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"/></svg>\n }\n <span class=\"osl-rg-group-row__key\">{{ asGroupRow(row)._colLabel }}:</span>\n <span class=\"osl-rg-group-row__value\">{{ asGroupRow(row)._label }}</span>\n <span class=\"osl-rg-group-row__count\">{{ asGroupRow(row)._count | number }} rows</span>\n </div>\n\n } @else {\n <div class=\"osl-rg-row\"\n [class.osl-rg-row--striped]=\"striped && asDataRow(row)._rowIndex % 2 === 1\"\n [class.osl-rg-row--selected]=\"selectedRows.has(asDataRow(row)._data)\"\n [style.height.px]=\"rowHeight\"\n (click)=\"onRowClick(asDataRow(row)._data, $event)\">\n\n @if (rowSelection === 'multiple') {\n <div class=\"osl-rg-td osl-rg-td--checkbox\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\"\n [checked]=\"selectedRows.has(asDataRow(row)._data)\"\n (change)=\"toggleRowSelect(asDataRow(row)._data, $event)\" />\n </div>\n }\n\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-td\"\n [class.osl-rg-td--pinned]=\"col._pinned\"\n [ngClass]=\"getCellClass(asDataRow(row)._data, col)\"\n [style.width.px]=\"col._width\"\n [style.minWidth.px]=\"col._width\"\n [style.left.px]=\"col._pinned ? col._stickyLeft : null\"\n [style.position]=\"col._pinned ? 'sticky' : 'relative'\"\n [style.zIndex]=\"col._pinned ? 2 : 0\"\n [style.textAlign]=\"col.align ?? 'left'\"\n (dblclick)=\"copyCell(getCellDisplay(asDataRow(row)._data, col), $event)\"\n [title]=\"getCellDisplay(asDataRow(row)._data, col)\">\n <span class=\"osl-rg-cell-text\">{{ getCellDisplay(asDataRow(row)._data, col) }}</span>\n </div>\n }\n\n </div>\n }\n\n }\n }\n\n </div><!-- /osl-rg-inner -->\n </div><!-- /osl-rg-body -->\n\n <!-- \u2550\u2550 AGGREGATE FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (showAggregates && hasAnyAggregate() && !loading) {\n <div class=\"osl-rg-aggregate-row\" [style.minWidth.px]=\"totalWidth\">\n @if (rowSelection === 'multiple') { <div class=\"osl-rg-agg-cell osl-rg-agg-cell--checkbox\"></div> }\n @for (col of visibleCols; track col.key) {\n <div class=\"osl-rg-agg-cell\" [style.width.px]=\"col._width\" [style.minWidth.px]=\"col._width\" [style.textAlign]=\"col.align ?? 'left'\">\n @if (col.aggregate) {\n <span class=\"osl-rg-agg-label\">{{ col.aggregate | uppercase }}</span>\n <span class=\"osl-rg-agg-value\">{{ getAggregate(col) }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- \u2550\u2550 PAGINATION \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (isPaginated) {\n <div class=\"osl-rg-pagination\">\n <span class=\"osl-rg-pagination__info\">\n @if (loading) { Loading\u2026 }\n @else if (_total > 0) { Showing <strong>{{ startRecord | number }}\u2013{{ endRecord | number }}</strong> of <strong>{{ _total | number }}</strong> records }\n @else { No records }\n </span>\n <div class=\"osl-rg-pagination__controls\">\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage-1)\" [disabled]=\"currentPage===1||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m15 18-6-6 6-6\"/></svg>\n Prev\n </button>\n @for (page of pageNumbers; track $index) {\n @if (page === -1) { <span class=\"osl-rg-page-ellipsis\">\u2026</span> }\n @else {\n <button class=\"osl-rg-page-btn osl-rg-page-btn--num\" [class.osl-rg-page-btn--active]=\"page===currentPage\"\n [disabled]=\"loading\" (click)=\"goToPage(page)\">{{ page }}</button>\n }\n }\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(currentPage+1)\" [disabled]=\"currentPage===totalPages||loading\">\n Next\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m9 18 6-6-6-6\"/></svg>\n </button>\n <button class=\"osl-rg-page-btn osl-rg-page-btn--nav\" (click)=\"goToPage(totalPages)\" [disabled]=\"currentPage===totalPages||loading\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"m13 17 5-5-5-5M6 17l5-5-5-5\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-pagination__size\">\n <span class=\"osl-rg-pagination__size-label\">Rows per page</span>\n <select class=\"osl-rg-page-size\" [ngModel]=\"pageSize\" (ngModelChange)=\"onPageSizeChange($event)\" [disabled]=\"loading\">\n @for (opt of pageSizeOptions; track opt) { <option [value]=\"opt\">{{ opt }}</option> }\n </select>\n </div>\n </div>\n }\n\n</div><!-- /osl-rg -->\n\n\n<!-- \u2550\u2550 EXCEL FILTER DROPDOWN (fixed, outside overflow containers) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (openFilterKey && excelFilterState[openFilterKey]) {\n <div class=\"osl-rg-excel-filter\"\n [style.top.px]=\"filterDropdownPos.top\"\n [style.left.px]=\"filterDropdownPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-excel-filter__header\">\n <div class=\"osl-rg-excel-filter__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" style=\"flex-shrink:0;\"><path d=\"M22 3H2l8 9.46V19l4 2V12.46z\"/></svg>\n Filter: {{ getColumnByKey(openFilterKey)?.label }}\n </div>\n @if (hasActiveFilter(openFilterKey)) {\n <button class=\"osl-rg-excel-filter__clear-btn\" (click)=\"clearExcelFilter(openFilterKey!)\">Clear</button>\n }\n </div>\n <div class=\"osl-rg-excel-filter__search\">\n <input class=\"osl-rg-excel-filter__search-input\" placeholder=\"Search values\u2026\"\n [(ngModel)]=\"excelFilterState[openFilterKey].search\" />\n </div>\n <div class=\"osl-rg-excel-filter__select-all\">\n <label class=\"osl-rg-excel-filter__item osl-rg-excel-filter__item--all\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"isAllExcelChecked(openFilterKey)\"\n (change)=\"toggleAllExcelFilter(openFilterKey!, $any($event.target).checked)\" />\n <span class=\"osl-rg-excel-filter__item-label\">(Select All)</span>\n </label>\n </div>\n <div class=\"osl-rg-excel-filter__list\">\n @for (item of getFilteredExcelItems(openFilterKey); track item.value) {\n <label class=\"osl-rg-excel-filter__item\">\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [(ngModel)]=\"item.checked\" />\n <span class=\"osl-rg-excel-filter__item-label\">{{ item.label }}</span>\n </label>\n }\n </div>\n <div class=\"osl-rg-excel-filter__footer\">\n <button class=\"osl-rg-excel-filter__apply\" (click)=\"applyExcelFilter(openFilterKey!)\">Apply Filter</button>\n <button class=\"osl-rg-excel-filter__cancel\" (click)=\"openFilterKey=null\">Cancel</button>\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONTEXT MENU (fixed) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (columnMenuKey && getColumnByKey(columnMenuKey); as menuCol) {\n <div class=\"osl-rg-col-menu\"\n [style.top.px]=\"columnMenuPos.top\" [style.left.px]=\"columnMenuPos.left\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-col-menu__header\">{{ menuCol.label }}</div>\n @if (menuCol.sortable) {\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:true,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 19V5m-7 7 7-7 7 7\"/></svg>\n Sort A \u2192 Z\n </button>\n <button class=\"osl-rg-col-menu__item\"\n (click)=\"sortStates.clear(); sortStates.set(menuCol.key,{key:menuCol.key,asc:false,index:0}); sortCounter=1; processData(); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14m7-7-7 7-7-7\"/></svg>\n Sort Z \u2192 A\n </button>\n }\n @if (menuCol.groupable) {\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"addGroup(menuCol.key); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"7\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z\"/></svg>\n Group by this\n </button>\n }\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item\" (click)=\"toggleColumnPin(menuCol)\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n {{ menuCol._pinned ? 'Unpin column' : 'Pin to left' }}\n </button>\n <button class=\"osl-rg-col-menu__item\" (click)=\"autoSizeColumn(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3M8 15l-5 3 5 3M16 15l5 3-5 3\"/></svg>\n Auto-size column\n </button>\n <div class=\"osl-rg-col-menu__divider\"></div>\n <button class=\"osl-rg-col-menu__item osl-rg-col-menu__item--danger\" (click)=\"toggleColumnVisibility(menuCol); closeColumnMenu()\">\n <svg class=\"rg-icon rg-icon--sm\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n Hide column\n </button>\n </div>\n}\n\n\n<!-- \u2550\u2550 COLUMN CONFIG SIDE DRAWER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (showColumnConfig) {\n <div class=\"osl-rg-config-backdrop\" (click)=\"showColumnConfig=false\"></div>\n <div class=\"osl-rg-config-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"osl-rg-config-panel__header\">\n <div class=\"osl-rg-config-panel__title\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"16\" height=\"16\">\n <path d=\"M4 6h16M4 12h16M4 18h16\"/>\n <circle cx=\"8\" cy=\"6\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"16\" cy=\"12\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n <circle cx=\"8\" cy=\"18\" r=\"2\" fill=\"white\" stroke=\"currentColor\" stroke-width=\"2\"/>\n </svg>\n Column Settings\n </div>\n <button class=\"osl-rg-config-panel__close\" (click)=\"showColumnConfig=false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"osl-rg-config-panel__actions\">\n <button class=\"osl-rg-config-action\" (click)=\"autoSizeAll()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M21 6H3M21 18H3\"/><path d=\"M8 3 3 6l5 3M16 3l5 3-5 3\"/></svg>\n Auto-size all\n </button>\n <button class=\"osl-rg-config-action\" (click)=\"showAllColumns()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n Show all\n </button>\n </div>\n <div class=\"osl-rg-config-hint\">Drag to reorder \u00B7 Toggle to show/hide \u00B7 Pin to freeze</div>\n <div class=\"osl-rg-config-list\" cdkDropList (cdkDropListDropped)=\"onConfigColumnReorder($event)\">\n @for (col of _cols; track col.key) {\n <div class=\"osl-rg-config-item\" cdkDrag [class.osl-rg-config-item--hidden]=\"col._hidden\">\n <svg cdkDragHandle class=\"rg-drag-handle\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"9\" cy=\"5\" r=\"1.5\"/><circle cx=\"15\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"9\" cy=\"19\" r=\"1.5\"/><circle cx=\"15\" cy=\"19\" r=\"1.5\"/>\n </svg>\n <input type=\"checkbox\" class=\"osl-rg-checkbox\" [checked]=\"!col._hidden\" (change)=\"toggleColumnVisibility(col)\" />\n <span class=\"osl-rg-config-item__label\" [class.osl-rg-config-item__label--hidden]=\"col._hidden\">{{ col.label }}</span>\n <button class=\"osl-rg-config-item__pin-btn\" (click)=\"toggleColumnPin(col)\"\n [class.osl-rg-config-item__pin-btn--active]=\"col._pinned\"\n [title]=\"col._pinned ? 'Unpin' : 'Pin to left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\"><path d=\"M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z\"/></svg>\n </button>\n </div>\n }\n </div>\n </div>\n}\n\n\n<!-- \u2550\u2550 COPY TOAST \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (copiedCell !== null) {\n <div class=\"osl-rg-copy-toast\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"13\" height=\"13\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Copied to clipboard\n </div>\n}\n", styles: ["@keyframes rg-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes rg-fade-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes rg-slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.rg-icon{width:15px;height:15px;flex-shrink:0;display:block}.rg-icon--sm{width:14px;height:14px}.rg-icon--xs{width:12px;height:12px}.rg-icon--excel{width:18px;height:18px}.rg-drag-handle{width:14px;height:14px;flex-shrink:0;color:#d1d5db;cursor:grab;display:block}.rg-drag-handle:active{cursor:grabbing}.osl-rg{position:relative;display:flex;flex-direction:column;width:100%;font-family:inherit;font-size:13px;color:#111827;background:#fff;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px #00000012,0 1px 2px #0000000a;-webkit-user-select:none;user-select:none}.osl-rg-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;background:linear-gradient(135deg,#f8faff,#f3f4f6);border-bottom:1px solid var(--osl-border-color, #e5e7eb);flex-shrink:0;flex-wrap:wrap}.osl-rg-toolbar__left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.osl-rg-toolbar__right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.osl-rg-title{font-size:14px;font-weight:700;color:#111827;letter-spacing:-.01em}.osl-rg-record-count{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#9ca3af;font-weight:500}.osl-rg-record-count svg{color:#d1d5db}.osl-rg-filter-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;font-size:11px;color:#1d4ed8;font-weight:600}.osl-rg-filter-badge__dot{width:6px;height:6px;border-radius:50%;background:#2563eb;animation:rg-pulse 1.5s ease-in-out infinite;flex-shrink:0}.osl-rg-toolbar-btn{display:inline-flex;align-items:center;gap:5px;height:32px;padding:0 11px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;color:#5a6478;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s ease}.osl-rg-toolbar-btn svg{flex-shrink:0}.osl-rg-toolbar-btn:hover{background:#f3f4f6;color:#1f2937;border-color:#c4c9d4;box-shadow:0 1px 3px #0000000f}.osl-rg-toolbar-btn--on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.osl-rg-toolbar-btn--active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600}.osl-rg-toolbar-btn--danger{background:#fff5f5;border-color:#fca5a5;color:#dc2626}.osl-rg-toolbar-btn--danger:hover{background:#fee2e2;border-color:#f87171}.osl-rg-toolbar-btn--excel{background:linear-gradient(135deg,#1a7a3e,#1d6f42);border-color:#166534;color:#fff;font-weight:600;box-shadow:0 1px 3px #1665344d}.osl-rg-toolbar-btn--excel:hover{background:linear-gradient(135deg,#166534,#145a2c);border-color:#14532d;color:#fff;box-shadow:0 2px 6px #16653466}.osl-rg-toolbar-btn--pdf{background:linear-gradient(135deg,#e53935,#c62828);border-color:#b71c1c;color:#fff;font-weight:600;box-shadow:0 1px 3px #b71c1c4d}.osl-rg-toolbar-btn--pdf:hover{background:linear-gradient(135deg,#c62828,#ad1515);border-color:#ad1515;color:#fff;box-shadow:0 2px 6px #b71c1c66}.rg-icon--pdf{width:18px;height:18px}.osl-rg-global-search{position:relative;display:inline-flex;align-items:center}.osl-rg-global-search__icon{position:absolute;left:9px;width:14px;height:14px;color:#9ca3af;pointer-events:none;display:block}.osl-rg-global-search__input{height:32px;padding:0 30px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:7px;background:#fff;font-size:12.5px;font-family:inherit;color:#111827;outline:none;width:210px;transition:border-color .15s,width .2s,box-shadow .15s}.osl-rg-global-search__input::placeholder{color:#b0b7c3}.osl-rg-global-search__input:focus{border-color:var(--osl-primary, #2563eb);width:260px;box-shadow:0 0 0 3px #2563eb1a}.osl-rg-global-search__clear{position:absolute;right:6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:4px;transition:all .12s}.osl-rg-global-search__clear svg{width:12px;height:12px}.osl-rg-global-search__clear:hover{color:#374151;background:#0000000d}.osl-rg-group-panel{display:flex;align-items:center;gap:8px;padding:6px 14px;background:linear-gradient(135deg,#fafbff,#f5f7ff);border-bottom:1px dashed #dde5ff;min-height:40px;flex-shrink:0;flex-wrap:wrap}.osl-rg-group-panel__label{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0}.osl-rg-group-panel__label svg{color:#9ca3af}.osl-rg-group-panel__hint{font-size:11px;color:#b0b7c3;font-style:italic}.osl-rg-group-panel__clear{margin-left:auto;font-size:11px;color:#6b7280;background:none;border:none;cursor:pointer;text-decoration:underline;flex-shrink:0;padding:0;font-family:inherit}.osl-rg-group-panel__clear:hover{color:#dc2626}.osl-rg-group-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex:1;min-height:26px}.osl-rg-group-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 6px 3px 5px;background:linear-gradient(135deg,#dbeafe,#eff6ff);border:1px solid #bfdbfe;border-radius:20px;font-size:12px;font-weight:600;color:#1e40af;cursor:grab;-webkit-user-select:none;user-select:none;box-shadow:0 1px 2px #2563eb1a;transition:box-shadow .1s}.osl-rg-group-chip:active{cursor:grabbing;box-shadow:0 2px 6px #2563eb33}.osl-rg-group-chip .rg-drag-handle{color:#93c5fd}.osl-rg-group-chip__remove{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;background:transparent;cursor:pointer;padding:0;border-radius:50%;transition:all .1s}.osl-rg-group-chip__remove svg{width:10px;height:10px;color:#93c5fd}.osl-rg-group-chip__remove:hover{background:#dbeafe}.osl-rg-group-chip__remove:hover svg{color:#1e40af}.osl-rg-table-area{display:flex;flex-direction:column;flex:1 1 0;overflow:hidden;position:relative;min-height:0}.osl-rg-header-wrap{overflow:hidden;flex-shrink:0;border-bottom:2px solid #dde5ff;background:linear-gradient(180deg,#f0f4ff,#e8efff);z-index:10;position:relative}.osl-rg-header-row{display:flex;align-items:stretch}.osl-rg-header-row--groups{border-bottom:1px solid #c7d7f0}.osl-rg-th--group-span{display:flex;align-items:center;justify-content:center;height:28px;min-height:28px;cursor:default;padding:0;border-right:1px solid #dde5ff}.osl-rg-th--group-span:hover{background:transparent!important}.osl-rg-th--group-span:last-child{border-right:none}.osl-rg-th--group-span--labeled{background:linear-gradient(135deg,#dbeafe,#eff6ff);border-bottom:2px solid #93c5fd}.osl-rg-th-group-label{font-size:11px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 10px;text-align:center;width:100%}.osl-rg-header-cols{display:flex}.osl-rg-th{position:relative;display:flex;flex-direction:column;background:transparent;border-right:1px solid #dde5ff;cursor:pointer;flex-shrink:0;transition:background .12s;overflow:visible}.osl-rg-th:last-child{border-right:none}.osl-rg-th:hover{background:#2563eb0f}.osl-rg-th--pinned{background:linear-gradient(180deg,#e8efff,#dde8ff);border-right:1px solid #bfdbfe;z-index:3}.osl-rg-th--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.14),transparent);pointer-events:none;z-index:4}.osl-rg-th--sorted{background:#6366f114}.osl-rg-th--filtered{background:#f59e0b12}.osl-rg-th-drag-placeholder{background:#e0e7ff;border:2px dashed #6366f1;border-radius:4px;opacity:.6}.osl-rg-th-pin-badge{position:absolute;top:3px;right:24px;color:var(--osl-primary, #2563eb);opacity:.6;display:flex;align-items:center}.osl-rg-th-content{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 8px 0 10px;height:36px;min-height:36px}.osl-rg-th-label{font-size:11.5px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.osl-rg-th-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.osl-rg-sort-icon{display:inline-flex;align-items:center;position:relative;color:#9ca3af;transition:color .12s;width:14px;height:14px}.osl-rg-sort-icon svg{width:13px;height:13px;display:block}.osl-rg-sort-icon--asc,.osl-rg-sort-icon--desc{color:var(--osl-primary, #2563eb)}.osl-rg-sort-idle{opacity:.3}.osl-rg-sort-badge{position:absolute;top:-4px;right:-6px;font-size:8px;font-weight:700;background:var(--osl-primary, #2563eb);color:#fff;border-radius:50%;width:11px;height:11px;display:flex;align-items:center;justify-content:center;line-height:1}.osl-rg-filter-btn,.osl-rg-col-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:#b0b7c3;cursor:pointer;padding:0;border-radius:5px;transition:all .12s}.osl-rg-filter-btn svg,.osl-rg-col-menu-btn svg{width:13px;height:13px;display:block}.osl-rg-filter-btn:hover,.osl-rg-col-menu-btn:hover{background:#00000012;color:#374151}.osl-rg-filter-btn--active{color:var(--osl-primary, #2563eb)!important;background:#2563eb1f!important}.osl-rg-col-search{position:relative;display:flex;align-items:center;padding:3px 6px;border-top:1px solid #e0e7ff;background:#ffffff80}.osl-rg-col-search__icon{position:absolute;left:10px;width:12px;height:12px;display:block;pointer-events:none;color:#c4cad6}.osl-rg-col-search__input{width:100%;height:23px;padding:0 18px 0 22px;border:1px solid #dde5ff;border-radius:5px;font-size:11px;font-family:inherit;background:#fff;color:#111827;outline:none;transition:border-color .12s,box-shadow .12s}.osl-rg-col-search__input::placeholder{color:#c4cad6}.osl-rg-col-search__input:focus{border-color:var(--osl-primary, #2563eb);box-shadow:0 0 0 2px #2563eb1a}.osl-rg-col-search__clear{position:absolute;right:8px;display:flex;align-items:center;width:14px;height:14px;border:none;background:transparent;color:#9ca3af;cursor:pointer;padding:0;border-radius:3px;transition:all .1s}.osl-rg-col-search__clear svg{width:10px;height:10px;display:block}.osl-rg-col-search__clear:hover{color:#374151;background:#0000000f}.osl-rg-resize-handle{position:absolute;right:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5}.osl-rg-resize-handle:after{content:\"\";position:absolute;right:2px;top:15%;bottom:15%;width:2px;border-radius:2px;background:#a5b4fc;opacity:0;transition:opacity .15s}.osl-rg-resize-handle:hover:after{opacity:1}.osl-rg-th:hover .osl-rg-resize-handle:after{opacity:.4}.osl-rg-excel-filter{position:fixed;z-index:9999;width:250px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 32px #00000026,0 2px 8px #00000012;animation:rg-fade-up .14s ease;overflow:hidden}.osl-rg-excel-filter__header{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(135deg,#f0f4ff,#e8efff);border-bottom:1px solid #dde5ff}.osl-rg-excel-filter__title{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:#1e3a6e;text-transform:uppercase;letter-spacing:.06em}.osl-rg-excel-filter__clear-btn{font-size:11px;color:#dc2626;background:none;border:none;cursor:pointer;font-weight:600;padding:2px 6px;border-radius:4px;font-family:inherit;transition:background .1s}.osl-rg-excel-filter__clear-btn:hover{background:#fee2e2}.osl-rg-excel-filter__search{padding:7px 8px;border-bottom:1px solid #f3f4f6;position:relative}.osl-rg-excel-filter__search-input{width:100%;height:28px;padding:0 8px 0 28px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .12s;color:#111827}.osl-rg-excel-filter__search-input::placeholder{color:#b0b7c3}.osl-rg-excel-filter__search-input:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-excel-filter__select-all{padding:0 8px;border-bottom:2px solid #f3f4f6;background:#fafbff}.osl-rg-excel-filter__list{max-height:210px;overflow-y:auto;padding:3px 8px}.osl-rg-excel-filter__list::-webkit-scrollbar{width:5px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-excel-filter__list::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-excel-filter__item{display:flex;align-items:center;gap:8px;padding:5px 4px;font-size:12.5px;color:#374151;cursor:pointer;border-radius:5px;transition:background .1s}.osl-rg-excel-filter__item:hover{background:#f3f4f6}.osl-rg-excel-filter__item--all{font-weight:600;color:#1f2937;padding:6px 4px}.osl-rg-excel-filter__item-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.osl-rg-excel-filter__footer{display:flex;gap:6px;padding:8px 10px;border-top:1px solid #f3f4f6;background:#f9fafb}.osl-rg-excel-filter__apply{flex:1;height:30px;border:none;border-radius:6px;font-size:12px;font-family:inherit;font-weight:600;cursor:pointer;background:var(--osl-primary, #2563eb);color:#fff;transition:background .12s}.osl-rg-excel-filter__apply:hover{background:#1d4ed8}.osl-rg-excel-filter__cancel{height:30px;padding:0 14px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;background:#fff;color:#6b7280;transition:all .12s}.osl-rg-excel-filter__cancel:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-th--checkbox,.osl-rg-td--checkbox{display:flex;align-items:center;justify-content:center;width:36px;min-width:36px;flex-shrink:0;border-right:1px solid #dde5ff}.osl-rg-checkbox{width:14px;height:14px;cursor:pointer;accent-color:var(--osl-primary, #2563eb);flex-shrink:0}.osl-rg-body{overflow-x:auto;overflow-y:auto;flex-shrink:0}.osl-rg-body::-webkit-scrollbar{width:8px;height:8px}.osl-rg-body::-webkit-scrollbar-track{background:#f9fafb}.osl-rg-body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:4px}.osl-rg-body::-webkit-scrollbar-thumb:hover{background:#9ca3af}.osl-rg-body::-webkit-scrollbar-corner{background:#f9fafb}.osl-rg-inner{display:block}.osl-rg-row{display:flex;align-items:stretch;border-bottom:1px solid #f3f4f6;transition:background .08s;cursor:default}.osl-rg-row:hover{background:#f0f5ff!important}.osl-rg-row--striped{background:#fafbff}.osl-rg-row--selected{background:#eff6ff!important;border-bottom-color:#dbeafe}.osl-rg-row--skeleton{pointer-events:none}.osl-rg-td{display:flex;align-items:center;padding:0 12px;border-right:1px solid #f0f2f7;overflow:hidden;background:inherit;flex-shrink:0}.osl-rg-td:last-child{border-right:none}.osl-rg-td--pinned{background:#fff;border-right:1px solid #e0e7ff;z-index:2}.osl-rg-td--pinned:after{content:\"\";position:absolute;right:-5px;top:0;bottom:0;width:5px;background:linear-gradient(to right,rgba(37,99,235,.07),transparent);pointer-events:none}.osl-rg-cell-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;font-size:13px;color:#1f2937;line-height:1.4}.rg-profit-pos .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-profit-neg .osl-rg-cell-text{color:#dc2626;font-weight:600}.rg-status-delivered .osl-rg-cell-text{color:#15803d;font-weight:600}.rg-status-shipped .osl-rg-cell-text{color:#2563eb;font-weight:600}.rg-status-processing .osl-rg-cell-text{color:#d97706;font-weight:600}.rg-status-pending .osl-rg-cell-text{color:#6b7280;font-weight:500}.rg-status-cancelled .osl-rg-cell-text{color:#dc2626;font-weight:500;text-decoration:line-through;opacity:.75}.osl-rg-group-row{display:flex;align-items:center;gap:7px;border-bottom:1px solid #e5e7eb;font-weight:600;cursor:pointer;flex-shrink:0;transition:filter .1s}.osl-rg-group-row--l0{background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1e40af;border-left:4px solid #3b82f6}.osl-rg-group-row--l1{background:linear-gradient(135deg,#f5f3ff,#ede9fe);color:#5b21b6;border-left:4px solid #8b5cf6}.osl-rg-group-row--l2{background:linear-gradient(135deg,#fff7ed,#fef3c7);color:#92400e;border-left:4px solid #f59e0b}.osl-rg-group-row:hover{filter:brightness(.97)}.osl-rg-group-row__key{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;opacity:.65;flex-shrink:0}.osl-rg-group-row__value{font-size:13px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-group-row__count{font-size:11px;font-weight:600;padding:2px 9px;border-radius:12px;background:#00000017;margin-right:12px;flex-shrink:0}.osl-rg-group-chevron{width:16px;height:16px;flex-shrink:0;opacity:.75;display:block}.osl-rg-aggregate-row{display:flex;align-items:center;border-top:2px solid #e5e7eb;background:linear-gradient(135deg,#f0fdf4,#f9fafb);flex-shrink:0;min-height:38px}.osl-rg-agg-cell{display:flex;align-items:center;gap:5px;padding:0 12px;border-right:1px solid #e5e7eb;height:38px;flex-shrink:0;overflow:hidden}.osl-rg-agg-cell--checkbox{width:36px;min-width:36px}.osl-rg-agg-label{font-size:9.5px;font-weight:800;color:#9ca3af;text-transform:uppercase;letter-spacing:.06em;flex-shrink:0;background:#e5e7eb;padding:1px 4px;border-radius:3px}.osl-rg-agg-value{font-size:12px;font-weight:700;color:#15803d;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-rg-skel{height:12px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6,#e5e7eb 75%);background-size:200% 100%;animation:rg-pulse 1.5s ease-in-out infinite}.osl-rg-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 20px;color:#9ca3af;text-align:center}.osl-rg-empty__icon{margin-bottom:18px;opacity:.7}.osl-rg-empty__title{font-size:15px;font-weight:600;color:#374151;margin:0 0 6px}.osl-rg-empty__sub{font-size:13px;color:#9ca3af;margin:0 0 18px;max-width:290px;line-height:1.5}.osl-rg-empty__action{height:34px;padding:0 18px;background:var(--osl-primary, #2563eb);color:#fff;border:none;border-radius:7px;font-size:13px;font-family:inherit;font-weight:600;cursor:pointer;transition:background .15s;box-shadow:0 2px 6px #2563eb4d}.osl-rg-empty__action:hover{background:#1d4ed8}.osl-rg-pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--osl-border-color, #e5e7eb);background:linear-gradient(135deg,#f8faff,#f3f4f6);flex-shrink:0;flex-wrap:wrap}.osl-rg-pagination__info{font-size:12px;color:#6b7280;min-width:160px;white-space:nowrap}.osl-rg-pagination__info strong{color:#1f2937;font-weight:600}.osl-rg-pagination__controls{display:flex;align-items:center;gap:3px}.osl-rg-pagination__size{display:flex;align-items:center;gap:7px;min-width:120px;justify-content:flex-end}.osl-rg-pagination__size-label{font-size:11.5px;color:#9ca3af;white-space:nowrap}.osl-rg-page-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s;white-space:nowrap}.osl-rg-page-btn svg{display:block}.osl-rg-page-btn:hover:not(:disabled){background:#f3f4f6;border-color:#9ca3af;box-shadow:0 1px 2px #0000000f}.osl-rg-page-btn:disabled{opacity:.35;cursor:not-allowed}.osl-rg-page-btn--num{min-width:30px;padding:0;font-size:12.5px}.osl-rg-page-btn--active{background:var(--osl-primary, #2563eb);border-color:var(--osl-primary, #2563eb);color:#fff;font-weight:700;box-shadow:0 2px 5px #2563eb59}.osl-rg-page-btn--active:hover:not(:disabled){background:#1d4ed8;border-color:#1d4ed8}.osl-rg-page-ellipsis{display:inline-flex;align-items:center;justify-content:center;width:26px;color:#9ca3af;font-size:13px;pointer-events:none}.osl-rg-page-size{height:30px;padding:0 8px;border:1px solid var(--osl-border-color, #e5e7eb);border-radius:6px;background:#fff;font-size:12px;font-family:inherit;color:#374151;cursor:pointer;outline:none;transition:border-color .15s}.osl-rg-page-size:focus{border-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu{position:fixed;z-index:9999;min-width:210px;background:#fff;border:1px solid #e5e7eb;border-radius:11px;box-shadow:0 12px 36px #00000029,0 3px 10px #00000014;overflow:hidden;animation:rg-fade-up .13s cubic-bezier(.16,1,.3,1)}.osl-rg-col-menu__header{padding:9px 14px 7px;font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #f3f4f6;background:#fafbff}.osl-rg-col-menu__divider{height:1px;background:#f3f4f6;margin:2px 0}.osl-rg-col-menu__item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;background:transparent;border:none;border-left:3px solid transparent;border-bottom:none;text-align:left;font-size:13px;font-weight:500;font-family:inherit;color:#374151;cursor:pointer;transition:all .1s}.osl-rg-col-menu__item svg{color:#9ca3af;display:block}.osl-rg-col-menu__item:hover{background:#f0f4ff;color:var(--osl-primary, #2563eb);border-left-color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item:hover svg{color:var(--osl-primary, #2563eb)}.osl-rg-col-menu__item--danger:hover{background:#fff5f5;color:#dc2626;border-left-color:#dc2626}.osl-rg-col-menu__item--danger:hover svg{color:#dc2626}.osl-rg-config-backdrop{position:fixed;inset:0;z-index:9000;background:#0003;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.osl-rg-config-panel{position:fixed;top:0;right:0;bottom:0;z-index:9001;width:300px;background:#fff;border-left:1px solid #e5e7eb;box-shadow:-6px 0 24px #00000021;display:flex;flex-direction:column;animation:rg-slide-in .2s cubic-bezier(.16,1,.3,1)}.osl-rg-config-panel__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#f0f4ff,#e8efff);flex-shrink:0}.osl-rg-config-panel__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;color:#1e3a6e}.osl-rg-config-panel__title svg{color:#3b82f6}.osl-rg-config-panel__close{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#9ca3af;cursor:pointer;border-radius:7px;padding:0;transition:all .12s}.osl-rg-config-panel__close svg{width:16px;height:16px;display:block}.osl-rg-config-panel__close:hover{background:#00000012;color:#374151}.osl-rg-config-panel__actions{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#fafbff}.osl-rg-config-hint{padding:6px 16px;font-size:10.5px;color:#b0b7c3;font-style:italic;border-bottom:1px solid #f3f4f6;flex-shrink:0}.osl-rg-config-action{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#374151;font-size:12px;font-family:inherit;font-weight:500;cursor:pointer;transition:all .12s}.osl-rg-config-action svg{display:block}.osl-rg-config-action:hover{background:#f3f4f6;border-color:#9ca3af}.osl-rg-config-list{flex:1;overflow-y:auto;padding:6px 0}.osl-rg-config-list::-webkit-scrollbar{width:5px}.osl-rg-config-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}.osl-rg-config-list::-webkit-scrollbar-thumb:hover{background:#d1d5db}.osl-rg-config-item{display:flex;align-items:center;gap:9px;padding:7px 14px;transition:background .1s;cursor:grab}.osl-rg-config-item:hover{background:#f9fafb}.osl-rg-config-item:active{cursor:grabbing}.osl-rg-config-item--hidden{opacity:.5}.osl-rg-config-item__label{flex:1;font-size:13px;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.osl-rg-config-item__label--hidden{color:#9ca3af;font-style:italic}.osl-rg-config-item__pin-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #e5e7eb;background:#fff;color:#d1d5db;cursor:pointer;padding:0;border-radius:5px;transition:all .12s;flex-shrink:0}.osl-rg-config-item__pin-btn svg{display:block}.osl-rg-config-item__pin-btn:hover,.osl-rg-config-item__pin-btn--active{background:#eff6ff;border-color:#93c5fd;color:var(--osl-primary, #2563eb)}.osl-rg-copy-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:99999;display:flex;align-items:center;gap:7px;background:#1f2937;color:#fff;font-size:12.5px;font-weight:500;padding:8px 18px;border-radius:22px;animation:rg-fade-up .2s ease;box-shadow:0 6px 16px #0000004d;pointer-events:none;white-space:nowrap;font-family:inherit}.osl-rg-copy-toast svg{flex-shrink:0;color:#4ade80}\n"] }]
|
|
3615
3898
|
}], propDecorators: { columns: [{
|
|
3616
3899
|
type: Input
|
|
3617
3900
|
}], datasource: [{
|
|
@@ -3640,6 +3923,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
3640
3923
|
type: Input
|
|
3641
3924
|
}], title: [{
|
|
3642
3925
|
type: Input
|
|
3926
|
+
}], pdfExportFromGrid: [{
|
|
3927
|
+
type: Input
|
|
3928
|
+
}], pdfConfig: [{
|
|
3929
|
+
type: Input
|
|
3643
3930
|
}], pageChange: [{
|
|
3644
3931
|
type: Output
|
|
3645
3932
|
}], pageSizeChange: [{
|
|
@@ -3681,13 +3968,18 @@ class OslReportForm {
|
|
|
3681
3968
|
skeletonLoading = false;
|
|
3682
3969
|
skeletonTheme = 'light';
|
|
3683
3970
|
pageSize = 50;
|
|
3971
|
+
pdfConfig = {};
|
|
3972
|
+
_grid;
|
|
3973
|
+
get _mergedPdfConfig() {
|
|
3974
|
+
return { ...this.pdfConfig, reportName: this.pdfConfig.reportName ?? this.title };
|
|
3975
|
+
}
|
|
3976
|
+
constructor(cd) {
|
|
3977
|
+
this.cd = cd;
|
|
3978
|
+
}
|
|
3684
3979
|
selectedType = 'grid';
|
|
3685
3980
|
generating = false;
|
|
3686
3981
|
resultDatasource = [];
|
|
3687
3982
|
showResultView = false;
|
|
3688
|
-
constructor(cd) {
|
|
3689
|
-
this.cd = cd;
|
|
3690
|
-
}
|
|
3691
3983
|
ngOnInit() {
|
|
3692
3984
|
if (this.generateOptions.length > 0 && !this.generateOptions.includes(this.selectedType)) {
|
|
3693
3985
|
this.selectedType = this.generateOptions[0];
|
|
@@ -3706,20 +3998,28 @@ class OslReportForm {
|
|
|
3706
3998
|
this.resultDatasource = result || [];
|
|
3707
3999
|
if (this.selectedType === 'grid') {
|
|
3708
4000
|
this.showResultView = true;
|
|
4001
|
+
this.cd.markForCheck();
|
|
4002
|
+
// Let the view render the grid before triggering export
|
|
4003
|
+
// await new Promise(r => setTimeout(r, 50));
|
|
4004
|
+
// if (this.selectedType === 'pdf') {
|
|
4005
|
+
// await this._grid?.exportPdf();
|
|
4006
|
+
// } else if (this.selectedType === 'excel') {
|
|
4007
|
+
// this._grid?.exportCsv();
|
|
4008
|
+
// }
|
|
3709
4009
|
}
|
|
3710
4010
|
}
|
|
3711
4011
|
}
|
|
3712
4012
|
finally {
|
|
3713
4013
|
this.generating = false;
|
|
4014
|
+
this.cd.markForCheck();
|
|
3714
4015
|
}
|
|
3715
|
-
this.cd.markForCheck();
|
|
3716
4016
|
}
|
|
3717
4017
|
backToForm() {
|
|
3718
4018
|
this.showResultView = false;
|
|
3719
4019
|
this.resultDatasource = [];
|
|
3720
4020
|
}
|
|
3721
|
-
reExportExcel() { this.
|
|
3722
|
-
reExportPdf() { this.
|
|
4021
|
+
reExportExcel() { this._grid?.exportCsv(); }
|
|
4022
|
+
reExportPdf() { this._grid?.exportPdf(); }
|
|
3723
4023
|
typeLabel(type) {
|
|
3724
4024
|
return type === 'pdf' ? 'PDF' : type === 'excel' ? 'Excel' : 'Grid';
|
|
3725
4025
|
}
|
|
@@ -3730,54 +4030,12 @@ class OslReportForm {
|
|
|
3730
4030
|
this._model = val;
|
|
3731
4031
|
this.modelChange.emit(val);
|
|
3732
4032
|
}
|
|
3733
|
-
_exportCsv(data) {
|
|
3734
|
-
if (!data.length || !this.reportColumns.length)
|
|
3735
|
-
return;
|
|
3736
|
-
const cols = this.reportColumns;
|
|
3737
|
-
const header = cols.map(c => `"${c.label}"`).join(',');
|
|
3738
|
-
const rows = data.map(row => cols.map(col => `"${String(row[col.key] ?? '').replace(/"/g, '""')}"`).join(','));
|
|
3739
|
-
const blob = new Blob([[header, ...rows].join('\n')], { type: 'text/csv;charset=utf-8;' });
|
|
3740
|
-
const a = Object.assign(document.createElement('a'), {
|
|
3741
|
-
href: URL.createObjectURL(blob),
|
|
3742
|
-
download: `${this.title || 'report'}.csv`,
|
|
3743
|
-
});
|
|
3744
|
-
a.click();
|
|
3745
|
-
URL.revokeObjectURL(a.href);
|
|
3746
|
-
}
|
|
3747
|
-
_exportPdf(data) {
|
|
3748
|
-
if (!data.length || !this.reportColumns.length)
|
|
3749
|
-
return;
|
|
3750
|
-
const cols = this.reportColumns;
|
|
3751
|
-
const headerRow = cols.map(c => `<th>${c.label}</th>`).join('');
|
|
3752
|
-
const bodyRows = data.map(row => `<tr>${cols.map(col => `<td>${String(row[col.key] ?? '')}</td>`).join('')}</tr>`).join('');
|
|
3753
|
-
const w = window.open('', '_blank');
|
|
3754
|
-
if (!w)
|
|
3755
|
-
return;
|
|
3756
|
-
w.document.write(`<!DOCTYPE html><html><head><title>${this.title}</title>
|
|
3757
|
-
<style>
|
|
3758
|
-
body{font-family:Arial,sans-serif;padding:24px;font-size:13px;color:#111827}
|
|
3759
|
-
h2{color:#1e3a8a;margin:0 0 4px;font-size:20px;font-weight:700}
|
|
3760
|
-
.subtitle{color:#6b7280;font-size:12px;margin-bottom:20px}
|
|
3761
|
-
table{border-collapse:collapse;width:100%}
|
|
3762
|
-
th{background:#1e3a8a;color:#fff;padding:9px 12px;text-align:left;font-weight:600;font-size:12px}
|
|
3763
|
-
td{padding:8px 12px;border-bottom:1px solid #e5e7eb;font-size:12px}
|
|
3764
|
-
tr:nth-child(even) td{background:#f8fafc}
|
|
3765
|
-
@media print{@page{margin:.8cm}}
|
|
3766
|
-
</style>
|
|
3767
|
-
</head><body>
|
|
3768
|
-
<h2>${this.title}</h2>
|
|
3769
|
-
<p class="subtitle">Generated on ${new Date().toLocaleString()}</p>
|
|
3770
|
-
<table><thead><tr>${headerRow}</tr></thead><tbody>${bodyRows}</tbody></table>
|
|
3771
|
-
<script>window.onload=function(){window.print();}<\/script>
|
|
3772
|
-
</body></html>`);
|
|
3773
|
-
w.document.close();
|
|
3774
|
-
}
|
|
3775
4033
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslReportForm, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3776
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslReportForm, isStandalone: false, selector: "osl-report-form", inputs: { title: "title", formElements: "formElements", model: "model", generateOptions: "generateOptions", reportColumns: "reportColumns", onGenerate: "onGenerate", skeletonLoading: "skeletonLoading", skeletonTheme: "skeletonTheme", pageSize: "pageSize" }, outputs: { modelChange: "modelChange" }, ngImport: i0, template: "<!-- \u2500\u2500 Form view (hidden when results are shown) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (!showResultView) {\n <div class=\"orf-card\">\n\n <!-- Header -->\n <div class=\"orf-header\">\n <div class=\"orf-header__left\">\n <div class=\"orf-header__icon-wrap\">\n <mat-icon class=\"orf-header__icon\">assessment</mat-icon>\n </div>\n <div class=\"orf-header__text\">\n <h4 class=\"orf-header__title\">{{ title }}</h4>\n <p class=\"orf-header__sub\">Configure parameters and generate report</p>\n </div>\n </div>\n\n <div class=\"orf-header__right\">\n @for (type of generateOptions; track type) {\n <button\n class=\"orf-type-btn\"\n [class.active]=\"selectedType === type\"\n [class.orf-type-btn--pdf]=\"type === 'pdf'\"\n [class.orf-type-btn--excel]=\"type === 'excel'\"\n [class.orf-type-btn--grid]=\"type === 'grid'\"\n (click)=\"selectType(type)\"\n type=\"button\"\n >\n <mat-icon>{{ typeIcon(type) }}</mat-icon>\n <span>{{ typeLabel(type) }}</span>\n </button>\n }\n </div>\n </div>\n\n <!-- Divider -->\n <div class=\"orf-divider\"></div>\n\n <!-- Form Body -->\n <div class=\"orf-body\">\n <osl-dynamic-form\n [elements]=\"formElements\"\n [model]=\"model\"\n (modelChange)=\"onModelChange($event)\"\n [skeletonLoading]=\"skeletonLoading\"\n [skeletonTheme]=\"skeletonTheme\"\n ></osl-dynamic-form>\n </div>\n\n <!-- Footer -->\n <div class=\"orf-footer\">\n <button\n class=\"orf-generate-btn\"\n (click)=\"generate()\"\n [disabled]=\"generating\"\n type=\"button\"\n >\n @if (generating) {\n <span class=\"orf-generate-btn__spinner\"></span>\n <span>Generating...</span>\n } @else {\n <mat-icon>{{ typeIcon(selectedType) }}</mat-icon>\n <span>Generate {{ typeLabel(selectedType) }}</span>\n <mat-icon class=\"orf-generate-btn__arrow\">arrow_forward</mat-icon>\n }\n </button>\n </div>\n\n </div>\n}\n\n<!-- \u2500\u2500 Full-screen results view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (showResultView) {\n <!-- <div class=\"orf-results-view\"> -->\n\n <!-- Results header bar -->\n <div class=\"orf-results-header\">\n\n <div class=\"orf-results-header__left\">\n <button class=\"orf-back-btn\" (click)=\"backToForm()\" type=\"button\">\n <mat-icon>arrow_back</mat-icon>\n <span>Back</span>\n </button>\n <div class=\"orf-results-header__sep\"></div>\n <mat-icon class=\"orf-results-header__report-icon\">assessment</mat-icon>\n <span class=\"orf-results-header__title\">{{ title }}</span>\n <span class=\"orf-results-badge\">\n <mat-icon>table_rows</mat-icon>\n {{ resultDatasource.length | number }} records\n </span>\n </div>\n\n <div class=\"orf-results-header__right\">\n @if (generateOptions.includes('excel')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--excel\" (click)=\"reExportExcel()\" type=\"button\">\n <mat-icon>table_chart</mat-icon>\n <span>Excel</span>\n </button>\n }\n <!-- @if (generateOptions.includes('pdf')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--pdf\" (click)=\"reExportPdf()\" type=\"button\">\n <mat-icon>picture_as_pdf</mat-icon>\n <span>PDF</span>\n </button>\n } -->\n </div>\n\n </div>\n\n <!-- Report grid -->\n <div class=\"orf-results-body\">\n <osl-report-grid\n [columns]=\"reportColumns\"\n [datasource]=\"resultDatasource\"\n [loading]=\"generating\"\n [title]=\"title\"\n tableHeight=\"calc(100vh - 365px)\"\n [pageSize]=\"pageSize\"\n [exportable]=\"true\"\n [showAggregates]=\"true\"\n [striped]=\"true\"\n ></osl-report-grid>\n </div>\n\n <!-- </div> -->\n}\n", styles: ["@keyframes orf-fade-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes orf-slide-in{0%{opacity:0;transform:translate(-10px)}to{opacity:1;transform:translate(0)}}@keyframes orf-spin{to{transform:rotate(360deg)}}.orf-card{background:#fff;border-radius:12px;border:1px solid #e5e7eb;box-shadow:0 4px 20px #00000014,0 1px 4px #0000000a;overflow:hidden}.orf-header{background:linear-gradient(135deg,#1e3a8a,#2563eb 55%,#60a5fa);padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px;position:relative;overflow:hidden;flex-wrap:wrap}.orf-header:before{content:\"\";position:absolute;width:260px;height:260px;border-radius:50%;background:#ffffff0d;top:-110px;right:10px;pointer-events:none}.orf-header:after{content:\"\";position:absolute;width:160px;height:160px;border-radius:50%;background:#ffffff12;bottom:-70px;right:200px;pointer-events:none}.orf-header__left{display:flex;align-items:center;gap:14px;position:relative;z-index:1}.orf-header__icon-wrap{width:50px;height:50px;border-radius:14px;background:#ffffff26;border:1px solid rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;flex-shrink:0;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.orf-header__icon{color:#fff;font-size:26px;width:26px;height:26px}.orf-header__text{display:flex;flex-direction:column;gap:3px}.orf-header__title{margin:0;font-size:19px;font-weight:700;color:#fff;letter-spacing:-.015em;line-height:1.2}.orf-header__sub{margin:0;font-size:12px;color:#ffffffad}.orf-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-wrap:wrap}.orf-type-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid rgba(255,255,255,.28);background:#ffffff1f;color:#ffffffd1;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit}.orf-type-btn mat-icon{font-size:16px;width:16px;height:16px}.orf-type-btn:hover{background:#ffffff38;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-type-btn.active{background:#fffffff2;border-color:transparent;color:#1e3a8a;box-shadow:0 4px 14px #00000038;transform:translateY(-1px);font-weight:600}.orf-type-btn.active mat-icon{color:#1e3a8a}.orf-type-btn--pdf.active{color:#dc2626}.orf-type-btn--pdf.active mat-icon{color:#dc2626}.orf-type-btn--excel.active{color:#16a34a}.orf-type-btn--excel.active mat-icon{color:#16a34a}.orf-type-btn--grid.active{color:#2563eb}.orf-type-btn--grid.active mat-icon{color:#2563eb}.orf-divider{height:1px;background:linear-gradient(90deg,#e5e7eb,#c7d2fe,#e5e7eb)}.orf-body{padding:20px 20px 4px}.orf-footer{padding:12px 20px 20px;display:flex;justify-content:flex-end}.orf-generate-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 22px;border-radius:8px;border:none;background:linear-gradient(135deg,#1e3a8a,#2563eb);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 16px #2563eb52;letter-spacing:.01em;font-family:inherit}.orf-generate-btn mat-icon{font-size:18px;width:18px;height:18px}.orf-generate-btn__arrow{transition:transform .2s ease;font-size:16px!important;width:16px!important;height:16px!important;margin-left:2px}.orf-generate-btn:hover:not([disabled]){background:linear-gradient(135deg,#1e40af,#1d4ed8);box-shadow:0 6px 22px #2563eb7a;transform:translateY(-1px)}.orf-generate-btn:hover:not([disabled]) .orf-generate-btn__arrow{transform:translate(4px)}.orf-generate-btn:active:not([disabled]){transform:translateY(0);box-shadow:0 2px 8px #2563eb38}.orf-generate-btn[disabled]{opacity:.72;cursor:not-allowed;box-shadow:none}.orf-generate-btn__spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;display:inline-block;animation:orf-spin .7s linear infinite;flex-shrink:0}.orf-results-view{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:#f1f5f9;animation:orf-fade-in .25s ease}.orf-results-header{flex-shrink:0;height:56px;background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#3b82f6);display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;position:relative;overflow:hidden;box-shadow:0 2px 12px #2563eb66}.orf-results-header:after{content:\"\";position:absolute;width:180px;height:180px;border-radius:50%;background:#ffffff0f;top:-80px;right:60px;pointer-events:none}.orf-results-header__left{display:flex;align-items:center;gap:10px;position:relative;z-index:1;min-width:0;flex:1}.orf-results-header__sep{width:1px;height:22px;background:#ffffff40;flex-shrink:0}.orf-results-header__report-icon{color:#ffffffd9;font-size:20px;width:20px;height:20px;flex-shrink:0}.orf-results-header__title{font-size:15px;font-weight:700;color:#fff;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.orf-results-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;background:#ffffff26;border:1px solid rgba(255,255,255,.2);color:#ffffffe6;font-size:11px;font-weight:600;white-space:nowrap;flex-shrink:0;animation:orf-slide-in .3s ease}.orf-results-badge mat-icon{font-size:13px;width:13px;height:13px}.orf-back-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:7px;border:1px solid rgba(255,255,255,.3);background:#ffffff24;color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;flex-shrink:0}.orf-back-btn mat-icon{font-size:17px;width:17px;height:17px;transition:transform .18s ease}.orf-back-btn:hover{background:#ffffff3d;border-color:#ffffff80}.orf-back-btn:hover mat-icon{transform:translate(-3px)}.orf-back-btn:active{transform:scale(.97)}.orf-results-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-shrink:0}.orf-reexport-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 13px;border-radius:7px;border:1px solid rgba(255,255,255,.28);background:#ffffff1f;color:#ffffffd9;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;white-space:nowrap}.orf-reexport-btn mat-icon{font-size:15px;width:15px;height:15px}.orf-reexport-btn:hover{background:#ffffff38;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-reexport-btn--excel:hover{background:#16a34a59;border-color:#16a34a80}.orf-reexport-btn--pdf:hover{background:#dc262659;border-color:#dc262680}.orf-results-body{flex:1;overflow:hidden;padding:12px 12px 0;display:flex;flex-direction:column}.orf-results-body osl-report-grid{flex:1;display:block}\n"], dependencies: [{ kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DynamicForm, selector: "osl-dynamic-form", inputs: ["elements", "model", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange"] }, { kind: "component", type: OslReportGrid, selector: "osl-report-grid", inputs: ["columns", "datasource", "loading", "totalRecords", "autoMode", "isPaginated", "pageSize", "tableHeight", "striped", "exportable", "rowHeight", "rowSelection", "showAggregates", "title"], outputs: ["pageChange", "pageSizeChange", "sortChange", "rowClick", "selectionChange"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }] });
|
|
4034
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslReportForm, isStandalone: false, selector: "osl-report-form", inputs: { title: "title", formElements: "formElements", model: "model", generateOptions: "generateOptions", reportColumns: "reportColumns", onGenerate: "onGenerate", skeletonLoading: "skeletonLoading", skeletonTheme: "skeletonTheme", pageSize: "pageSize", pdfConfig: "pdfConfig" }, outputs: { modelChange: "modelChange" }, viewQueries: [{ propertyName: "_grid", first: true, predicate: OslReportGrid, descendants: true }], ngImport: i0, template: "<!-- \u2500\u2500 Form view (hidden when results are shown) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (!showResultView) {\n <div class=\"orf-card\">\n\n <!-- Header -->\n <div class=\"orf-header\">\n <div class=\"orf-header__left\">\n <div class=\"orf-header__icon-wrap\">\n <svg class=\"orf-header__icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"3\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M8 16V11\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n <path d=\"M12 16V8\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n <path d=\"M16 16V13\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n </svg>\n </div>\n <div class=\"orf-header__text\">\n <h4 class=\"orf-header__title\">{{ title }}</h4>\n <p class=\"orf-header__sub\">Configure parameters and generate report</p>\n </div>\n </div>\n\n <div class=\"orf-header__right\">\n @for (type of generateOptions; track type) {\n <button\n class=\"orf-type-btn\"\n [class.active]=\"selectedType === type\"\n [class.orf-type-btn--pdf]=\"type === 'pdf'\"\n [class.orf-type-btn--excel]=\"type === 'excel'\"\n [class.orf-type-btn--grid]=\"type === 'grid'\"\n (click)=\"selectType(type)\"\n type=\"button\"\n >\n @if (type === 'pdf') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\n stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M14 2v6h6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M9 13h1.5a1.5 1.5 0 0 1 0 3H9v-4\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/>\n <path d=\"M14 13v4\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/>\n <path d=\"M14 13h1.5a1.5 1.5 0 0 1 0 3H14\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/>\n </svg>\n }\n @if (type === 'excel') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M2 8h20\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M8 8v13M14 8v13\" stroke=\"currentColor\" stroke-width=\"1.2\"/>\n <path d=\"M2 13h20M2 18h20\" stroke=\"currentColor\" stroke-width=\"1.2\"/>\n </svg>\n }\n @if (type === 'grid') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n </svg>\n }\n <span>{{ typeLabel(type) }}</span>\n </button>\n }\n </div>\n </div>\n\n <!-- Section divider with label -->\n <div class=\"orf-section-divider\">\n <div class=\"orf-section-divider__line\"></div>\n <span class=\"orf-section-divider__label\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M2 4h12M2 8h9M2 12h6\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/>\n </svg>\n Report Parameters\n </span>\n <div class=\"orf-section-divider__line\"></div>\n </div>\n\n <!-- Form Body -->\n <div class=\"orf-body\">\n <osl-dynamic-form\n [elements]=\"formElements\"\n [model]=\"model\"\n (modelChange)=\"onModelChange($event)\"\n [skeletonLoading]=\"skeletonLoading\"\n [skeletonTheme]=\"skeletonTheme\"\n ></osl-dynamic-form>\n </div>\n\n <!-- Footer -->\n <div class=\"orf-footer\">\n <button\n class=\"orf-generate-btn\"\n (click)=\"generate()\"\n [disabled]=\"generating\"\n type=\"button\"\n >\n @if (generating) {\n <span class=\"orf-generate-btn__spinner\"></span>\n <span>Generating...</span>\n } @else {\n @if (selectedType === 'pdf') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\n stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linejoin=\"round\"/>\n <path d=\"M14 2v6h6\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linejoin=\"round\"/>\n </svg>\n }\n @if (selectedType === 'excel') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <path d=\"M2 8h20M8 8v13M14 8v13M2 13h20M2 18h20\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n </svg>\n }\n @if (selectedType === 'grid') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n </svg>\n }\n <span>Generate {{ typeLabel(selectedType) }}</span>\n <svg class=\"orf-generate-btn__arrow\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M4 10h12M11 5l5 5-5 5\" stroke=\"currentColor\" stroke-width=\"1.8\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n }\n </button>\n </div>\n\n </div>\n}\n\n<!-- \u2500\u2500 Full-screen results view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (showResultView) {\n\n <!-- Results header bar -->\n <div class=\"orf-results-header\">\n\n <div class=\"orf-results-header__left\">\n <button class=\"orf-back-btn\" (click)=\"backToForm()\" type=\"button\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M16 10H4M9 5l-5 5 5 5\" stroke=\"currentColor\" stroke-width=\"1.8\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n <span>Back</span>\n </button>\n <div class=\"orf-results-header__sep\"></div>\n <svg class=\"orf-results-header__report-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"3\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M8 16V11M12 16V8M16 16V13\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n </svg>\n <span class=\"orf-results-header__title\">{{ title }}</span>\n <span class=\"orf-results-badge\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 16 16\" fill=\"none\">\n <rect x=\"2\" y=\"2\" width=\"12\" height=\"3\" rx=\"1\" fill=\"currentColor\"/>\n <rect x=\"2\" y=\"7\" width=\"12\" height=\"3\" rx=\"1\" fill=\"currentColor\" opacity=\".65\"/>\n <rect x=\"2\" y=\"12\" width=\"8\" height=\"2\" rx=\"1\" fill=\"currentColor\" opacity=\".35\"/>\n </svg>\n {{ resultDatasource.length | number }} records\n </span>\n </div>\n\n <!-- <div class=\"orf-results-header__right\">\n @if (generateOptions.includes('excel')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--excel\" (click)=\"reExportExcel()\" type=\"button\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M2 8h20M8 8v13M14 8v13M2 13h20M2 18h20\" stroke=\"currentColor\" stroke-width=\"1.3\"/>\n </svg>\n <span>Excel</span>\n </button>\n }\n @if (generateOptions.includes('pdf')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--pdf\" (click)=\"reExportPdf()\" type=\"button\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\n stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M14 2v6h6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span>PDF</span>\n </button>\n }\n </div> -->\n\n </div>\n\n <!-- Report grid -->\n <div class=\"orf-results-body\">\n <osl-report-grid\n [columns]=\"reportColumns\"\n [datasource]=\"resultDatasource\"\n [loading]=\"generating\"\n [title]=\"title\"\n tableHeight=\"calc(100vh - 365px)\"\n [pageSize]=\"pageSize\"\n [exportable]=\"true\"\n [pdfExportFromGrid]=\"true\"\n [pdfConfig]=\"_mergedPdfConfig\"\n [showAggregates]=\"true\"\n [striped]=\"true\"\n ></osl-report-grid>\n </div>\n\n}\n", styles: ["@keyframes orf-fade-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes orf-slide-in{0%{opacity:0;transform:translate(-10px)}to{opacity:1;transform:translate(0)}}@keyframes orf-spin{to{transform:rotate(360deg)}}@keyframes orf-shimmer{0%{transform:translate(-100%) skew(-12deg)}to{transform:translate(220%) skew(-12deg)}}@keyframes orf-badge-pop{0%{transform:scale(.7);opacity:0}70%{transform:scale(1.08)}to{transform:scale(1);opacity:1}}.orf-btn-icon{display:block;flex-shrink:0;width:16px;height:16px}.orf-card{background:#fff;border-radius:14px;border:1px solid #e5e7eb;box-shadow:0 4px 24px #00000012,0 1px 4px #0000000a;overflow:hidden;animation:orf-fade-in .28s ease}.orf-header{background:linear-gradient(135deg,#1e3a8a,#2563eb 55%,#60a5fa);padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px;position:relative;overflow:hidden;flex-wrap:wrap}.orf-header:before{content:\"\";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,.12) 1px,transparent 1px);background-size:20px 20px;pointer-events:none}.orf-header:after{content:\"\";position:absolute;width:260px;height:260px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.1) 0%,transparent 70%);top:-120px;right:-20px;pointer-events:none}.orf-header__left{display:flex;align-items:center;gap:14px;position:relative;z-index:1}.orf-header__icon-wrap{width:52px;height:52px;border-radius:14px;background:#ffffff24;border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;justify-content:center;flex-shrink:0;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 2px 8px #00000026,inset 0 1px #fff3}.orf-header__icon{width:26px;height:26px;color:#fff}.orf-header__text{display:flex;flex-direction:column;gap:3px}.orf-header__title{margin:0;font-size:19px;font-weight:700;color:#fff;letter-spacing:-.02em;line-height:1.2;text-shadow:0 1px 3px rgba(0,0,0,.15)}.orf-header__sub{margin:0;font-size:12px;color:#ffffffa6;letter-spacing:.01em}.orf-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-wrap:wrap}.orf-type-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;border-radius:9px;border:1px solid rgba(255,255,255,.28);background:#ffffff1c;color:#ffffffd1;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit}.orf-type-btn .orf-btn-icon{width:15px;height:15px;transition:transform .2s ease}.orf-type-btn:hover{background:#ffffff36;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-type-btn:hover .orf-btn-icon{transform:scale(1.1)}.orf-type-btn.active{background:#fffffff5;border-color:transparent;color:#1e3a8a;box-shadow:0 4px 16px #00000038,0 1px 3px #0000001a;transform:translateY(-1px);font-weight:600}.orf-type-btn.active .orf-btn-icon{color:#1e3a8a}.orf-type-btn--pdf.active,.orf-type-btn--pdf.active .orf-btn-icon{color:#dc2626}.orf-type-btn--excel.active,.orf-type-btn--excel.active .orf-btn-icon{color:#16a34a}.orf-type-btn--grid.active,.orf-type-btn--grid.active .orf-btn-icon{color:#2563eb}.orf-section-divider{display:flex;align-items:center;gap:12px;padding:0 24px;margin:18px 0 0}.orf-section-divider__line{flex:1;height:1px;background:linear-gradient(90deg,transparent,#e2e8f0,#c7d2fe,#e2e8f0,transparent)}.orf-section-divider__label{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;background:#f0f4ff;border:1px solid #c7d2fe;color:#4f46e5;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;flex-shrink:0}.orf-section-divider__label svg{width:12px;height:12px;color:#6366f1}.orf-body{padding:16px 20px 4px}.orf-footer{padding:12px 20px 20px;display:flex;justify-content:flex-end;align-items:center;gap:10px}.orf-generate-btn{display:inline-flex;align-items:center;gap:8px;padding:11px 24px;border-radius:10px;border:none;background:linear-gradient(135deg,#1e3a8a,#2563eb);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 18px #2563eb59;letter-spacing:.01em;font-family:inherit;position:relative;overflow:hidden}.orf-generate-btn:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.18) 50%,transparent 100%);transform:translate(-100%) skew(-12deg);pointer-events:none}.orf-generate-btn .orf-btn-icon{width:18px;height:18px}.orf-generate-btn__arrow{width:18px;height:18px;transition:transform .22s ease;flex-shrink:0}.orf-generate-btn:hover:not([disabled]){background:linear-gradient(135deg,#1e40af,#1d4ed8);box-shadow:0 6px 24px #2563eb80;transform:translateY(-1px)}.orf-generate-btn:hover:not([disabled]):after{animation:orf-shimmer .55s ease forwards}.orf-generate-btn:hover:not([disabled]) .orf-generate-btn__arrow{transform:translate(5px)}.orf-generate-btn:active:not([disabled]){transform:translateY(0);box-shadow:0 2px 8px #2563eb38}.orf-generate-btn[disabled]{opacity:.72;cursor:not-allowed;box-shadow:none}.orf-generate-btn__spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;display:inline-block;animation:orf-spin .7s linear infinite;flex-shrink:0}.orf-results-view{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:#f1f5f9;animation:orf-fade-in .25s ease}.orf-results-header{flex-shrink:0;height:56px;background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#3b82f6);display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;position:relative;overflow:hidden;box-shadow:0 2px 14px #2563eb73}.orf-results-header:before{content:\"\";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,.09) 1px,transparent 1px);background-size:16px 16px;pointer-events:none}.orf-results-header:after{content:\"\";position:absolute;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.08) 0%,transparent 70%);top:-90px;right:40px;pointer-events:none}.orf-results-header__left{display:flex;align-items:center;gap:10px;position:relative;z-index:1;min-width:0;flex:1}.orf-results-header__sep{width:1px;height:22px;background:#ffffff40;flex-shrink:0}.orf-results-header__report-icon{width:20px;height:20px;color:#ffffffd9;flex-shrink:0}.orf-results-header__title{font-size:15px;font-weight:700;color:#fff;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-shadow:0 1px 2px rgba(0,0,0,.12)}.orf-results-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:20px;background:#ffffff26;border:1px solid rgba(255,255,255,.22);color:#ffffffeb;font-size:11px;font-weight:600;white-space:nowrap;flex-shrink:0;animation:orf-badge-pop .35s cubic-bezier(.34,1.56,.64,1) both}.orf-results-badge .orf-btn-icon{width:13px;height:13px}.orf-back-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:8px;border:1px solid rgba(255,255,255,.3);background:#ffffff21;color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;flex-shrink:0;position:relative;z-index:1}.orf-back-btn .orf-btn-icon{width:16px;height:16px;transition:transform .18s ease}.orf-back-btn:hover{background:#ffffff3d;border-color:#ffffff80}.orf-back-btn:hover .orf-btn-icon{transform:translate(-3px)}.orf-back-btn:active{transform:scale(.97)}.orf-results-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-shrink:0}.orf-reexport-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 13px;border-radius:8px;border:1px solid rgba(255,255,255,.28);background:#ffffff1f;color:#ffffffd9;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;white-space:nowrap}.orf-reexport-btn .orf-btn-icon{width:15px;height:15px}.orf-reexport-btn:hover{background:#ffffff38;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-reexport-btn--excel:hover{background:#16a34a52;border-color:#16a34a8c}.orf-reexport-btn--pdf:hover{background:#dc262652;border-color:#dc26268c}.orf-results-body{flex:1;overflow:hidden;padding:12px 12px 0;display:flex;flex-direction:column}.orf-results-body osl-report-grid{flex:1;display:block}\n"], dependencies: [{ kind: "component", type: DynamicForm, selector: "osl-dynamic-form", inputs: ["elements", "model", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange"] }, { kind: "component", type: OslReportGrid, selector: "osl-report-grid", inputs: ["columns", "datasource", "loading", "totalRecords", "autoMode", "isPaginated", "pageSize", "tableHeight", "striped", "exportable", "rowHeight", "rowSelection", "showAggregates", "title", "pdfExportFromGrid", "pdfConfig"], outputs: ["pageChange", "pageSizeChange", "sortChange", "rowClick", "selectionChange"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }] });
|
|
3777
4035
|
}
|
|
3778
4036
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslReportForm, decorators: [{
|
|
3779
4037
|
type: Component,
|
|
3780
|
-
args: [{ selector: 'osl-report-form', standalone: false, template: "<!-- \u2500\u2500 Form view (hidden when results are shown) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (!showResultView) {\n <div class=\"orf-card\">\n\n <!-- Header -->\n <div class=\"orf-header\">\n <div class=\"orf-header__left\">\n <div class=\"orf-header__icon-wrap\">\n <mat-icon class=\"orf-header__icon\">assessment</mat-icon>\n </div>\n <div class=\"orf-header__text\">\n <h4 class=\"orf-header__title\">{{ title }}</h4>\n <p class=\"orf-header__sub\">Configure parameters and generate report</p>\n </div>\n </div>\n\n <div class=\"orf-header__right\">\n @for (type of generateOptions; track type) {\n <button\n class=\"orf-type-btn\"\n [class.active]=\"selectedType === type\"\n [class.orf-type-btn--pdf]=\"type === 'pdf'\"\n [class.orf-type-btn--excel]=\"type === 'excel'\"\n [class.orf-type-btn--grid]=\"type === 'grid'\"\n (click)=\"selectType(type)\"\n type=\"button\"\n >\n <mat-icon>{{ typeIcon(type) }}</mat-icon>\n <span>{{ typeLabel(type) }}</span>\n </button>\n }\n </div>\n </div>\n\n <!-- Divider -->\n <div class=\"orf-divider\"></div>\n\n <!-- Form Body -->\n <div class=\"orf-body\">\n <osl-dynamic-form\n [elements]=\"formElements\"\n [model]=\"model\"\n (modelChange)=\"onModelChange($event)\"\n [skeletonLoading]=\"skeletonLoading\"\n [skeletonTheme]=\"skeletonTheme\"\n ></osl-dynamic-form>\n </div>\n\n <!-- Footer -->\n <div class=\"orf-footer\">\n <button\n class=\"orf-generate-btn\"\n (click)=\"generate()\"\n [disabled]=\"generating\"\n type=\"button\"\n >\n @if (generating) {\n <span class=\"orf-generate-btn__spinner\"></span>\n <span>Generating...</span>\n } @else {\n <mat-icon>{{ typeIcon(selectedType) }}</mat-icon>\n <span>Generate {{ typeLabel(selectedType) }}</span>\n <mat-icon class=\"orf-generate-btn__arrow\">arrow_forward</mat-icon>\n }\n </button>\n </div>\n\n </div>\n}\n\n<!-- \u2500\u2500 Full-screen results view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (showResultView) {\n <!-- <div class=\"orf-results-view\"> -->\n\n <!-- Results header bar -->\n <div class=\"orf-results-header\">\n\n <div class=\"orf-results-header__left\">\n <button class=\"orf-back-btn\" (click)=\"backToForm()\" type=\"button\">\n <mat-icon>arrow_back</mat-icon>\n <span>Back</span>\n </button>\n <div class=\"orf-results-header__sep\"></div>\n <mat-icon class=\"orf-results-header__report-icon\">assessment</mat-icon>\n <span class=\"orf-results-header__title\">{{ title }}</span>\n <span class=\"orf-results-badge\">\n <mat-icon>table_rows</mat-icon>\n {{ resultDatasource.length | number }} records\n </span>\n </div>\n\n <div class=\"orf-results-header__right\">\n @if (generateOptions.includes('excel')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--excel\" (click)=\"reExportExcel()\" type=\"button\">\n <mat-icon>table_chart</mat-icon>\n <span>Excel</span>\n </button>\n }\n <!-- @if (generateOptions.includes('pdf')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--pdf\" (click)=\"reExportPdf()\" type=\"button\">\n <mat-icon>picture_as_pdf</mat-icon>\n <span>PDF</span>\n </button>\n } -->\n </div>\n\n </div>\n\n <!-- Report grid -->\n <div class=\"orf-results-body\">\n <osl-report-grid\n [columns]=\"reportColumns\"\n [datasource]=\"resultDatasource\"\n [loading]=\"generating\"\n [title]=\"title\"\n tableHeight=\"calc(100vh - 365px)\"\n [pageSize]=\"pageSize\"\n [exportable]=\"true\"\n [showAggregates]=\"true\"\n [striped]=\"true\"\n ></osl-report-grid>\n </div>\n\n <!-- </div> -->\n}\n", styles: ["@keyframes orf-fade-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes orf-slide-in{0%{opacity:0;transform:translate(-10px)}to{opacity:1;transform:translate(0)}}@keyframes orf-spin{to{transform:rotate(360deg)}}.orf-card{background:#fff;border-radius:12px;border:1px solid #e5e7eb;box-shadow:0 4px 20px #00000014,0 1px 4px #0000000a;overflow:hidden}.orf-header{background:linear-gradient(135deg,#1e3a8a,#2563eb 55%,#60a5fa);padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px;position:relative;overflow:hidden;flex-wrap:wrap}.orf-header:before{content:\"\";position:absolute;width:260px;height:260px;border-radius:50%;background:#ffffff0d;top:-110px;right:10px;pointer-events:none}.orf-header:after{content:\"\";position:absolute;width:160px;height:160px;border-radius:50%;background:#ffffff12;bottom:-70px;right:200px;pointer-events:none}.orf-header__left{display:flex;align-items:center;gap:14px;position:relative;z-index:1}.orf-header__icon-wrap{width:50px;height:50px;border-radius:14px;background:#ffffff26;border:1px solid rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;flex-shrink:0;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.orf-header__icon{color:#fff;font-size:26px;width:26px;height:26px}.orf-header__text{display:flex;flex-direction:column;gap:3px}.orf-header__title{margin:0;font-size:19px;font-weight:700;color:#fff;letter-spacing:-.015em;line-height:1.2}.orf-header__sub{margin:0;font-size:12px;color:#ffffffad}.orf-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-wrap:wrap}.orf-type-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid rgba(255,255,255,.28);background:#ffffff1f;color:#ffffffd1;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit}.orf-type-btn mat-icon{font-size:16px;width:16px;height:16px}.orf-type-btn:hover{background:#ffffff38;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-type-btn.active{background:#fffffff2;border-color:transparent;color:#1e3a8a;box-shadow:0 4px 14px #00000038;transform:translateY(-1px);font-weight:600}.orf-type-btn.active mat-icon{color:#1e3a8a}.orf-type-btn--pdf.active{color:#dc2626}.orf-type-btn--pdf.active mat-icon{color:#dc2626}.orf-type-btn--excel.active{color:#16a34a}.orf-type-btn--excel.active mat-icon{color:#16a34a}.orf-type-btn--grid.active{color:#2563eb}.orf-type-btn--grid.active mat-icon{color:#2563eb}.orf-divider{height:1px;background:linear-gradient(90deg,#e5e7eb,#c7d2fe,#e5e7eb)}.orf-body{padding:20px 20px 4px}.orf-footer{padding:12px 20px 20px;display:flex;justify-content:flex-end}.orf-generate-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 22px;border-radius:8px;border:none;background:linear-gradient(135deg,#1e3a8a,#2563eb);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 16px #2563eb52;letter-spacing:.01em;font-family:inherit}.orf-generate-btn mat-icon{font-size:18px;width:18px;height:18px}.orf-generate-btn__arrow{transition:transform .2s ease;font-size:16px!important;width:16px!important;height:16px!important;margin-left:2px}.orf-generate-btn:hover:not([disabled]){background:linear-gradient(135deg,#1e40af,#1d4ed8);box-shadow:0 6px 22px #2563eb7a;transform:translateY(-1px)}.orf-generate-btn:hover:not([disabled]) .orf-generate-btn__arrow{transform:translate(4px)}.orf-generate-btn:active:not([disabled]){transform:translateY(0);box-shadow:0 2px 8px #2563eb38}.orf-generate-btn[disabled]{opacity:.72;cursor:not-allowed;box-shadow:none}.orf-generate-btn__spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;display:inline-block;animation:orf-spin .7s linear infinite;flex-shrink:0}.orf-results-view{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:#f1f5f9;animation:orf-fade-in .25s ease}.orf-results-header{flex-shrink:0;height:56px;background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#3b82f6);display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;position:relative;overflow:hidden;box-shadow:0 2px 12px #2563eb66}.orf-results-header:after{content:\"\";position:absolute;width:180px;height:180px;border-radius:50%;background:#ffffff0f;top:-80px;right:60px;pointer-events:none}.orf-results-header__left{display:flex;align-items:center;gap:10px;position:relative;z-index:1;min-width:0;flex:1}.orf-results-header__sep{width:1px;height:22px;background:#ffffff40;flex-shrink:0}.orf-results-header__report-icon{color:#ffffffd9;font-size:20px;width:20px;height:20px;flex-shrink:0}.orf-results-header__title{font-size:15px;font-weight:700;color:#fff;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.orf-results-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;background:#ffffff26;border:1px solid rgba(255,255,255,.2);color:#ffffffe6;font-size:11px;font-weight:600;white-space:nowrap;flex-shrink:0;animation:orf-slide-in .3s ease}.orf-results-badge mat-icon{font-size:13px;width:13px;height:13px}.orf-back-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:7px;border:1px solid rgba(255,255,255,.3);background:#ffffff24;color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;flex-shrink:0}.orf-back-btn mat-icon{font-size:17px;width:17px;height:17px;transition:transform .18s ease}.orf-back-btn:hover{background:#ffffff3d;border-color:#ffffff80}.orf-back-btn:hover mat-icon{transform:translate(-3px)}.orf-back-btn:active{transform:scale(.97)}.orf-results-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-shrink:0}.orf-reexport-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 13px;border-radius:7px;border:1px solid rgba(255,255,255,.28);background:#ffffff1f;color:#ffffffd9;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;white-space:nowrap}.orf-reexport-btn mat-icon{font-size:15px;width:15px;height:15px}.orf-reexport-btn:hover{background:#ffffff38;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-reexport-btn--excel:hover{background:#16a34a59;border-color:#16a34a80}.orf-reexport-btn--pdf:hover{background:#dc262659;border-color:#dc262680}.orf-results-body{flex:1;overflow:hidden;padding:12px 12px 0;display:flex;flex-direction:column}.orf-results-body osl-report-grid{flex:1;display:block}\n"] }]
|
|
4038
|
+
args: [{ selector: 'osl-report-form', standalone: false, template: "<!-- \u2500\u2500 Form view (hidden when results are shown) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (!showResultView) {\n <div class=\"orf-card\">\n\n <!-- Header -->\n <div class=\"orf-header\">\n <div class=\"orf-header__left\">\n <div class=\"orf-header__icon-wrap\">\n <svg class=\"orf-header__icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"3\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M8 16V11\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n <path d=\"M12 16V8\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n <path d=\"M16 16V13\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n </svg>\n </div>\n <div class=\"orf-header__text\">\n <h4 class=\"orf-header__title\">{{ title }}</h4>\n <p class=\"orf-header__sub\">Configure parameters and generate report</p>\n </div>\n </div>\n\n <div class=\"orf-header__right\">\n @for (type of generateOptions; track type) {\n <button\n class=\"orf-type-btn\"\n [class.active]=\"selectedType === type\"\n [class.orf-type-btn--pdf]=\"type === 'pdf'\"\n [class.orf-type-btn--excel]=\"type === 'excel'\"\n [class.orf-type-btn--grid]=\"type === 'grid'\"\n (click)=\"selectType(type)\"\n type=\"button\"\n >\n @if (type === 'pdf') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\n stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M14 2v6h6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M9 13h1.5a1.5 1.5 0 0 1 0 3H9v-4\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/>\n <path d=\"M14 13v4\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/>\n <path d=\"M14 13h1.5a1.5 1.5 0 0 1 0 3H14\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/>\n </svg>\n }\n @if (type === 'excel') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M2 8h20\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M8 8v13M14 8v13\" stroke=\"currentColor\" stroke-width=\"1.2\"/>\n <path d=\"M2 13h20M2 18h20\" stroke=\"currentColor\" stroke-width=\"1.2\"/>\n </svg>\n }\n @if (type === 'grid') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n </svg>\n }\n <span>{{ typeLabel(type) }}</span>\n </button>\n }\n </div>\n </div>\n\n <!-- Section divider with label -->\n <div class=\"orf-section-divider\">\n <div class=\"orf-section-divider__line\"></div>\n <span class=\"orf-section-divider__label\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M2 4h12M2 8h9M2 12h6\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/>\n </svg>\n Report Parameters\n </span>\n <div class=\"orf-section-divider__line\"></div>\n </div>\n\n <!-- Form Body -->\n <div class=\"orf-body\">\n <osl-dynamic-form\n [elements]=\"formElements\"\n [model]=\"model\"\n (modelChange)=\"onModelChange($event)\"\n [skeletonLoading]=\"skeletonLoading\"\n [skeletonTheme]=\"skeletonTheme\"\n ></osl-dynamic-form>\n </div>\n\n <!-- Footer -->\n <div class=\"orf-footer\">\n <button\n class=\"orf-generate-btn\"\n (click)=\"generate()\"\n [disabled]=\"generating\"\n type=\"button\"\n >\n @if (generating) {\n <span class=\"orf-generate-btn__spinner\"></span>\n <span>Generating...</span>\n } @else {\n @if (selectedType === 'pdf') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\n stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linejoin=\"round\"/>\n <path d=\"M14 2v6h6\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linejoin=\"round\"/>\n </svg>\n }\n @if (selectedType === 'excel') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <path d=\"M2 8h20M8 8v13M14 8v13M2 13h20M2 18h20\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n </svg>\n }\n @if (selectedType === 'grid') {\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.8\"/>\n </svg>\n }\n <span>Generate {{ typeLabel(selectedType) }}</span>\n <svg class=\"orf-generate-btn__arrow\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M4 10h12M11 5l5 5-5 5\" stroke=\"currentColor\" stroke-width=\"1.8\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n }\n </button>\n </div>\n\n </div>\n}\n\n<!-- \u2500\u2500 Full-screen results view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (showResultView) {\n\n <!-- Results header bar -->\n <div class=\"orf-results-header\">\n\n <div class=\"orf-results-header__left\">\n <button class=\"orf-back-btn\" (click)=\"backToForm()\" type=\"button\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M16 10H4M9 5l-5 5 5 5\" stroke=\"currentColor\" stroke-width=\"1.8\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n <span>Back</span>\n </button>\n <div class=\"orf-results-header__sep\"></div>\n <svg class=\"orf-results-header__report-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"3\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M8 16V11M12 16V8M16 16V13\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"/>\n </svg>\n <span class=\"orf-results-header__title\">{{ title }}</span>\n <span class=\"orf-results-badge\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 16 16\" fill=\"none\">\n <rect x=\"2\" y=\"2\" width=\"12\" height=\"3\" rx=\"1\" fill=\"currentColor\"/>\n <rect x=\"2\" y=\"7\" width=\"12\" height=\"3\" rx=\"1\" fill=\"currentColor\" opacity=\".65\"/>\n <rect x=\"2\" y=\"12\" width=\"8\" height=\"2\" rx=\"1\" fill=\"currentColor\" opacity=\".35\"/>\n </svg>\n {{ resultDatasource.length | number }} records\n </span>\n </div>\n\n <!-- <div class=\"orf-results-header__right\">\n @if (generateOptions.includes('excel')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--excel\" (click)=\"reExportExcel()\" type=\"button\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M2 8h20M8 8v13M14 8v13M2 13h20M2 18h20\" stroke=\"currentColor\" stroke-width=\"1.3\"/>\n </svg>\n <span>Excel</span>\n </button>\n }\n @if (generateOptions.includes('pdf')) {\n <button class=\"orf-reexport-btn orf-reexport-btn--pdf\" (click)=\"reExportPdf()\" type=\"button\">\n <svg class=\"orf-btn-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\n stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M14 2v6h6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span>PDF</span>\n </button>\n }\n </div> -->\n\n </div>\n\n <!-- Report grid -->\n <div class=\"orf-results-body\">\n <osl-report-grid\n [columns]=\"reportColumns\"\n [datasource]=\"resultDatasource\"\n [loading]=\"generating\"\n [title]=\"title\"\n tableHeight=\"calc(100vh - 365px)\"\n [pageSize]=\"pageSize\"\n [exportable]=\"true\"\n [pdfExportFromGrid]=\"true\"\n [pdfConfig]=\"_mergedPdfConfig\"\n [showAggregates]=\"true\"\n [striped]=\"true\"\n ></osl-report-grid>\n </div>\n\n}\n", styles: ["@keyframes orf-fade-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes orf-slide-in{0%{opacity:0;transform:translate(-10px)}to{opacity:1;transform:translate(0)}}@keyframes orf-spin{to{transform:rotate(360deg)}}@keyframes orf-shimmer{0%{transform:translate(-100%) skew(-12deg)}to{transform:translate(220%) skew(-12deg)}}@keyframes orf-badge-pop{0%{transform:scale(.7);opacity:0}70%{transform:scale(1.08)}to{transform:scale(1);opacity:1}}.orf-btn-icon{display:block;flex-shrink:0;width:16px;height:16px}.orf-card{background:#fff;border-radius:14px;border:1px solid #e5e7eb;box-shadow:0 4px 24px #00000012,0 1px 4px #0000000a;overflow:hidden;animation:orf-fade-in .28s ease}.orf-header{background:linear-gradient(135deg,#1e3a8a,#2563eb 55%,#60a5fa);padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:16px;position:relative;overflow:hidden;flex-wrap:wrap}.orf-header:before{content:\"\";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,.12) 1px,transparent 1px);background-size:20px 20px;pointer-events:none}.orf-header:after{content:\"\";position:absolute;width:260px;height:260px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.1) 0%,transparent 70%);top:-120px;right:-20px;pointer-events:none}.orf-header__left{display:flex;align-items:center;gap:14px;position:relative;z-index:1}.orf-header__icon-wrap{width:52px;height:52px;border-radius:14px;background:#ffffff24;border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;justify-content:center;flex-shrink:0;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 2px 8px #00000026,inset 0 1px #fff3}.orf-header__icon{width:26px;height:26px;color:#fff}.orf-header__text{display:flex;flex-direction:column;gap:3px}.orf-header__title{margin:0;font-size:19px;font-weight:700;color:#fff;letter-spacing:-.02em;line-height:1.2;text-shadow:0 1px 3px rgba(0,0,0,.15)}.orf-header__sub{margin:0;font-size:12px;color:#ffffffa6;letter-spacing:.01em}.orf-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-wrap:wrap}.orf-type-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;border-radius:9px;border:1px solid rgba(255,255,255,.28);background:#ffffff1c;color:#ffffffd1;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit}.orf-type-btn .orf-btn-icon{width:15px;height:15px;transition:transform .2s ease}.orf-type-btn:hover{background:#ffffff36;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-type-btn:hover .orf-btn-icon{transform:scale(1.1)}.orf-type-btn.active{background:#fffffff5;border-color:transparent;color:#1e3a8a;box-shadow:0 4px 16px #00000038,0 1px 3px #0000001a;transform:translateY(-1px);font-weight:600}.orf-type-btn.active .orf-btn-icon{color:#1e3a8a}.orf-type-btn--pdf.active,.orf-type-btn--pdf.active .orf-btn-icon{color:#dc2626}.orf-type-btn--excel.active,.orf-type-btn--excel.active .orf-btn-icon{color:#16a34a}.orf-type-btn--grid.active,.orf-type-btn--grid.active .orf-btn-icon{color:#2563eb}.orf-section-divider{display:flex;align-items:center;gap:12px;padding:0 24px;margin:18px 0 0}.orf-section-divider__line{flex:1;height:1px;background:linear-gradient(90deg,transparent,#e2e8f0,#c7d2fe,#e2e8f0,transparent)}.orf-section-divider__label{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;background:#f0f4ff;border:1px solid #c7d2fe;color:#4f46e5;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;flex-shrink:0}.orf-section-divider__label svg{width:12px;height:12px;color:#6366f1}.orf-body{padding:16px 20px 4px}.orf-footer{padding:12px 20px 20px;display:flex;justify-content:flex-end;align-items:center;gap:10px}.orf-generate-btn{display:inline-flex;align-items:center;gap:8px;padding:11px 24px;border-radius:10px;border:none;background:linear-gradient(135deg,#1e3a8a,#2563eb);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 18px #2563eb59;letter-spacing:.01em;font-family:inherit;position:relative;overflow:hidden}.orf-generate-btn:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.18) 50%,transparent 100%);transform:translate(-100%) skew(-12deg);pointer-events:none}.orf-generate-btn .orf-btn-icon{width:18px;height:18px}.orf-generate-btn__arrow{width:18px;height:18px;transition:transform .22s ease;flex-shrink:0}.orf-generate-btn:hover:not([disabled]){background:linear-gradient(135deg,#1e40af,#1d4ed8);box-shadow:0 6px 24px #2563eb80;transform:translateY(-1px)}.orf-generate-btn:hover:not([disabled]):after{animation:orf-shimmer .55s ease forwards}.orf-generate-btn:hover:not([disabled]) .orf-generate-btn__arrow{transform:translate(5px)}.orf-generate-btn:active:not([disabled]){transform:translateY(0);box-shadow:0 2px 8px #2563eb38}.orf-generate-btn[disabled]{opacity:.72;cursor:not-allowed;box-shadow:none}.orf-generate-btn__spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;display:inline-block;animation:orf-spin .7s linear infinite;flex-shrink:0}.orf-results-view{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:#f1f5f9;animation:orf-fade-in .25s ease}.orf-results-header{flex-shrink:0;height:56px;background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#3b82f6);display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;position:relative;overflow:hidden;box-shadow:0 2px 14px #2563eb73}.orf-results-header:before{content:\"\";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,.09) 1px,transparent 1px);background-size:16px 16px;pointer-events:none}.orf-results-header:after{content:\"\";position:absolute;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.08) 0%,transparent 70%);top:-90px;right:40px;pointer-events:none}.orf-results-header__left{display:flex;align-items:center;gap:10px;position:relative;z-index:1;min-width:0;flex:1}.orf-results-header__sep{width:1px;height:22px;background:#ffffff40;flex-shrink:0}.orf-results-header__report-icon{width:20px;height:20px;color:#ffffffd9;flex-shrink:0}.orf-results-header__title{font-size:15px;font-weight:700;color:#fff;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-shadow:0 1px 2px rgba(0,0,0,.12)}.orf-results-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:20px;background:#ffffff26;border:1px solid rgba(255,255,255,.22);color:#ffffffeb;font-size:11px;font-weight:600;white-space:nowrap;flex-shrink:0;animation:orf-badge-pop .35s cubic-bezier(.34,1.56,.64,1) both}.orf-results-badge .orf-btn-icon{width:13px;height:13px}.orf-back-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:8px;border:1px solid rgba(255,255,255,.3);background:#ffffff21;color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;flex-shrink:0;position:relative;z-index:1}.orf-back-btn .orf-btn-icon{width:16px;height:16px;transition:transform .18s ease}.orf-back-btn:hover{background:#ffffff3d;border-color:#ffffff80}.orf-back-btn:hover .orf-btn-icon{transform:translate(-3px)}.orf-back-btn:active{transform:scale(.97)}.orf-results-header__right{display:flex;align-items:center;gap:8px;position:relative;z-index:1;flex-shrink:0}.orf-reexport-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 13px;border-radius:8px;border:1px solid rgba(255,255,255,.28);background:#ffffff1f;color:#ffffffd9;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s ease;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);font-family:inherit;white-space:nowrap}.orf-reexport-btn .orf-btn-icon{width:15px;height:15px}.orf-reexport-btn:hover{background:#ffffff38;border-color:#ffffff73;color:#fff;transform:translateY(-1px)}.orf-reexport-btn--excel:hover{background:#16a34a52;border-color:#16a34a8c}.orf-reexport-btn--pdf:hover{background:#dc262652;border-color:#dc26268c}.orf-results-body{flex:1;overflow:hidden;padding:12px 12px 0;display:flex;flex-direction:column}.orf-results-body osl-report-grid{flex:1;display:block}\n"] }]
|
|
3781
4039
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { title: [{
|
|
3782
4040
|
type: Input
|
|
3783
4041
|
}], formElements: [{
|
|
@@ -3798,6 +4056,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
3798
4056
|
type: Input
|
|
3799
4057
|
}], pageSize: [{
|
|
3800
4058
|
type: Input
|
|
4059
|
+
}], pdfConfig: [{
|
|
4060
|
+
type: Input
|
|
4061
|
+
}], _grid: [{
|
|
4062
|
+
type: ViewChild,
|
|
4063
|
+
args: [OslReportGrid]
|
|
3801
4064
|
}] } });
|
|
3802
4065
|
|
|
3803
4066
|
class FormStructureModule {
|