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
- processData() {
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
- if (!this.excelFilterState[key])
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.datasource) {
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
- lines.push(cols.map(c => `"${c.headerGroup ? `${c.label}(${c.headerGroup})` : c.label}"`).join(','));
3530
- this.getFilteredRows().forEach(row => lines.push(cols.map(col => `"${String(this.getCellDisplay(row, col)).replace(/"/g, '""')}"`).join(',')));
3531
- const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
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._exportCsv(this.resultDatasource); }
3722
- reExportPdf() { this._exportPdf(this.resultDatasource); }
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 {