Compare commits

...

3 Commits

Author SHA1 Message Date
ahasani 93ebc80da3 feat: enhance sale invoice component with dynamic invoice ID handling and navigation improvements
Production CI / validate-and-build (push) Failing after 1m6s
2026-06-17 14:57:44 +03:30
ahasani 4e4cc08224 feat: enhance invoice handling with success messages and payment data integration
Production CI / validate-and-build (push) Failing after 1m7s
2026-06-17 10:03:06 +03:30
ahasani e5f53c2265 set loading in correction and return form 2026-06-17 09:21:20 +03:30
10 changed files with 88 additions and 65 deletions
@@ -1,6 +1,8 @@
import { TspProviderResponseStatus } from '@/shared/catalog'; import { TspProviderResponseStatus } from '@/shared/catalog';
import { IPosSaleInvoicesSummaryResponse } from './io';
export interface IPosSaleInvoiceFiscalActionResponse { export interface IPosSaleInvoiceFiscalActionResponse {
invoice: IPosSaleInvoicesSummaryResponse;
message?: string; message?: string;
status: TspProviderResponseStatus; status: TspProviderResponseStatus;
} }
@@ -5,6 +5,8 @@
[sendToTspLoading]="sendToTspLoading()" [sendToTspLoading]="sendToTspLoading()"
[inquiryLoading]="inquiryLoading()" [inquiryLoading]="inquiryLoading()"
[resendToTspLoading]="resendToTspLoading()" [resendToTspLoading]="resendToTspLoading()"
[correctionLoading]="correctionLoading()"
[returnFromSaleLoading]="backFromSaleLoading()"
(onSendToTsp)="sendToTsp()" (onSendToTsp)="sendToTsp()"
(onResendToTsp)="resendToTsp()" (onResendToTsp)="resendToTsp()"
(onInquiry)="inquiry()" (onInquiry)="inquiry()"
@@ -1,10 +1,10 @@
import { ToastService } from '@/core/services/toast.service'; import { ToastService } from '@/core/services/toast.service';
import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util'; import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util';
import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component'; import { SharedSaleInvoiceSingleViewComponent } from '@/shared/components/invoices/sale-invoice-single-view.component';
import pageParamsUtils from '@/utils/page-params.utils';
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { finalize } from 'rxjs'; import { distinctUntilChanged, finalize, map } from 'rxjs';
import { posSaleInvoicesNamedRoutes } from '../constants'; import { posSaleInvoicesNamedRoutes } from '../constants';
import { IPosCorrectionRequest } from '../models/correction'; import { IPosCorrectionRequest } from '../models/correction';
import { IPosReturnFromSaleRequest } from '../models/returnFromSale'; import { IPosReturnFromSaleRequest } from '../models/returnFromSale';
@@ -20,8 +20,20 @@ export class PosSaleInvoiceComponent {
private readonly store = inject(PosSaleInvoiceStore); private readonly store = inject(PosSaleInvoiceStore);
private readonly toastService = inject(ToastService); private readonly toastService = inject(ToastService);
pageParams = computed(() => pageParamsUtils(this.route)); invoiceId = signal<string>('');
invoiceId = signal<string>(this.pageParams()['invoiceId']);
constructor() {
this.route.paramMap
.pipe(
map((params) => params.get('invoiceId') ?? ''),
distinctUntilChanged(),
takeUntilDestroyed()
)
.subscribe((invoiceId) => {
this.invoiceId.set(invoiceId);
this.store.getData(invoiceId);
});
}
inquiryLoading = signal(false); inquiryLoading = signal(false);
sendToTspLoading = signal(false); sendToTspLoading = signal(false);
@@ -94,7 +106,12 @@ export class PosSaleInvoiceComponent {
this.correctionLoading.set(false); this.correctionLoading.set(false);
}) })
) )
.subscribe(); .subscribe((res) => {
this.toastService.success({
text: `اصلاحیه‌ی صورت‌حساب با شماره‌ی ${res.invoice?.invoice_number} با موفقیت ایجاد شد.`,
});
this.store.updateStatus(res.status);
});
} }
returnFromSale(data: IPosReturnFromSaleRequest) { returnFromSale(data: IPosReturnFromSaleRequest) {
this.backFromSaleLoading.set(true); this.backFromSaleLoading.set(true);
@@ -105,7 +122,12 @@ export class PosSaleInvoiceComponent {
this.backFromSaleLoading.set(false); this.backFromSaleLoading.set(false);
}) })
) )
.subscribe(); .subscribe((res) => {
this.toastService.success({
text: `برگشت از فروش با شماره‌ی ${res.invoice?.invoice_number} با موفقیت ایجاد شد.`,
});
this.store.updateStatus(res.status);
});
} }
revoke() { revoke() {
this.revokeLoading.set(true); this.revokeLoading.set(true);
@@ -116,10 +138,11 @@ export class PosSaleInvoiceComponent {
this.revokeLoading.set(false); this.revokeLoading.set(false);
}) })
) )
.subscribe(); .subscribe((res) => {
} this.toastService.success({
text: `صورت‌حساب شماره‌ی ${res.invoice?.invoice_number} با موفقیت ابطال شد.`,
ngOnInit() { });
this.store.getData(this.invoiceId()); this.store.updateStatus(res.status);
});
} }
} }
@@ -43,7 +43,7 @@
<pos-order-price-info-card [info]="totalPriceInfo" /> <pos-order-price-info-card [info]="totalPriceInfo" />
<app-form-footer-actions [loading]="submitLoading()" (onCancel)="close()" /> <app-form-footer-actions [loading]="loading || submitLoading()" (onCancel)="close()" />
</form> </form>
<shared-light-bottomsheet <shared-light-bottomsheet
@@ -42,6 +42,7 @@ export class SharedCorrectionFormComponent extends AbstractForm<
@Input() beforeSubmit?: (payload: CorrectionInvoiceFormValue) => Promise<boolean> | boolean; @Input() beforeSubmit?: (payload: CorrectionInvoiceFormValue) => Promise<boolean> | boolean;
@Input({ required: true }) invoiceDate!: string; @Input({ required: true }) invoiceDate!: string;
@Input() visible = false; @Input() visible = false;
@Input() loading = false;
form = this.fb.group({ form = this.fb.group({
invoice_date: [this.invoiceDate], invoice_date: [this.invoiceDate],
@@ -22,5 +22,5 @@
<pos-order-price-info-card [info]="totalPriceInfo" /> <pos-order-price-info-card [info]="totalPriceInfo" />
<app-form-footer-actions (onCancel)="close()" /> <app-form-footer-actions [loading]="loading || submitLoading()" (onCancel)="close()" />
</form> </form>
@@ -4,18 +4,18 @@
@if (invoice.referenced_by) { @if (invoice.referenced_by) {
<p-message severity="warn" [closable]="false"> <p-message severity="warn" [closable]="false">
این صورت‌حساب، مرجع صورت‌حساب شماره‌ی این صورت‌حساب، مرجع صورت‌حساب شماره‌ی
<a [href]="referenceByInvoiceRoute()" class="text-lg font-bold"> <span (click)="navigateToRelatedInvoice(invoice.referenced_by.id)" class="text-lg font-bold">
{{ invoice.referenced_by.invoice_number }} {{ invoice.referenced_by.invoice_number }}
</a> </span>
می‌باشد. می‌باشد.
</p-message> </p-message>
} }
@if (invoice.reference_invoice) { @if (invoice.reference_invoice) {
<p-message severity="info" [closable]="false"> <p-message severity="info" [closable]="false">
صورت‌حساب مرجع به شماره‌ی صورت‌حساب مرجع به شماره‌ی
<a [href]="referenceInvoiceRoute()" class="text-lg font-bold"> <span (click)="navigateToRelatedInvoice(invoice.reference_invoice.id)" class="text-lg font-bold">
{{ invoice.reference_invoice.invoice_number }} {{ invoice.reference_invoice.invoice_number }}
</a> </span>
می‌باشد. می‌باشد.
</p-message> </p-message>
} }
@@ -5,8 +5,8 @@ import {
} from '@/shared/catalog'; } from '@/shared/catalog';
import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType'; import { CatalogInvoiceSettlementTypeTagComponent } from '@/shared/catalog/invoiceSettlementType';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { Divider } from 'primeng/divider'; import { Divider } from 'primeng/divider';
import { Message } from 'primeng/message'; import { Message } from 'primeng/message';
@@ -37,6 +37,7 @@ export class SaleInvoiceSingleInfoCardComponent {
@Input() variant: TSaleInvoiceSingleViewVariant = 'full'; @Input() variant: TSaleInvoiceSingleViewVariant = 'full';
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
columns: IColumn[] = [ columns: IColumn[] = [
{ {
@@ -107,34 +108,18 @@ export class SaleInvoiceSingleInfoCardComponent {
}, },
]; ];
referenceInvoiceRoute = computed(() => { navigateToRelatedInvoice(invoiceId: string) {
const referenceInvoiceId = this.invoice?.reference_invoice?.id;
const pagePath = this.route.pathFromRoot const pagePath = this.route.pathFromRoot
.flatMap((route) => route.snapshot.url.map((segment) => segment.path)) .flatMap((route) => route.snapshot.url.map((segment) => segment.path))
.join('/'); .join('/');
if (!pagePath || !referenceInvoiceId) { if (pagePath && invoiceId) {
return pagePath;
}
const pathParts = pagePath.split('/').filter(Boolean); const pathParts = pagePath.split('/').filter(Boolean);
pathParts[pathParts.length - 1] = referenceInvoiceId; pathParts[pathParts.length - 1] = invoiceId;
return `/${pathParts.join('/')}`; this.router.navigateByUrl(`/${pathParts.join('/')}`, {
}); skipLocationChange: false,
referenceByInvoiceRoute = computed(() => {
const referenceInvoiceId = this.invoice?.referenced_by?.id;
const pagePath = this.route.pathFromRoot
.flatMap((route) => route.snapshot.url.map((segment) => segment.path))
.join('/');
if (!pagePath || !referenceInvoiceId) {
return pagePath;
}
const pathParts = pagePath.split('/').filter(Boolean);
pathParts[pathParts.length - 1] = referenceInvoiceId;
return `/${pathParts.join('/')}`;
}); });
} }
}
}
@@ -7,7 +7,7 @@
<ng-template #actions> <ng-template #actions>
<p-button (click)="showQrCode()" icon="pi pi-qrcode" outlined size="small" /> <p-button (click)="showQrCode()" icon="pi pi-qrcode" outlined size="small" />
<p-button (click)="printInvoice()" icon="pi pi-print" outlined size="small" label="چاپ" /> <p-button (click)="printInvoice()" icon="pi pi-print" outlined size="small" label="چاپ" />
@if (!isApplication && canDoActionOnInvoice()) { @if (!isApplication && canDoActionOnInvoice) {
<p-button (click)="menu.toggle($event)" icon="pi pi-ellipsis-v" label="عملیات بیشتر" outlined size="small"> <p-button (click)="menu.toggle($event)" icon="pi pi-ellipsis-v" label="عملیات بیشتر" outlined size="small">
</p-button> </p-button>
<p-menu #menu [model]="moreActionMenuItems()" [popup]="true"></p-menu> <p-menu #menu [model]="moreActionMenuItems()" [popup]="true"></p-menu>
@@ -18,7 +18,7 @@
<shared-sale-invoice-single-info-card [loading]="loading" [invoice]="invoice" [variant]="variant" /> <shared-sale-invoice-single-info-card [loading]="loading" [invoice]="invoice" [variant]="variant" />
</div> </div>
@if (isApplication && moreActionMenuItems().length && canDoActionOnInvoice()) { @if (isApplication && moreActionMenuItems().length && canDoActionOnInvoice) {
<div class="border-surface-border bg-surface-card sticky bottom-0 z-10 mt-4 w-full rounded-t-2xl border-t p-3"> <div class="border-surface-border bg-surface-card sticky bottom-0 z-10 mt-4 w-full rounded-t-2xl border-t p-3">
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap justify-center gap-2">
@for (action of moreActionMenuItems(); track action.label || $index) { @for (action of moreActionMenuItems(); track action.label || $index) {
@@ -35,7 +35,7 @@
</div> </div>
} }
@if (canDoActionOnInvoice()) { @if (canDoActionOnInvoice) {
<shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش"> <shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش">
<shared-return-form <shared-return-form
[initialValues]="invoice.items" [initialValues]="invoice.items"
@@ -50,6 +50,7 @@
[initialValues]="invoice.items" [initialValues]="invoice.items"
[invoiceDate]="invoice.invoice_date" [invoiceDate]="invoice.invoice_date"
[beforeSubmit]="beforeCorrectionSubmitHandler.bind(this)" [beforeSubmit]="beforeCorrectionSubmitHandler.bind(this)"
[loading]="correctionLoading || false"
(onSubmit)="correctionSubmit($event)" (onSubmit)="correctionSubmit($event)"
(onClose)="cancelCorrection()" /> (onClose)="cancelCorrection()" />
</shared-light-bottomsheet> </shared-light-bottomsheet>
@@ -64,7 +65,6 @@
(onClose)="cancelCorrectionPayment()" /> (onClose)="cancelCorrectionPayment()" />
} }
} }
}
<shared-light-bottomsheet [(visible)]="showQrCodeDialog" header="کد QR"> <shared-light-bottomsheet [(visible)]="showQrCodeDialog" header="کد QR">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@@ -72,7 +72,8 @@
برای امکان دسترسی به صورت‌حساب به صورت آنلاین، کد QR را اسکن کنید. برای امکان دسترسی به صورت‌حساب به صورت آنلاین، کد QR را اسکن کنید.
</span> </span>
<p-card class="mx-auto"> <p-card class="mx-auto">
<qrcode [qrdata]="publicInvoiceRoute()" [width]="220" [errorCorrectionLevel]="'M'"></qrcode> <qrcode [qrdata]="publicInvoiceRoute" [width]="220" [errorCorrectionLevel]="'M'"></qrcode>
</p-card> </p-card>
</div> </div>
</shared-light-bottomsheet> </shared-light-bottomsheet>
}
@@ -110,19 +110,21 @@ export class SharedSaleInvoiceSingleViewComponent {
showQrCodeDialog = signal(false); showQrCodeDialog = signal(false);
showCorrectionPaymentDialog = signal(false); showCorrectionPaymentDialog = signal(false);
correctionRequiredPayment = signal(0); correctionRequiredPayment = signal(0);
paymentData = signal<Maybe<IPayment>>(null);
readonly isApplication = config.isPosApplication; readonly isApplication = config.isPosApplication;
private pendingCorrectionSubmitResolver: Maybe<(allowed: boolean) => void> = null; private pendingCorrectionSubmitResolver: Maybe<(allowed: boolean, payment?: IPayment) => void> =
null;
publicInvoiceRoute = computed(() => { get publicInvoiceRoute() {
if (this.invoice) { if (this.invoice) {
return `${window.location.origin}/sale-invoices/${this.invoice.id}`; return `${window.location.origin}/sale-invoices/${this.invoice.id}`;
} }
return ''; return '';
}); }
canDoActionOnInvoice = computed(() => { get canDoActionOnInvoice() {
return !this.invoice?.referenced_by; return !this.invoice?.referenced_by;
}); }
showErrors = () => {}; showErrors = () => {};
inquiry = () => { inquiry = () => {
@@ -282,6 +284,7 @@ export class SharedSaleInvoiceSingleViewComponent {
total_amount, total_amount,
discount_amount, discount_amount,
tax_amount, tax_amount,
payments: this.paymentData() || undefined,
}; };
} }
@@ -310,7 +313,12 @@ export class SharedSaleInvoiceSingleViewComponent {
return false; return false;
} }
return (await this.beforeCorrectionSubmit?.(mapped)) ?? true; return (
(await this.beforeCorrectionSubmit?.({
...mapped,
payments: this.paymentData() || undefined,
})) ?? true
);
} }
confirmCorrectionPayment(_event: { confirmCorrectionPayment(_event: {
@@ -318,7 +326,8 @@ export class SharedSaleInvoiceSingleViewComponent {
settlementType: 'CASH' | 'CREDIT' | 'MIXED'; settlementType: 'CASH' | 'CREDIT' | 'MIXED';
}) { }) {
this.showCorrectionPaymentDialog.set(false); this.showCorrectionPaymentDialog.set(false);
this.pendingCorrectionSubmitResolver?.(true); this.paymentData.set(_event.payment);
this.pendingCorrectionSubmitResolver?.(true, _event.payment);
this.pendingCorrectionSubmitResolver = null; this.pendingCorrectionSubmitResolver = null;
} }