update many things
This commit is contained in:
+11
-13
@@ -1,20 +1,20 @@
|
||||
<uikit-field
|
||||
[label]="preparedLabel()"
|
||||
[label]="label"
|
||||
[name]="name"
|
||||
[control]="selectedType.value === 'amount' ? amountControl : percentageControl"
|
||||
[showLabel]="!!label"
|
||||
[showErrors]="showErrors"
|
||||
>
|
||||
@if (labelSuffix) {
|
||||
<ng-template #labelView>
|
||||
<div class="flex gap-1 items-center">
|
||||
<uikit-label [name]="name">
|
||||
{{ preparedLabel() }}
|
||||
</uikit-label>
|
||||
<ng-template #labelView>
|
||||
<div class="flex gap-1 items-center justify-between">
|
||||
<uikit-label [name]="name">
|
||||
{{ preparedLabel() }}
|
||||
</uikit-label>
|
||||
@if (labelSuffix) {
|
||||
<ng-container [ngTemplateOutlet]="labelSuffix"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<p-inputgroup>
|
||||
@if (selectedType.value === "amount") {
|
||||
@@ -24,9 +24,7 @@
|
||||
[attr.name]="name"
|
||||
[attr.placeholder]="placeholder"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[class]="
|
||||
` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`
|
||||
"
|
||||
[class]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
|
||||
dir="ltr"
|
||||
[required]="required"
|
||||
[invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)"
|
||||
|
||||
+4
-4
@@ -128,12 +128,12 @@ export class AmountPercentageInputComponent {
|
||||
if (notValid) {
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار فیلد باید بیشتر از ${min} باشد.`,
|
||||
text: `مقدار ${this.label} باید بیشتر از ${formatWithCurrency(min)} باشد.`,
|
||||
});
|
||||
}
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار فیلد باید کمتر از ${max} باشد.`,
|
||||
text: `مقدار ${this.label} باید کمتر از ${formatWithCurrency(max)} باشد.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@ export class AmountPercentageInputComponent {
|
||||
if (isPercentageType) {
|
||||
const amountValue = (this.baseAmount * newValue) / 100;
|
||||
newValueToSet = newValue + '';
|
||||
this.amountControl.setValue(amountValue);
|
||||
this.amountControl.setValue(isNaN(amountValue) ? 0 : amountValue);
|
||||
if (notValid) {
|
||||
this.percentageControl.setValue(newValue);
|
||||
}
|
||||
} else {
|
||||
const percentageValue = (newValue / this.baseAmount) * 100;
|
||||
const percentageValue = ((newValue / this.baseAmount) * 100).toFixed(2);
|
||||
newValueToSet = newValue + '';
|
||||
this.percentageControl.setValue(percentageValue);
|
||||
this.preparedLabel.set(`${this.label} (${percentageValue} درصد)`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p-breadcrumb [model]="items()"></p-breadcrumb>
|
||||
<div class="absolute left-0 inset-y-0">
|
||||
<div class="me-4 flex items-center justify-center h-full">
|
||||
<i class="pi pi-question-circle" pTooltip="محتوای آموزشی" tooltipPosition="bottom"></i>
|
||||
<i class="pi pi-question-circle" pTooltip="راهنما" tooltipPosition="bottom"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,18 +2,24 @@
|
||||
<ng-template #title>
|
||||
<ng-container [ngTemplateOutlet]="header">
|
||||
<div class="flex items-center gap-10 justify-between">
|
||||
<h5 class="font-bold py-1.5">{{ cardTitle }}</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (backRoute) {
|
||||
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" size="small" outlined [routerLink]="backRoute" />
|
||||
}
|
||||
<h5 class="font-bold py-1.5">{{ cardTitle }}</h5>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<ng-container [ngTemplateOutlet]="moreActions"></ng-container>
|
||||
@if (showRefresh) {
|
||||
<p-button icon="pi pi-refresh" outlined (click)="refresh()"></p-button>
|
||||
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
|
||||
}
|
||||
@if (editable) {
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
[icon]="`pi ${editMode ? 'pi-times' : 'pi-pencil'}`"
|
||||
class="p-button-text p-button-plain"
|
||||
outlined
|
||||
size="small"
|
||||
(click)="onEditClick()"
|
||||
></button>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
signal,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import { RouterLink, UrlTree } from '@angular/router';
|
||||
import { Button, ButtonDirective } from 'primeng/button';
|
||||
import { Card } from 'primeng/card';
|
||||
|
||||
@@ -16,12 +17,13 @@ import { Card } from 'primeng/card';
|
||||
selector: 'app-card-data',
|
||||
standalone: true,
|
||||
templateUrl: './card-data.component.html',
|
||||
imports: [Card, CommonModule, ButtonDirective, Button],
|
||||
imports: [Card, CommonModule, ButtonDirective, Button, RouterLink],
|
||||
})
|
||||
export class AppCardComponent {
|
||||
@Input() cardTitle!: string;
|
||||
@Input() editable: boolean = false;
|
||||
@Input() showRefresh: boolean = false;
|
||||
@Input() backRoute?: UrlTree | string | any[];
|
||||
|
||||
@Input()
|
||||
set editMode(v: boolean) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'field-sku',
|
||||
template: `<catalog-sku-select label="شناسهی کالا" [control]="control" [name]="name" />`,
|
||||
template: `<catalog-sku-select label="شناسه کالا" [control]="control" [name]="name" />`,
|
||||
imports: [ReactiveFormsModule, CatalogSkuSelectComponent],
|
||||
})
|
||||
export class SkuComponent {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="grow">
|
||||
<div class="flex items-center gap-2">
|
||||
@if (backRoute) {
|
||||
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="backRoute" />
|
||||
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined size="small" [routerLink]="backRoute" />
|
||||
}
|
||||
<span class="text-xl font-bold">{{ pageTitle }}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="inline-flex gap-2 items-center w-full">
|
||||
<span class="text-muted-color text-base font-normal flex-auto grow-0">{{ label }}:</span>
|
||||
<div
|
||||
class="shrink-0 grow"
|
||||
class="shrink-0 grow max-sm:text-left"
|
||||
[ngClass]="{
|
||||
'text-right': alignment === 'start',
|
||||
'text-center': alignment === 'center',
|
||||
|
||||
@@ -30,6 +30,7 @@ export class KeyValueComponent {
|
||||
| 'thumbnail'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'number'
|
||||
| 'simple' = 'simple';
|
||||
|
||||
@Input() trueValueToShow?: string;
|
||||
@@ -89,6 +90,9 @@ export class KeyValueComponent {
|
||||
case 'has':
|
||||
value = 'ندارد';
|
||||
break;
|
||||
case 'number':
|
||||
value = '0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="h-full overflow-auto">
|
||||
<div
|
||||
[ngClass]="{
|
||||
'px-4': captionBox,
|
||||
'pb-4': true,
|
||||
}"
|
||||
>
|
||||
@if (captionBox) {
|
||||
<div class="pt-4">
|
||||
<ng-container [ngTemplateOutlet]="captionBox"></ng-container>
|
||||
</div>
|
||||
<hr />
|
||||
}
|
||||
<div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': captionBox }">
|
||||
@for (item of items; track `gridView_${$index}`) {
|
||||
<div class="card border border-surface-border bg-surface-50! mb-0! rounded-2xl p-4!">
|
||||
<div class="listKeyValue">
|
||||
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) {
|
||||
@if (col.type !== "index") {
|
||||
<app-key-value [label]="col.header">
|
||||
@if (col.type === "thumbnail") {
|
||||
<div
|
||||
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
|
||||
(click)="openThumbnailModal(item)"
|
||||
>
|
||||
@if (item && col?.field && item[col!.field!]) {
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
}
|
||||
</div>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
||||
></ng-container>
|
||||
} @else if (col.canCopy) {
|
||||
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
|
||||
} @else {
|
||||
<span [class]="getPreviewClasses(item, col)">
|
||||
{{ getCell(item, col) }}
|
||||
</span>
|
||||
}
|
||||
</app-key-value>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (showEdit || showDelete || showDetails) {
|
||||
<hr />
|
||||
<div class="flex justify-center gap-2 mt-2">
|
||||
@if (showEdit) {
|
||||
<p-button icon="pi pi-pencil" label="ویرایش" size="small" (click)="edit(item)"> </p-button>
|
||||
}
|
||||
@if (showDelete) {
|
||||
<p-button icon="pi pi-trash" label="حذف" size="small" (click)="remove(item)"> </p-button>
|
||||
}
|
||||
@if (showDetails) {
|
||||
<p-button icon="pi pi-eye" label="مشاهده" size="small" (click)="details(item)"> </p-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (loading) {
|
||||
@for (i of [1, 2, 3, 4]; track `grid_view_loading${$index}`) {
|
||||
<div class="w-full h-40">
|
||||
<p-skeleton height="100%"></p-skeleton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showPaginator) {
|
||||
<ng-container [ngTemplateOutlet]="paginator"> </ng-container>
|
||||
}
|
||||
@if (items.length === 0 && !loading) {
|
||||
<ng-container [ngTemplateOutlet]="emptyMessageCard"></ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,252 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { UikitCopyComponent } from '@/uikit';
|
||||
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
ContentChild,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
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';
|
||||
import { KeyValueComponent } from '../key-value.component/key-value.component';
|
||||
|
||||
type TDataType =
|
||||
| 'simple'
|
||||
| 'price'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'number';
|
||||
export interface IColumn<T = any> {
|
||||
field: T extends object ? keyof T | string : string;
|
||||
header: string;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
canCopy?: boolean;
|
||||
type?: TDataType;
|
||||
|
||||
nestedOption?: {
|
||||
path: string;
|
||||
type?: TDataType;
|
||||
};
|
||||
thumbnailOptions?: {
|
||||
editable: boolean;
|
||||
deletable: boolean;
|
||||
showPreview: boolean;
|
||||
};
|
||||
dateOption?: {
|
||||
expiredMode?: boolean;
|
||||
dateTime?: boolean;
|
||||
onlyTime?: boolean;
|
||||
};
|
||||
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-data-list-grid-view',
|
||||
templateUrl: './page-data-list-grid-view.component.html',
|
||||
host: {
|
||||
class: 'block w-full h-full overflow-hidden',
|
||||
},
|
||||
imports: [
|
||||
CommonModule,
|
||||
TableModule,
|
||||
ButtonModule,
|
||||
SkeletonModule,
|
||||
PaginatorModule,
|
||||
DrawerModule,
|
||||
UikitCopyComponent,
|
||||
KeyValueComponent,
|
||||
],
|
||||
})
|
||||
export class AppPageDataListGridView<I = any> {
|
||||
@Input({ required: true }) pageTitle!: string;
|
||||
@Input({ required: true }) columns!: IColumn[];
|
||||
@Input({ required: true }) items!: any[];
|
||||
@Input({ required: true }) loading!: boolean;
|
||||
@Input({ required: true }) emptyPlaceholderTitle!: string;
|
||||
@Input() addNewCtaLabel?: string;
|
||||
@Input() emptyPlaceholderDescription?: string = '';
|
||||
@Input() emptyPlaceholderCtaLabel?: string;
|
||||
@Input() currentPage?: number = 1;
|
||||
@Input() showEdit: boolean = false;
|
||||
@Input() showDelete: boolean = false;
|
||||
@Input() showDetails: boolean = false;
|
||||
@Input() showAdd: boolean = false;
|
||||
@Input() showRefresh: boolean = true;
|
||||
@Input() isFiltered: boolean = false;
|
||||
@Input() fullHeight?: boolean = false;
|
||||
@Input() height: string = '';
|
||||
@Input() expandable?: boolean = false;
|
||||
@Input() expandableItemKey?: string = '';
|
||||
@Input() expandColumns?: Maybe<IColumn[]> = null;
|
||||
@Input() dataKey?: string = 'items';
|
||||
@Input() showPaginator?: boolean;
|
||||
|
||||
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
|
||||
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
|
||||
@ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null;
|
||||
@ContentChild('paginator', { static: true }) paginator!: TemplateRef<any> | null;
|
||||
@ContentChild('captionBox', { static: true }) captionBox!: TemplateRef<any> | null;
|
||||
@ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef<any> | null;
|
||||
|
||||
@Output() onEdit = new EventEmitter<any>();
|
||||
@Output() onDelete = new EventEmitter<any>();
|
||||
@Output() onDetails = new EventEmitter<any>();
|
||||
@Output() onAdd = new EventEmitter<void>();
|
||||
@Output() onChangePage = new EventEmitter<any>();
|
||||
@Output() onThumbnailClick = new EventEmitter<any>();
|
||||
@Output() onRefresh = new EventEmitter();
|
||||
|
||||
filterDrawerVisible = signal<boolean>(false);
|
||||
expandedRows: { [key: string]: boolean } = {};
|
||||
|
||||
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.onChangePage.emit($event);
|
||||
};
|
||||
|
||||
openThumbnailModal = (item: I) => {
|
||||
// this.
|
||||
};
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
getCell(item: Record<string, any>, column: IColumn): any {
|
||||
if (!item || !column) return '';
|
||||
try {
|
||||
let { field, type } = column;
|
||||
|
||||
if (column.customDataModel) {
|
||||
return this.renderCustom(column, item);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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, 'ریال');
|
||||
case 'number':
|
||||
return data || 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
refresh() {
|
||||
this.onRefresh.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<div class="h-full flex flex-col gap-4">
|
||||
<p-table
|
||||
[columns]="columns"
|
||||
[scrollable]="true"
|
||||
[value]="items"
|
||||
[loading]="loading"
|
||||
columnResizeMode="fit"
|
||||
stripedRows
|
||||
[showFirstLastIcon]="false"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
[dataKey]="dataKey"
|
||||
class="max-sm:hidden! grow flex! flex-col overflow-hidden"
|
||||
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''"
|
||||
>
|
||||
@if (captionBox) {
|
||||
<ng-template pTemplate="caption">
|
||||
<ng-container [ngTemplateOutlet]="captionBox"> </ng-container>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-template #header let-columns>
|
||||
<tr>
|
||||
@for (col of columns; track col.header) {
|
||||
<th
|
||||
[style]="
|
||||
col.type === 'index' ? { width: '3rem', minWidth: '3rem' } : { width: col.width, minWidth: col.minWidth }
|
||||
"
|
||||
>
|
||||
{{ col.header }}
|
||||
</th>
|
||||
}
|
||||
@if (actionsCount()) {
|
||||
<th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th>
|
||||
}
|
||||
@if (expandable) {
|
||||
<th style="width: 3rem"></th>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #body let-item let-i="rowIndex" let-expanded="expanded">
|
||||
<tr>
|
||||
@for (col of columns; track col.field) {
|
||||
<td>
|
||||
@if (col.type === "index") {
|
||||
{{ i * (currentPage || 1) + 1 }}
|
||||
} @else if (col.type === "thumbnail") {
|
||||
<div
|
||||
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
|
||||
(click)="openThumbnailModal(item)"
|
||||
>
|
||||
@if (item[col.field]) {
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
}
|
||||
</div>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
||||
></ng-container>
|
||||
} @else if (col.canCopy) {
|
||||
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
|
||||
} @else {
|
||||
<span [class]="getPreviewClasses(item, col)">
|
||||
{{ getCell(item, col) }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (actionsCount()) {
|
||||
<td
|
||||
table-action-row
|
||||
type="td"
|
||||
[showDetails]="showDetails"
|
||||
[showDelete]="showDelete"
|
||||
[showEdit]="showEdit"
|
||||
(edit)="edit(item)"
|
||||
(delete)="remove(item)"
|
||||
(details)="details(item)"
|
||||
></td>
|
||||
}
|
||||
|
||||
@if (expandable) {
|
||||
<td>
|
||||
<p-button
|
||||
type="button"
|
||||
pRipple
|
||||
[pRowToggler]="item"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
[plain]="true"
|
||||
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"
|
||||
/>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
@if (expandable && expandColumns && expandableItemKey) {
|
||||
<ng-template #expandedrow let-item>
|
||||
<tr>
|
||||
<td [attr.colspan]="columns.length + 1">
|
||||
<div class="px-4">
|
||||
@if (item[expandableItemKey]?.length) {
|
||||
<p-table
|
||||
[columns]="expandColumns || []"
|
||||
[scrollable]="true"
|
||||
[value]="item[expandableItemKey]"
|
||||
[loading]="loading"
|
||||
columnResizeMode="fit"
|
||||
stripedRows
|
||||
[showFirstLastIcon]="false"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
class="grow flex! flex-col overflow-hidden"
|
||||
[tableStyleClass]="!item.items?.length && !loading ? 'h-full' : ''"
|
||||
>
|
||||
<ng-template #header let-columns>
|
||||
<tr>
|
||||
@for (col of expandColumns; track col.header) {
|
||||
<th
|
||||
[style]="
|
||||
col.type === 'index'
|
||||
? { width: '3rem', minWidth: '3rem' }
|
||||
: { width: col.width, minWidth: col.minWidth }
|
||||
"
|
||||
>
|
||||
{{ col.header }}
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #body let-item let-i="rowIndex">
|
||||
<tr>
|
||||
@for (col of expandColumns; track col.field) {
|
||||
<td>
|
||||
{{ item.name }}
|
||||
@if (col.type === "index") {
|
||||
{{ i * (currentPage || 1) + 1 }}
|
||||
} @else if (col.type === "thumbnail") {
|
||||
<div
|
||||
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
|
||||
(click)="openThumbnailModal(item)"
|
||||
>
|
||||
@if (item[col.field]) {
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
}
|
||||
</div>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
||||
></ng-container>
|
||||
} @else if (col.canCopy) {
|
||||
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
|
||||
} @else {
|
||||
<span [class]="getPreviewClasses(item, col)">
|
||||
{{ getCell(item, col) }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #emptymessage>
|
||||
<tr class="">
|
||||
<td [colSpan]="(expandColumns.length + 1).toString()" class="w-full">
|
||||
<uikit-empty-state
|
||||
[title]="emptyPlaceholderTitle"
|
||||
[description]="emptyPlaceholderDescription"
|
||||
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[showCTA]="showAdd"
|
||||
(ctaClick)="openAddForm()"
|
||||
></uikit-empty-state>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
} @else {
|
||||
<div class="grid grid-cols-3 gap-4 py-2">
|
||||
@for (col of expandColumns; track $index) {
|
||||
<app-key-value
|
||||
[label]="col.header"
|
||||
[value]="item[expandableItemKey][col.field]"
|
||||
[type]="col.type || 'simple'"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-template #emptymessage>
|
||||
<tr class="">
|
||||
<td [colSpan]="(columns.length + 1).toString()" class="w-full">
|
||||
<ng-container [ngTemplateOutlet]="emptyMessageCard"></ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingbody let-columns>
|
||||
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
|
||||
<tr style="height: 46px">
|
||||
@for (col of columns; track col) {
|
||||
<td>
|
||||
<p-skeleton></p-skeleton>
|
||||
</td>
|
||||
}
|
||||
@if (actionsCount()) {
|
||||
<td>
|
||||
<p-skeleton></p-skeleton>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
@if (showPaginator) {
|
||||
<ng-container [ngTemplateOutlet]="paginator"> </ng-container>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,255 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit';
|
||||
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
ContentChild,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
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';
|
||||
import { KeyValueComponent } from '../key-value.component/key-value.component';
|
||||
import { TableActionRowComponent } from '../table-action-row.component';
|
||||
|
||||
type TDataType =
|
||||
| 'simple'
|
||||
| 'price'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'number';
|
||||
export interface IColumn<T = any> {
|
||||
field: T extends object ? keyof T | string : string;
|
||||
header: string;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
canCopy?: boolean;
|
||||
type?: TDataType;
|
||||
|
||||
nestedOption?: {
|
||||
path: string;
|
||||
type?: TDataType;
|
||||
};
|
||||
thumbnailOptions?: {
|
||||
editable: boolean;
|
||||
deletable: boolean;
|
||||
showPreview: boolean;
|
||||
};
|
||||
dateOption?: {
|
||||
expiredMode?: boolean;
|
||||
dateTime?: boolean;
|
||||
onlyTime?: boolean;
|
||||
};
|
||||
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-data-list-table-view',
|
||||
templateUrl: './page-data-list-table-view.component.html',
|
||||
host: {
|
||||
class: 'block w-full h-full overflow-hidden',
|
||||
},
|
||||
imports: [
|
||||
CommonModule,
|
||||
TableActionRowComponent,
|
||||
UikitEmptyStateComponent,
|
||||
TableModule,
|
||||
ButtonModule,
|
||||
SkeletonModule,
|
||||
PaginatorModule,
|
||||
DrawerModule,
|
||||
UikitCopyComponent,
|
||||
KeyValueComponent,
|
||||
],
|
||||
})
|
||||
export class AppPageDataListTableView<I = any> {
|
||||
@Input({ required: true }) pageTitle!: string;
|
||||
@Input({ required: true }) columns!: IColumn[];
|
||||
@Input({ required: true }) items!: any[];
|
||||
@Input({ required: true }) loading!: boolean;
|
||||
@Input({ required: true }) emptyPlaceholderTitle!: string;
|
||||
@Input() addNewCtaLabel?: string;
|
||||
@Input() emptyPlaceholderDescription?: string = '';
|
||||
@Input() emptyPlaceholderCtaLabel?: string;
|
||||
@Input() currentPage?: number = 1;
|
||||
@Input() showEdit: boolean = false;
|
||||
@Input() showDelete: boolean = false;
|
||||
@Input() showDetails: boolean = false;
|
||||
@Input() showAdd: boolean = false;
|
||||
@Input() showRefresh: boolean = true;
|
||||
@Input() isFiltered: boolean = false;
|
||||
@Input() fullHeight?: boolean = false;
|
||||
@Input() height: string = '';
|
||||
@Input() expandable?: boolean = false;
|
||||
@Input() expandableItemKey?: string = '';
|
||||
@Input() expandColumns?: Maybe<IColumn[]> = null;
|
||||
@Input() dataKey?: string = 'items';
|
||||
@Input() showPaginator?: boolean;
|
||||
|
||||
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
|
||||
@ContentChild('paginator', { static: true }) paginator!: TemplateRef<any> | null;
|
||||
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
|
||||
@ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null;
|
||||
@ContentChild('captionBox', { static: true }) captionBox!: TemplateRef<any> | null;
|
||||
@ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef<any> | null;
|
||||
|
||||
@Output() onEdit = new EventEmitter<any>();
|
||||
@Output() onDelete = new EventEmitter<any>();
|
||||
@Output() onDetails = new EventEmitter<any>();
|
||||
@Output() onAdd = new EventEmitter<void>();
|
||||
@Output() onChangePage = new EventEmitter<any>();
|
||||
@Output() onThumbnailClick = new EventEmitter<any>();
|
||||
@Output() onRefresh = new EventEmitter();
|
||||
|
||||
filterDrawerVisible = signal<boolean>(false);
|
||||
expandedRows: { [key: string]: boolean } = {};
|
||||
|
||||
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.onChangePage.emit($event);
|
||||
};
|
||||
|
||||
openThumbnailModal = (item: I) => {
|
||||
// this.
|
||||
};
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
getCell(item: Record<string, any>, column: IColumn): any {
|
||||
if (!item || !column) return '';
|
||||
try {
|
||||
let { field, type } = column;
|
||||
|
||||
if (column.customDataModel) {
|
||||
return this.renderCustom(column, item);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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, 'ریال');
|
||||
case 'number':
|
||||
return data || 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
refresh() {
|
||||
this.onRefresh.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
<div class="h-full bg-surface-overlay rounded-(--p-card-border-radius) overflow-hidden">
|
||||
<div class="h-full flex flex-col gap-4">
|
||||
<p-table
|
||||
@if (!isMobile) {
|
||||
<app-page-data-list-table-view
|
||||
[pageTitle]="pageTitle"
|
||||
[columns]="columns"
|
||||
[scrollable]="true"
|
||||
[value]="items"
|
||||
[items]="items"
|
||||
[loading]="loading"
|
||||
columnResizeMode="fit"
|
||||
stripedRows
|
||||
[showFirstLastIcon]="false"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
[dataKey]="dataKey"
|
||||
class="grow flex! flex-col overflow-hidden"
|
||||
[tableStyleClass]="!items.length && !loading ? 'h-full' : ''"
|
||||
[emptyPlaceholderTitle]="emptyPlaceholderTitle"
|
||||
[emptyPlaceholderDescription]="emptyPlaceholderDescription"
|
||||
[emptyPlaceholderCtaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[addNewCtaLabel]="addNewCtaLabel"
|
||||
[currentPage]="currentPage"
|
||||
[showEdit]="showEdit"
|
||||
[showDelete]="showDelete"
|
||||
[showDetails]="showDetails"
|
||||
(onEdit)="edit($event)"
|
||||
(onDelete)="remove($event)"
|
||||
(onDetails)="details($event)"
|
||||
>
|
||||
@if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) {
|
||||
<ng-template pTemplate="caption">
|
||||
<ng-template #captionBox>
|
||||
<ng-container [ngTemplateOutlet]="caption">
|
||||
<div class="flex justify-between items-center gap-4">
|
||||
<h5 class="font-bold">{{ pageTitle }}</h5>
|
||||
@@ -27,15 +31,21 @@
|
||||
icon="pi pi-filter"
|
||||
variant="outlined"
|
||||
badgeSeverity="info"
|
||||
size="small"
|
||||
[badge]="isFiltered ? '1' : undefined"
|
||||
(click)="openFilter()"
|
||||
></p-button>
|
||||
}
|
||||
@if (showRefresh) {
|
||||
<p-button icon="pi pi-refresh" outlined (click)="refresh()"></p-button>
|
||||
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
|
||||
}
|
||||
@if (showAdd) {
|
||||
<p-button label="{{ addNewCtaLabel }}" icon="pi pi-plus" (click)="openAddForm()"></p-button>
|
||||
<p-button
|
||||
label="{{ addNewCtaLabel }}"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
(click)="openAddForm()"
|
||||
></p-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -43,226 +53,100 @@
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-template #header let-columns>
|
||||
<tr>
|
||||
@for (col of columns; track col.header) {
|
||||
<th
|
||||
[style]="
|
||||
col.type === 'index'
|
||||
? { width: '3rem', minWidth: '3rem' }
|
||||
: { width: col.width, minWidth: col.minWidth }
|
||||
"
|
||||
>
|
||||
{{ col.header }}
|
||||
</th>
|
||||
}
|
||||
@if (actionsCount()) {
|
||||
<th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th>
|
||||
}
|
||||
@if (expandable) {
|
||||
<th style="width: 3rem"></th>
|
||||
}
|
||||
</tr>
|
||||
<ng-template #emptyMessageCard>
|
||||
<uikit-empty-state
|
||||
[title]="emptyPlaceholderTitle"
|
||||
[description]="emptyPlaceholderDescription"
|
||||
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[showCTA]="showAdd"
|
||||
(ctaClick)="openAddForm()"
|
||||
></uikit-empty-state>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #body let-item let-i="rowIndex" let-expanded="expanded">
|
||||
<tr>
|
||||
@for (col of columns; track col.field) {
|
||||
<td>
|
||||
@if (col.type === "index") {
|
||||
{{ i * (currentPage || 1) + 1 }}
|
||||
} @else if (col.type === "thumbnail") {
|
||||
<div
|
||||
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
|
||||
(click)="openThumbnailModal(item)"
|
||||
>
|
||||
@if (item[col.field]) {
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
<ng-template #paginator>
|
||||
<app-paginator
|
||||
[currentPage]="currentPage || 1"
|
||||
[totalRecords]="totalRecords"
|
||||
[perPage]="perPage || 10"
|
||||
[loading]="loading"
|
||||
(onChange)="onPage($event)"
|
||||
/>
|
||||
</ng-template>
|
||||
</app-page-data-list-table-view>
|
||||
} @else {
|
||||
<app-page-data-list-grid-view
|
||||
[pageTitle]="pageTitle"
|
||||
[columns]="columns"
|
||||
[items]="items"
|
||||
[loading]="loading"
|
||||
[emptyPlaceholderTitle]="emptyPlaceholderTitle"
|
||||
[emptyPlaceholderDescription]="emptyPlaceholderDescription"
|
||||
[emptyPlaceholderCtaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[addNewCtaLabel]="addNewCtaLabel"
|
||||
[currentPage]="currentPage"
|
||||
[showEdit]="showEdit"
|
||||
[showDelete]="showDelete"
|
||||
[showDetails]="showDetails"
|
||||
(onEdit)="edit($event)"
|
||||
(onDelete)="remove($event)"
|
||||
(onDetails)="details($event)"
|
||||
>
|
||||
@if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) {
|
||||
<ng-template #captionBox>
|
||||
<ng-container [ngTemplateOutlet]="caption">
|
||||
<div class="flex justify-between items-center gap-4">
|
||||
<h5 class="font-bold">{{ pageTitle }}</h5>
|
||||
@if (showAdd || filter || showRefresh || moreActions) {
|
||||
<div class="flex items-center gap-2">
|
||||
<ng-container [ngTemplateOutlet]="moreActions" />
|
||||
@if (filter) {
|
||||
<p-button
|
||||
label="فیلتر"
|
||||
icon="pi pi-filter"
|
||||
variant="outlined"
|
||||
badgeSeverity="info"
|
||||
size="small"
|
||||
[badge]="isFiltered ? '1' : undefined"
|
||||
(click)="openFilter()"
|
||||
></p-button>
|
||||
}
|
||||
@if (showRefresh) {
|
||||
<p-button icon="pi pi-refresh" outlined size="small" (click)="refresh()"></p-button>
|
||||
}
|
||||
@if (showAdd) {
|
||||
<p-button
|
||||
label="{{ addNewCtaLabel }}"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
(click)="openAddForm()"
|
||||
></p-button>
|
||||
}
|
||||
</div>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
||||
></ng-container>
|
||||
} @else if (col.canCopy) {
|
||||
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
|
||||
} @else {
|
||||
<span [class]="getPreviewClasses(item, col)">
|
||||
{{ getCell(item, col) }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (actionsCount()) {
|
||||
<td
|
||||
table-action-row
|
||||
type="td"
|
||||
[showDetails]="showDetails"
|
||||
[showDelete]="showDelete"
|
||||
[showEdit]="showEdit"
|
||||
(edit)="edit(item)"
|
||||
(delete)="remove(item)"
|
||||
(details)="details(item)"
|
||||
></td>
|
||||
}
|
||||
|
||||
@if (expandable) {
|
||||
<td>
|
||||
<p-button
|
||||
type="button"
|
||||
pRipple
|
||||
[pRowToggler]="item"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
[plain]="true"
|
||||
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"
|
||||
/>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
@if (expandable && expandColumns && expandableItemKey) {
|
||||
<ng-template #expandedrow let-item>
|
||||
<tr>
|
||||
<td [attr.colspan]="columns.length + 1">
|
||||
<div class="px-4">
|
||||
@if (item[expandableItemKey]?.length) {
|
||||
<p-table
|
||||
[columns]="expandColumns || []"
|
||||
[scrollable]="true"
|
||||
[value]="item[expandableItemKey]"
|
||||
[loading]="loading"
|
||||
columnResizeMode="fit"
|
||||
stripedRows
|
||||
[showFirstLastIcon]="false"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
class="grow flex! flex-col overflow-hidden"
|
||||
[tableStyleClass]="!item.items?.length && !loading ? 'h-full' : ''"
|
||||
>
|
||||
<ng-template #header let-columns>
|
||||
<tr>
|
||||
@for (col of expandColumns; track col.header) {
|
||||
<th
|
||||
[style]="
|
||||
col.type === 'index'
|
||||
? { width: '3rem', minWidth: '3rem' }
|
||||
: { width: col.width, minWidth: col.minWidth }
|
||||
"
|
||||
>
|
||||
{{ col.header }}
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #body let-item let-i="rowIndex">
|
||||
<tr>
|
||||
@for (col of expandColumns; track col.field) {
|
||||
<td>
|
||||
{{ item.name }}
|
||||
@if (col.type === "index") {
|
||||
{{ i * (currentPage || 1) + 1 }}
|
||||
} @else if (col.type === "thumbnail") {
|
||||
<div
|
||||
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-50 cursor-pointer"
|
||||
(click)="openThumbnailModal(item)"
|
||||
>
|
||||
@if (item[col.field]) {
|
||||
<img [src]="item[col.field]" class="w-full h-full object-cover" />
|
||||
}
|
||||
</div>
|
||||
} @else if (getTemplate(col)) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="getTemplate(col)"
|
||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
||||
></ng-container>
|
||||
} @else if (col.canCopy) {
|
||||
<uikit-copy [text]="getCell(item, col)"></uikit-copy>
|
||||
} @else {
|
||||
<span [class]="getPreviewClasses(item, col)">
|
||||
{{ getCell(item, col) }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #emptymessage>
|
||||
<tr class="">
|
||||
<td [colSpan]="(expandColumns.length + 1).toString()" class="w-full">
|
||||
<uikit-empty-state
|
||||
[title]="emptyPlaceholderTitle"
|
||||
[description]="emptyPlaceholderDescription"
|
||||
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[showCTA]="showAdd"
|
||||
(ctaClick)="openAddForm()"
|
||||
></uikit-empty-state>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
} @else {
|
||||
<div class="grid grid-cols-3 gap-4 py-2">
|
||||
@for (col of expandColumns; track $index) {
|
||||
<app-key-value
|
||||
[label]="col.header"
|
||||
[value]="item[expandableItemKey][col.field]"
|
||||
[type]="col.type || 'simple'"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-template #emptymessage>
|
||||
<tr class="">
|
||||
<td [colSpan]="(columns.length + 1).toString()" class="w-full">
|
||||
<uikit-empty-state
|
||||
[title]="emptyPlaceholderTitle"
|
||||
[description]="emptyPlaceholderDescription"
|
||||
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[showCTA]="showAdd"
|
||||
(ctaClick)="openAddForm()"
|
||||
></uikit-empty-state>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #emptyMessageCard>
|
||||
<uikit-empty-state
|
||||
[title]="emptyPlaceholderTitle"
|
||||
[description]="emptyPlaceholderDescription"
|
||||
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
|
||||
[showCTA]="showAdd"
|
||||
(ctaClick)="openAddForm()"
|
||||
></uikit-empty-state>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingbody let-columns>
|
||||
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
|
||||
<tr style="height: 46px">
|
||||
@for (col of columns; track col) {
|
||||
<td>
|
||||
<p-skeleton></p-skeleton>
|
||||
</td>
|
||||
}
|
||||
@if (actionsCount()) {
|
||||
<td>
|
||||
<p-skeleton></p-skeleton>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<ng-template #paginator>
|
||||
<app-paginator
|
||||
[currentPage]="currentPage || 1"
|
||||
[totalRecords]="totalRecords"
|
||||
[perPage]="perPage || 10"
|
||||
[loading]="loading"
|
||||
(onChange)="onPage($event)"
|
||||
/>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
@if (showPaginator) {
|
||||
<app-paginator
|
||||
[currentPage]="currentPage || 1"
|
||||
[totalRecords]="totalRecords"
|
||||
[perPage]="perPage || 10"
|
||||
[loading]="loading"
|
||||
(onChange)="onPage($event)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</app-page-data-list-grid-view>
|
||||
}
|
||||
|
||||
@if (filter) {
|
||||
<p-drawer
|
||||
[visible]="filterDrawerVisible()"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { PaginatorComponent, UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit';
|
||||
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
|
||||
import { PaginatorComponent, UikitEmptyStateComponent } from '@/uikit';
|
||||
import { jalaliDiff } from '@/utils';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
Renderer2,
|
||||
@@ -19,10 +19,19 @@ import { DrawerModule } from 'primeng/drawer';
|
||||
import { PaginatorModule } from 'primeng/paginator';
|
||||
import { SkeletonModule } from 'primeng/skeleton';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { KeyValueComponent } from '../key-value.component/key-value.component';
|
||||
import { TableActionRowComponent } from '../table-action-row.component';
|
||||
import { AppPageDataListGridView } from './page-data-list-grid-view.component';
|
||||
import { AppPageDataListTableView } from './page-data-list-table-view.component';
|
||||
|
||||
type TDataType = 'simple' | 'price' | 'boolean' | 'date' | 'nested' | 'index' | 'id' | 'thumbnail';
|
||||
type TDataType =
|
||||
| 'simple'
|
||||
| 'price'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'nested'
|
||||
| 'index'
|
||||
| 'id'
|
||||
| 'thumbnail'
|
||||
| 'number';
|
||||
export interface IColumn<T = any> {
|
||||
field: T extends object ? keyof T | string : string;
|
||||
header: string;
|
||||
@@ -56,19 +65,18 @@ export interface IColumn<T = any> {
|
||||
},
|
||||
imports: [
|
||||
CommonModule,
|
||||
TableActionRowComponent,
|
||||
UikitEmptyStateComponent,
|
||||
TableModule,
|
||||
ButtonModule,
|
||||
SkeletonModule,
|
||||
PaginatorModule,
|
||||
DrawerModule,
|
||||
PaginatorComponent,
|
||||
UikitCopyComponent,
|
||||
KeyValueComponent,
|
||||
AppPageDataListTableView,
|
||||
AppPageDataListGridView,
|
||||
UikitEmptyStateComponent,
|
||||
],
|
||||
})
|
||||
export class PageDataListComponent<I> {
|
||||
export class PageDataListComponent<I = any> {
|
||||
constructor(
|
||||
private host: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
@@ -77,7 +85,7 @@ export class PageDataListComponent<I> {
|
||||
@Input({ required: true }) pageTitle!: string;
|
||||
@Input() addNewCtaLabel?: string;
|
||||
@Input({ required: true }) columns!: IColumn[];
|
||||
@Input({ required: true }) items!: I[];
|
||||
@Input({ required: true }) items!: any[];
|
||||
@Input({ required: true }) loading!: boolean;
|
||||
@Input() emptyPlaceholderTitle: string = 'موردی برای نمایش وجود ندارد';
|
||||
@Input() emptyPlaceholderDescription?: string = '';
|
||||
@@ -123,17 +131,19 @@ export class PageDataListComponent<I> {
|
||||
);
|
||||
// if (this.fullHeight) {
|
||||
// }
|
||||
|
||||
this.updateViewportMode();
|
||||
}
|
||||
|
||||
edit = (item: I) => {
|
||||
edit = (item: any) => {
|
||||
this.onEdit.emit(item);
|
||||
};
|
||||
|
||||
remove = (item: I) => {
|
||||
remove = (item: any) => {
|
||||
this.onDelete.emit(item);
|
||||
};
|
||||
|
||||
details = (item: I) => {
|
||||
details = (item: any) => {
|
||||
this.onDetails.emit(item);
|
||||
};
|
||||
|
||||
@@ -183,84 +193,18 @@ export class PageDataListComponent<I> {
|
||||
}
|
||||
}
|
||||
|
||||
getCell(item: Record<string, any>, column: IColumn): any {
|
||||
if (!item || !column) return '';
|
||||
try {
|
||||
let { field, type } = column;
|
||||
|
||||
if (column.customDataModel) {
|
||||
return this.renderCustom(column, item);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
refresh() {
|
||||
this.onRefresh.emit();
|
||||
}
|
||||
|
||||
isMobile = false;
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize() {
|
||||
this.updateViewportMode();
|
||||
}
|
||||
|
||||
private updateViewportMode() {
|
||||
this.isMobile = typeof window !== 'undefined' && window.innerWidth <= 768;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user