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 { IPosReturnFromSaleRequest } from '@/domains/pos/modules/saleInvoices/models/returnFromSale'; import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; import { PosInfoStore } from '@/domains/pos/store'; import { TspProviderResponseStatus } from '@/shared/catalog'; import { SharedLightBottomsheetComponent } from '@/shared/components'; import { ISaleInvoiceFullResponse } from '@/shared/components/invoices/sale-invoice-full-response.model'; import { PageLoadingComponent } from '@/shared/components/page-loading.component'; import { UikitEmptyStateComponent } from '@/uikit'; import { formatJalali, formatWithCurrency } from '@/utils'; 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 { TableModule } from 'primeng/table'; import { Observable } from 'rxjs'; 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 { 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, ], }) 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; @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); publicInvoiceRoute = computed(() => { if (this.invoice) { return `${window.location.origin}/sale-invoices/${this.invoice.id}`; } return ''; }); 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(); const defaultPrintItems = [ { title: 'اطلاعات صورت‌حساب', items: [ { label: 'شماره', value: this.invoice.invoice_number.toString(), show: true, }, { label: 'شماره منحصر به فرد مالیاتی', value: this.invoice.tax_id || '-', show: true, }, { label: 'نوع', value: this.invoice.type.translate, show: mustPrintConfig.invoice_template, }, { label: 'موضوع', value: this.invoice.pos.complex.business_activity.guild.name, show: mustPrintConfig.invoice_template ?? true, }, { label: 'تاریخ', value: formatJalali(this.invoice.invoice_date), show: true, }, { label: 'وضعیت صدور', value: this.invoice.status.translate, show: mustPrintConfig.show_payment_info ?? true, }, ].filter((item) => item.show), }, { title: `اطلاعات فروشنده`, items: [ { label: 'فروشگاه', value: this.invoice.pos.complex.business_activity.name, show: mustPrintConfig.business_name, }, { label: 'شعبه', value: this.invoice.pos.complex.name, show: mustPrintConfig.complex_name, }, { label: 'پایانه‌ فروش', value: this.invoice.pos.name, show: mustPrintConfig.pos_name, }, { label: 'شماره اقتصادی', value: this.invoice.pos.complex.business_activity.economic_code, show: mustPrintConfig.economic_code, }, ].filter((item) => item.show), }, { title: `اطلاعات خریدار`, items: [ { label: 'نام خریدار', value: (this.invoice.customer ? this.invoice.customer.type === 'LEGAL' ? this.invoice.customer.legal?.company_name : `${this.invoice.customer.individual?.first_name} ${this.invoice.customer.individual?.last_name}` : this.invoice.unknown_customer?.name) || '-', show: mustPrintConfig.customer_name, }, { label: 'شماره اقتصادی', value: (this.invoice.customer ? this.invoice.customer.type === 'LEGAL' ? this.invoice.customer.legal?.economic_code : this.invoice.customer.individual?.economic_code || '-' : '-') || '-', show: mustPrintConfig.customer_economic_code, }, ].filter((item) => item.show), }, ].filter((item) => item.items.length); if (mustPrintConfig.show_items) { for (let item of this.invoice.items) { defaultPrintItems.push({ title: 'جزییات صورت‌حساب', items: [ { label: 'شرح', value: item.good_snapshot.name, show: mustPrintConfig.item_name ?? true, }, { label: 'شناسه کالا / خدمت', value: item.sku_code, show: mustPrintConfig.item_sku ?? true, }, { label: 'تعداد / مقدار', value: `${item.quantity} ${item.measure_unit_text}`, show: mustPrintConfig.item_quantity ?? true, }, { label: 'قیمت واحد', value: formatWithCurrency(item.unit_price), show: mustPrintConfig.item_base_amount ?? true, }, { label: 'اجرت', value: formatWithCurrency(item.payload.wages), show: (mustPrintConfig.item_wage ?? true) && item.good_snapshot.pricing_model === 'GOLD', }, { label: 'سود', value: formatWithCurrency(item.payload.profit), show: (mustPrintConfig.item_profit ?? true) && item.good_snapshot.pricing_model === 'GOLD', }, { label: 'حق‌العمل', value: formatWithCurrency(item.payload.commission), show: (mustPrintConfig.item_commission ?? true) && item.good_snapshot.pricing_model === 'GOLD', }, { label: 'مالیات بر ارزش افزوده', value: formatWithCurrency(item.tax_amount), show: mustPrintConfig.item_tax ?? true, }, { label: 'تخفیف', value: formatWithCurrency(item.discount_amount), show: mustPrintConfig.item_discount ?? true, }, { label: 'مبلغ کل', value: formatWithCurrency(item.total_amount), show: mustPrintConfig.item_total_amount ?? true, }, ], }); } } console.log(defaultPrintItems); return defaultPrintItems; } 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, }; } correctionSubmit(event: CorrectionInvoiceFormValue) { this.sendCorrection(this.mapCorrectionRequest(event)); } private mapReturnFromSaleRequest(event: ReturnFromSaleFormValue): IPosReturnFromSaleRequest { const invoiceItems = this.invoice?.items || []; const itemMap = new Map(invoiceItems.map((item) => [item.id, item])); const items: IPosOrderItem[] = (event.items || []) .map((item) => { const source = itemMap.get(item.id); if (!source) return null; const quantity = Number(item.quantity || 0); const unit_price = Number(source.unit_price || 0); const discount_amount = Number(source.discount_amount || 0); const base_total_amount = unit_price * quantity; const total_amount = Number(source.total_amount || 0); const tax_amount = Number(source.tax_amount || 0); return { good_id: source.good_id, service_id: source.service_id || undefined, quantity, unit_price, discount_amount, base_total_amount, total_amount, tax_amount, measure_unit: source.good_snapshot.measure_unit, payload: source.payload as any, notes: source.notes, image_url: source.good_snapshot.image_url, } as IPosOrderItem; }) .filter((item): item is IPosOrderItem => !!item); return { invoice_date: this.invoice?.invoice_date || '', items, total_amount: items.reduce((sum, item) => sum + Number(item.total_amount || 0), 0), discount_amount: items.reduce((sum, item) => sum + Number(item.discount_amount || 0), 0), tax_amount: items.reduce((sum, item) => sum + Number(item.tax_amount || 0), 0), }; } returnSubmit(event: ReturnFromSaleFormValue) { this.sendReturnFromSale(this.mapReturnFromSaleRequest(event)); } backToPrevPage() { this.navigationService.back(this.backRoute); } showQrCode() { this.showQrCodeDialog.set(true); } }