Compare commits

...

2 Commits

Author SHA1 Message Date
ahasani 13c791d86f feat: add expandable rows and accordion details to grid and table views 2026-05-10 19:56:02 +03:30
ahasani cb6be84cb9 feat: remove AGENT.md and add new agents.md with updated repository-specific instructions
refactor: update shared-saleInvoice.component.ts to simplify SKU code handling

refactor: modify goods.component.html to optimize payload form dialog rendering

fix: adjust goods.component.ts to ensure payload form visibility logic is correct

fix: update order-section.component.ts to improve customer dialog and payment form visibility

style: enhance form.component.html button styling for better responsiveness

fix: change abstract-form.ts to reset form with default values on initialization

refactor: streamline page-data-list-grid-view.component.html for better caption handling

refactor: update page-data-list-grid-view.component.ts to remove unused methods and improve readability

refactor: simplify page-data-list-table-view.component.html and remove redundant code

refactor: clean up page-data-list-table-view.component.ts by removing unnecessary methods

feat: enhance page-data-list.component.html with new caption and paginator templates

feat: create page-data-value.component.ts to encapsulate data rendering logic for grid and table views

style: update presets.ts to modify color palette for better UI consistency
2026-05-10 17:55:30 +03:30
14 changed files with 297 additions and 403 deletions
View File
@@ -10,7 +10,7 @@ import {
} from '@/shared/components/pageDataList/page-data-list.component'; } from '@/shared/components/pageDataList/page-data-list.component';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { UikitEmptyStateComponent } from '@/uikit'; import { UikitEmptyStateComponent } from '@/uikit';
import { formatJalali, getGoodUnitTypeProperties } from '@/utils'; import { formatJalali } from '@/utils';
import { import {
Component, Component,
computed, computed,
@@ -91,6 +91,10 @@ export class ConsumerSaleInvoiceSharedComponent {
path: 'good.name', path: 'good.name',
}, },
}, },
{
field: 'sku_code',
header: 'شناسه کالا',
},
{ {
field: 'unit_price', field: 'unit_price',
header: 'قیمت واحد', header: 'قیمت واحد',
@@ -100,7 +104,7 @@ export class ConsumerSaleInvoiceSharedComponent {
field: 'quantity', field: 'quantity',
header: 'مقدار', header: 'مقدار',
customDataModel(item) { customDataModel(item) {
return `${item.quantity} ${getGoodUnitTypeProperties(item.good.unit_type).quantitySymbolText}`; return `${item.quantity} ${item.measure_unit_text}`;
}, },
}, },
{ {
@@ -36,11 +36,7 @@
} }
</div> </div>
@defer (when showPayloadForm()) { @if (showPayloadForm() && selectedGoodToAdd(); as selectedGood) {
<pos-payload-form-dialog <pos-payload-form-dialog [(visible)]="showPayloadForm" [good]="selectedGood" (onClose)="onClosePayloadForm()" />
[(visible)]="showPayloadForm"
[good]="selectedGoodToAdd()!"
(onClose)="onClosePayloadForm()"
/>
} }
</div> </div>
@@ -67,14 +67,14 @@ export class PosGoodsComponent {
} }
addGood(good: IGoodResponse) { addGood(good: IGoodResponse) {
this.showPayloadForm.set(true); this.showPayloadForm.set(false);
this.selectedGoodToAdd.set(null);
console.log('good', good);
this.selectedGoodToAdd.set(good); this.selectedGoodToAdd.set(good);
queueMicrotask(() => this.showPayloadForm.set(true));
} }
onClosePayloadForm() { onClosePayloadForm() {
this.showPayloadForm.set(false);
this.selectedGoodToAdd.set(null); this.selectedGoodToAdd.set(null);
} }
} }
@@ -72,13 +72,13 @@ export class PosOrderSectionComponent {
} }
openCustomerDialog() { openCustomerDialog() {
// this.this.store.submitOrder(); this.isVisibleCustomerForm.set(false);
this.isVisibleCustomerForm.set(true); queueMicrotask(() => this.isVisibleCustomerForm.set(true));
} }
submitAndPay() { submitAndPay() {
// this.this.store.submitOrder(); this.isVisiblePaymentForm.set(false);
this.isVisiblePaymentForm.set(true); queueMicrotask(() => this.isVisiblePaymentForm.set(true));
} }
addMoreGoods() { addMoreGoods() {
@@ -27,6 +27,6 @@
[discountAmount]="discountAmount()" [discountAmount]="discountAmount()"
[taxAmount]="taxAmount()" [taxAmount]="taxAmount()"
/> />
<p-button (onClick)="submit()"> {{ preparedCTAText() }} </p-button> <p-button class="sm:w-auto w-full" (onClick)="submit()"> {{ preparedCTAText() }} </p-button>
</div> </div>
</form> </form>
@@ -37,14 +37,14 @@ export abstract class AbstractForm<
} }
ngOnInit() { ngOnInit() {
this.form.patchValue(this.initialValues ?? {}); this.form.reset(this.initialValues ?? this.defaultValues);
// Object.entries(this.form.controls).forEach(([key, control]) => { // Object.entries(this.form.controls).forEach(([key, control]) => {
// console.log(key, control); // console.log(key, control);
// }); // });
} }
ngOnChanges() { ngOnChanges() {
this.form.patchValue(this.initialValues ?? {}); this.form.reset(this.initialValues ?? this.defaultValues);
} }
onSuccess(response: Response): void {} onSuccess(response: Response): void {}
@@ -1,50 +1,57 @@
<div class="h-full overflow-auto border border-surface-border cardShadow"> <div [ngClass]="{ 'h-full overflow-auto': true, 'border border-surface-border cardShadow': hasCaption }">
<div <div
[ngClass]="{ [ngClass]="{
'px-4': captionBox, 'px-4': hasCaption,
'pb-4': true, 'pb-4': true,
}" }"
> >
@if (captionBox) { @if (hasCaption && captionBox) {
<div class="pt-4"> <div class="pt-4">
<ng-container [ngTemplateOutlet]="captionBox"></ng-container> <ng-container [ngTemplateOutlet]="captionBox"></ng-container>
</div> </div>
<hr /> <hr />
} }
<div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': captionBox }"> <div class="grid grid-cols-1 gap-3" [ngClass]="{ 'pt-4': hasCaption }">
@for (item of items; track `gridView_${$index}`) { @for (item of items; track `gridView_${$index}`) {
<div class="card border border-surface-border bg-surface-0! mb-0! rounded-2xl p-4!"> <div class="card border border-surface-border bg-surface-0! mb-0! rounded-2xl p-4!">
<div class="listKeyValue"> <div [ngClass]="{ listKeyValue: true, 'mb-2': expandable }">
@for (col of columns; track `gridView_${col.field.toString()}_${$index}`) { @for (col of columns; track `gridView_${col.field.toString()}_${$index}`) {
@if (col.type !== "index") { @if (col.type !== "index") {
<app-key-value [label]="col.header" [variant]="col.variant"> <app-key-value [label]="col.header" [variant]="col.variant">
@if (col.type === "thumbnail") { <app-page-data-value [item]="item" [column]="col" (thumbnailClick)="openThumbnailModal($event)" />
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-100 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 (col.variant === "tag") {
<p-tag [value]="getCell(item, col)" [severity]="col.tagOptions?.severity || 'contrast'"></p-tag>
} @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> </app-key-value>
} }
} }
</div> </div>
@if (expandable) {
<p-accordion [unstyled]="true" [value]="['0']">
<p-accordion-panel value="0" class="border! rounded-lg! overflow-hidden">
<p-accordion-header class="bg-surface-200! text-text-color! py-2!">
<ng-template #toggleicon let-active="active">
<div class="flex items-center gap-4 w-full">
<i [ngClass]="{ 'pi pi-chevron-left text-xs! transition': true, '-rotate-90': active }"></i>
جزییات بیشتر
</div>
</ng-template>
</p-accordion-header>
<p-accordion-content>
<div class="listKeyValue pt-4">
@for (col of expandColumns; track `gridView_expand_${col.field.toString()}_${$index}`) {
@if (col.type !== "index") {
<app-key-value [label]="col.header" [variant]="col.variant">
<app-page-data-value
[item]="item[expandableItemKey!]"
[column]="col"
(thumbnailClick)="openThumbnailModal($event)"
/>
</app-key-value>
}
}
</div>
</p-accordion-content>
</p-accordion-panel>
</p-accordion>
}
@if (showEdit || showDelete || showDetails) { @if (showEdit || showDelete || showDetails) {
<hr class="my-2" /> <hr class="my-2" />
<div class="flex justify-center gap-2 mt-4"> <div class="flex justify-center gap-2 mt-4">
@@ -1,6 +1,4 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { UikitCopyComponent } from '@/uikit';
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
Component, Component,
@@ -12,14 +10,15 @@ import {
signal, signal,
TemplateRef, TemplateRef,
} from '@angular/core'; } from '@angular/core';
import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { DrawerModule } from 'primeng/drawer'; import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator'; import { PaginatorModule } from 'primeng/paginator';
import { SkeletonModule } from 'primeng/skeleton'; import { SkeletonModule } from 'primeng/skeleton';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { Tag } from 'primeng/tag';
import { KeyValueComponent } from '../key-value.component/key-value.component'; import { KeyValueComponent } from '../key-value.component/key-value.component';
import { IColumn } from './page-data-list.component'; import { IColumn } from './page-data-list.component';
import { PageDataValueComponent } from './page-data-value.component';
@Component({ @Component({
selector: 'app-page-data-list-grid-view', selector: 'app-page-data-list-grid-view',
@@ -34,9 +33,12 @@ import { IColumn } from './page-data-list.component';
SkeletonModule, SkeletonModule,
PaginatorModule, PaginatorModule,
DrawerModule, DrawerModule,
UikitCopyComponent,
KeyValueComponent, KeyValueComponent,
Tag, PageDataValueComponent,
Accordion,
AccordionPanel,
AccordionHeader,
AccordionContent,
], ],
}) })
export class AppPageDataListGridView<I = any> { export class AppPageDataListGridView<I = any> {
@@ -62,6 +64,7 @@ export class AppPageDataListGridView<I = any> {
@Input() expandColumns?: Maybe<IColumn[]> = null; @Input() expandColumns?: Maybe<IColumn[]> = null;
@Input() dataKey?: string = 'items'; @Input() dataKey?: string = 'items';
@Input() showPaginator?: boolean; @Input() showPaginator?: boolean;
@Input() hasCaption: boolean = false;
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null; @ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null; @ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
@@ -113,99 +116,6 @@ export class AppPageDataListGridView<I = any> {
// this. // 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(() => { actionsCount = computed(() => {
let totalCount = 0; let totalCount = 0;
if (this.showEdit) totalCount += 1; if (this.showEdit) totalCount += 1;
@@ -20,6 +20,9 @@
<ng-template #header let-columns> <ng-template #header let-columns>
<tr> <tr>
@if (expandable) {
<th style="width: 3rem"></th>
}
@if (showIndex) { @if (showIndex) {
<th [style]="{ width: '3rem', minWidth: '3rem' }"></th> <th [style]="{ width: '3rem', minWidth: '3rem' }"></th>
} }
@@ -35,14 +38,24 @@
@if (actionsCount()) { @if (actionsCount()) {
<th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th> <th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th>
} }
@if (expandable) {
<th style="width: 3rem"></th>
}
</tr> </tr>
</ng-template> </ng-template>
<ng-template #body let-item let-i="rowIndex" let-expanded="expanded"> <ng-template #body let-item let-i="rowIndex" let-expanded="expanded">
<tr> <tr>
@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-left'"
/>
</td>
}
@if (showIndex) { @if (showIndex) {
<td> <td>
{{ i * (currentPage || 1) + 1 }} {{ i * (currentPage || 1) + 1 }}
@@ -50,29 +63,7 @@
} }
@for (col of columns; track col.field) { @for (col of columns; track col.field) {
<td> <td>
@if (col.type === "thumbnail") { <app-page-data-value [item]="item" [column]="col" (thumbnailClick)="openThumbnailModal($event)" />
<div
class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-100 cursor-pointer"
(click)="openThumbnailModal(item)"
>
@if (item[col.field]) {
<img [src]="item[col.field]" class="w-full h-full object-cover" />
}
</div>
} @else if (col.variant === "tag") {
<p-tag [value]="getCell(item, col)" [severity]="col.tagOptions?.severity || 'contrast'"></p-tag>
} @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> </td>
} }
@if (actionsCount()) { @if (actionsCount()) {
@@ -87,20 +78,6 @@
(details)="details(item)" (details)="details(item)"
></td> ></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> </tr>
</ng-template> </ng-template>
@@ -145,26 +122,12 @@
{{ item.name }} {{ item.name }}
@if (col.type === "index") { @if (col.type === "index") {
{{ i * (currentPage || 1) + 1 }} {{ 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 { } @else {
<span [class]="getPreviewClasses(item, col)"> <app-page-data-value
{{ getCell(item, col) }} [item]="item"
</span> [column]="col"
(thumbnailClick)="openThumbnailModal($event)"
/>
} }
</td> </td>
} }
@@ -1,6 +1,5 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit'; import { UikitEmptyStateComponent } from '@/uikit';
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
Component, Component,
@@ -17,9 +16,9 @@ import { DrawerModule } from 'primeng/drawer';
import { PaginatorModule } from 'primeng/paginator'; import { PaginatorModule } from 'primeng/paginator';
import { SkeletonModule } from 'primeng/skeleton'; import { SkeletonModule } from 'primeng/skeleton';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { Tag } from 'primeng/tag';
import { KeyValueComponent } from '../key-value.component/key-value.component'; import { KeyValueComponent } from '../key-value.component/key-value.component';
import { TableActionRowComponent } from '../table-action-row.component'; import { TableActionRowComponent } from '../table-action-row.component';
import { PageDataValueComponent } from './page-data-value.component';
import { IColumn } from './page-data-list.component'; import { IColumn } from './page-data-list.component';
@Component({ @Component({
@@ -37,9 +36,8 @@ import { IColumn } from './page-data-list.component';
SkeletonModule, SkeletonModule,
PaginatorModule, PaginatorModule,
DrawerModule, DrawerModule,
UikitCopyComponent,
KeyValueComponent, KeyValueComponent,
Tag, PageDataValueComponent,
], ],
}) })
export class AppPageDataListTableView<I = any> { export class AppPageDataListTableView<I = any> {
@@ -66,12 +64,13 @@ export class AppPageDataListTableView<I = any> {
@Input() dataKey?: string = 'items'; @Input() dataKey?: string = 'items';
@Input() showPaginator?: boolean; @Input() showPaginator?: boolean;
@Input() showIndex?: boolean = true; @Input() showIndex?: boolean = true;
@Input() hasCaption: boolean = false;
@ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null; @ContentChild('filter', { static: true }) filter!: TemplateRef<any> | null;
@ContentChild('paginator', { static: true }) paginator!: TemplateRef<any> | null; @ContentChild('paginator', { static: true }) paginator!: TemplateRef<any> | null;
@ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null; @ContentChild('moreActions', { static: true }) moreActions!: TemplateRef<any> | null;
@ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null; @ContentChild('expandableTemp', { static: false }) expandableTemp!: TemplateRef<any> | null;
@ContentChild('captionBox', { static: true }) captionBox!: TemplateRef<any> | null; @ContentChild('captionBox', { static: false }) captionBox!: TemplateRef<any> | null;
@ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef<any> | null; @ContentChild('emptyMessageCard', { static: true }) emptyMessageCard!: TemplateRef<any> | null;
@Output() onEdit = new EventEmitter<any>(); @Output() onEdit = new EventEmitter<any>();
@@ -117,99 +116,6 @@ export class AppPageDataListTableView<I = any> {
// this. // 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(() => { actionsCount = computed(() => {
let totalCount = 0; let totalCount = 0;
if (this.showEdit) totalCount += 1; if (this.showEdit) totalCount += 1;
@@ -1,4 +1,70 @@
<div class="h-full bg-surface-overlay rounded-(--p-card-border-radius) overflow-hidden"> <div class="h-full bg-surface-overlay rounded-(--p-card-border-radius) overflow-hidden">
<ng-template #captionTemplate let-isMobileView="isMobileView">
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
<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 (showAll && allItemsPageRoute) {
@if (isMobileView) {
<a routerLink pButton [routerLink]="allItemsPageRoute" icon="pi pi-eye" outlined size="small"></a>
} @else {
<a routerLink pButton [routerLink]="allItemsPageRoute" outlined size="small">نمایش همه</a>
}
}
@if (showAdd) {
@if (isMobileView) {
<p-button icon="pi pi-plus" size="small" (click)="openAddForm()"></p-button>
} @else {
<p-button
label="{{ addNewCtaLabel }}"
icon="pi pi-plus"
size="small"
(click)="openAddForm()"
></p-button>
}
}
</div>
}
</div>
</ng-container>
}
</ng-template>
<ng-template #emptyMessageTemplate>
<uikit-empty-state
[title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription"
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[showCTA]="showAdd"
(ctaClick)="openAddForm()"
></uikit-empty-state>
</ng-template>
<ng-template #paginatorTemplate>
<app-paginator
[currentPage]="currentPage || 1"
[totalRecords]="totalRecords"
[perPage]="perPage || 10"
[loading]="loading"
(onChange)="onPage($event)"
/>
</ng-template>
@if (!isMobile) { @if (!isMobile) {
<app-page-data-list-table-view <app-page-data-list-table-view
[pageTitle]="pageTitle" [pageTitle]="pageTitle"
@@ -14,66 +80,24 @@
[showDelete]="showDelete" [showDelete]="showDelete"
[showDetails]="showDetails" [showDetails]="showDetails"
[showIndex]="showIndex" [showIndex]="showIndex"
[hasCaption]="!!(pageTitle || showAdd || filter || showRefresh || moreActions)"
[expandable]="expandable"
[expandableItemKey]="expandableItemKey"
[expandColumns]="expandColumns"
(onEdit)="edit($event)" (onEdit)="edit($event)"
(onDelete)="remove($event)" (onDelete)="remove($event)"
(onDetails)="details($event)" (onDetails)="details($event)"
> >
<ng-template #captionBox> @if (pageTitle || showAdd || filter || showRefresh || moreActions) {
@if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) { <ng-template #captionBox>
<ng-container [ngTemplateOutlet]="caption"> <ng-container [ngTemplateOutlet]="captionTemplate" [ngTemplateOutletContext]="{ isMobileView: false }" />
<div class="flex justify-between items-center gap-4"> </ng-template>
<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 (showAll && allItemsPageRoute) {
<a routerLink pButton [routerLink]="allItemsPageRoute" outlined size="small">نمایش همه</a>
}
@if (showAdd) {
<p-button
label="{{ addNewCtaLabel }}"
icon="pi pi-plus"
size="small"
(click)="openAddForm()"
></p-button>
}
</div>
}
</div>
</ng-container>
}
</ng-template>
<ng-template #emptyMessageCard> <ng-template #emptyMessageCard>
<uikit-empty-state <ng-container [ngTemplateOutlet]="emptyMessageTemplate" />
[title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription"
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[showCTA]="showAdd"
(ctaClick)="openAddForm()"
></uikit-empty-state>
</ng-template> </ng-template>
<ng-template #paginator> <ng-template #paginator>
<app-paginator <ng-container [ngTemplateOutlet]="paginatorTemplate" />
[currentPage]="currentPage || 1"
[totalRecords]="totalRecords"
[perPage]="perPage || 10"
[loading]="loading"
(onChange)="onPage($event)"
/>
</ng-template> </ng-template>
</app-page-data-list-table-view> </app-page-data-list-table-view>
} @else { } @else {
@@ -90,61 +114,24 @@
[showEdit]="showEdit" [showEdit]="showEdit"
[showDelete]="showDelete" [showDelete]="showDelete"
[showDetails]="showDetails" [showDetails]="showDetails"
[hasCaption]="!!(pageTitle || showAdd || filter || showRefresh || moreActions)"
[expandable]="expandable"
[expandableItemKey]="expandableItemKey"
[expandColumns]="expandColumns"
(onEdit)="edit($event)" (onEdit)="edit($event)"
(onDelete)="remove($event)" (onDelete)="remove($event)"
(onDetails)="details($event)" (onDetails)="details($event)"
> >
<ng-template #captionBox> <ng-template #captionBox>
@if (pageTitle || showAdd || filter || caption || showRefresh || moreActions) { @if (pageTitle || showAdd || filter || showRefresh || moreActions) {
<ng-container [ngTemplateOutlet]="caption"> <ng-container [ngTemplateOutlet]="captionTemplate" [ngTemplateOutletContext]="{ isMobileView: true }" />
<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 (showAll && allItemsPageRoute) {
<a routerLink pButton [routerLink]="allItemsPageRoute" icon="pi pi-eye" outlined size="small"></a>
}
@if (showAdd) {
<p-button icon="pi pi-plus" size="small" (click)="openAddForm()"></p-button>
}
</div>
}
</div>
</ng-container>
} }
</ng-template> </ng-template>
<ng-template #emptyMessageCard> <ng-template #emptyMessageCard>
<uikit-empty-state <ng-container [ngTemplateOutlet]="emptyMessageTemplate" />
[title]="emptyPlaceholderTitle"
[description]="emptyPlaceholderDescription"
[ctaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[showCTA]="showAdd"
(ctaClick)="openAddForm()"
></uikit-empty-state>
</ng-template> </ng-template>
<ng-template #paginator> <ng-template #paginator>
<app-paginator <ng-container [ngTemplateOutlet]="paginatorTemplate" />
[currentPage]="currentPage || 1"
[totalRecords]="totalRecords"
[perPage]="perPage || 10"
[loading]="loading"
(onChange)="onPage($event)"
/>
</ng-template> </ng-template>
</app-page-data-list-grid-view> </app-page-data-list-grid-view>
} }
@@ -0,0 +1,100 @@
import { formatJalali, formatWithCurrency, jalaliDiff } from '@/utils';
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
import { Tag } from 'primeng/tag';
import { UikitCopyComponent } from '@/uikit';
import { IColumn } from './page-data-list.component';
@Component({
selector: 'app-page-data-value',
standalone: true,
imports: [CommonModule, Tag, UikitCopyComponent],
template: `
@if (column.type === 'thumbnail') {
<div class="w-20 h-20 rounded-2xl overflow-hidden bg-surface-100 cursor-pointer" (click)="thumbnailClick.emit(item)">
@if (readFieldValue(item, column.field)) {
<img [src]="readFieldValue(item, column.field)" class="w-full h-full object-cover" />
}
</div>
} @else if (column.variant === 'tag') {
<p-tag [value]="getCell(item, column)" [severity]="column.tagOptions?.severity || 'contrast'"></p-tag>
} @else if (getTemplate(column)) {
<ng-container [ngTemplateOutlet]="getTemplate(column)" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
} @else if (column.canCopy) {
<uikit-copy [text]="getCell(item, column)"></uikit-copy>
} @else {
<span [class]="getPreviewClasses(item, column)">{{ getCell(item, column) }}</span>
}
`,
})
export class PageDataValueComponent {
@Input({ required: true }) item!: Record<string, any>;
@Input({ required: true }) column!: IColumn;
@Output() thumbnailClick = new EventEmitter<any>();
readFieldValue(item: Record<string, any>, field: IColumn['field']) {
return item?.[String(field)];
}
getPreviewClasses(item: Record<string, any>, column: IColumn): string {
if (!item || !column || column.customDataModel) return '';
const data = item[String(column.field)];
if (column.type === 'date' && column.dateOption?.expiredMode && jalaliDiff(new Date(), data) > 0) {
return 'text-error';
}
return '';
}
getCell(item: Record<string, any>, column: IColumn): any {
if (!item || !column) return '';
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: any, key: string) => (obj && obj[key] !== undefined ? obj[key] : null), item);
type = column.nestedOption?.type || 'simple';
}
switch (type) {
case 'id':
return data ? `${data.slice(0, 5)}...${data.slice(data.length - 5)}` : '-';
case 'date':
return data ? formatJalali(data) : '-';
case 'boolean':
return data ? 'بله' : 'خیر';
case 'price':
return formatWithCurrency(data, false, 'ریال');
case 'number':
return data || 0;
}
if (data === undefined || data === null) return '-';
if (typeof data === 'object') return data.title || '-';
return data || '-';
}
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;
}
}
+23 -2
View File
@@ -31,6 +31,9 @@ const MyPreset = definePreset(Aura, {
}, },
}, },
semantic: { semantic: {
...updateSurfacePalette({
0: '#fcfcfc',
}),
primary: { primary: {
0: '{surface.0}', 0: '{surface.0}',
50: '{surface.50}', 50: '{surface.50}',
@@ -45,9 +48,27 @@ const MyPreset = definePreset(Aura, {
900: '{surface.900}', 900: '{surface.900}',
950: '{surface.950}', 950: '{surface.950}',
}, },
colorScheme: { colorScheme: {
colorScheme: {
light: {},
dark: {},
},
light: { light: {
surface: {
0: '#fcfcfc',
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
primary: { primary: {
color: '{primary.950}', color: '{primary.950}',
contrastColor: '#f0f0f0', contrastColor: '#f0f0f0',
@@ -79,7 +100,7 @@ const MyPreset = definePreset(Aura, {
}, },
primitive: { primitive: {
...updateSurfacePalette({ ...updateSurfacePalette({
0: '#f0f0f0', 0: '#fcfcfc',
50: '#f8fafc', 50: '#f8fafc',
100: '#f1f5f9', 100: '#f1f5f9',
200: '#e2e8f0', 200: '#e2e8f0',