import { Maybe } from '@/core'; import { NativeBridgeService } from '@/core/services'; import { NavigationService } from '@/core/services/navigation.service'; import { ToastService } from '@/core/services/toast.service'; import { PosConfigPrintService } from '@/domains/pos/modules/configs/components/print/services/main.service'; import { IPosReturnFromSaleItem, IPosReturnFromSaleRequest, } from '@/domains/pos/modules/saleInvoices/models/returnFromSale'; import { IPayment } from '@/domains/pos/modules/shop/models'; import { PosInfoStore } from '@/domains/pos/store'; import { TspProviderResponseStatus } from '@/shared/catalog'; import { SharedLightBottomsheetComponent } from '@/shared/components'; import { PosPaymentFormDialogComponent } from '@/shared/components/invoices/payment'; import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model'; import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { UikitEmptyStateComponent } from '@/uikit'; import { Component, computed, EventEmitter, inject, Input, Output, signal, SimpleChanges, TemplateRef, ViewChild, } from '@angular/core'; import { Router } from '@angular/router'; import { QRCodeComponent } from 'angularx-qrcode'; import { Button, ButtonDirective } from 'primeng/button'; import { Card } from 'primeng/card'; import { Menu } from 'primeng/menu'; import { TableModule } from 'primeng/table'; import { Observable } from 'rxjs'; import config from 'src/config'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; import { InnerPagesHeaderComponent } from '../innerPagesHeader/inner-pages-header.component'; import { SharedCorrectionFormComponent } from './correctionForm'; import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models'; import { prepareInvoicePrintPayload } from './prepare-print.util'; import { SharedReturnFormComponent } from './returnForm/form.component'; import { SaleInvoiceSingleInfoCardComponent, TSaleInvoiceSingleViewVariant, } from './sale-invoice-single-info-card.component'; type TActionMenuItem = { label: string; icon: string; command: () => void; neededStatus?: TspProviderResponseStatus; severity?: 'secondary' | 'info' | 'warn' | 'danger' | 'contrast'; }; @Component({ selector: 'shared-sale-invoice-single-view', templateUrl: './sale-invoice-single-view.component.html', imports: [ TableModule, PageLoadingComponent, UikitEmptyStateComponent, Button, SharedLightBottomsheetComponent, SharedReturnFormComponent, SharedCorrectionFormComponent, ButtonDirective, InnerPagesHeaderComponent, SaleInvoiceSingleInfoCardComponent, QRCodeComponent, Card, Menu, PosPaymentFormDialogComponent, ], }) export class SharedSaleInvoiceSingleViewComponent { private readonly nativeBridge = inject(NativeBridgeService); private readonly posInfoStore = inject(PosInfoStore); private readonly toastService = inject(ToastService); //TODO: below service Must be transform from pos to shared. private readonly service = inject(PosConfigPrintService); private readonly confirmationService = inject(AppConfirmationService); private readonly router = inject(Router); private readonly navigationService = inject(NavigationService); @Input({ required: true }) loading!: boolean; @Input() invoice!: Maybe; @Input() variant: TSaleInvoiceSingleViewVariant = 'full'; @Input() backRoute?: string; @Input() sendToTspLoading: boolean = false; @Input() resendToTspLoading: boolean = false; @Input() inquiryLoading: boolean = false; @Input() correctionLoading: boolean = false; @Input() returnFromSaleLoading: boolean = false; @Input() beforeCorrectionSubmit?: (data: ICorrectionRequest) => Promise | boolean; @Output() onRefresh = new EventEmitter(); @Output() onSendToTsp = new EventEmitter>(); @Output() onResendToTsp = new EventEmitter>(); @Output() onInquiry = new EventEmitter>(); @Output() onCorrection = new EventEmitter(); @Output() onReturnFromSale = new EventEmitter(); @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef; moreActionMenuItems = signal([]); showCorrectionForm = signal(false); showReturnFromSaleForm = signal(false); showQrCodeDialog = signal(false); showCorrectionPaymentDialog = signal(false); correctionRequiredPayment = signal(0); paymentData = signal>(null); readonly isApplication = config.isPosApplication; private pendingCorrectionSubmitResolver: Maybe<(allowed: boolean, payment?: IPayment) => void> = null; publicInvoiceRoute = computed(() => { if (this.invoice) { return `${window.location.origin}/sale-invoices/${this.invoice.id}`; } return ''; }); canDoActionOnInvoice = computed(() => { return !this.invoice?.referenced_by; }); showErrors = () => {}; inquiry = () => { this.onInquiry.emit(); }; send = () => { this.onSendToTsp.emit(); }; sendCorrection = (event: ICorrectionRequest) => { this.onCorrection.emit(event); }; resend = () => { this.onResendToTsp.emit(); }; sendReturnFromSale = (event: IPosReturnFromSaleRequest) => { this.onReturnFromSale.emit(event); }; showCorrection = () => { this.showCorrectionForm.set(true); }; showReturnFromSale = () => { this.showReturnFromSaleForm.set(true); }; private readonly actions: TActionMenuItem[] = [ { label: 'ارسال مجدد صورت‌حساب', icon: 'pi pi-send', neededStatus: TspProviderResponseStatus.SEND_FAILURE, severity: 'info', command: () => this.resend(), }, { label: 'ارسال صورت‌حساب', icon: 'pi pi-send', neededStatus: TspProviderResponseStatus.NOT_SEND, severity: 'info', command: () => this.send(), }, { label: 'استعلام وضعیت', icon: 'pi pi-info', neededStatus: TspProviderResponseStatus.FISCAL_QUEUED, severity: 'info', command: () => this.inquiry(), }, { label: 'دلایل خطا', icon: 'pi pi-question', neededStatus: TspProviderResponseStatus.FAILURE, severity: 'danger', command: () => this.showErrors(), }, { label: 'ابطال', icon: 'pi pi-eraser', neededStatus: TspProviderResponseStatus.SUCCESS, severity: 'danger', command: () => this.showRevokeConfirmation(), }, { label: 'اصلاح', icon: 'pi pi-file-edit', neededStatus: TspProviderResponseStatus.SUCCESS, severity: 'warn', command: () => this.showCorrection(), }, { label: 'برگشت از فروش', icon: 'pi pi-cart-minus', neededStatus: TspProviderResponseStatus.SUCCESS, severity: 'warn', command: () => this.showReturnFromSale(), }, ]; get actionLoading() { return this.sendToTspLoading || this.inquiryLoading || this.resendToTspLoading; } async showRevokeConfirmation() { await this.confirmationService.ask({ header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`, message: `در صورت تایید ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number} بر روی دکمه‌ی ابطال کلیک کنید.`, acceptLabel: 'ابطال', acceptButtonProps: { severity: 'dangerdock', }, variant: 'danger', accept: async () => { // this.deferredInstallPrompt.prompt(); // this.deferredInstallPrompt.userChoice.finally(() => { // this.deferredInstallPrompt = null; // this.canInstall.set(false); // window.location.reload(); // }); // }, }, }); } ngOnChanges(changes: SimpleChanges) { if (changes['invoice'] && this.invoice) { const invoiceStatus = this.invoice.status.value; this.moreActionMenuItems.set( this.actions.filter((action) => action.neededStatus === invoiceStatus) ); } } readonly posName = computed(() => { if (this.posInfoStore.entity()) { const { name, businessActivity, complex } = this.posInfoStore.entity()!; return `${name} (${businessActivity.name} - ${complex.name})`; } return ''; }); preparePrint = async () => { if (this.invoice) { const mustPrintConfig = await this.service.getVisibleItems(); return prepareInvoicePrintPayload(this.invoice, mustPrintConfig, this.posName()); } return null; }; printInvoice = async () => { try { const printItems = await this.preparePrint(); if (printItems) { this.nativeBridge.print(printItems); } } catch (error) { return this.toastService.error({ text: 'چاپ صورت‌حساب با خطا مواجه شد. لطفا دوباره تلاش کنید.', }); } }; refresh() { this.onRefresh.emit(); } private mapCorrectionRequest(event: CorrectionInvoiceFormValue): ICorrectionRequest { const items = (event.items || []).map(({ id, pricing_model, ...item }) => item); const total_amount = items.reduce((sum, item) => sum + Number(item.total_amount || 0), 0); const discount_amount = items.reduce((sum, item) => sum + Number(item.discount_amount || 0), 0); const tax_amount = items.reduce((sum, item) => sum + Number(item.tax_amount || 0), 0); return { invoice_date: event.invoice_date, items, total_amount, discount_amount, tax_amount, payments: this.paymentData() || undefined, }; } correctionSubmit(event: CorrectionInvoiceFormValue) { this.sendCorrection(this.mapCorrectionRequest(event)); } async beforeCorrectionSubmitHandler(event: CorrectionInvoiceFormValue): Promise { const mapped = this.mapCorrectionRequest(event); const oldTotalAmount = Number(this.invoice?.total_amount || 0); const newTotalAmount = Number(mapped.total_amount || 0); if (newTotalAmount <= oldTotalAmount) { return true; } const requiredAmount = newTotalAmount - oldTotalAmount; this.correctionRequiredPayment.set(requiredAmount); this.showCorrectionPaymentDialog.set(true); const allowByDialog = await new Promise((resolve) => { this.pendingCorrectionSubmitResolver = resolve; }); if (!allowByDialog) { return false; } return ( (await this.beforeCorrectionSubmit?.({ ...mapped, payments: this.paymentData() || undefined, })) ?? true ); } confirmCorrectionPayment(_event: { payment: IPayment; settlementType: 'CASH' | 'CREDIT' | 'MIXED'; }) { this.showCorrectionPaymentDialog.set(false); this.paymentData.set(_event.payment); this.pendingCorrectionSubmitResolver?.(true, _event.payment); this.pendingCorrectionSubmitResolver = null; } cancelCorrectionPayment() { this.showCorrectionPaymentDialog.set(false); this.pendingCorrectionSubmitResolver?.(false); this.pendingCorrectionSubmitResolver = null; } cancelCorrection() { this.showCorrectionForm.set(false); this.cancelCorrectionPayment(); } cancelReturn() { this.showReturnFromSaleForm.set(false); } private mapReturnFromSaleRequest(event: ReturnFromSaleFormValue): IPosReturnFromSaleRequest { const invoiceItems = this.invoice?.items || []; const itemMap = new Map(invoiceItems.map((item) => [item.id, item])); const items: IPosReturnFromSaleItem[] = (event.items || []) .map((item) => { const source = itemMap.get(item.id); if (!source) return null; const quantity = Number(item.quantity || 0); return { good_id: source.good_id, quantity, }; }) .filter((item): item is IPosReturnFromSaleItem => !!item); return { invoice_date: this.invoice?.invoice_date || '', items, }; } returnSubmit(event: ReturnFromSaleFormValue) { this.sendReturnFromSale(this.mapReturnFromSaleRequest(event)); } backToPrevPage() { this.navigationService.back(this.backRoute); } showQrCode() { this.showQrCodeDialog.set(true); } }