import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, AfterViewInit, ChangeDetectorRef, ElementRef} from '@angular/core'; import { MatPaginator, MatPaginatorIntl, PageEvent } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; import { PagerTranslationOptions } from './models/pagerTranslationOptions'; import { HandleMenuAction } from './interface/handle-menu-action.interface'; import { MatSort, Sort } from '@angular/material/sort'; import { SortConfig } from './interface/sortConfig.interface'; import { PaginConfig } from './interface/paginConfig.interface'; import { Trash2 as iconTrash, CircleSlash2 as circleSlash2, User, Info, Trash2Icon as trash2 } from 'lucide-angular'; import { Lucide } from './interface/lucide.interface'; import { HandleAction } from './interface/handle-action.interface'; import { ISearchConfig, MenuOptions } from './interface/menu-options.interface'; import { ActionByItemEvent, EmptyDataInfo, TableColumns, TableColumnsActions } from './interface'; import { SORT_DIRECTION } from './enum/sort-direction.enum'; import { PAGINATOR } from './enum/paginator.enum'; import { TEXT_POSITION } from './enum'; import { CellUpdate, GridUpdate } from './interface/cell-update.interface'; import { FormControl } from '@angular/forms'; export interface Task { completed: boolean; id?: number; subtasks?: Task[]; } @Component({ selector: 'kit-data-grid', templateUrl: './grid.component.html', }) export class GridComponent implements OnInit, OnChanges, AfterViewInit { @ViewChild(MatSort) sort: MatSort = new MatSort; @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator | undefined; @ViewChild(MatPaginator, { static: true }) paginator2: MatPaginator | undefined; @ViewChild('paginatorContainer', { static: false }) paginatorContainer!: ElementRef; @Input() data: any[] = []; @Input() dataLength: number = 0; @Input() uniqueList: any[] = []; @Input() columns: TableColumns[] = []; @Input() displayedColumns: string[] = []; @Input() pagerTranslationOptions: PagerTranslationOptions| null = null; @Input() isHtmlDataSource = false; @Input() isLoading = false; @Input() hasMore = false; @Input() isTotalPaginator: boolean = false; @Input() sortConfig: SortConfig = { sortableColumns: [], defaultSortColumn: '', defaultSortDirection: SORT_DIRECTION.ASC }; @Input() enableRowSelection: boolean = false; // Input y Ouput de Paginador personalizado // @Input() paginConfig?: PaginConfig = { totalElements: 0, elementsPerPage: 10, currentPage: 0 }; @Input() emptyDataInfo!:EmptyDataInfo; @Input() cellUpdates?: GridUpdate; @Input() hideHeaderCheckbox: boolean = false; @Input() hideHeaderDeleteButton: boolean = false; @Input() filterOptions: MenuOptions[] = []; @Output() pageChanged = new EventEmitter(); @Output() filterChanged = new EventEmitter(); selectedPaginator: PAGINATOR = PAGINATOR.PAGINATOR_1; totalElements: number = 0; // currentPageIndex: number = 0; totalPages: number = 0; ///// @Output() clickEvent: EventEmitter = new EventEmitter(); @Output() clickEventItem: EventEmitter = new EventEmitter(); @Output() clickEventMenuAction: EventEmitter = new EventEmitter(); @Output() deleteEvent: EventEmitter> = new EventEmitter(); @Output() deleteRowEvent: EventEmitter = new EventEmitter(); @Output() searchAgain: EventEmitter = new EventEmitter(); @Output() rowSelected = new EventEmitter(); @Output() radioSelected = new EventEmitter(); @Output() actionByItemClick = new EventEmitter(); readonly userIcon: Lucide = { iconSvg: User, size: '16', color: '#5D6F85', } @Input() seachAutocompleteconfig: ISearchConfig= { placeholder: '', label: '', errorMessage: '', icon: this.userIcon }; public menuFilterControl = new FormControl(''); // o sin validadores private lastUpdateTimestamp: number = 0; public iconTrash:Lucide = { iconSvg: iconTrash, color: "#5D6F85", size:'16', class:'lucide-angular-icon' } public circleSlash2 :Lucide = { iconSvg: circleSlash2, color: "#5D6F85", size:'16', class:'lucide-angular-icon' } public trash2 :Lucide = { iconSvg: trash2, color: "#5D6F85", size:'16', class:'lucide-angular-icon' } public informationIcon : Lucide = { iconSvg: Info, color: "#5D6F85", size:'16', class:'lucide-angular-icon-info' } public TEXT_POSITION = TEXT_POSITION; selection = new SelectionModel(true, []); dataSource = new MatTableDataSource([]); resultsLength = 0; length = 0; elementsPerPage: number = 10; hidePageSize = true; disabled = false; isSelect = false; public selectedRadioElement: any = null; isScrolled = false; constructor(private paginatorIntl: MatPaginatorIntl,private paginatorConfig: MatPaginatorIntl,private cdr:ChangeDetectorRef) { } ngOnInit(): void { if (this.paginator) { this.dataSource.paginator = this.paginator; } const existeSelect = this.columns.find(item => item.columnDef === 'select'); if (existeSelect) { this.isSelect = true; } if(this.pagerTranslationOptions) { this.setPagerTranslationOptions() } } ngAfterViewInit() { this.paginatorPaginLabel() this.initializeSort(); this.customizePaginator(); } customizePaginator() { this.paginatorIntl.getRangeLabel = (page: number, pageSize: number, length: number) => { if (length === 0) { return `0 de 0`; } const startIndex = page * pageSize; // Índice basado en 0 const endIndex = Math.min(startIndex + pageSize, length); // Asegurar que no sobrepase el total return `${startIndex + 1} - ${endIndex}`; }; this.paginatorIntl.changes.next(); // Notifica a la UI que debe actualizarse this.cdr.detectChanges(); // Fuerza la detección de cambios en el componente } ngOnChanges(changes: SimpleChanges): void { // Tu lógica existente if (changes['data']) { this.dataSource.data = this.data; if(this.length === 0){ this.selection.clear() this.selectedRadioElement = null; } } if(changes['pagerTranslationOptions']) { if(this.pagerTranslationOptions) { this.setPagerTranslationOptions() } } if (changes['paginConfig']) { this.initializePaginatorConfig(); this.calculateTotalPages(); if (this.paginator2) { this.paginator2.pageIndex = this.currentPageIndex; } } // NUEVA LÓGICA: Procesar actualizaciones de celdas if (changes['cellUpdates'] && this.cellUpdates) { this.processCellUpdates(); } // NUEVA LÓGICA: if (changes['filterOptions']) { this.filterOptions = this.filterOptions.map(option => { return { ...option, textOption: option.textOption || '' // Asegurar que textOption esté definido }; }); this.validatorError(); } } private processCellUpdates(): void { if (!this.cellUpdates || !this.cellUpdates.updates.length) { return; } // Evitar procesar la misma actualización múltiples veces const updateTimestamp = this.cellUpdates.timestamp || Date.now(); if (updateTimestamp <= this.lastUpdateTimestamp) { return; } // Crear una copia de los datos actuales const updatedData = [...this.dataSource.data]; let hasChanges = false; // Aplicar cada actualización this.cellUpdates.updates.forEach(update => { const rowIndex = this.findRowIndex(updatedData, update); if (rowIndex !== -1) { // Crear una copia del objeto fila para mantener inmutabilidad const updatedRow = { ...updatedData[rowIndex] }; // Actualizar el valor de la columna específica if (Object.prototype.hasOwnProperty.call(updatedRow, update.columnDef)) { updatedRow[update.columnDef] = update.newValue; updatedData[rowIndex] = updatedRow; hasChanges = true; } } }); if (hasChanges) { this.dataSource.data = updatedData; this.lastUpdateTimestamp = updateTimestamp; this.cdr.detectChanges(); } } public shouldShowProgressBar(element: any): boolean { return element.progress !== null && element.progress !== undefined && element.progress !== '' && element.progress >= 0; } private findRowIndex(data: any[], update: CellUpdate): number { const identifier = update.rowIdentifier || 'id'; return data.findIndex(row => { // Buscar por el ID específico proporcionado if (row[identifier] === update.rowId) { return true; } // Fallback: buscar por 'id' si no se encuentra con el identificador especificado if (identifier !== 'id' && row.id === update.rowId) { return true; } // Fallback adicional: buscar por 'rowId' si existe if (row.rowId === update.rowId) { return true; } return false; }); } initializePaginatorConfig(): void { if (this.paginConfig) { this.totalElements = this.paginConfig.totalElements || 0; this.elementsPerPage = this.paginConfig.elementsPerPage || 10; this.currentPageIndex = this.paginConfig.currentPage || 0; } this.length = 0 } calculateTotalPages(): void { this.totalPages = Math.ceil(this.totalElements / this.elementsPerPage); } onSearchAgain(){ this.searchAgain.emit() } onPageChange(event: PageEvent) { this.currentPageIndex = event.pageIndex; this.pageChanged.emit(event); } private initializeSort() { if (this.sort) { this.dataSource.sort = this.sort; if (this.sortConfig.defaultSortColumn && this.isSortable(this.sortConfig.defaultSortColumn)) { this.sort.active = this.sortConfig.defaultSortColumn; this.sort.direction = this.sortConfig.defaultSortDirection; } this.sort.sortChange.subscribe((sortState: Sort) => { this.handleSortChange(sortState);}) } } private paginatorPaginLabel(){ let rangeSepratorLabel = 'de'; if(this.pagerTranslationOptions && this.pagerTranslationOptions.rangeSepratorLabel){ rangeSepratorLabel = this.pagerTranslationOptions.rangeSepratorLabel } if(this.paginator){ this.paginator._intl.getRangeLabel = (page: number, pageSize: number, length: number) => { if (length === 0 || pageSize === 0) { return `0 ${rangeSepratorLabel} ${length}`; } const startIndex = page * pageSize; const endIndex = Math.min(startIndex + pageSize, length); return `${startIndex + 1} ${rangeSepratorLabel} ${endIndex}`; }; this.paginator._intl.changes.next(); } if(this.paginator2){ this.paginator2._intl.getRangeLabel = (page: number, pageSize: number, length: number) => { if (length === 0 || pageSize === 0) { return `0 ${rangeSepratorLabel} ${length}`; } const startIndex = page * pageSize; const endIndex = Math.min(startIndex + pageSize, length); return `${startIndex + 1} ${rangeSepratorLabel} ${endIndex}`; }; this.paginator2._intl.changes.next(); } } handleSortChange(sortState: Sort) { if (sortState.direction === '') { sortState.direction = 'asc'; } this.dataSource.sort = this.sort; this.sort.direction = sortState.direction; this.dataSource._updateChangeSubscription(); } isSortable(columnDef: string): boolean { return this.sortConfig.sortableColumns.includes(columnDef); } detectUnique(value: string): boolean { return /Unique/i.test(value); } deleteSelectedRows() { const arrayDeIDs = this.selection.selected.map(elemento => elemento.id); return this.deleteEvent.emit(arrayDeIDs); } deleteSelectedRow(element: any) { return this.deleteRowEvent.emit(element); } handleAction(action: TableColumnsActions, element: unknown, templateId?: string): void { this.clearMenuFilters() return this.clickEvent.emit({'action_clicked': action, 'element': element, 'idSubRow':templateId}); } clearMenuFilters() { this.menuFilterControl.reset(); this.cdr.detectChanges(); } handleMenuAction(menuOption: MenuOptions, element: unknown, templateId?: string): void { return this.clickEventMenuAction.emit({'action_clicked': menuOption, 'element': element, 'idSubRow':templateId}); } onActionByItemClick(action: TableColumnsActions, element: any, column: TableColumns): void { const rowIndex = this.dataSource.data.indexOf(element); this.actionByItemClick.emit({ action, row: element, column, columnDef: column.columnDef, rowIndex }); } isAllSelected() { const numSelected = this.selection.selected.length; const numRows = this.dataSource.data.length; return numSelected === numRows; } toggleAllRows() { if (this.isAllSelected()) { this.selection.clear(); } else { this.selection.select(...this.dataSource.data); } if (this.enableRowSelection) { this.rowSelected.emit(this.selection.selected); } } onRowSelected(event: any, element: any) { // Primero ejecutamos la funcionalidad original if (event) { this.selection.toggle(element); } if (this.enableRowSelection) { this.rowSelected.emit( this.selection.selected ); } } onRadioSelected(element: any): void { this.selectedRadioElement = element; this.radioSelected.emit(element); } isRadioSelected(element: any): boolean { return this.selectedRadioElement === element; } checkboxLabel(row?: any): string { if (!row) { return `${this.isAllSelected() ? 'deselect' : 'select'} all`; } return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`; } isActionDisabled(element: any, action: any): boolean { // Verifica si el elemento tiene disabledActions y si el ID de la acción está incluido return element.disabledActions?.includes(action.id) ?? false; } private setPagerTranslationOptions() { if(this.pagerTranslationOptions) { this.paginatorConfig.itemsPerPageLabel = this.pagerTranslationOptions.itemsPerPageLabel; this.paginatorConfig.firstPageLabel = this.pagerTranslationOptions.firstPageLabel; this.paginatorConfig.lastPageLabel = this.pagerTranslationOptions.lastPageLabel; this.paginatorConfig.nextPageLabel = this.pagerTranslationOptions.nextPageLabel; this.paginatorConfig.previousPageLabel = this.pagerTranslationOptions.previousPageLabel; this.paginatorConfig.getRangeLabel = (page: number, pageSize: number, length: number) => { if (length == 0 || pageSize == 0) { return `0 ${this.pagerTranslationOptions?.rangeSepratorLabel ? this.pagerTranslationOptions?.rangeSepratorLabel : 'of'} ${length}`; } length = Math.max(length, 0); const startIndex = page * pageSize; const endIndex = startIndex < length ? Math.min(startIndex + pageSize, length) : startIndex + pageSize; return `${startIndex + 1} - ${endIndex} ${this.pagerTranslationOptions?.rangeSepratorLabel ? this.pagerTranslationOptions?.rangeSepratorLabel : 'of'} ${length}`; }; } } public includedRowId(actions?: TableColumnsActions[]) { return actions?.find((action) => action.rowId) } public filterActions(actions: TableColumnsActions[]|undefined, elementRowActionId:string|undefined) { if(this.includedRowId(actions) && elementRowActionId) { return actions?.filter((action) => action.rowId === elementRowActionId) } else if(this.includedRowId(actions) && !elementRowActionId){ return actions?.filter((action) => !action.rowId) } else { return actions } } public getInitials(name: string){ const initials = name .split(' ') .map(word => word.charAt(0)) .join(''); return initials; } public onMenuFilterChange(): void { const filterValue = this.menuFilterControl.value?.toLowerCase(); this.filterChanged.emit(filterValue); this.cdr.detectChanges(); } validatorError(): void { const value = this.menuFilterControl.value?.trim() || ''; if (value.length > 0 && this.filterOptions.length === 0) { this.menuFilterControl.setErrors({ noResults: true }); } else { this.menuFilterControl.setErrors(null); } } onTableScroll(event: Event): void { const target = event.target as HTMLElement; this.isScrolled = target.scrollTop > 0; const header = target.querySelector('.mat-mdc-header-row'); if (header) { if (this.isScrolled) { header.classList.add('scrolled'); } else { header.classList.remove('scrolled'); } } } }