feat: update app version and build details in ngsw-config.json

fix: add baseZIndex to p-toast component in app.component.ts

feat: enhance pagination support in saleInvoices list component

refactor: modify saleInvoices service to accept pagination query

feat: update API routes for saleInvoices to support pagination

fix: adjust layout for consumer accounts in partner module

style: improve layout for consumer business activities in partner module

fix: update license info display in dashboard component

fix: ensure price info card displays default values for discount and tax

refactor: clean up gold payload form component

fix: adjust root component layout for mobile view

refactor: unify consumer account list configuration across modules

chore: remove deprecated partner consumer account list config

chore: remove deprecated superAdmin consumer account list config

feat: add new consumer account list configuration

style: add summary list styling in customize.scss

chore: switch API base URL for TIS environment
This commit is contained in:
2026-05-16 14:49:22 +03:30
parent 4f76056ac0
commit 8c98e53427
29 changed files with 219 additions and 172 deletions
@@ -1,6 +1,7 @@
import { Maybe } from '@/core';
import {
IListingResponse,
IPaginatedQuery,
IPaginatedResponse,
IResponseMetadata,
} from '@/core/models/service.model';
@@ -82,6 +83,16 @@ export abstract class AbstractList<ResponseModel, RequestParams = null> {
}
changePage(page: number) {
// this.
const perPage = this.responseMetaData()?.perPage;
this.requestPayload.update(
(prev) =>
({
...(prev || {}),
page,
...(perPage ? ({ pageSize: perPage } as IPaginatedQuery) : {}),
}) as any,
);
this.responseMetaData.update((prev) => (prev ? { ...prev, page } : prev));
this.getData();
}
}
@@ -28,9 +28,7 @@
[inputStyleClass]="` w-full hasSuffix text-left ${size === 'large' ? 'p-inputtext-lg' : size === 'small' ? 'p-inputtext-sm' : ''}`"
[required]="required"
[invalid]="amountControl.invalid && (amountControl.touched || amountControl.dirty)"
(onInput)="onPriceInput($event)"
/>
<!-- (input)="onInput($event)" -->
} @else {
<input
pInputText
@@ -50,7 +48,6 @@
"
[required]="required"
[invalid]="percentageControl.invalid && (percentageControl.touched || percentageControl.dirty)"
(input)="onInput($event)"
/>
}
@@ -19,7 +19,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputGroup } from 'primeng/inputgroup';
import { InputGroupAddon } from 'primeng/inputgroupaddon';
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber';
import { InputNumberModule } from 'primeng/inputnumber';
import { InputText } from 'primeng/inputtext';
import { Select } from 'primeng/select';
@@ -90,83 +90,85 @@ export class AmountPercentageInputComponent {
});
preparedLabel = signal(this.label);
private lastValidAmount = 0;
private lastValidPercentage = 0;
onPriceInput($event: InputNumberInputEvent) {
this.onInput($event.originalEvent, true);
private parseValue(value: Maybe<any>) {
const normalized = `${value ?? ''}`.replace(/,/g, '');
const parsed = parseFloat(normalized);
return Number.isFinite(parsed) ? parsed : 0;
}
onInput($event: Event, isPriceType: boolean = false) {
// @ts-ignore
let value = $event.target.value as string;
// @ts-ignore
const isDotInput = $event.data === '.';
private getRangeLabel(value: number, isPercentageType: boolean) {
return isPercentageType ? `${value}%` : formatWithCurrency(value);
}
if (isDotInput) {
private isInRange(value: number, min = 0, max = Number.MAX_SAFE_INTEGER) {
return value >= min && value <= max;
}
private showOutOfRangeToast(value: number, isPercentageType: boolean, min = 0, max = Number.MAX_SAFE_INTEGER) {
if (value < min) {
this.toastService.warn({
text: `مقدار ${this.label} باید بیشتر از ${this.getRangeLabel(min, isPercentageType)} باشد.`,
});
}
if (value > max) {
this.toastService.warn({
text: `مقدار ${this.label} باید کمتر از ${this.getRangeLabel(max, isPercentageType)} باشد.`,
});
}
}
private getAmountRange() {
const min = this.minAmount ?? 0;
const maxFromInput = this.maxAmount;
const max = maxFromInput ?? (this.baseAmount > 0 ? this.baseAmount : Number.MAX_SAFE_INTEGER);
return { min, max };
}
private getPercentageRange() {
const min = this.minPercentage ?? 0;
const max = this.maxPercentage ?? 100;
return { min, max };
}
private syncFromPercentage(rawValue: Maybe<any>) {
const { min, max } = this.getPercentageRange();
const percentageValue = this.parseValue(rawValue);
if (!this.isInRange(percentageValue, min, max)) {
this.showOutOfRangeToast(percentageValue, true, min, max);
this.percentageControl.setValue(this.lastValidPercentage, { emitEvent: false });
return;
}
if (isPriceType) {
value = value.replace(/,/g, '');
}
this.lastValidPercentage = percentageValue;
const amountValue = (this.baseAmount * percentageValue) / 100;
this.lastValidAmount = Number.isFinite(amountValue) ? amountValue : 0;
const newValueToSet = this.modifyControlsData(value);
if (newValueToSet !== null) {
// @ts-ignore
$event.target.value = newValueToSet;
}
this.percentageControl.setValue(this.lastValidPercentage, { emitEvent: false });
this.amountControl.setValue(this.lastValidAmount, { emitEvent: false });
this.preparedLabel.set(this.prepareLabel(true));
}
private modifyControlsData(value: string) {
let newValueToSet: Maybe<string> = null;
let newValue = parseFloat(value);
const isPercentageType = this.selectedType.value === 'percentage';
const min = (isPercentageType ? this.minPercentage : this.minAmount) || 0;
const max = (isPercentageType ? this.maxPercentage : this.maxAmount) || Number.MAX_SAFE_INTEGER;
const maxValidator = max < newValue;
const minValidator = min > newValue;
const notValid = minValidator || maxValidator;
if (notValid) {
if (minValidator) {
this.toastService.warn({
text: `مقدار ${this.label} باید بیشتر از ${formatWithCurrency(min)} باشد.`,
});
}
if (maxValidator) {
this.toastService.warn({
text: `مقدار ${this.label} باید کمتر از ${formatWithCurrency(max)} باشد.`,
});
}
newValue = parseFloat(value.slice(0, value.length - 1));
if (newValue < 0 || isNaN(newValue)) {
newValue = 0;
}
private syncFromAmount(rawValue: Maybe<any>) {
const { min, max } = this.getAmountRange();
const amountValue = this.parseValue(rawValue);
if (!this.isInRange(amountValue, min, max)) {
this.showOutOfRangeToast(amountValue, false, min, max);
this.amountControl.setValue(this.lastValidAmount, { emitEvent: false });
return;
}
if (isPercentageType) {
const amountValue = (this.baseAmount * newValue) / 100;
newValueToSet = newValue + '';
this.amountControl.setValue(isNaN(amountValue) ? 0 : amountValue, { emitEvent: false });
if (notValid) {
this.percentageControl.setValue(newValue, { emitEvent: false });
}
} else {
const percentageValue = ((newValue / this.baseAmount) * 100).toFixed(2);
newValueToSet = newValue + '';
this.percentageControl.setValue(percentageValue, { emitEvent: false });
this.preparedLabel.set(`${this.label} (${percentageValue} درصد)`);
if (notValid) {
this.amountControl.setValue(newValue, { emitEvent: false });
}
}
this.preparedLabel.set(this.prepareLabel(isPercentageType));
this.lastValidAmount = amountValue;
const percentageValue = this.baseAmount
? ((amountValue / this.baseAmount) * 100).toFixed(2)
: '0';
this.lastValidPercentage = this.parseValue(percentageValue);
return newValue;
this.amountControl.setValue(this.lastValidAmount, { emitEvent: false });
this.percentageControl.setValue(percentageValue, { emitEvent: false });
this.preparedLabel.set(this.prepareLabel(false));
}
private prepareLabel(isPercentageType: boolean) {
@@ -177,25 +179,44 @@ export class AmountPercentageInputComponent {
}
ngOnInit() {
const amountValue = Number(this.amountControl.value || 0);
const percentageValue = Number(this.percentageControl.value || 0);
const isPercentageType = percentageValue > 0 && amountValue <= 0;
console.log(amountValue, percentageValue, isPercentageType);
this.preparedLabel.set(this.prepareLabel(isPercentageType));
const isPercentageType = this.defaultType === 'percentage';
this.selectedType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
this.preparedLabel.set(this.prepareLabel(value === 'percentage'));
});
this.selectedType.setValue(isPercentageType ? 'percentage' : 'amount');
this.percentageControl.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => {
if (this.selectedType.value === 'percentage') {
this.syncFromPercentage(value);
}
});
this.amountControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
if (this.selectedType.value === 'amount') {
this.syncFromAmount(value);
}
});
this.selectedType.setValue(isPercentageType ? 'percentage' : 'amount', { emitEvent: false });
this.lastValidAmount = this.parseValue(this.amountControl.value);
this.lastValidPercentage = this.parseValue(this.percentageControl.value);
if (isPercentageType) {
this.syncFromPercentage(this.percentageControl.value);
} else {
this.syncFromAmount(this.amountControl.value);
}
}
ngOnChanges() {
const amountValue = Number(this.amountControl.value || 0);
const percentageValue = Number(this.percentageControl.value || 0);
const isPercentageType = percentageValue > 0 && amountValue <= 0;
const isPercentageType = this.selectedType.value === 'percentage';
if (isPercentageType) {
this.syncFromPercentage(this.percentageControl.value);
} else {
this.syncFromAmount(this.amountControl.value);
}
this.preparedLabel.set(this.prepareLabel(isPercentageType));
}
}
@@ -58,7 +58,7 @@
}
@if (showIndex) {
<td>
{{ i * (currentPage || 1) + 1 }}
{{ i + 1 + (currentPage && perPage ? (currentPage - 1) * perPage : 0) }}
</td>
}
@for (col of columns; track col.field) {
@@ -18,8 +18,8 @@ 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 { PageDataValueComponent } from './page-data-value.component';
import { IColumn } from './page-data-list.component';
import { PageDataValueComponent } from './page-data-value.component';
@Component({
selector: 'app-page-data-list-table-view',
@@ -50,6 +50,7 @@ export class AppPageDataListTableView<I = any> {
@Input() emptyPlaceholderDescription?: string = '';
@Input() emptyPlaceholderCtaLabel?: string;
@Input() currentPage?: number = 1;
@Input() perPage?: number = 1;
@Input() showEdit: boolean = false;
@Input() showDelete: boolean = false;
@Input() showDetails: boolean = false;
@@ -76,6 +76,7 @@
[emptyPlaceholderCtaLabel]="emptyPlaceholderCtaLabel || addNewCtaLabel"
[addNewCtaLabel]="addNewCtaLabel"
[currentPage]="currentPage"
[perPage]="perPage"
[showEdit]="showEdit"
[showDelete]="showDelete"
[showDetails]="showDetails"
@@ -84,9 +85,11 @@
[expandable]="expandable"
[expandableItemKey]="expandableItemKey"
[expandColumns]="expandColumns"
[showPaginator]="showPaginator"
(onEdit)="edit($event)"
(onDelete)="remove($event)"
(onDetails)="details($event)"
(onChangePage)="onPage($event)"
>
@if (pageTitle || showAdd || filter || showRefresh || moreActions) {
<ng-template #captionBox>
@@ -176,6 +176,8 @@ export class PageDataListComponent<I = any> {
};
onPage = ($event: any) => {
console.log('$event', $event);
this.onChangePage.emit($event);
};
@@ -1,6 +1,6 @@
import { IListConfig } from './list-config.model';
export const partnerConsumerAccountListConfig: IListConfig = {
export const consumerAccountListConfig: IListConfig = {
pageTitle: 'مدیریت حساب‌های مشتری',
addNewCtaLabel: 'افزودن حساب',
emptyPlaceholderTitle: 'حسابی یافت نشد',
@@ -27,7 +27,7 @@ export const partnerConsumerAccountListConfig: IListConfig = {
field: 'status',
header: 'وضعیت',
type: 'nested',
nestedOption: { path: 'account.status.translate' },
nestedOption: { path: 'status.translate' },
variant: 'tag',
},
],
@@ -2,6 +2,7 @@ export * from './account-list.const';
export * from './business-activity-list.const';
export * from './category-list.const';
export * from './complex-list.const';
export * from './consumer-account-list.const';
export * from './consumer-list.const';
export * from './device-brand-list.const';
export * from './device-list.const';
@@ -9,12 +10,10 @@ export * from './good-list.const';
export * from './license-list.const';
export * from './list-config.model';
export * from './partner-account-list.const';
export * from './partner-consumer-account-list.const';
export * from './partner-customer-list.const';
export * from './partner-list.const';
export * from './pos-list.const';
export * from './provider-list.const';
export * from './sale-invoice-list.const';
export * from './sku-list.const';
export * from './superAdmin-consumer-account-list.const';
export * from './user-list.const';
@@ -1,35 +0,0 @@
import { IListConfig } from './list-config.model';
export const superAdminConsumerAccountListConfig: IListConfig = {
pageTitle: 'مدیریت حساب‌های مشتری',
addNewCtaLabel: 'افزودن حساب',
emptyPlaceholderTitle: 'حسابی یافت نشد',
emptyPlaceholderDescription: 'برای افزودن حساب، روی دکمهٔ بالا کلیک کنید.',
columns: [
{
field: 'account',
header: 'نام کاربری',
type: 'nested',
nestedOption: { path: 'account.username' },
},
{ field: 'role', header: 'نوع حساب', type: 'nested', nestedOption: { path: 'role.translate' } },
{
field: 'pos',
header: 'پایانه',
customDataModel(item: any) {
return `${item.pos.name} (${item.pos.complex.name} - ${item.pos.complex.business_activity.name})`;
},
},
{
field: 'status',
header: 'وضعیت',
type: 'nested',
nestedOption: { path: 'account.status.translate' },
},
{
field: 'created_at',
header: 'تاریخ ایجاد',
type: 'date',
},
],
};