ng-prime-tools
Version:
An advanced PrimeNG table for Angular
592 lines (588 loc) • 397 kB
JavaScript
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