UNPKG

ng-prime-tools

Version:

An advanced PrimeNG table for Angular

592 lines (588 loc) 397 kB
import * as i0 from '@angular/core'; import { Pipe, EventEmitter, Component, Input, Output, ViewChild, NgModule, Injectable, HostListener, HostBinding } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i2$1 from '@angular/forms'; import { FormControl, Validators, ReactiveFormsModule, FormsModule, FormGroup } from '@angular/forms'; import * as i2 from 'primeng/table'; import { TableModule } from 'primeng/table'; import * as i1$1 from 'primeng/api'; import { ConfirmEventType, ConfirmationService, MessageService } from 'primeng/api'; import * as i4 from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext'; import * as i3 from 'primeng/button'; import { ButtonModule } from 'primeng/button'; import * as i6 from 'primeng/calendar'; import { CalendarModule } from 'primeng/calendar'; import * as i8 from 'primeng/multiselect'; import { MultiSelectModule } from 'primeng/multiselect'; import * as i9 from 'primeng/tag'; import { TagModule } from 'primeng/tag'; import * as i10 from 'primeng/iconfield'; import { IconFieldModule } from 'primeng/iconfield'; import * as i11 from 'primeng/inputicon'; import { InputIconModule } from 'primeng/inputicon'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import * as i8$1 from 'primeng/inputnumber'; import { InputNumberModule } from 'primeng/inputnumber'; import * as i9$1 from 'primeng/panel'; import { PanelModule } from 'primeng/panel'; import * as i3$1 from 'primeng/checkbox'; import { CheckboxModule } from 'primeng/checkbox'; import * as i3$2 from 'primeng/inputgroup'; import { InputGroupModule } from 'primeng/inputgroup'; import * as i4$1 from 'primeng/inputgroupaddon'; import { InputGroupAddonModule } from 'primeng/inputgroupaddon'; import * as i3$3 from 'primeng/inputswitch'; import { InputSwitchModule } from 'primeng/inputswitch'; import * as i3$4 from 'primeng/inputtextarea'; import { InputTextareaModule } from 'primeng/inputtextarea'; import * as i3$5 from 'primeng/dropdown'; import { DropdownModule } from 'primeng/dropdown'; import { Chart, registerables } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import * as i1$2 from '@angular/router'; import { RouterModule, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; import * as i3$6 from 'primeng/breadcrumb'; import { BreadcrumbModule } from 'primeng/breadcrumb'; import * as i3$7 from 'primeng/confirmdialog'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { DialogModule } from 'primeng/dialog'; import * as i2$2 from 'primeng/toast'; import { ToastModule } from 'primeng/toast'; /** * Calculates the width required for a column based on the header text (column title). * It uses the Canvas API to measure text width dynamically. * * @param {TableColumn} col - The column metadata containing the header title and code. * @param {string} [font='16px Arial'] - The font to use for measurement (defaults to '16px Arial'). * @returns {number} - The calculated width of the column in pixels. */ function calculateTextWidth(col, font = '16px Arial') { // Create a canvas context for measuring text const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) { return 100; // Fallback width if canvas context is not available } // Set the font to match the provided font or the document body font context.font = font || getComputedStyle(document.body).font; // Measure the header text width const headerWidth = context.measureText(col.title).width; // Return the width with some padding return Math.ceil(headerWidth + 20); // Add padding for extra space } class CustomCurrencyPipe { transform(value, currency, decimalPlaces, thousandSeparator = 'comma', decimalSeparator = 'dot') { let formattedValue; if (decimalPlaces !== undefined) { formattedValue = value.toFixed(decimalPlaces); } else { formattedValue = value.toString(); } const thousandSeparatorChar = thousandSeparator === 'space' ? ' ' : ','; const decimalSeparatorChar = decimalSeparator === 'comma' ? ',' : '.'; formattedValue = formattedValue.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparatorChar); if (decimalSeparatorChar === ',') { formattedValue = formattedValue.replace('.', ','); } if (currency) { let formattedCurrency; switch (currency) { case 'MAD': formattedCurrency = `${formattedValue} DH`; break; case 'USD': formattedCurrency = `$${formattedValue}`; break; default: formattedCurrency = `${formattedValue} ${currency}`; } return formattedCurrency; } return formattedValue; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.11", ngImport: i0, type: CustomCurrencyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.11", ngImport: i0, type: CustomCurrencyPipe, isStandalone: true, name: "customCurrency" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.11", ngImport: i0, type: CustomCurrencyPipe, decorators: [{ type: Pipe, args: [{ name: 'customCurrency', standalone: true, }] }] }); class CustomDatePipe { transform(value, format = 'dd/MM/yyyy') { if (!value) return null; if (typeof value === 'string') { const parts = value.split('/'); if (parts.length === 3) { const day = parseInt(parts[0], 10); const month = parseInt(parts[1], 10) - 1; const year = parseInt(parts[2], 10); const date = new Date(year, month, day); if (isNaN(date.getTime())) return null; const options = { year: 'numeric', month: '2-digit', day: '2-digit', }; return new Intl.DateTimeFormat('en-GB', options).format(date); } } else if (value instanceof Date) { // If the value is already a Date object const options = { year: 'numeric', month: '2-digit', day: '2-digit', }; return new Intl.DateTimeFormat('en-GB', options).format(value); } return null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.11", ngImport: i0, type: CustomDatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.11", ngImport: i0, type: CustomDatePipe, name: "customDate" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.11", ngImport: i0, type: CustomDatePipe, decorators: [{ type: Pipe, args: [{ name: 'customDate', }] }] }); class PTAdvancedPrimeTableComponent { constructor() { // Inputs this.data = []; this.columns = []; this.totalRecords = 0; this.rowsPerPage = []; this.hasSearchFilter = false; this.hasExportExcel = false; this.hasExportPDF = false; this.hasColumnFilter = false; this.isPaginated = false; this.actions = []; this.isSortable = false; this.loading = false; this.maxHeight = null; // Outputs: Events emitted to the parent component this.filter = new EventEmitter(); this.search = new EventEmitter(); this.exportExcelEvent = new EventEmitter(); this.exportPdfEvent = new EventEmitter(); this.TableTypeEnum = TableTypeEnum; this.searchValue = ''; this.filters = {}; this.validCurrencyCodes = ['USD', 'EUR', 'MAD']; this.iconWidth = 77; // Component state properties this.isDelete = false; this.isEdit = false; this.rows = 0; // Data management properties this.dataMap = new Map(); this.map = new Map(); this.optionEntries = new Map(); this.optionValues = []; this.globalFilterFields = []; // CRUD operation handlers this.Delete = () => { }; this.initEditableRow = () => { }; this.saveEditableRow = () => { }; this.cancelEditableRow = () => { }; this.hasGroupedColumns = false; } ngOnInit() { this.hasGroupedColumns = this.columns.some((col) => col.children && col.children.length > 0); this.globalFilterFields = this.columns .filter((col) => col.code !== undefined && col.isFilter !== false) .map((col) => col.code); this.initializePagination(); this.initializeActions(); // Set default value for isSortable this.columns.forEach((col) => { if (col.type === TableTypeEnum.ACTION) { col.isEditable = false; col.isFilter = false; col.isSortable = false; } if (col.type === TableTypeEnum.COMPOSED) { this.initializeComposedFilters(col); } if (col.isSortable === undefined) { col.isSortable = true; } if (col.isEditable === undefined) { col.isEditable = true; } if (col.isFilter !== false && col.code !== undefined) { this.globalFilterFields.push(col.code); } if (!col.width) { col.width = this.calculateColumnWidth(col); } }); } // Initialize filters for composed columns initializeComposedFilters(col) { col.composedNames?.forEach((composedName) => { this.globalFilterFields.push(col.code + '.' + composedName); this.filters[composedName] = { options: col.filterOptions, value: [], label: 'Filter by ' + composedName, placeholder: 'Select options for ' + composedName, }; }); } // Get the column type for composed fields (STRING, IMAGE, etc.) getComposedFieldType(col, composedName) { // Ensure that col.composedNames and col.composedTypes are valid arrays if (col.composedNames && col.composedTypes) { const index = col.composedNames.indexOf(composedName); // Check if index is a valid number (not -1) and within bounds of composedTypes array if (index >= 0 && index < col.composedTypes.length) { return col.composedTypes[index]; // Safe access of composedTypes array } } return undefined; // Return undefined if no valid index is found or composedNames/composedTypes are not valid } onComposedFilterChange(composedName, selectedValues) { console.log('Selected Values:', selectedValues); console.log('Data Before Filtering:', this.data); // Update the filter value with the selected values this.filters[composedName].value = selectedValues; // Emit the filter event to notify the parent component (if needed) this.filter.emit(this.filters); // Get the filter value (joining array into a string) const filterValue = selectedValues.join(','); console.log('Filter Values to be Applied:', filterValue); // Apply global filter using PrimeNG's filterGlobal method this.dt.filterGlobal(filterValue, 'contains'); } onFilter(event) { this.totalRecords = event.filteredValue?.length || 0; } onCalendarFilterChange(event, columnCode, filterCallback) { // Log the data's date format for debugging console.log('Data Before Filtering:', this.data.map((item) => item[columnCode])); console.log('event : ' + event); // Convert the event value to a string (in the desired date format) const filterValue = event ? new Date(event) : null; console.log('filterValue : ' + filterValue); // If the filterValue is empty, do not trigger filterCallback if (!filterValue) { return; } // Manually trigger the filterCallback after updating the value (passing the string value) filterCallback(filterValue); const filterValueString = event ? this.formatDate(event) : ''; // Call the onFilter event to update totalRecords this.onFilter({ filteredValue: this.data.filter((item) => { const columnValue = item[columnCode]; // If the column value is a string, use it as is for comparison if (columnValue) { // Convert the item value to a string (in the same format) const itemDateString = this.formatDate(new Date(columnValue)); return itemDateString === filterValueString; // Compare the string dates } return false; }), }); } // Filter logic for composed columns (to check against multi-select values) filterComposedData(item, composedName, value) { if (Array.isArray(value) && value.length > 0) { return value.some((filterValue) => item[composedName]?.toLowerCase().includes(filterValue.toLowerCase())); } return true; } // Function to calculate column width based on text in header and data calculateColumnWidth(col) { const calculatedWidth = calculateTextWidth(col, col.title); const totalWidth = calculatedWidth + this.iconWidth + 20; return `${totalWidth}px`; } getHeaderWidth(col) { // Remove 'px' from col.width and convert it to a number const widthWithoutPx = parseInt(col.width?.replace('px', '') || '0', 10); // Add 20 to the calculated width const headerWidth = widthWithoutPx + 20; // Return the new width in 'px' return `${headerWidth}px`; } clear(table) { table.clear(); this.searchValue = ''; } parseDate(dateString) { const parts = dateString.split('/'); if (parts.length === 3) { // Assuming date format is DD/MM/YYYY const day = parseInt(parts[0], 10); const month = parseInt(parts[1], 10) - 1; const year = parseInt(parts[2], 10); const date = new Date(year, month, day); return isNaN(date.getTime()) ? null : date; } return null; } initializePagination() { if (this.isPaginated) { // Check if rowsPerPage is undefined or an empty array if (!this.rowsPerPage || this.rowsPerPage.length === 0) { this.rowsPerPage = [20, 30, 40]; } this.rows = this.rowsPerPage[0]; } } initializeActions() { if (this.actions) { this.actions.forEach((action) => { switch (action.code) { case 'delete': this.isDelete = true; this.Delete = (value) => action.action(value); break; case 'edit': this.initializeEditActions(action); break; default: this.isDelete = false; this.isEdit = false; } }); } } initializeEditActions(action) { this.isEdit = true; this.initEditableRow = (data) => action.action.init(data); this.saveEditableRow = (data) => { const record = this.map.get(data.id); action.action.save(data, record); this.dataMap.clear(); }; this.cancelEditableRow = (item) => console.log(item); } onChange(event, id, key) { const target = event.target; this.changeHandler(id, key, target.value); } changeHandler(id, key, value) { let column = this.columns.find((item) => item.code === key); if (!this.map.get(id)) { if (column?.type === TableTypeEnum.DATE) { let date = this.parseDate(value); this.dataMap.set(key, date); } else { this.dataMap.set(key, value); } this.map.set(id, new Map(this.dataMap)); } else { let mapItem = this.map.get(id); if (column?.type === TableTypeEnum.DATE) { let date = this.parseDate(value); mapItem.set(key, date); } else { mapItem.set(key, value); } } } getColumnFilterType(column) { switch (column.type) { case TableTypeEnum.STRING: return 'text'; case TableTypeEnum.AMOUNT: case TableTypeEnum.NUMBER: return 'numeric'; case TableTypeEnum.DATE: return 'date'; case TableTypeEnum.MULTISELECT: return 'multiSelect'; case TableTypeEnum.BOOLEAN: return 'boolean'; case TableTypeEnum.COMPOSED: return 'composed'; default: return 'text'; } } // State Check Methods isEditable(key) { let column = this.columns.find((item) => item.code === key); return column?.isEditable !== false; } isMultiSelect(key) { let column = this.columns.find((item) => item.code === key); if (column?.type === TableTypeEnum.MULTISELECT && column.options && column.code !== undefined) { this.optionEntries = new Map([ [column.code, Object.values(column.options)], ]); this.optionValues = this.optionEntries.get(key) || []; return true; } return false; } isDatePicker(key) { return (this.columns.find((item) => item.code === key)?.type === TableTypeEnum.DATE); } // Utility Methods dateConverter(value) { return new Date(value).toLocaleDateString('en-US'); } getCurrencySymbol(column) { return column.type === TableTypeEnum.AMOUNT && column.currency && this.isValidCurrencyCode(column.currency) ? column.currency : undefined; } isValidCurrencyCode(currencyCode) { return this.validCurrencyCodes.includes(currencyCode); } filterGlobal(event) { const target = event.target; const value = target.value.toLowerCase(); // Create a new filtered dataset const filteredData = this.data.filter((item) => { return this.globalFilterFields.some((field) => { const column = this.columns.find((col) => col.code === field); if (!column) { return false; } // Handle different column types if (column.type === TableTypeEnum.DATE) { const itemDate = this.formatDate(item[field]); return itemDate && itemDate.includes(value); } else if (column.type === TableTypeEnum.AMOUNT || column.type === TableTypeEnum.NUMBER) { return (item[field] && item[field].toString().toLowerCase().includes(value)); } else if (column.type === TableTypeEnum.COMPOSED) { // Handle composed type by searching for text in the composed cells return this.filterComposedColumn(item[field], value); } else { return (item[field] && item[field].toString().toLowerCase().includes(value)); } }); }); // Update the table's value this.dt.value = filteredData; // After filtering, update the totalRecords this.totalRecords = filteredData.length ?? 0; } filterComposedColumn(composedData, value) { if (composedData) { // Iterate over composed keys and check if any key's value contains the search text return Object.keys(composedData).some((key) => { const cellValue = composedData[key]; if (typeof cellValue === 'string') { return cellValue.toLowerCase().includes(value); } return false; }); } return false; } formatDate(date) { if (!date) return ''; if (date instanceof Date) { // Handle Date object const day = date.getDate().toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const year = date.getFullYear().toString(); return `${day}/${month}/${year}`; } else if (typeof date === 'string') { // Handle string date const parts = date.split('/'); if (parts.length === 3) { return `${parts[0]}/${parts[1]}/${parts[2]}`; } else { // Handle partial dates return date; } } return ''; } exportExcel() { this.exportExcelEvent.emit(); } exportPdf() { this.exportPdfEvent.emit(); } getImageStyle(style) { if (style) { const imageStyle = { width: style.width || 'auto', height: style.height || 'auto', }; // Fallback to empty string if margin, marginLeft, or marginRight are undefined if (style.margin) { imageStyle.margin = style.margin; } if (style.marginLeft) { imageStyle.marginLeft = style.marginLeft; } if (style.marginRight) { imageStyle.marginRight = style.marginRight; } if (style.marginTop) { imageStyle.marginTop = style.marginTop; } if (style.marginBottom) { imageStyle.marginBottom = style.marginBottom; } return imageStyle; } return {}; } getTitleStyle(style) { if (style) { return { color: style.color || 'inherit', fontSize: style.fontSize || 'inherit', textAlign: style.position || 'left', }; } return {}; } formatNumber(value, decimalPlaces, thousandSeparator = 'comma', decimalSeparator = 'dot') { if (value === null || value === undefined || isNaN(value)) return ''; // Convert the number to a string with full precision if decimalPlaces is undefined let formattedNumber = decimalPlaces !== undefined ? value.toFixed(decimalPlaces) : value.toString(); // Replace decimal separator (default is "dot") if (decimalSeparator === 'comma') { formattedNumber = formattedNumber.replace('.', ','); } // Apply thousand separator only if the number is >= 1000 if (thousandSeparator && Math.abs(value) >= 1000) { const parts = formattedNumber.split(decimalSeparator === 'comma' ? ',' : '.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator === 'comma' ? ',' : ' '); formattedNumber = parts.join(decimalSeparator === 'comma' ? ',' : '.'); } return formattedNumber; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.11", ngImport: i0, type: PTAdvancedPrimeTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.11", type: PTAdvancedPrimeTableComponent, selector: "pt-advanced-prime-table", inputs: { data: "data", columns: "columns", totalRecords: "totalRecords", rowsPerPage: "rowsPerPage", hasSearchFilter: "hasSearchFilter", hasExportExcel: "hasExportExcel", hasExportPDF: "hasExportPDF", hasColumnFilter: "hasColumnFilter", isPaginated: "isPaginated", actions: "actions", isSortable: "isSortable", loading: "loading", maxHeight: "maxHeight" }, outputs: { filter: "filter", search: "search", exportExcelEvent: "exportExcelEvent", exportPdfEvent: "exportPdfEvent" }, viewQueries: [{ propertyName: "dt", first: true, predicate: ["dt"], descendants: true }], ngImport: i0, template: "<div class=\"pt-advanced-prime-table table-container\">\n <p-table\n #dt\n [value]=\"data\"\n [loading]=\"loading\"\n [rows]=\"rows\"\n [paginator]=\"isPaginated\"\n [globalFilterFields]=\"globalFilterFields\"\n [rowsPerPageOptions]=\"rowsPerPage\"\n dataKey=\"id\"\n styleClass=\"p-datatable-gridlines\"\n styleClass=\"p-datatable-striped\"\n editMode=\"row\"\n [scrollable]=\"true\"\n [scrollHeight]=\"maxHeight !== null ? maxHeight : undefined\"\n (onFilter)=\"onFilter($event)\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"flex\">\n <div>\n <h3>Total: {{ totalRecords }}</h3>\n </div>\n\n <div>\n <!-- Clear filters -->\n <button\n *ngIf=\"hasSearchFilter\"\n pButton\n icon=\"pi pi-filter-slash\"\n class=\"p-button-rounded p-button-text\"\n (click)=\"clear(dt)\"\n title=\"Clear filters\"\n ></button>\n\n <!-- Export to Excel Button -->\n <button\n *ngIf=\"hasExportExcel\"\n pButton\n icon=\"pi pi-file-excel\"\n class=\"p-button-rounded p-button-text\"\n (click)=\"exportExcel()\"\n title=\"Export to Excel\"\n ></button>\n\n <!-- Export to PDF Button -->\n <button\n *ngIf=\"hasExportPDF\"\n pButton\n icon=\"pi pi-file-pdf\"\n class=\"p-button-rounded p-button-text\"\n (click)=\"exportPdf()\"\n title=\"Export to PDF\"\n ></button>\n </div>\n <div class=\"ml-auto\" *ngIf=\"hasSearchFilter\">\n <!-- Add this wrapper div with ml-auto class -->\n <p-iconField iconPosition=\"left\" class=\"ml-auto\">\n <p-inputIcon>\n <i class=\"pi pi-search\"></i>\n </p-inputIcon>\n <input\n pInputText\n type=\"text\"\n [(ngModel)]=\"searchValue\"\n (input)=\"filterGlobal($event)\"\n placeholder=\"Search keyword\"\n />\n </p-iconField>\n </div>\n </div>\n </ng-template>\n\n <ng-template pTemplate=\"header\">\n <tr class=\"sticky-header\">\n <ng-container *ngFor=\"let col of columns\">\n <th\n *ngIf=\"!col.children; else groupHeader\"\n [style.width]=\"getHeaderWidth(col)\"\n [style.padding]=\"'0px'\"\n colspan=\"1\"\n >\n <ng-container\n *ngIf=\"isSortable && col.isSortable !== false; else noSortHeader\"\n >\n <th\n pSortableColumn=\"{{ col.code }}\"\n [style.width]=\"getHeaderWidth(col)\"\n >\n <div\n class=\"header-container d-flex align-items-center justify-content-between\"\n [style.width]=\"col.width\"\n [style.padding]=\"'0px'\"\n [style.margin]=\"'10px'\"\n >\n <span>{{ col.title }}</span>\n <div\n class=\"icons d-flex align-items-center\"\n [style.width]=\"'77px'\"\n >\n <p-sortIcon field=\"{{ col.code }}\" />\n <ng-container *ngIf=\"col.isFilter !== false\">\n <p-columnFilter\n display=\"menu\"\n [field]=\"col.code\"\n display=\"menu\"\n [type]=\"getColumnFilterType(col)\"\n *ngIf=\"col.type === TableTypeEnum.COMPOSED\"\n showClearButton=\"false\"\n showApplyButton=\"false\"\n >\n <!-- TableTypeEnum.COMPOSED -->\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filterCallback=\"filterCallback\"\n (onFilter)=\"onFilter($event)\"\n >\n <div *ngFor=\"let composedName of col.composedNames\">\n <ng-container\n *ngIf=\"\n getComposedFieldType(col, composedName) ===\n TableTypeEnum.STRING\n \"\n >\n <p-multiSelect\n [ngModel]=\"filters[composedName]?.value\"\n [options]=\"filters[composedName]?.options\"\n (onChange)=\"\n onComposedFilterChange(\n composedName,\n $event.value\n )\n \"\n [placeholder]=\"\n filters[composedName]?.placeholder\n \"\n [display]=\"'chip'\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"custom-multiselect-item\">\n <img\n *ngIf=\"item.image\"\n [src]=\"item.image\"\n alt=\"icon\"\n class=\"filter-image\"\n />\n <span>{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-container>\n </div>\n\n <!-- Define itemTemplate here -->\n <ng-template let-item pTemplate=\"item\">\n <div class=\"custom-multiselect-item\">\n <img\n *ngIf=\"item.image\"\n [src]=\"item.image\"\n alt=\"icon\"\n class=\"filter-image\"\n />\n <span>{{ item.label }}</span>\n </div>\n </ng-template>\n </ng-template>\n </p-columnFilter>\n\n <!-- other TableTypeEnum.XXX -->\n <p-columnFilter\n display=\"menu\"\n [field]=\"col.code\"\n display=\"menu\"\n [type]=\"getColumnFilterType(col)\"\n *ngIf=\"col.type !== TableTypeEnum.COMPOSED\"\n hideOnClear=\"true\"\n >\n <!-- TableTypeEnum.NUMBER -->\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filterCallback=\"filterCallback\"\n *ngIf=\"col.type === TableTypeEnum.NUMBER\"\n (onFilter)=\"onFilter($event)\"\n >\n <input\n pInputText\n type=\"number\"\n [step]=\"\n col.decimalPlaces\n ? '0.' + '1'.padEnd(col.decimalPlaces, '0')\n : 'any'\n \"\n [ngModel]=\"value\"\n (ngModelChange)=\"filterCallback($event)\"\n [placeholder]=\"'Enter a number'\"\n />\n </ng-template>\n\n <!-- TableTypeEnum.DATE -->\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filterCallback=\"filterCallback\"\n *ngIf=\"col.type === TableTypeEnum.DATE\"\n (onFilter)=\"onFilter($event)\"\n >\n <p-calendar\n [ngModel]=\"value\"\n (ngModelChange)=\"\n onCalendarFilterChange(\n $event,\n col.code!,\n filterCallback\n )\n \"\n [dateFormat]=\"'dd/mm/yy'\"\n [placeholder]=\"'Choose a date'\"\n ></p-calendar>\n </ng-template>\n\n <!-- TableTypeEnum.MULTISELECT -->\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filterCallback=\"filterCallback\"\n *ngIf=\"col.type === TableTypeEnum.MULTISELECT\"\n (onFilter)=\"onFilter($event)\"\n >\n <p-multiSelect\n [options]=\"col.filterOptions\"\n [ngModel]=\"value\"\n (ngModelChange)=\"filterCallback($event)\"\n [display]=\"'chip'\"\n [placeholder]=\"'Choose option'\"\n class=\"custom-multiselect\"\n ></p-multiSelect>\n </ng-template>\n </p-columnFilter>\n </ng-container>\n </div>\n </div>\n </th>\n </ng-container>\n <ng-template #noSortHeader>\n <th>\n <div class=\"header-container\">\n <span>{{ col.title }}</span>\n <ng-container *ngIf=\"col.isFilter !== false\">\n <p-columnFilter\n *ngIf=\"col.type === 'AMOUNT'\"\n display=\"menu\"\n [field]=\"col.code\"\n [type]=\"getColumnFilterType(col)\"\n [currency]=\"getCurrencySymbol(col)\"\n ></p-columnFilter>\n\n <p-columnFilter\n *ngIf=\"col.type !== 'AMOUNT'\"\n display=\"menu\"\n [field]=\"col.code\"\n [type]=\"getColumnFilterType(col)\"\n >\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filterCallback=\"filterCallback\"\n *ngIf=\"getColumnFilterType(col) === 'date'\"\n >\n <p-calendar\n [ngModel]=\"value\"\n (ngModelChange)=\"filterCallback($event)\"\n [dateFormat]=\"'dd/mm/yy'\"\n ></p-calendar>\n </ng-template>\n\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filterCallback=\"filterCallback\"\n *ngIf=\"getColumnFilterType(col) === 'multiSelect'\"\n >\n <p-multiSelect\n [options]=\"col.filterOptions\"\n [ngModel]=\"value\"\n (ngModelChange)=\"filterCallback($event)\"\n [display]=\"'chip'\"\n placeholder=\"Select\"\n class=\"custom-multiselect\"\n ></p-multiSelect>\n </ng-template>\n </p-columnFilter>\n </ng-container>\n </div>\n </th>\n </ng-template>\n </th>\n <!-- Grouped headers -->\n <ng-template #groupHeader>\n <th\n [attr.colspan]=\"col.children?.length\"\n [style.width]=\"getHeaderWidth(col)\"\n [style.text-align]=\"'center'\"\n >\n <span>{{ col.title }}</span>\n </th>\n </ng-template>\n </ng-container>\n </tr>\n <!-- Child headers (Second Row) -->\n <tr *ngIf=\"hasGroupedColumns\">\n <ng-container *ngFor=\"let col of columns\">\n <ng-container *ngIf=\"col.children\">\n <th\n *ngFor=\"let child of col.children\"\n [style.width]=\"getHeaderWidth(child)\"\n [style.padding]=\"'0px'\"\n >\n <!-- Sortable/Filterable header logic for child columns -->\n </th>\n </ng-container>\n </ng-container>\n </tr>\n </ng-template>\n\n <!-- Empty message template -->\n <ng-template pTemplate=\"emptymessage\">\n <div class=\"empty-message\">\n <i class=\"pi pi-info-circle\"></i>\n <p>No records available to display.</p>\n </div>\n </ng-template>\n\n <!-- Body -->\n <ng-template\n pTemplate=\"body\"\n let-data\n let-editing=\"editing\"\n let-ri=\"rowIndex\"\n >\n <!-- Render a table row and make it editable if `isEdit` is true -->\n <tr *ngIf=\"!loading\" [pEditableRow]=\"isEdit ? data : null\">\n <!-- Loop through each column -->\n <ng-container *ngFor=\"let col of columns\">\n <!-- Check if the column has children -->\n <ng-container *ngIf=\"!col.children; else childColumns\">\n <!-- Render a single cell for columns without children -->\n <ng-container\n *ngIf=\"col.code !== undefined && data[col.code] !== undefined\"\n >\n <td\n *ngIf=\"isEditable(col.code); else normalTD\"\n [style.width]=\"getHeaderWidth(col)\"\n >\n <!-- Editable input for the column -->\n <ng-container *ngIf=\"isMultiSelect(col.code); else datePicker\">\n <p-cellEditor>\n <ng-template pTemplate=\"input\">\n <p-multiSelect\n appendTo=\"body\"\n [ngModel]=\"data[col.code]\"\n [style]=\"{ width: '100%' }\"\n (ngModelChange)=\"\n changeHandler(data.id, col.code, $event)\n \"\n [options]=\"optionValues\"\n ></p-multiSelect>\n </ng-template>\n <ng-template pTemplate=\"output\">\n <div class=\"multi-select-container\">\n <ng-container *ngFor=\"let rec of data[col.code]\">\n <p-tag [value]=\"rec\"></p-tag>\n </ng-container>\n </div>\n </ng-template>\n </p-cellEditor>\n </ng-container>\n\n <ng-template #datePicker>\n <ng-container\n *ngIf=\"isDatePicker(col.code); else normalInput\"\n >\n <p-cellEditor>\n <ng-template pTemplate=\"input\">\n <p-calendar\n [inputId]=\"data[col.code]\"\n [ngModel]=\"data[col.code]\"\n (ngModelChange)=\"\n changeHandler(data.id, col.code, $event)\n \"\n [dateFormat]=\"'dd/mm/yy'\"\n ></p-calendar>\n </ng-template>\n <ng-template pTemplate=\"output\">\n {{ data[col.code] | customDate }}\n </ng-template>\n </p-cellEditor>\n </ng-container>\n </ng-template>\n\n <ng-template #normalInput>\n <p-cellEditor>\n <ng-template pTemplate=\"input\">\n <input\n pInputText\n type=\"text\"\n [ngModel]=\"data[col.code]\"\n (change)=\"onChange($event, data.id, col.code)\"\n />\n </ng-template>\n <ng-template pTemplate=\"output\">\n <ng-container\n *ngIf=\"\n col.type === TableTypeEnum.AMOUNT;\n else normalOutput\n \"\n >\n {{\n data[col.code]\n | customCurrency\n : getCurrencySymbol(col)\n : col.decimalPlaces\n : col.thousandSeparator\n : col.decimalSeparator\n }}\n </ng-container>\n <ng-template #normalOutput>\n {{ data[col.code] }}\n </ng-template>\n </ng-template>\n </p-cellEditor>\n </ng-template>\n </td>\n\n <ng-template #normalTD>\n <td [style.width]=\"getHeaderWidth(col)\">\n <!-- COMPOSED -->\n <ng-container *ngIf=\"col.type === TableTypeEnum.COMPOSED\">\n <div class=\"composed-cell\">\n <ng-container\n *ngFor=\"\n let composedName of col.composedNames;\n let i = index\n \"\n >\n <!-- Check if the composedType is IMAGE -->\n <ng-container\n *ngIf=\"\n col.composedTypes &&\n col.composedTypes[i] === TableTypeEnum.IMAGE\n \"\n >\n <img\n [src]=\"data[col.code][composedName]\"\n alt=\"composed-img\"\n class=\"composed-image\"\n [ngStyle]=\"getImageStyle(col.composedStyles?.[composedName])\"\n />\n </ng-container>\n\n <!-- Check if the composedType is STRING -->\n <ng-container\n *ngIf=\"\n col.composedTypes &&\n col.composedTypes[i] === TableTypeEnum.STRING\n \"\n >\n <span\n class=\"composed-text\"\n [ngStyle]=\"getTitleStyle(col.composedStyles?.[composedName])\"\n >\n {{ data[col.code][composedName] }}\n </span>\n </ng-container>\n </ng-container>\n </div>\n </ng-container>\n\n <!-- AMOUNT-->\n <ng-container *ngIf=\"col.type === TableTypeEnum.AMOUNT\">\n {{\n data[col.code]\n | customCurrency\n : getCurrencySymbol(col)\n : col.decimalPlaces\n : col.thousandSeparator\n : col.decimalSeparator\n }}\n </ng-container>\n\n <!-- NUMBER-->\n <ng-container *ngIf=\"col.type === TableTypeEnum.NUMBER\">\n {{\n formatNumber(\n data[col.code],\n col.decimalPlaces,\n col.thousandSeparator,\n col.decimalSeparator\n )\n }}\n </ng-container>\n\n <!-- DATE -->\n <ng-container *ngIf=\"col.type === TableTypeEnum.DATE\">\n <!-- Format the date using your formatDate method -->\n {{ formatDate(data[col.code]) }}\n </ng-container>\n\n <!-- STRING, MULTISELECT-->\n <ng-container\n *ngIf=\"\n [\n TableTypeEnum.STRING,\n TableTypeEnum.MULTISELECT\n ].includes(col.type!)\n \"\n >\n {{ data[col.code] }}\n </ng-container>\n </td>\n </ng-template>\n </ng-container>\n </ng-container>\n\n <!-- Render child columns if the column has children -->\n <ng-template #childColumns>\n <ng-container *ngFor=\"let child of col.children\">\n <ng-container\n *ngIf=\"\n child.code !== undefined && data[child.code] !== undefined\n \"\n >\n <td [style.width]=\"getHeaderWidth(child)\">\n <!-- Render editable or normal cells for child columns -->\n <ng-container\n *ngIf=\"isEditable(child.code); else childNormalTD\"\n >\n <p-cellEditor>\n <ng-template pTemplate=\"input\">\n <input\n pInputText\n type=\"text\"\n [ngModel]=\"data[child.code]\"\n (change)=\"onChange($event, data.id, child.code)\"\n />\n </ng-template>\n <ng-template pTemplate=\"output\">\n {{ data[child.code] }}\n </ng-template>\n </p-cellEditor>\n </ng-container>\n\n <ng-template #childNormalTD>\n {{ data[child.code] }}\n </ng-template>\n </td>\n </ng-container>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <!-- Render action buttons if there are any actions defined -->\n <td *ngIf=\"actions?.length\">\n <div class=\"action-buttons-container\">\n <div *ngIf=\"isDelete\">\n <button\n pButton\n pRipple\n type=\"button\"\n icon=\"pi pi-trash\"\n (click)=\"Delete(data.id)\"\n class=\"p-button-rounded p-button-text\"\n ></button>\n </div>\n <div>\n <button\n pInitEditableRow\n *ngIf=\"!editing\"\n pButton\n pRipple\n type=\"button\"\n icon=\"pi pi-pencil\"\n (click)=\"initEditableRow(data)\"\n class=\"p-button-rounded p-button-text\"\n ></button>\n <button\n *ngIf=\"editing\"\n pSaveEditableRow\n pButton\n pRipple\n type=\"button\"\n icon=\"pi pi-check\"\n (click)=\"saveEditableRow(data)\"\n class=\"p-button-rounded p-button-text\"\n ></button>\n <button\n *ngIf=\"editing\"\n pCancelEditableRow\n pButton\n pRipple\n type=\"button\"\n icon=\"pi pi-times\"\n (click)=\"cancelEditableRow(data)\"\n class=\"p-button-rounded p-button-text\"\n ></button>\n </div>\n </div>\n </td>\n </tr>\n </ng-template