2026-04-23 01:22:44 +03:30
|
|
|
import { Maybe } from '@/core';
|
2025-12-04 21:07:18 +03:30
|
|
|
import { PaginatorComponent, UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit';
|
2026-04-16 22:19:46 +03:30
|
|
|
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
|
2025-12-04 21:07:18 +03:30
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
|
|
import {
|
|
|
|
|
Component,
|
|
|
|
|
computed,
|
|
|
|
|
ContentChild,
|
2026-03-10 13:36:45 +03:30
|
|
|
ElementRef,
|
2025-12-04 21:07:18 +03:30
|
|
|
EventEmitter,
|
|
|
|
|
Input,
|
|
|
|
|
Output,
|
2026-03-10 13:36:45 +03:30
|
|
|
Renderer2,
|
2025-12-04 21:07:18 +03:30
|
|
|
signal,
|
|
|
|
|
TemplateRef,
|
|
|
|
|
} from '@angular/core';
|
|
|
|
|
import { ButtonModule } from 'primeng/button';
|
|
|
|
|
import { DrawerModule } from 'primeng/drawer';
|
|
|
|
|
import { PaginatorModule } from 'primeng/paginator';
|
|
|
|
|
import { SkeletonModule } from 'primeng/skeleton';
|
|
|
|
|
import { TableModule } from 'primeng/table';
|
2026-04-23 01:22:44 +03:30
|
|
|
import { KeyValueComponent } from '../key-value.component/key-value.component';
|
2025-12-04 21:07:18 +03:30
|
|
|
import { TableActionRowComponent } from '../table-action-row.component';
|
|
|
|
|
|
2026-04-24 02:23:47 +03:30
|
|
|
type TDataType = 'simple' | 'price' | 'boolean' | 'date' | 'nested' | 'index' | 'id' | 'thumbnail';
|
2025-12-09 20:17:00 +03:30
|
|
|
export interface IColumn<T = any> {
|
|
|
|
|
field: T extends object ? keyof T | string : string;
|
2025-12-04 21:07:18 +03:30
|
|
|
header: string;
|
|
|
|
|
width?: string;
|
|
|
|
|
minWidth?: string;
|
|
|
|
|
canCopy?: boolean;
|
2026-04-24 02:23:47 +03:30
|
|
|
type?: TDataType;
|
|
|
|
|
|
|
|
|
|
nestedOption?: {
|
|
|
|
|
path: string;
|
|
|
|
|
type?: TDataType;
|
|
|
|
|
};
|
2026-04-16 22:19:46 +03:30
|
|
|
thumbnailOptions?: {
|
|
|
|
|
editable: boolean;
|
|
|
|
|
deletable: boolean;
|
|
|
|
|
showPreview: boolean;
|
|
|
|
|
};
|
|
|
|
|
dateOption?: {
|
|
|
|
|
expiredMode?: boolean;
|
|
|
|
|
dateTime?: boolean;
|
|
|
|
|
onlyTime?: boolean;
|
|
|
|
|
};
|
2025-12-09 20:17:00 +03:30
|
|
|
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
2025-12-04 21:07:18 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-page-data-list',
|
|
|
|
|
templateUrl: './page-data-list.component.html',
|
2026-03-16 00:35:34 +03:30
|
|
|
host: {
|
2026-04-13 13:22:40 +03:30
|
|
|
class: 'block w-full h-full overflow-hidden',
|
2026-03-16 00:35:34 +03:30
|
|
|
},
|
2025-12-04 21:07:18 +03:30
|
|
|
imports: [
|
|
|
|
|
CommonModule,
|
|
|
|
|
TableActionRowComponent,
|
|
|
|
|
UikitEmptyStateComponent,
|
|
|
|
|
TableModule,
|
|
|
|
|
ButtonModule,
|
|
|
|
|
SkeletonModule,
|
|
|
|
|
PaginatorModule,
|
|
|
|
|
DrawerModule,
|
|
|
|
|
PaginatorComponent,
|
|
|
|
|
UikitCopyComponent,
|
2026-04-23 01:22:44 +03:30
|
|
|
KeyValueComponent,
|
2025-12-04 21:07:18 +03:30
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
export class PageDataListComponent<I> {
|
2026-03-10 13:36:45 +03:30
|
|
|
constructor(
|
|
|
|
|
private host: ElementRef,
|
|
|
|
|
private renderer: Renderer2,
|
|
|
|
|
) {}
|
2025-12-04 21:07:18 +03:30
|
|
|
|
2026-03-16 00:35:34 +03:30
|
|
|
@Input({ required: true }) pageTitle!: string;
|
2025-12-04 21:07:18 +03:30
|
|
|
@Input() addNewCtaLabel?: string;
|
2026-03-16 00:35:34 +03:30
|
|
|
@Input({ required: true }) columns!: IColumn[];
|
|
|
|
|
@Input({ required: true }) items!: I[];
|
|
|
|
|
@Input({ required: true }) loading!: boolean;
|
2025-12-04 21:07:18 +03:30
|
|
|
@Input() emptyPlaceholderTitle: string = 'موردی برای نمایش وجود ندارد';
|
|
|
|
|
@Input() emptyPlaceholderDescription?: string = '';
|
|
|
|
|
@Input() emptyPlaceholderCtaLabel?: string;
|
|
|
|
|
@Input() totalRecords!: number;
|
|
|
|
|
@Input() perPage?: number = 10;
|
|
|
|
|
@Input() currentPage?: number = 1;
|
|
|
|
|
@Input() showEdit: boolean = false;
|
|
|
|
|
@Input() showDelete: boolean = false;
|
|
|
|
|
@Input() showDetails: boolean = false;
|
|
|
|
|
@Input() showAdd: boolean = false;
|
2026-04-24 23:01:44 +03:30
|
|
|
@Input() showRefresh: boolean = true;
|
2025-12-04 21:07:18 +03:30
|
|
|
@Input() isFiltered: boolean = false;
|
2026-03-10 13:36:45 +03:30
|
|
|
@Input() fullHeight?: boolean = false;
|
2026-03-16 00:35:34 +03:30
|
|
|
@Input() height: string = '';
|
2026-04-23 01:22:44 +03:30
|
|
|
@Input() expandable?: boolean = false;
|
|
|
|
|
@Input() expandableItemKey?: string = '';
|
|
|
|
|
@Input() expandColumns?: Maybe<IColumn[]> = null;
|
|
|
|
|
@Input() dataKey?: string = 'items';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
|
|
|
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
|
|
|
|
|
@ContentChild('caption', { static: true }) caption!: TemplateRef<any> | null;
|
2026-04-13 13:22:40 +03:30
|
|
|
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
|
2026-04-23 01:22:44 +03:30
|
|
|
@ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null;
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
|
|
|
@Output() onEdit = new EventEmitter<I>();
|
|
|
|
|
@Output() onDelete = new EventEmitter<I>();
|
|
|
|
|
@Output() onDetails = new EventEmitter<I>();
|
|
|
|
|
@Output() onAdd = new EventEmitter<void>();
|
|
|
|
|
@Output() pageChange = new EventEmitter<any>();
|
2026-04-16 22:19:46 +03:30
|
|
|
@Output() onThumbnailClick = new EventEmitter<I>();
|
2026-04-24 23:01:44 +03:30
|
|
|
@Output() onRefresh = new EventEmitter();
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
|
|
|
filterDrawerVisible = signal<boolean>(false);
|
2026-04-23 01:22:44 +03:30
|
|
|
expandedRows: { [key: string]: boolean } = {};
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
|
|
|
ngOnInit() {
|
2026-03-16 00:35:34 +03:30
|
|
|
if (this.height)
|
|
|
|
|
this.renderer.setStyle(
|
|
|
|
|
this.host.nativeElement,
|
|
|
|
|
'height',
|
|
|
|
|
this.fullHeight ? '100cqmin' : this.height,
|
|
|
|
|
);
|
2025-12-04 23:34:00 +03:30
|
|
|
// if (this.fullHeight) {
|
|
|
|
|
// }
|
2025-12-04 21:07:18 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
edit = (item: I) => {
|
|
|
|
|
this.onEdit.emit(item);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
remove = (item: I) => {
|
|
|
|
|
this.onDelete.emit(item);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
details = (item: I) => {
|
|
|
|
|
this.onDetails.emit(item);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
openAddForm = () => {
|
|
|
|
|
this.onAdd.emit();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
openFilter = () => {
|
|
|
|
|
this.filterDrawerVisible.set(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
closeFilter = () => {
|
|
|
|
|
this.filterDrawerVisible.set(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onPage = ($event: any) => {
|
|
|
|
|
this.pageChange.emit($event);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-16 22:19:46 +03:30
|
|
|
openThumbnailModal = (item: I) => {
|
|
|
|
|
// this.
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-04 21:07:18 +03:30
|
|
|
get showPaginator() {
|
|
|
|
|
return !!(this.totalRecords && this.perPage && this.totalRecords > this.perPage);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 22:19:46 +03:30
|
|
|
getPreviewClasses(item: Record<string, any>, column: IColumn): any {
|
|
|
|
|
if (!item || !column || column.customDataModel) return '';
|
|
|
|
|
try {
|
|
|
|
|
const { field } = column;
|
|
|
|
|
|
|
|
|
|
const data = item[String(field)];
|
|
|
|
|
switch (column.type) {
|
|
|
|
|
case 'date':
|
|
|
|
|
if (column.dateOption?.expiredMode) {
|
|
|
|
|
if (jalaliDiff(new Date(), data) > 0) {
|
|
|
|
|
return 'text-error';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
default:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 21:07:18 +03:30
|
|
|
getCell(item: Record<string, any>, column: IColumn): any {
|
|
|
|
|
if (!item || !column) return '';
|
|
|
|
|
try {
|
2026-04-24 02:23:47 +03:30
|
|
|
let { field, type } = column;
|
2025-12-26 01:20:44 +03:30
|
|
|
|
2025-12-04 21:07:18 +03:30
|
|
|
if (column.customDataModel) {
|
|
|
|
|
return this.renderCustom(column, item);
|
|
|
|
|
}
|
2026-04-24 02:23:47 +03:30
|
|
|
|
|
|
|
|
let data = item[String(field)];
|
|
|
|
|
if (column.type === 'nested') {
|
|
|
|
|
const path = column.nestedOption?.path;
|
|
|
|
|
if (!path) return '-';
|
|
|
|
|
data = path
|
|
|
|
|
.split('.')
|
|
|
|
|
.reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : null), item);
|
|
|
|
|
type = column.nestedOption?.type || 'simple';
|
2025-12-06 17:48:16 +03:30
|
|
|
}
|
|
|
|
|
|
2026-04-24 02:23:47 +03:30
|
|
|
if (type) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'id':
|
|
|
|
|
if (!data) return '-';
|
|
|
|
|
return `${data.slice(0, 5)}...${data.slice(data.length - 5, data.length)}`;
|
|
|
|
|
case 'date':
|
|
|
|
|
if (!data) return '-';
|
|
|
|
|
return formatJalali(data);
|
|
|
|
|
case 'boolean':
|
|
|
|
|
return data ? 'بله' : 'خیر';
|
|
|
|
|
case 'price':
|
|
|
|
|
return formatWithCurrency(data, false, 'ریال');
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-04 21:07:18 +03:30
|
|
|
if (data === undefined || data === null) {
|
|
|
|
|
return '-';
|
|
|
|
|
}
|
|
|
|
|
if (typeof data === 'object') {
|
|
|
|
|
return data.title || '-';
|
|
|
|
|
}
|
|
|
|
|
return data || '-';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return '-';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getTemplate(col: IColumn): TemplateRef<any> | null {
|
|
|
|
|
const v = col.customDataModel;
|
|
|
|
|
return v instanceof TemplateRef ? (v as TemplateRef<any>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderCustom(column: IColumn, item: any): any {
|
|
|
|
|
const v = column.customDataModel;
|
|
|
|
|
|
|
|
|
|
if (!v) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (typeof v === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
return (v as (item: any) => any)(item);
|
|
|
|
|
} catch {
|
|
|
|
|
return '-';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof v === 'string') return v;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionsCount = computed(() => {
|
|
|
|
|
let totalCount = 0;
|
|
|
|
|
if (this.showEdit) totalCount += 1;
|
|
|
|
|
if (this.showDelete) totalCount += 1;
|
|
|
|
|
if (this.showDetails) totalCount += 1;
|
|
|
|
|
return totalCount;
|
|
|
|
|
});
|
2026-04-24 23:01:44 +03:30
|
|
|
|
|
|
|
|
refresh() {
|
|
|
|
|
this.onRefresh.emit();
|
|
|
|
|
}
|
2025-12-04 21:07:18 +03:30
|
|
|
}
|