import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Invoice } from '@app/invoices/models/invoice.model'; import { BaseComp } from '@app/shared/base/base.component'; import { ActivatedRoute } from '@angular/router'; import { globals, RoleIds } from '@app/shared/global'; import { SelectItem } from 'primeng/api'; import { InvoiceService } from '@app/domain/services/invoice.service'; import { DateUtils, Utils } from '@app/shared/utils'; import { Dropdown } from 'primeng/dropdown'; import { saveAs } from 'file-saver'; import { LoaderService } from '@app/shared/loader/loader.service'; import { invoiceStatus } from '@app/shared/global'; import { CurrencyPipe, DatePipe, Location } from '@angular/common'; import { InputNumber } from 'primeng/inputnumber'; import { DomUtils } from '@app/shared/dom-util'; import { MultiSelect } from 'primeng/multiselect'; @Component({ selector: 'agm-invoice-detail', templateUrl: './invoice-detail.component.html', styleUrls: ['./invoice-detail.component.css'], providers: [DatePipe, CurrencyPipe] }) export class InvoiceDetailComponent extends BaseComp implements OnInit, OnDestroy { readonly invoiceStatus = invoiceStatus; invoice; jobCols; paymentCols; billToCols; printPreviewDlg = false; logPaymentDlg = false; logPaymentForm: any = {}; paymentMethods: SelectItem[]; printJobCols = []; printDetail; logPaymentList: any[] = []; exportDlg = false; canExport = false; @ViewChild('clientPayLogRef') paymentLogClient: Dropdown; @ViewChild('amount') amount: InputNumber; @ViewChild('pmList') pmList: Dropdown; get jobPricingDetails() { const subTotal = Utils.arraySum(this.invoice.jobs.map(i => (+i.totalAmount).roundToDecimalPlace(2))); return { subTotal, }; } billToListPriceObject(item) { const billToPrice = this.invoiceSvc.calculateClientPayment(item, this.jobPricingDetails.subTotal); if (this.invoice.status == invoiceStatus.VOID) { billToPrice.total = 0; billToPrice.due = 0; } return billToPrice; } get totalSplit() { return Utils.arraySum(this.invoice.clients.map(i => +i.split)); } get totalSubtotal() { return Utils.arraySum(this.invoice.clients.map(i => this.billToListPriceObject(i).subTotal)); } get totalExcludingTax() { return Utils.arraySum(this.invoice.clients.map(i => this.billToListPriceObject(i).totalExcludingTax)); } get totalTotal() { return Utils.arraySum(this.invoice.clients.map(i => this.billToListPriceObject(i).total)); } get totalPaid() { return Utils.arraySum(this.invoice.clients.map(i => this.billToListPriceObject(i).paid)); } get totalDue() { return Utils.arraySum(this.invoice.clients.map(i => this.billToListPriceObject(i).due)); } get canAccessInvoice() { return this.authSvc.canAccessInvoice; } paymentMethodText(method) { switch (method) { case 'transfer': return $localize`:@@wireTransfer:Wire transfer`; case 'credit': return $localize`:@@creditCard:Credit card`; case 'debit': return $localize`:@@debitCard:Debit card`; case 'cash': return $localize`:@@cash:Cash`; } } constructor( private readonly route: ActivatedRoute, private readonly invoiceSvc: InvoiceService, private readonly loaderSvc: LoaderService, private readonly datePipe: DatePipe, private readonly location: Location ) { super(); this.jobCols = [ { field: '_id', header: $localize`:@@id:Id`, width: '10%' }, { field: 'name', header: globals.name }, { field: 'totalAmount', header: $localize`:@@totalAmount:Total Amount`, width: '20%' }, ]; this.paymentCols = [ { field: 'client.name', header: $localize`:@@billTo:Bill to`, filtered: true, filterMatchMode: 'contains' }, { field: 'paymentDate', header: $localize`:@@paymentDate:Payment Date`, filtered: true, filterMatchMode: 'contains' }, { field: 'paymentMethod', header: $localize`:@@paymentMethod:Payment Method`, filtered: false, filterMatchMode: 'contains' }, { field: 'amountPaid', header: $localize`:@@amountPaid:Amount Paid`, filtered: false, filterMatchMode: 'contains' }, { field: 'amountDue', header: $localize`:@@amountDue:Amount Due`, filtered: false, filterMatchMode: 'contains' }, ]; this.billToCols = [ { field: 'billTo', header: $localize`:@@billTo:Bill to`, filtered: false, filterMatchMode: 'contains', width: '15%' }, { field: 'address', header: globals.address, filtered: false, filterMatchMode: 'contains', width: '20%' }, { field: 'split', header: $localize`:@@split:Split`, filtered: false, filterMatchMode: 'contains', width: '65px' }, { field: 'subtotal', header: $localize`:@@subtotal:Subtotal`, filtered: false, filterMatchMode: 'contains' }, { field: 'discount', header: $localize`:@@discount:Discount`, filtered: false, filterMatchMode: 'contains', width: '95px' }, { field: 'totalExcludingTax', header: $localize`:@@totalExcludingTax:Total Excluding Tax`, filtered: false, filterMatchMode: 'contains' }, { field: 'taxRate', header: $localize`:@@taxRate:Tax Rate`, filtered: false, filterMatchMode: 'contains', width: '85px' }, { field: 'total', header: $localize`:@@total:Total`, filtered: false, filterMatchMode: 'contains' }, { field: 'amountPaid', header: $localize`:@@amountPaid:Amount Paid`, filtered: true, filterMatchMode: 'contains' }, { field: 'amountDue', header: $localize`:@@amountDue:Amount Due`, filtered: true, filterMatchMode: 'contains' }, { field: 'action', header: $localize`:@@actions:Actions`, width: '65px' }, ]; this.paymentMethods = [ { label: $localize`:@@wireTransfer:Wire transfer`, value: 'transfer' }, { label: $localize`:@@creditCard:Credit card`, value: 'credit' }, { label: $localize`:@@debitCard:Debit card`, value: 'debit' }, { label: $localize`:@@cash:Cash`, value: 'cash' }, ]; this.printJobCols = [ { field: 'id', header: globals.job + '#', width: '10%' }, { field: 'item', header: globals.item, width: '30%' }, { field: 'quantity', header: $localize`:@@quantity:Quantity`, width: '10%' }, { field: 'unit_cost', header: $localize`:@@unitCost:Unit Cost`, width: '20%' }, { field: 'split', header: '%' + $localize`:@@split:Split`, width: '10%' }, { field: 'total', header: $localize`:@@total:Total`, width: '20%' }, ]; } ngOnInit(): void { this.sub$ = this.route.data.subscribe((data) => { const invoice = data[0]?.invoice as Invoice || null; if (invoice) { if (invoice._id === '0') { this.location.back(); } this.canExport = [invoiceStatus.OPEN, invoiceStatus.PAID, invoiceStatus.UNCOLLECTIBLE].includes(invoice.status as any); this.invoice = { ...invoice, }; this.fetchLogPayment(this.invoice._id); this.invoice.clients = this.invoice.clients.map(c => ({ ...c, ...this.billToListPriceObject(c) })); // Track invoice viewed this.gaSvc.trackInvoiceViewed({ invoice_id: invoice._id, invoice_status: invoice.status, invoice_amount: this.calculateInvoiceAmount(invoice), view_source: 'direct_link', client_id: invoice.clients?.[0]?.billTo?._id || 'unknown', user_id: this.getAnalyticsUserId(), user_role: this.getAnalyticsUserRole(), platform: 'web' }); } else { this.goBack(); } }); } fetchLogPayment(id) { this.invoiceSvc.getLogPaymentByInvoice(id).subscribe((logs: any[]) => { if (logs) { this.logPaymentList = logs; } }, (err) => { this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.load).replace('#thing#', $localize`:@@logPayment:Log Payment`)); }); } fetchInvoiceDetail(id) { this.invoiceSvc.getInvoiceById(id).subscribe(invoice => { if (invoice) { this.invoice = { ...invoice, }; this.invoice.clients = this.invoice.clients.map(c => ({ ...c, ...this.billToListPriceObject(c) })); } else { this.location.back(); } }, (err) => { this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.load).replace('#thing#', globals.invoice)); this.location.back(); }); } get canEdit(): boolean { return this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM]); } printInvoice(client) { this.invoiceSvc.getPrintDetail(this.invoice._id, client.billTo._id).subscribe((print: any) => { if (print) { this.printDetail = this.invoiceSvc.generatePrintSummary(print); this.printPreviewDlg = true; } }, (err) => { this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.load).replace('#thing#', $localize`:@@printPreview:Print preview`)); }); } cancelPrint() { this.printPreviewDlg = false; } confirmPrint() { window.onbeforeprint = () => { document.title = `Agmission_invoice_${this.invoice.code}_${this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH-mm-ss')}`; }; window.onafterprint = () => { document.title = 'AgNav - AgMission'; }; window.print(); } goToEdit() { this.router.navigate([`/invoices/edit/${this.invoice._id}`]); } openLogPaymentDlg() { this.logPaymentForm = { client: '', amount: 0, amountDue: 0, paymentMethod: 'transfer', paymentDate: new Date() }; this.logPaymentDlg = true; this.focusClient(); } focusClient() { setTimeout(() => { this.paymentLogClient.focus(); this.paymentLogClient.show(); }, 500); } saveLog() { const currDate = new Date(); if (DateUtils.isSameDate(this.logPaymentForm.paymentDate, currDate) == 2) { this.msgSvc.addFailedMsg($localize`:@@paymentDateInvalid:Please ensure that the payment date is set for today or any date in the past.`); return; } const payload = { ...this.logPaymentForm, invoiceId: this.invoice._id, clientId: this.logPaymentForm.client?.billTo?._id, amount: this.logPaymentForm.amount.toString() }; delete payload.amountDue; this.invoiceSvc.createLogPayment(payload).subscribe(log => { if (log) { // Track payment logging this.gaSvc.trackInvoicePaymentLogged({ invoice_id: this.invoice._id, payment_amount: this.logPaymentForm.amount || 0, payment_method: this.gaHelpers.mapPaymentMethod(this.logPaymentForm.paymentMethod), payment_date: this.logPaymentForm.paymentDate?.toISOString().split('T')[0] || new Date().toISOString().split('T')[0], remaining_balance: this.gaHelpers.calculateRemainingBalance(this.invoice), days_to_payment: this.gaHelpers.calculateDaysToPayment(new Date(this.invoice.openDate), this.logPaymentForm.paymentDate), payment_reference: this.gaHelpers.generatePaymentReference(this.invoice?.code || 'INV'), user_id: this.getAnalyticsUserId(), user_role: this.getAnalyticsUserRole(), platform: 'web' }); this.logPaymentDlg = false; this.msgSvc.addSuccessMsg($localize`:@@logPaymentSucceeded: Create log payment succeeded`); this.fetchInvoiceDetail(this.invoice._id); this.fetchLogPayment(this.invoice._id); } }, err => { this.msgSvc.addFailedMsg($localize`:@@logPaymentFailed:Failed to create log payment`); this.logPaymentDlg = false; }); } changePaymentLogClient(e) { this.logPaymentForm.amountDue = e.value.due; this.logPaymentForm.amount = e.value.due; } cancelLog() { this.logPaymentDlg = false; } exportInvoice() { this.exportDlg = true; } cancelExport() { this.exportDlg = false; } downloadCSV() { this.invoiceSvc.downloadInvoiceCSV(this.invoice._id).subscribe( (res) => { try { saveAs(res, `Agmission_invoice_${this.invoice.code}_${this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH-mm-ss')}.csv`); // Track invoice export this.gaSvc.trackInvoiceExported({ invoice_id: this.invoice._id, export_format: 'csv', invoice_amount: this.calculateInvoiceAmount(this.invoice), export_method: 'single', includes_job_details: true, user_id: this.getAnalyticsUserId(), user_role: this.getAnalyticsUserRole(), platform: 'web' }); } catch (error) { alert('Sorry. Your browser does not support this feature !'); } this.exportDlg = false; }, (err) => { this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.download).replace('#thing#', globals.invoice)); }); } downloadIIF() { this.invoiceSvc.downloadInvoiceIIF(this.invoice._id).subscribe( (res) => { try { saveAs(res, `Agmission_invoice_${this.invoice.code}_${this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH-mm-ss')}.iif`); // Track invoice export this.gaSvc.trackInvoiceExported({ invoice_id: this.invoice._id, export_format: 'iif', invoice_amount: this.calculateInvoiceAmount(this.invoice), export_method: 'single', includes_job_details: true, user_id: this.getAnalyticsUserId(), user_role: this.getAnalyticsUserRole(), platform: 'web' }); } catch (error) { alert('Sorry. Your browser does not support this feature !'); } this.exportDlg = false; }, (err) => { this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.download).replace('#thing#', globals.invoice)); }); } goBack() { this.location.back(); } hide(elts: (Dropdown | MultiSelect)[]) { DomUtils.hide(elts) } private calculateInvoiceAmount(invoice: any): number { if (!invoice) return 0; if (invoice.status == invoiceStatus.VOID) { return 0; } return Utils.arraySum(invoice?.clients?.map(client => this.billToListPriceObject(client).total) || [0]); } ngOnDestroy() { super.ngOnDestroy(); } }