Compare commits

...

2 Commits

Author SHA1 Message Date
ahasani 2c90f8091e feat: replace national_id with economic_code in customer forms and models
Production CI / validate-and-build (push) Failing after 1m1s
2026-06-15 19:34:23 +03:30
ahasani d6aa165592 feat(payment): update payment bridge and form components
- Refactor PosPaymentBridgeAbstract to enforce PosPaymentResult type for emitPaymentResultForTest method.
- Simplify PosPaymentBridgeService by removing commented-out code and improving error handling.
- Replace PosPaymentFormDialogComponent with SharedInvoicePaymentFormDialog in root.component.html for better payment handling.
- Enhance root.component.ts to manage payment form visibility and submission logic.
- Update light-bottomsheet.component.html for improved styling.
- Add new return form features including item removal and total price calculation in returnForm components.
- Introduce new payment form dialog for handling invoice payments with detailed structure and validation.
- Implement payment handling logic in sale-invoice-single-view component to support correction payments.
- Ensure proper integration of payment components in the shared components index.
2026-06-15 17:15:28 +03:30
23 changed files with 315 additions and 197 deletions
@@ -73,14 +73,14 @@ export class NativeBridgeService {
} }
pay(request: INativePayRequest): any { pay(request: INativePayRequest): any {
if (request.amount <= 10_000) { // if (request.amount <= 1_000) {
const errorMessage = 'برای مقادیر زیر ۱۰۰ هزار ریال، پرداخت ممکن نیست.'; // const errorMessage = 'برای مقادیر زیر ۱۰ هزار ریال، پرداخت ممکن نیست.';
this.toastService.warn({ text: errorMessage, life: 3000 }); // this.toastService.warn({ text: errorMessage, life: 3000 });
return { // return {
success: false, // success: false,
error: errorMessage, // error: errorMessage,
}; // };
} // }
this.toastService.info({ text: 'در حال پردازش پرداخت...' }); this.toastService.info({ text: 'در حال پردازش پرداخت...' });
try { try {
// @ts-ignore // @ts-ignore
@@ -28,8 +28,6 @@ export class ConsumerCustomerSaleInvoiceComponent {
constructor() { constructor() {
effect(() => { effect(() => {
if (this.invoice()?.id) { if (this.invoice()?.id) {
console.log('inja');
this.setBreadcrumb(); this.setBreadcrumb();
} }
}); });
@@ -1,5 +1,5 @@
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-input [control]="form.controls.name" name="name" label="نام مشتری" /> <app-input [control]="form.controls.name" name="name" label="نام مشتری" />
<app-input [control]="form.controls.national_id" name="national_id" label="شماره ملی" type="nationalId" /> <app-input [control]="form.controls.economic_code" name="economic_code" label="کد اقتصادی" />
<app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" /> <app-form-footer-actions submitLabel="ثبت مشتری و ادامه" (onCancel)="close()" />
</form> </form>
@@ -1,4 +1,3 @@
import { nationalIdValidator } from '@/core/validators/national-id.validator';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { InputComponent } from '@/shared/components'; import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
@@ -19,10 +18,7 @@ export class CustomerUnknownFormComponent extends AbstractForm<IUnknownCustomer,
override showSuccessMessage = false; override showSuccessMessage = false;
form = this.fb.group({ form = this.fb.group({
national_id: [ economic_code: [this.store?.customer().info?.customer_unknown?.economic_code || ''],
this.store?.customer().info?.customer_unknown?.national_id || '',
[nationalIdValidator()],
],
name: [this.store?.customer().info?.customer_unknown?.name || ''], name: [this.store?.customer().info?.customer_unknown?.name || ''],
}); });
@@ -85,8 +85,6 @@ export class PosOrderSectionComponent {
} }
submitAndPay() { submitAndPay() {
console.log('submitAndPay');
this.onSubmit.emit(); this.onSubmit.emit();
} }
@@ -15,7 +15,7 @@ export interface ILegalCustomer {
} }
export interface IUnknownCustomer { export interface IUnknownCustomer {
national_id: string; economic_code?: string;
name: string; name: string;
} }
@@ -2,5 +2,5 @@ import { PosPaymentResult } from '../models';
export abstract class PosPaymentBridgeAbstract { export abstract class PosPaymentBridgeAbstract {
abstract registerPaymentResultListener(handler: (payload: PosPaymentResult) => void): () => void; abstract registerPaymentResultListener(handler: (payload: PosPaymentResult) => void): () => void;
abstract emitPaymentResultForTest(payload: unknown): void; abstract emitPaymentResultForTest(payload: PosPaymentResult): void;
} }
@@ -11,25 +11,9 @@ export class PosPaymentBridgeService extends PosPaymentBridgeAbstract {
window.WebV = { window.WebV = {
onPaymentResult: (payload: PosPaymentResult) => { onPaymentResult: (payload: PosPaymentResult) => {
try { try {
// const parsedPayload: PosPaymentResult = JSON.parse(payload);
// this.toastServices.info({ text: payload.data?.customerCardNO || '', life: 10000 });
// this.toastServices.info({
// text: payload.data?.responseCode || 'No response code',
// life: 10000,
// });
// this.toastServices.info({ text: payload.data?.rrn || 'No response code', life: 10000 });
// this.toastServices.info({ text: payload.data?.stan || 'No response code', life: 10000 });
// this.toastServices.info({
// text: payload.data?.terminalId || 'No response code',
// life: 10000,
// });
// this.toastServices.info({
// text: payload.data?.transactionDateTime || 'No response code',
// life: 10000,
// });
handler(payload); handler(payload);
} catch (e) { } catch (e) {
this.toastServices.error({ text: 'Failed to parse payment result', life: 10000 }); // this.toastServices.error({ text: 'Failed to parse payment result', life: 10000 });
this.toastServices.error({ this.toastServices.error({
text: e instanceof Error ? e.message : String(e), text: e instanceof Error ? e.message : String(e),
life: 10000, life: 10000,
@@ -40,7 +40,13 @@
(onSubmit)="submitOrder()" /> (onSubmit)="submitOrder()" />
} }
@if (isVisiblePaymentForm()) { @if (isVisiblePaymentForm()) {
<pos-payment-form-dialog [(visible)]="isVisiblePaymentForm" (onSubmit)="submitPayment($event)" /> <shared-invoice-payment-form-dialog
[(visible)]="isVisiblePaymentForm"
[totalAmount]="priceInfo().totalAmount"
[submitOrderLoading]="submitOrderLoading()"
[isGoldGuild]="isGoldGuild()"
[isUnknownCustomer]="isUnknownCustomer()"
(onSubmitPayment)="submitPayment($event)" />
} }
@if (responseInvoice()) { @if (responseInvoice()) {
@@ -2,16 +2,17 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { PosInfoStore } from '@/domains/pos/store/pos.store'; import { PosInfoStore } from '@/domains/pos/store/pos.store';
import { AbstractIsMobileComponent } from '@/shared/abstractClasses/abstract-is-mobile'; import { AbstractIsMobileComponent } from '@/shared/abstractClasses/abstract-is-mobile';
import { PosPaymentFormDialogComponent } from '@/shared/components/invoices/payment';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { finalize } from 'rxjs';
import { PosOrderSectionDialogComponent } from '../components/order/order-section-dialog.component'; import { PosOrderSectionDialogComponent } from '../components/order/order-section-dialog.component';
import { PosOrderSectionComponent } from '../components/order/order-section.component'; import { PosOrderSectionComponent } from '../components/order/order-section.component';
import { PosOrderSubmittedDialogComponent } from '../components/order/order-submitted-dialog.component'; import { PosOrderSubmittedDialogComponent } from '../components/order/order-submitted-dialog.component';
import { PosPaymentFormDialogComponent } from '../components/payment/form-dialog.component';
import { PosGoodsComponent } from '../components/views/goods.component'; import { PosGoodsComponent } from '../components/views/goods.component';
import { IPosOrderResponse } from '../models'; import { IPayment, IPosOrderResponse } from '../models';
import { PosLandingStore } from '../store/main.store'; import { PosLandingStore } from '../store/main.store';
@Component({ @Component({
@@ -45,6 +46,11 @@ export class PosShopComponent extends AbstractIsMobileComponent {
readonly error = computed(() => this.infoStore.error()); readonly error = computed(() => this.infoStore.error());
readonly inOrderGoods = computed(() => this.landingStore.inOrderGoods()); readonly inOrderGoods = computed(() => this.landingStore.inOrderGoods());
readonly priceInfo = computed(() => this.landingStore.orderPricingInfo()); readonly priceInfo = computed(() => this.landingStore.orderPricingInfo());
readonly submitOrderLoading = computed(() => this.landingStore.submitOrderLoading());
readonly isGoldGuild = computed(
() => this.infoStore.entity()?.guild.code.toLowerCase() === 'gold'
);
readonly isUnknownCustomer = computed(() => this.landingStore.customer().type === 'UNKNOWN');
// getData() { // getData() {
// this.infoStore.getData().subscribe(); // this.infoStore.getData().subscribe();
@@ -66,10 +72,25 @@ export class PosShopComponent extends AbstractIsMobileComponent {
this.isVisiblePaymentForm.set(true); this.isVisiblePaymentForm.set(true);
} }
submitPayment(invoice: IPosOrderResponse) { submitPayment(event: {
this.closeInvoiceBottomSheet(); payment: IPayment;
this.responseInvoice.set(invoice); settlementType: 'CASH' | 'CREDIT' | 'MIXED';
this.isVisibleSuccessDialog.set(true); }) {
this.landingStore.setSettlementType(event.settlementType);
this.landingStore.setPayment(event.payment);
this.landingStore
.submitOrder()
.pipe(
finalize(() => {
this.isVisiblePaymentForm.set(false);
})
)
.subscribe((invoice) => {
this.closeInvoiceBottomSheet();
this.responseInvoice.set(invoice as IPosOrderResponse);
this.isVisibleSuccessDialog.set(true);
});
} }
closeSuccessDialog() { closeSuccessDialog() {
@@ -19,7 +19,7 @@
} }
</header> </header>
} }
<div class="light-bottomsheet-body p-4"> <div class="light-bottomsheet-body mx-auto w-full max-w-xl p-4">
@if (contentRendered) { @if (contentRendered) {
<ng-content></ng-content> <ng-content></ng-content>
} }
+1
View File
@@ -9,6 +9,7 @@ export * from './fields';
export * from './inlineConfirmation/inline-confirmation.component'; export * from './inlineConfirmation/inline-confirmation.component';
export * from './inlineEdit/inline-edit.component'; export * from './inlineEdit/inline-edit.component';
export * from './input/input.component'; export * from './input/input.component';
export * from './invoices/payment';
export * from './key-value.component/key-value.component'; export * from './key-value.component/key-value.component';
export * from './passwordInput/password-input.component'; export * from './passwordInput/password-input.component';
export * from './posPayment'; export * from './posPayment';
@@ -7,13 +7,13 @@
(onHide)="close()"> (onHide)="close()">
<div class="form-group"> <div class="form-group">
<form [formGroup]="form" (submit)="submit()"> <form [formGroup]="form" (submit)="submit()">
<app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount().toString()" type="price" /> <app-key-value label="مبلغ قابل پرداخت" [value]="totalAmount.toString()" type="price" />
<app-key-value label="مبلغ باقی‌مانده" [value]="remainedAmount().toString()" type="price" /> <app-key-value label="مبلغ باقی‌مانده" [value]="remainedAmount().toString()" type="price" />
<hr /> <hr />
</form> </form>
<div class="form-group"> <div class="form-group">
@if (!isUnknownCustomer()) { @if (!isUnknownCustomer) {
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<uikit-label> نوع تسویه‌حساب </uikit-label> <uikit-label> نوع تسویه‌حساب </uikit-label>
<p-selectButton <p-selectButton
@@ -24,6 +24,9 @@
optionValue="value" /> optionValue="value" />
</div> </div>
} }
amount:{{ payByTerminalSteps()[0]?.amount || '' }} - terminalId:
{{ payByTerminalSteps()[0]?.info?.terminal_id || '' }}- payed:{{ payByTerminalSteps()[0]?.payed || '' }}
<uikit-field label="تعداد مراحل پرداخت با پوز" name="pay_by_terminal_steps"> <uikit-field label="تعداد مراحل پرداخت با پوز" name="pay_by_terminal_steps">
<p-select <p-select
[options]="payByTerminalSteps()" [options]="payByTerminalSteps()"
@@ -68,7 +71,7 @@
</button> </button>
</ng-template> </ng-template>
</app-price-input> </app-price-input>
@if (isGoldGuild()) { @if (isGoldGuild) {
<app-price-input [control]="form.controls.set_off" name="setOff" label="تهاتر" [max]="setOffMax()"> <app-price-input [control]="form.controls.set_off" name="setOff" label="تهاتر" [max]="setOffMax()">
<ng-template #suffixTemp> <ng-template #suffixTemp>
<button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')"> <button pButton size="small" type="button" [disabled]="!remainedAmount()" (click)="fillRemained('SET_OFF')">
@@ -80,7 +83,7 @@
<app-form-footer-actions <app-form-footer-actions
submitLabel="ثبت اطلاعات پرداخت و ادامه" submitLabel="ثبت اطلاعات پرداخت و ادامه"
[loading]="submitOrderLoading()" [loading]="submitOrderLoading"
(onCancel)="close()" /> (onCancel)="close()" />
</form> </form>
</div> </div>
@@ -1,33 +1,38 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { NativeBridgeService } from '@/core/services'; import { NativeBridgeService } from '@/core/services';
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { PosInfoStore } from '@/domains/pos/store'; import {
IPayment,
PosPaymentResult,
TerminalSuccessPaymentPayload,
TOrderPaymentTypes,
} from '@/domains/pos/modules/shop/models';
import { PosPaymentBridgeAbstract } from '@/domains/pos/modules/shop/services/payment-bridge.abstract';
import { PosPaymentBridgeService } from '@/domains/pos/modules/shop/services/payment-bridge.service';
import { AbstractFormDialog } from '@/shared/abstractClasses'; import { AbstractFormDialog } from '@/shared/abstractClasses';
import { KeyValueComponent } from '@/shared/components'; import { KeyValueComponent } from '@/shared/components';
import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component'; import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { PriceInputComponent } from '@/shared/components/input/price-input.component'; import { PriceInputComponent } from '@/shared/components/input/price-input.component';
import { UikitFieldComponent, UikitLabelComponent } from '@/uikit'; import { UikitFieldComponent, UikitLabelComponent } from '@/uikit';
import { Component, computed, inject, signal } from '@angular/core'; import { formatWithCurrency } from '@/utils';
import {
Component,
EventEmitter,
inject,
Input,
Output,
signal,
SimpleChanges,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Select } from 'primeng/select'; import { Select } from 'primeng/select';
import { SelectButton } from 'primeng/selectbutton'; import { SelectButton } from 'primeng/selectbutton';
import { catchError, throwError } from 'rxjs';
import {
IPayment,
IPosOrderResponse,
PosPaymentResult,
TerminalSuccessPaymentPayload,
TOrderPaymentTypes,
} from '../../models';
import { PosPaymentBridgeAbstract } from '../../services/payment-bridge.abstract';
import { PosPaymentBridgeService } from '../../services/payment-bridge.service';
import { PosLandingStore } from '../../store/main.store';
@Component({ @Component({
selector: 'pos-payment-form-dialog', selector: 'shared-invoice-payment-form-dialog',
templateUrl: './form-dialog.component.html', templateUrl: './form-dialog.component.html',
imports: [ imports: [
FormsModule, FormsModule,
@@ -44,9 +49,16 @@ import { PosLandingStore } from '../../store/main.store';
], ],
providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }], providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }],
}) })
export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, IPosOrderResponse> { export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment, any> {
private readonly store = inject(PosLandingStore); @Input() totalAmount = 0;
private readonly posInfo = inject(PosInfoStore); @Input() submitOrderLoading = false;
@Input() isGoldGuild = false;
@Input() isUnknownCustomer = false;
@Output() onSubmitPayment = new EventEmitter<{
payment: IPayment;
settlementType: 'CASH' | 'CREDIT' | 'MIXED';
}>();
private readonly paymentBridge = inject(PosPaymentBridgeAbstract); private readonly paymentBridge = inject(PosPaymentBridgeAbstract);
private readonly nativeBridgeService = inject(NativeBridgeService); private readonly nativeBridgeService = inject(NativeBridgeService);
private readonly toastServices = inject(ToastService); private readonly toastServices = inject(ToastService);
@@ -67,18 +79,11 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
]; ];
readonly selectedSettlementType = signal<'CASH' | 'CREDIT' | 'MIXED'>('CASH'); readonly selectedSettlementType = signal<'CASH' | 'CREDIT' | 'MIXED'>('CASH');
readonly remainedAmount = signal(0);
readonly totalAmount = computed(() => this.store.orderPricingInfo().totalAmount);
readonly remainedAmount = signal(this.totalAmount());
setOffMax = signal(this.remainedAmount()); setOffMax = signal(this.remainedAmount());
cashMax = signal(this.remainedAmount()); cashMax = signal(this.remainedAmount());
terminalsMax = signal([this.remainedAmount()]); terminalsMax = signal([this.remainedAmount()]);
readonly submitOrderLoading = computed(() => this.store.submitOrderLoading());
readonly isGoldGuild = computed(() => this.posInfo.entity()?.guild.code.toLowerCase() === 'gold');
readonly isUnknownCustomer = computed(() => this.store.customer().type === 'UNKNOWN');
payByTerminalSteps = signal( payByTerminalSteps = signal(
Array.from({ length: 10 }, (_, i) => ({ Array.from({ length: 10 }, (_, i) => ({
value: i + 1, value: i + 1,
@@ -95,35 +100,51 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
private readonly injectedPaymentResultHandler = (payload: PosPaymentResult) => { private readonly injectedPaymentResultHandler = (payload: PosPaymentResult) => {
if (payload.status === 'SUCCESS') { if (payload.status === 'SUCCESS') {
const paidStepId = Number(payload.id || 0); const paidStepId = this.resolvePaidStepId(payload);
this.payByTerminalSteps.update((prev) => const terminalId = payload.data?.terminalId || '';
prev.map((step, index) => { const stan = payload.data?.stan || '';
if (step.value === paidStepId) { const rrn = payload.data?.rrn || '';
return { const customerCardNo = payload.data?.customerCardNO || '';
...step, const transactionDateTime = payload.data?.transactionDateTime;
payed: true,
amount: this.terminalControls[step.value - 1]?.value || 0, const targetIndex = Math.max(0, paidStepId - 1);
info: { const prevSteps = this.payByTerminalSteps();
customer_card_no: payload.data?.customerCardNO || '', const steps = [...prevSteps];
description: payload.message || '', const currentStep = steps[targetIndex];
rrn: payload.data?.rrn || '', if (!currentStep) return;
stan: payload.data?.stan || '',
terminal_id: payload.data?.terminalId || '', steps[targetIndex] = {
transaction_date_time: new Date(payload.data?.transactionDateTime || ''), ...currentStep,
}, payed: true,
}; amount: Number(this.terminalControls[targetIndex]?.value || currentStep.amount || 0),
} info: {
return step; customer_card_no: customerCardNo,
}) description: payload.message || '',
); rrn,
const terminalControl = this.terminalControls[paidStepId - 1]; stan,
terminal_id: terminalId,
transaction_date_time: transactionDateTime ? new Date(transactionDateTime) : new Date(),
},
};
this.payByTerminalSteps.set(steps);
const terminalControl = this.terminalControls[targetIndex];
if (terminalControl) { if (terminalControl) {
terminalControl.disable({ onlySelf: true, emitEvent: false }); terminalControl.disable({ onlySelf: true, emitEvent: false });
} }
const amount = Number( const amount = Number(
terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0 terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0
); );
this.toastServices.success({ text: `پرداخت با موفقیت انجام شد. مبلغ: ${amount}` }); let successMessage = '';
if (this.payByTerminalSteps()?.length > 1) {
successMessage = `پرداخت مرحله ${paidStepId} به مبلغ ${formatWithCurrency(amount)} با موفقیت انجام شد.`;
} else {
successMessage = `پرداخت به مبلغ ${formatWithCurrency(amount)} با موفقیت انجام شد.`;
}
this.toastServices.success({
text: successMessage,
});
} else { } else {
this.toastServices.error({ text: payload.message || 'پرداخت با دستگاه انجام نشد.' }); this.toastServices.error({ text: payload.message || 'پرداخت با دستگاه انجام نشد.' });
} }
@@ -147,7 +168,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
(value.terminals || []).reduce((acc, curr) => (acc || 0) + (curr || 0), 0) || 0; (value.terminals || []).reduce((acc, curr) => (acc || 0) + (curr || 0), 0) || 0;
const totalValue = (value.cash || 0) + (value.set_off || 0) + terminalTotal; const totalValue = (value.cash || 0) + (value.set_off || 0) + terminalTotal;
const remainedAmount = this.totalAmount() - totalValue; const remainedAmount = this.totalAmount - totalValue;
const setOffMax = remainedAmount + (value.set_off || 0); const setOffMax = remainedAmount + (value.set_off || 0);
const cashMax = remainedAmount + (value.cash || 0); const cashMax = remainedAmount + (value.cash || 0);
@@ -280,6 +301,26 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
override showSuccessMessage = false; override showSuccessMessage = false;
private resetLimits() {
const amount = Number(this.totalAmount || 0);
this.remainedAmount.set(amount);
this.cashMax.set(amount);
this.setOffMax.set(amount);
this.terminalsMax.set(this.terminalControls.map(() => amount));
}
override ngOnInit(): void {
super.ngOnInit();
this.resetLimits();
}
override ngOnChanges(changes: SimpleChanges): void {
super.ngOnChanges(changes);
if (changes['totalAmount']) {
this.resetLimits();
}
}
ngOnDestroy() { ngOnDestroy() {
this.restorePaymentCallback?.(); this.restorePaymentCallback?.();
} }
@@ -293,8 +334,6 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
} }
} }
this.store.setSettlementType(this.selectedSettlementType());
const rawPayment = this.form.getRawValue(); const rawPayment = this.form.getRawValue();
const terminalPayments = this.payByTerminalSteps() const terminalPayments = this.payByTerminalSteps()
@@ -310,50 +349,39 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
transaction_date_time: step.info.transaction_date_time || new Date(), transaction_date_time: step.info.transaction_date_time || new Date(),
description: step.info.description || '', description: step.info.description || '',
})); }));
const payment: IPayment = { const payment: IPayment = {
cash: Number(rawPayment.cash || 0), cash: Number(rawPayment.cash || 0),
set_off: Number(rawPayment.set_off || 0), set_off: Number(rawPayment.set_off || 0),
terminals: terminalPayments, terminals: terminalPayments,
}; };
this.store.setPayment(payment);
const terminalPaymentIsDone = this.checkTerminalPayments(); const terminalPaymentIsDone = this.checkTerminalPayments();
if (terminalPaymentIsDone) { if (terminalPaymentIsDone) {
// this.toastService.info({ this.onSubmitPayment.emit({
// text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`, payment,
// }); settlementType: this.selectedSettlementType(),
this.store });
.submitOrder()
.pipe(
catchError((err) => {
return throwError(() => err);
})
)
.subscribe((res) => {
this.close();
this.onSubmit.emit(res);
});
} }
} }
private checkTerminalPayments(): boolean { private checkTerminalPayments(): boolean {
const pendingTerminalPayments = this.getPendingTerminalPayments(); const pendingTerminalPayments = this.getPendingTerminalPayments();
if (pendingTerminalPayments.length) { if (pendingTerminalPayments.length) {
if (this.nativeBridgeService.isEnabled()) { // if (this.nativeBridgeService.isEnabled()) {
const nextPending = pendingTerminalPayments[0]; const nextPending = pendingTerminalPayments[0];
this.payByTerminal(nextPending.stepId, nextPending.amount); this.payByTerminal(nextPending.stepId, nextPending.amount);
this.toastServices.info({ this.toastServices.info({
text: 'پس از تایید پرداخت هر مرحله، برای مرحله بعد دوباره روی ثبت پرداخت بزنید.', text: 'پس از تایید پرداخت هر مرحله، برای مرحله بعد دوباره روی ثبت پرداخت بزنید.',
life: 3500, life: 3500,
}); });
} else { // } else {
this.toastServices.error({ // this.toastServices.error({
text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.', // text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.',
}); // });
return false; // return false;
} // }
return false; return false;
} }
@@ -362,16 +390,22 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
} }
private payByTerminal(id: number, amount: number) { private payByTerminal(id: number, amount: number) {
// this.paymentBridge.emitPaymentResultForTest({
// status: 'SUCCESS',
// id: id.toString(),
// data: {
// customerCardNO: '123',
// responseCode: '2',
// rrn: 'rrn',
// stan: 'stan',
// terminalId: 'terminalId',
// transactionDateTime: new Date().toISOString(),
// },
// });
const payResult = this.nativeBridgeService.pay({ const payResult = this.nativeBridgeService.pay({
amount, amount,
id: String(id), id: String(id),
}); });
// if (!payResult.success) {
// return this.toastServices.warn({
// text: payResult.error || 'پرداخت با دستگاه انجام نشد. لطفا دوباره تلاش کنید.',
// });
// }
} }
private isTerminalStepPaid(stepId: number): boolean { private isTerminalStepPaid(stepId: number): boolean {
@@ -395,8 +429,6 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
.filter(({ stepId, amount }) => amount > 0 && !this.isTerminalStepPaid(stepId)); .filter(({ stepId, amount }) => amount > 0 && !this.isTerminalStepPaid(stepId));
} }
override onSuccess(response: IPosOrderResponse): void {}
private extractAmountFromPaymentResult(payload: unknown): number | null { private extractAmountFromPaymentResult(payload: unknown): number | null {
if (!payload || typeof payload !== 'object') return null; if (!payload || typeof payload !== 'object') return null;
const amount = (payload as { amount?: unknown }).amount; const amount = (payload as { amount?: unknown }).amount;
@@ -405,4 +437,14 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
} }
return null; return null;
} }
private resolvePaidStepId(payload: PosPaymentResult): number {
const rawId = Number(payload.id || 0);
if (rawId > 0 && this.payByTerminalSteps().some((step) => step.value === rawId)) return rawId;
const pendingStep = this.terminalControls.findIndex(
(control, index) => Number(control.value || 0) > 0 && !this.isTerminalStepPaid(index + 1)
);
return pendingStep >= 0 ? pendingStep + 1 : 1;
}
} }
@@ -0,0 +1 @@
export * from './form-dialog.component';
@@ -13,9 +13,14 @@
<shared-return-form-item <shared-return-form-item
[control]="item.controls.quantity" [control]="item.controls.quantity"
[good]="goodItems()[$index]!.good_snapshot" [good]="goodItems()[$index]!.good_snapshot"
[index]="$index" /> [index]="$index"
[maxQuantity]="item.controls.maxQuantity.value"
[canRemove]="canRemoveItem()"
(onRemove)="removeItem($index)" />
</div> </div>
} }
<pos-order-price-info-card [info]="totalPriceInfo" />
<app-form-footer-actions /> <app-form-footer-actions />
</form> </form>
@@ -1,4 +1,5 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { POSOrderPriceInfoCardComponent } from '@/domains/pos/modules/shop/components/order/price-info-card.component';
import { AbstractForm } from '@/shared/abstractClasses'; import { AbstractForm } from '@/shared/abstractClasses';
import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils'; import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils';
import { Component, computed, Input, signal, SimpleChanges } from '@angular/core'; import { Component, computed, Input, signal, SimpleChanges } from '@angular/core';
@@ -32,6 +33,7 @@ type BackFromSaleFormValue = {
ReactiveFormsModule, ReactiveFormsModule,
FieldInvoiceDateComponent, FieldInvoiceDateComponent,
SharedReturnFormItemComponent, SharedReturnFormItemComponent,
POSOrderPriceInfoCardComponent,
], ],
}) })
export class SharedReturnFormComponent extends AbstractForm< export class SharedReturnFormComponent extends AbstractForm<
@@ -54,11 +56,33 @@ export class SharedReturnFormComponent extends AbstractForm<
return this.form.controls.items; return this.form.controls.items;
} }
canRemoveItem() {
return this.items.length > 1;
}
removeItem(index: number) {
if (!this.canRemoveItem()) {
this.toastService.warn({ text: 'حداقل یک آیتم باید باقی بماند.' });
return;
}
this.items.removeAt(index);
this.form.controls.items.updateValueAndValidity();
this.recalculateTotalPriceInfo();
}
preparedCalculation = signal<Maybe<any>>(null); preparedCalculation = signal<Maybe<any>>(null);
totalPriceInfo = {
totalBaseAmount: 0,
discountAmount: 0,
taxAmount: 0,
totalAmount: 0,
};
init() { init() {
this.initForm(); this.initForm();
this.prepareCalculation(); this.prepareCalculation();
this.recalculateTotalPriceInfo();
} }
prepareCalculation() { prepareCalculation() {
@@ -85,6 +109,38 @@ export class SharedReturnFormComponent extends AbstractForm<
}) })
); );
}); });
this.items.valueChanges.subscribe(() => {
this.recalculateTotalPriceInfo();
});
}
private recalculateTotalPriceInfo() {
const initialMap = new Map((this.initialValues || []).map((item) => [String(item.id), item]));
const currentItems = this.items.getRawValue();
this.totalPriceInfo = currentItems.reduce(
(acc, current) => {
const source = initialMap.get(String(current.id || ''));
if (!source) return acc;
const maxQuantity = Number(current.maxQuantity || 0);
const quantity = Number(current.quantity || 0);
const ratio = maxQuantity > 0 ? quantity / maxQuantity : 0;
acc.totalBaseAmount += Number(source.unit_price || 0) * quantity;
acc.discountAmount += Number(source.discount_amount || 0) * ratio;
acc.taxAmount += Number(source.tax_amount || 0) * ratio;
acc.totalAmount += Number(source.total_amount || 0) * ratio;
return acc;
},
{
totalBaseAmount: 0,
discountAmount: 0,
taxAmount: 0,
totalAmount: 0,
}
);
} }
override ngOnInit(): void { override ngOnInit(): void {
@@ -98,15 +154,17 @@ export class SharedReturnFormComponent extends AbstractForm<
} }
override submitForm() { override submitForm() {
const payload = this.items.getRawValue().map((item) => ({ const rawItems = this.items.getRawValue();
const payload = rawItems.map((item) => ({
id: item.id || '', id: item.id || '',
quantity: Number(item.quantity || 0), quantity: Number(item.quantity || 0),
maxQuantity: Number(item.maxQuantity || 0), maxQuantity: Number(item.maxQuantity || 0),
})); }));
const hasChanges = payload.some( const initialMap = new Map(
(item, index) => item.quantity !== Number(this.initialValues?.[index]?.quantity || 0) (this.initialValues || []).map((item) => [String(item.id), Number(item.quantity || 0)])
); );
const hasChanges = payload.some((item) => item.quantity !== Number(initialMap.get(item.id) || 0));
if (!hasChanges) { if (!hasChanges) {
this.toastService.warn({ this.toastService.warn({
@@ -1,13 +1,34 @@
<div> <div class="border-surface-border rounded-xl border p-2">
<p-divider align="right"> <div class="flex items-center gap-3">
{{ index + 1 }}- <b>{{ good.name }}</b> <img
</p-divider> [src]="good.image_url || ''"
<app-input loading="lazy"
[control]="control" decoding="async"
name="quantity" class="bg-surface-100 border-surface-border h-20 w-20 shrink-0 rounded-md border object-cover" />
[label]="good.measure_unit.name" <div class="flex grow flex-col gap-2 overflow-hidden py-1">
type="number" <div class="flex items-center justify-between gap-2">
[suffix]="good.measure_unit.name" <span class="overflow-hidden text-base font-bold text-nowrap text-ellipsis">
[max]="control.defaultValue || undefined" {{ good.name }}
[min]="0" /> </span>
<button
pButton
type="button"
icon="pi pi-trash"
size="small"
severity="danger"
outlined
[disabled]="!canRemove"
(click)="remove()"></button>
</div>
<span class="text-muted-color text-sm">مقدار: {{ maxQuantity || 0 }} {{ good.measure_unit.name }}</span>
<app-input
[control]="control"
name="quantity"
label="مقدار"
type="number"
[suffix]="good.measure_unit.name"
[max]="maxQuantity || undefined"
[min]="0" />
</div>
</div>
</div> </div>
@@ -1,7 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { Component, Input, signal, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, Output, signal, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Divider } from 'primeng/divider'; import { ButtonDirective } from 'primeng/button';
import { InputComponent } from '../../input/input.component'; import { InputComponent } from '../../input/input.component';
import { Goodsnapshot } from '../sale-invoice-full-response.model'; import { Goodsnapshot } from '../sale-invoice-full-response.model';
@@ -20,12 +20,15 @@ type BackFromSaleFormValue = {
@Component({ @Component({
selector: 'shared-return-form-item', selector: 'shared-return-form-item',
templateUrl: './item-form.component.html', templateUrl: './item-form.component.html',
imports: [InputComponent, ReactiveFormsModule, Divider], imports: [InputComponent, ReactiveFormsModule, ButtonDirective],
}) })
export class SharedReturnFormItemComponent { export class SharedReturnFormItemComponent {
@Input({ required: true }) control!: FormControl<number | null>; @Input({ required: true }) control!: FormControl<number | null>;
@Input({ required: true }) good!: Goodsnapshot; @Input({ required: true }) good!: Goodsnapshot;
@Input({ required: true }) index!: number; @Input({ required: true }) index!: number;
@Input() canRemove = true;
@Input() maxQuantity: number | null = null;
@Output() onRemove = new EventEmitter<void>();
preparedCalculation = signal<Maybe<any>>(null); preparedCalculation = signal<Maybe<any>>(null);
@@ -42,4 +45,8 @@ export class SharedReturnFormItemComponent {
this.prepareCalculation(); this.prepareCalculation();
} }
} }
remove() {
this.onRemove.emit();
}
} }
@@ -150,5 +150,5 @@ interface ConsumerAccount {
interface UnknownCustomer { interface UnknownCustomer {
name?: string; name?: string;
national_code?: string; economic_code?: string;
} }
@@ -61,7 +61,7 @@
<app-key-value label="نوع مشتری" value="حقوقی" /> <app-key-value label="نوع مشتری" value="حقوقی" />
<app-key-value label="نام شرکت" [value]="invoice.customer.legal?.company_name" /> <app-key-value label="نام شرکت" [value]="invoice.customer.legal?.company_name" />
<app-key-value label="کد اقتصادی" [value]="invoice.customer.legal?.economic_code" /> <app-key-value label="کد اقتصادی" [value]="invoice.customer.legal?.economic_code" />
<app-key-value label="کد ثبتی" [value]="invoice.customer.legal?.registration_number" /> <app-key-value label="شماره ثبت" [value]="invoice.customer.legal?.registration_number" />
<app-key-value label="کد پستی" [value]="invoice.customer.legal?.postal_code" /> <app-key-value label="کد پستی" [value]="invoice.customer.legal?.postal_code" />
} }
</div> </div>
@@ -69,7 +69,7 @@
<div class="listKeyValue"> <div class="listKeyValue">
<app-key-value label="نوع مشتری" value="نوع دوم" /> <app-key-value label="نوع مشتری" value="نوع دوم" />
<app-key-value label="عنوان" [value]="invoice.unknown_customer.name" /> <app-key-value label="عنوان" [value]="invoice.unknown_customer.name" />
<app-key-value label="کد ثبتی" [value]="invoice.unknown_customer.national_code" /> <app-key-value label="کد اقتصادی" [value]="invoice.unknown_customer.economic_code" />
</div> </div>
} @else { } @else {
<p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p> <p class="text-text-color pt-3 pb-5 text-center">اطلاعات مشتری ثبت نشده است.</p>
@@ -60,26 +60,12 @@
</div> </div>
</shared-light-bottomsheet> </shared-light-bottomsheet>
<shared-light-bottomsheet [(visible)]="showCorrectionPaymentDialog" header="پرداخت اصلاحی"> <shared-invoice-payment-form-dialog
<div class="flex flex-col gap-4"> [(visible)]="showCorrectionPaymentDialog"
<div class="text-sm">مبلغ اصلاحی افزایش یافته است. لطفاً مبلغ پرداختی را ثبت کنید.</div> [totalAmount]="correctionRequiredPayment()"
<div class="border-surface-border rounded-lg border p-3"> [submitOrderLoading]="false"
<div class="text-muted-color text-xs">حداقل مبلغ موردنیاز</div> [isGoldGuild]="false"
<div class="text-lg font-bold" [appPriceMask]="correctionRequiredPayment()"></div> [isUnknownCustomer]="true"
</div> (onSubmitPayment)="confirmCorrectionPayment($event)"
<div class="flex flex-col gap-2"> (onClose)="cancelCorrectionPayment()" />
<label class="text-sm font-medium">مبلغ پرداختی</label>
<input
type="number"
min="0"
class="border-surface-border w-full rounded-md border px-3 py-2"
[value]="correctionPaymentAmount()"
(input)="onCorrectionPaymentAmountChange($event)" />
</div>
<div class="flex justify-end gap-2">
<button pButton type="button" severity="secondary" label="انصراف" (click)="cancelCorrectionPayment()"></button>
<button pButton type="button" label="تایید پرداخت" (click)="confirmCorrectionPayment()"></button>
</div>
</div>
</shared-light-bottomsheet>
} }
@@ -4,13 +4,13 @@ import { NavigationService } from '@/core/services/navigation.service';
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import { PosConfigPrintService } from '@/domains/pos/modules/configs/components/print/services/main.service'; import { PosConfigPrintService } from '@/domains/pos/modules/configs/components/print/services/main.service';
import { IPosReturnFromSaleRequest } from '@/domains/pos/modules/saleInvoices/models/returnFromSale'; import { IPosReturnFromSaleRequest } from '@/domains/pos/modules/saleInvoices/models/returnFromSale';
import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; import { IPayment, IPosOrderItem } from '@/domains/pos/modules/shop/models';
import { PosInfoStore } from '@/domains/pos/store'; import { PosInfoStore } from '@/domains/pos/store';
import { TspProviderResponseStatus } from '@/shared/catalog'; import { TspProviderResponseStatus } from '@/shared/catalog';
import { SharedLightBottomsheetComponent } from '@/shared/components'; import { SharedLightBottomsheetComponent } from '@/shared/components';
import { PosPaymentFormDialogComponent } from '@/shared/components/invoices/payment';
import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model'; import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model';
import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { PageLoadingComponent } from '@/shared/components/page-loading.component';
import { PriceMaskDirective } from '@/shared/directives';
import { UikitEmptyStateComponent } from '@/uikit'; import { UikitEmptyStateComponent } from '@/uikit';
import { import {
Component, Component,
@@ -67,8 +67,8 @@ type TActionMenuItem = {
SaleInvoiceSingleInfoCardComponent, SaleInvoiceSingleInfoCardComponent,
QRCodeComponent, QRCodeComponent,
Card, Card,
PriceMaskDirective,
Menu, Menu,
PosPaymentFormDialogComponent,
], ],
}) })
export class SharedSaleInvoiceSingleViewComponent { export class SharedSaleInvoiceSingleViewComponent {
@@ -105,7 +105,6 @@ export class SharedSaleInvoiceSingleViewComponent {
showQrCodeDialog = signal(false); showQrCodeDialog = signal(false);
showCorrectionPaymentDialog = signal(false); showCorrectionPaymentDialog = signal(false);
correctionRequiredPayment = signal(0); correctionRequiredPayment = signal(0);
correctionPaymentAmount = signal(0);
readonly isApplication = config.isPosApplication; readonly isApplication = config.isPosApplication;
private pendingCorrectionSubmitResolver: Maybe<(allowed: boolean) => void> = null; private pendingCorrectionSubmitResolver: Maybe<(allowed: boolean) => void> = null;
@@ -292,7 +291,6 @@ export class SharedSaleInvoiceSingleViewComponent {
const requiredAmount = newTotalAmount - oldTotalAmount; const requiredAmount = newTotalAmount - oldTotalAmount;
this.correctionRequiredPayment.set(requiredAmount); this.correctionRequiredPayment.set(requiredAmount);
this.correctionPaymentAmount.set(requiredAmount);
this.showCorrectionPaymentDialog.set(true); this.showCorrectionPaymentDialog.set(true);
const allowByDialog = await new Promise<boolean>((resolve) => { const allowByDialog = await new Promise<boolean>((resolve) => {
@@ -306,17 +304,10 @@ export class SharedSaleInvoiceSingleViewComponent {
return (await this.beforeCorrectionSubmit?.(mapped)) ?? true; return (await this.beforeCorrectionSubmit?.(mapped)) ?? true;
} }
onCorrectionPaymentAmountChange(event: Event) { confirmCorrectionPayment(_event: {
const value = Number((event.target as HTMLInputElement)?.value || 0); payment: IPayment;
this.correctionPaymentAmount.set(Number.isFinite(value) ? value : 0); settlementType: 'CASH' | 'CREDIT' | 'MIXED';
} }) {
confirmCorrectionPayment() {
if (this.correctionPaymentAmount() < this.correctionRequiredPayment()) {
this.toastService.warn({ text: 'مبلغ پرداختی باید حداقل برابر با افزایش مبلغ باشد.' });
return;
}
this.showCorrectionPaymentDialog.set(false); this.showCorrectionPaymentDialog.set(false);
this.pendingCorrectionSubmitResolver?.(true); this.pendingCorrectionSubmitResolver?.(true);
this.pendingCorrectionSubmitResolver = null; this.pendingCorrectionSubmitResolver = null;