feat: enhance form components with validation and dynamic properties; update payment handling and UI interactions

This commit is contained in:
2026-05-24 19:40:02 +03:30
parent cdd2bd6bee
commit eb671d5949
15 changed files with 283 additions and 865 deletions
@@ -11,8 +11,7 @@
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
name="wages"
label="اجرت"
/>
label="اجرت" />
{{ initialValues?.payload?.profit }}
<app-amount-percentage-input
@@ -22,8 +21,7 @@
[minAmount]="0"
[maxAmount]="unitWithQuantity()"
name="commission"
label="حق‌العمل"
/>
label="حق‌العمل" />
<app-amount-percentage-input
[percentageControl]="form.controls.payload.controls.profit_percentage"
[amountControl]="form.controls.payload.controls.profit"
@@ -31,8 +29,7 @@
[minAmount]="0"
[maxAmount]="totalAmountBeforeProfit()"
name="profit"
label="سود"
/>
label="سود" />
</form>
<app-amount-percentage-input
@@ -42,8 +39,7 @@
[minAmount]="0"
[maxAmount]="baseAmountForDiscountCalculation()"
name="discount"
label="تخفیف"
>
label="تخفیف">
<ng-template #labelSuffix>
<p-selectButton
[options]="discountTypeItems"
@@ -51,19 +47,17 @@
[allowEmpty]="false"
optionLabel="label"
optionValue="value"
(onChange)="changeDiscountCalculation($event)"
/>
(onChange)="changeDiscountCalculation($event)" />
</ng-template>
</app-amount-percentage-input>
<hr />
<div class="flex flex-col gap-4 justify-center w-full">
<div class="flex w-full flex-col justify-center gap-4">
<pos-form-dialog-amount-card-template
[totalAmount]="totalAmount()"
[baseTotalAmount]="baseTotalAmount()"
[discountAmount]="form.controls.discount.value || 0"
[taxAmount]="taxAmount()"
/>
<button pButton class="sm:w-auto w-full" (click)="submit()">{{ preparedCTAText() }}</button>
[taxAmount]="taxAmount()" />
<button pButton class="w-full sm:w-auto" (click)="submit()">{{ preparedCTAText() }}</button>
</div>
</div>
@@ -84,7 +84,7 @@ export class PosGoldPayloadFormComponent extends AbstractForm<
const form = this.fb.group({
unit_price: [
this.initialValues?.unit_price || this.goldDefaultUnitPrice() || 0,
[Validators.required],
[Validators.required, greaterThanValidator(0)],
],
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
discount: [this.initialValues?.discount || 0],
@@ -1,4 +1,5 @@
import ISummary from '@/core/models/summary';
import { greaterThanValidator } from '@/core/validators';
import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components';
import { AmountPercentageInputComponent } from '@/shared/components/amountPercentageInput/amount-percentage-input.component';
@@ -32,8 +33,11 @@ export class PosStandardPayloadFormComponent extends AbstractForm<
private readonly initialForm = () => {
const form = this.fb.group({
unit_price: [this.initialValues?.unit_price || 0, [Validators.required]],
quantity: [this.initialValues?.quantity || 0, [Validators.required]],
unit_price: [
this.initialValues?.unit_price || 0,
[Validators.required, greaterThanValidator(0)],
],
quantity: [this.initialValues?.quantity || 0, [Validators.required, greaterThanValidator(0)]],
discount_amount: [this.initialValues?.discount || 0, [Validators.min(0)]],
discount_percentage: [0, [Validators.min(0), Validators.max(100)]],
});
@@ -4,8 +4,7 @@
[modal]="true"
[style]="{ 'max-height': '90svh', height: 'auto' }"
[closable]="true"
(onHide)="close()"
>
(onHide)="close()">
<div class="form-group">
<form [formGroup]="form" (submit)="submit()">
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" />
@@ -14,6 +13,15 @@
</form>
<div class="form-group">
<div class="flex items-center justify-between gap-3">
<uikit-label> نوع تسویه‌حساب </uikit-label>
<p-selectButton
[options]="settlementTypeItems"
[(ngModel)]="selectedSettlementType"
[allowEmpty]="false"
optionLabel="label"
optionValue="value" />
</div>
<uikit-field label="تعداد مراحل پرداخت با پوز" name="pay_by_terminal_steps">
<p-select
[options]="payByTerminalSteps()"
@@ -23,8 +31,7 @@
optionValue="value"
[disabled]="!remainedAmount()"
class="w-full"
(onChange)="changePayByTerminalSteps($event.value)"
/>
(onChange)="changePayByTerminalSteps($event.value)" />
</uikit-field>
@for (terminalControl of terminalControls; track $index) {
<app-input
@@ -33,16 +40,14 @@
[label]="$index === 0 ? 'پرداخت با پوز' : 'پرداخت با پوز - مرحله ' + ($index + 1)"
type="price"
[max]="terminalsMax()[$index]"
[disabled]="payByTerminalSteps()[$index].payed"
>
[disabled]="payByTerminalSteps()[$index].payed">
<ng-template #suffixTemp>
<button
pButton
size="small"
type="button"
[disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
(click)="fillTerminalRemained($index)"
>
(click)="fillTerminalRemained($index)">
باقی‌مانده مبلغ
</button>
</ng-template>
@@ -55,27 +60,27 @@
name="cash"
label="نقدی/ کارت‌خوان دیگر/ کارت به کارت"
type="price"
[max]="cashMax()"
>
[max]="cashMax()">
<ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('CASH')">
باقی‌مانده مبلغ
</button>
</ng-template>
</app-input>
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
<ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
باقی‌مانده مبلغ
</button>
</ng-template>
</app-input>
@if (isGoldGuild()) {
<app-input [control]="form.controls.set_off" name="setOff" label="تهاتر" type="price" [max]="setOffMax()">
<ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
باقی‌مانده مبلغ
</button>
</ng-template>
</app-input>
}
<app-form-footer-actions
submitLabel="ثبت اطلاعات پرداخت و ادامه"
[loading]="submitOrderLoading()"
(onCancel)="close()"
/>
(onCancel)="close()" />
</form>
</div>
</shared-light-bottomsheet>
@@ -1,16 +1,18 @@
import { Maybe } from '@/core';
import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service';
import { PosInfoStore } from '@/domains/pos/store';
import { AbstractFormDialog } from '@/shared/abstractClasses';
import { InputComponent, KeyValueComponent } from '@/shared/components';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { UikitFieldComponent } from '@/uikit';
import { UikitFieldComponent, UikitLabelComponent } from '@/uikit';
import { Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { Select } from 'primeng/select';
import { SelectButton } from 'primeng/selectbutton';
import { catchError, throwError } from 'rxjs';
import {
IPayment,
@@ -36,15 +38,35 @@ import { PosLandingStore } from '../../store/main.store';
SharedLightBottomsheetComponent,
Select,
UikitFieldComponent,
SelectButton,
UikitLabelComponent,
],
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
})
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> {
private readonly store = inject(PosLandingStore);
private readonly posInfo = inject(PosInfoStore);
private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
private readonly nativeBridgeService = inject(NativeBridgeService);
private readonly toastServices = inject(ToastService);
readonly settlementTypeItems = [
{
label: 'نقدی',
value: 'CASH',
},
{
label: 'نسیه',
value: 'CREDIT',
},
{
label: 'نقدی / نسیه',
value: 'MIXED',
},
];
readonly selectedSettlementType = signal<'CASH' | 'CREDIT' | 'MIXED'>('CASH');
readonly totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
readonly remainedAmount = signal(this.totalAmount());
@@ -52,7 +74,8 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
cashMax = signal(this.remainedAmount());
terminalsMax = signal([this.remainedAmount()]);
submitOrderLoading = computed(() => this.store.submitOrderLoading());
readonly submitOrderLoading = computed(() => this.store.submitOrderLoading());
readonly isGoldGuild = computed(() => this.posInfo.entity()?.guild.code.toLowerCase() === 'gold');
payByTerminalSteps = signal(
Array.from({ length: 10 }, (_, i) => ({
@@ -260,12 +283,16 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
}
override submitForm() {
if (this.remainedAmount() > 0) {
return this.toastServices.warn({
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
});
if (this.selectedSettlementType() === 'CASH') {
if (this.remainedAmount() > 0) {
return this.toastServices.warn({
text: 'فاکتور تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..',
});
}
}
this.store.setSettlementType(this.selectedSettlementType());
const rawPayment = this.form.getRawValue();
const terminalPayments = this.payByTerminalSteps()
@@ -292,9 +319,9 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
const terminalPaymentIsDone = this.checkTerminalPayments();
if (terminalPaymentIsDone) {
this.toastService.info({
text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
});
// this.toastService.info({
// text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
// });
this.store
.submitOrder()
.pipe(
@@ -35,7 +35,7 @@
}
</div>
@if (showPayloadForm() && selectedGoodToAdd(); as selectedGood) {
@if (selectedGoodToAdd(); as selectedGood) {
<pos-payload-form-dialog
[(visible)]="showPayloadForm"
[good]="selectedGood"
+1
View File
@@ -8,6 +8,7 @@ import { IPosOrderItem } from './types';
export interface IPosOrderRequest {
total_amount: number;
invoice_date: string;
settlement_type: 'CASH' | 'CREDIT' | 'MIXED';
payments: IPayment;
items: IPosOrderItem[];
notes?: string;
@@ -29,6 +29,7 @@ interface IPosLandingState {
};
invoiceDate: string;
orderNote?: string;
settlement_type: 'CASH' | 'CREDIT' | 'MIXED';
payments: IPayment;
submitOrderLoading: boolean;
}
@@ -42,6 +43,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
inOrderGoods: [],
viewType: 'grid',
searchQuery: '',
settlement_type: 'CASH',
customerDetails: {
type: CustomerType.UNKNOWN,
},
@@ -81,7 +83,7 @@ export class PosLandingStore {
return this.state$().goods?.filter((s) => s.category.id === this.state$().activeGoodCategory);
});
readonly filteredGoods = computed(() =>
this.activatedCategoryGoods()?.filter((good) => good.name.includes(this.state$().searchQuery)),
this.activatedCategoryGoods()?.filter((good) => good.name.includes(this.state$().searchQuery))
);
readonly customer = computed(() => this.state$().customerDetails);
@@ -251,6 +253,10 @@ export class PosLandingStore {
this.setState({ payments });
}
setSettlementType(settlement_type: 'CASH' | 'CREDIT' | 'MIXED') {
this.setState({ settlement_type });
}
submitOrder() {
this.setState({ submitOrderLoading: true });
const orderPayload: IPosOrderRequest = {
@@ -264,12 +270,13 @@ export class PosLandingStore {
})),
invoice_date: new Date().toISOString(),
payments: this.payments(),
settlement_type: this.state$().settlement_type,
total_amount: this.orderPricingInfo().totalAmount,
};
this.toastServices.info({
text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
});
// this.toastServices.info({
// text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
// });
let res: Maybe<IPosOrderResponse> = null;
@@ -297,7 +304,7 @@ export class PosLandingStore {
}
return _res;
}),
})
);
}
}
@@ -33,11 +33,9 @@
</div>
</button>
}
@if (showInvoiceBottomSheet()) {
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [modal]="true" [closable]="true" header="فاکتور">
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
</shared-light-bottomsheet>
}
<shared-light-bottomsheet [(visible)]="showInvoiceBottomSheet" [closable]="true" header="جزییات فاکتور">
<pos-order-section (onAddMoreGoods)="closeInvoiceBottomSheet()" (onSubmit)="submitOrder()" />
</shared-light-bottomsheet>
@if (isVisiblePaymentForm()) {
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" />