401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
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();
|
|
}
|
|
}
|