agmission/Development/client/src/app/job/job-edit/job-edit.component.ts

1281 lines
39 KiB
TypeScript

import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subject, Observable, timer } from 'rxjs';
import { switchMap, takeUntil, retryWhen, delayWhen, take } from 'rxjs/operators';
import { MenuItem, SelectItem, SelectItemGroup } from 'primeng/api';
import { FileUpload } from 'primeng/fileupload';
import { saveAs } from 'file-saver';
import cloneDeep from 'clone-deep';
import { NumUtils, StringUtils, UnitUtils, Utils } from '@app/shared/utils';
import { MODE, AppProduct, ITEM, LoadOption, IUIJob } from '../models/job.model';
import * as fromEntity from '@app//entities/reducers';
import * as productActions from '@app//entities/actions/product.actions';
import * as pilotActions from '@app//entities/actions/pilot.actions';
import * as vehicleActions from '@app//entities/actions/vehicle.actions';
import * as authActions from '@app//auth/actions/auth.actions';
import * as jobActions from '../actions/job.actions';
import * as fromCostingItems from '@app/invoices/reducers';
import { JobService } from '@app/domain/services/job.service';
import { createNewProduct } from '@app/entities/models/product.model';
import { createNewPilot } from '@app/entities/models/pilot.model';
import { createNewVehicle } from '@app/entities/models/vehicle.model';
import { RoleIds, globals, ProdType, jobInvoiceStatus, ProdTypes, Units, GC } from '@app/shared/global';
import { AppComponent } from '@app/app.component';
import { UnitPipe } from '@app/shared/pipes/unit.pipe';
import { ProductTypePipe } from '@app/shared/pipes/product-type.pipe';
import { Crop } from '@app/entities/models/crop.model';
import { AppFile } from '@app/domain/models/shared.model';
import { BaseComp } from '@app/shared/base/base.component';
import { InvoiceService } from '@app/domain/services/invoice.service';
import { CostingItemUnitPipe } from '@app/invoices/pipes/costing-item-unit.pipe';
import { Location } from '@angular/common';
import { InputNumber } from 'primeng/inputnumber';
import { selectLimit } from '@app/reducers';
import { Acre } from '@app/domain/models/subscription.model';
import { SUB, SubTexts, SubType } from '@app/profile/common';
@Component({
selector: 'agm-job-edit',
templateUrl: './job-edit.component.html',
styleUrls: ['./job-edit.component.css']
})
export class JobEditComponent extends BaseComp implements OnInit, AfterViewInit, OnDestroy {
readonly globals = globals;
readonly GC = GC;
readonly ITEM = ITEM;
readonly UnitUtils = UnitUtils;
readonly MAX_VALUE = 999999;
readonly MIN_VALUE = 0;
readonly SubTexts = SubTexts;
readonly resolveFieldData = Utils.resolveFieldData;
readonly NAME = 'name';
readonly TYPE = 'type';
readonly RATE = 'rate';
readonly ACTION = 'action';
readonly TOOLS = 'tools';
readonly SIZE = 'size';
readonly WHEN = 'when';
private mode = MODE.NONE;
private stoppedImportPoll;
private okDl = false;
prodCols: any[];
uploadCols: any[];
curProduct: any;
prodRate = 10; // default rate/area unit value
prodUnit;
selectedItem: IUIJob;
pollingImport = false;
importStatus = 0;
useDefRate = false;
@ViewChild('newPilot') newPilot: ElementRef;
addingNewPilot = false;
@ViewChild('newProduct') newProduct: ElementRef;
addingNewProduct = false;
@ViewChild('jobNameRef') jobName: ElementRef;
@ViewChild('newVehicle') newVehicle: ElementRef;
addingNewVehicle = false;
@ViewChild('uploader') uploader: FileUpload;
@ViewChild('billableArea') billableArea: InputNumber;
@ViewChild('quantity') quantity: InputNumber;
rateUnits: SelectItem[];
prodUnits: SelectItem[];
updateOps: SelectItem[];
uploadSettings: any;
status: SelectItem[] = [...GC.selJobStatuses];
prodTypes: SelectItem[];
byAreaUnit: string;
byAreaUnitL: string;
grpedProds: SelectItemGroup[] = [];
srcUsers: any[];
tarUsers: any[];
uploadUrl = '/imports/uploadJob';
uploadedFiles = [];
dlLogs = [];
uploadErrorMsg = '';
cloneId = 0;
curAppId; // Current Application Id of the current importing file
canWrite = false;
dlOps: SelectItem[];
pilots: SelectItem[] = [];
vehicles: SelectItem[] = [];
downloadOptions: MenuItem[];
crops: Crop[];
addingNewCropJob = false;
totalCoverage: number;
private _job: IUIJob = null;
get job(): IUIJob { return this._job; }
set job(job: IUIJob) {
job.loadOp.date = new Date(job.loadOp.date);
this._job = job;
this.selectedItem = cloneDeep(job, true);
this.checkOKDl();
}
get canDownload() {
return this.okDl;
}
get defaultRateUnit(): number {
return UnitUtils.defRateUnit(this.selectedItem.measureUnit);
}
get isArchived(): boolean {
return this.selectedItem.isArchived;
}
loadTypes: SelectItem[] = [
{ label: $localize`:@@normal:Normal`, value: 0 },
{ label: $localize`:@@equal:Equal`, value: 1 }
];
loadSheetDlgOn: boolean = false;
acre: Acre;
// invoice costing section
costingItems: SelectItem[]; // Costing items dropdown list
costingItemForm;
currencyUnit;
costingErrorMsg = '';
get canAccessInvoice() {
return this.authSvc.canAccessInvoice;
}
get canCreateInvoice() {
return this.job
&& this.job.status != 0
&& this.job.costings?.billableAmount
&& this.job.invoiceStatus == jobInvoiceStatus.NONE
&& !this.isBillableAreaRequired
&& this.canAccessInvoice;
}
get canViewInvoice() {
return this.selectedItem
&& this.selectedItem.invoiceId
&& this.selectedItem.invoiceStatus == jobInvoiceStatus.INVOICED;
}
get canEditCosting() {
return this.selectedItem && this.selectedItem.invoiceStatus !== jobInvoiceStatus.INVOICED;
}
get costingChanged() {
return !Utils.deepEquals(this.job.costings, this.selectedItem.costings);
}
get isBillableAreaRequired(): boolean {
return !this.selectedItem.costings.billableArea && this.selectedItem.costings.items.length > 0 && this.isBillableStatus(this.selectedItem.status);
}
constructor(
public app: AppComponent,
private readonly jobSvc: JobService,
private readonly route: ActivatedRoute,
private readonly unitPipe: UnitPipe,
private readonly costingItemUnitPipe: CostingItemUnitPipe,
private readonly prodTypePipe: ProductTypePipe,
protected readonly cdRef: ChangeDetectorRef,
private readonly ngZone: NgZone,
private readonly invoiceSvc: InvoiceService,
private readonly location: Location
) {
super(cdRef);
this.init();
this.prodCols = [
{ field: this.NAME, header: $localize`:@@name:Name`, width: "*" },
{ field: this.TYPE, header: $localize`:@@type:Type`, width: "14%" },
{ field: this.RATE, header: $localize`:@@rate:Rate`, width: "14%" },
{ field: 'unit', header: $localize`:@@unit:Unit`, width: "11%" },
{ field: this.ACTION, header: $localize`:@@action:Action`, width: "11%" },
];
this.uploadCols = [
{ field: this.NAME, header: $localize`:@@name:Name`, width: "48%" },
{ field: this.SIZE, header: $localize`:@@size:Size`, width: "13%" },
{ field: this.WHEN, header: $localize`:@@when:When` },
{ field: this.TOOLS, header: $localize`:@@tools:Tools`, width: "11%" },
];
this.costingItemForm = {
selCostingItem: null,
quantity: null
};
}
private init() {
this.stoppedImportPoll = new Subject();
this.downloadOptions = [
{
label: globals.agnav, icon: '', command: () => {
this.downLoadJob(1);
}
},
{
label: globals.agnavPrj, icon: '', command: () => {
this.downLoadJob(2);
}
},
{
label: globals.esriShape, icon: '', command: () => {
this.downLoadJob(3);
}
},
];
this.updateOps = [
{ label: $localize`:Overwrite all jobs items with what found in the file@@overwrite:Overwrite`, value: '3' },
{ label: $localize`:Import data from the zip file only@@dataOnly:Data Only`, value: '1' },
{ label: $localize`:Exclusion zones@@xcls:XCLs`, value: '4' },
];
this.status = [
{ label: globals.statusNew, value: 0 },
{ label: globals.statusReady, value: 1 },
{ label: globals.statusDownloaded, value: 2 },
{ label: globals.statusSprayed, value: 3 },
{ label: globals.statusArchived, value: 9 },
];
this.dlOps = [
{ label: globals.agnav, value: 1 },
{ label: globals.agnavPrj, value: 2 },
{ label: globals.esriShape, value: 3 }
];
this.uploadSettings = !Utils.isEmptyObj(this.appConf.settings.upload) ? Object.assign({}, this.appConf.settings.upload) : { updateOp: '2' };
this.canWrite = this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM, RoleIds.OFFICER, RoleIds.PILOT, RoleIds.CLIENT]);
this.prodTypes = [
{ label: ProdTypes[ProdType.ACTIVE], value: ProdType.ACTIVE },
{ label: ProdTypes[ProdType.CARRIER], value: ProdType.CARRIER },
];
this.prodUnits = [
{ label: this.unitPipe.transform(Units.OZ), value: Units.OZ },
{ label: this.unitPipe.transform(Units.GAL), value: Units.GAL },
{ label: this.unitPipe.transform(Units.LIT), value: Units.LIT },
{ label: this.unitPipe.transform(Units.LB), value: Units.LB },
{ label: this.unitPipe.transform(Units.KG), value: Units.KG }
];
}
ngOnInit() {
this.sub$ = this.route.data
.subscribe((data) => {
const job = data[0].job as IUIJob || null;
this.mode = data[0].mode;
if (!job) {
return;
}
if (this.mode === MODE.CLONE) {
delete job.invoiceId;
delete job.invoiceStatus;
}
if (!job.costings.currency) {
this.currencyUnit = this.invoiceSvc.defaultSetting.currency;
job.costings.currency = this.invoiceSvc.defaultSetting.currency;
} else {
this.currencyUnit = job.costings.currency;
}
if (!job.costings.billableArea) {
job.costings.billableArea = job.ttSprArea ?? 0;
}
const defUnitChanged = this.mode != MODE.EDIT && ['pt', 'es'].indexOf(this.authSvc.locale) != -1;
if (defUnitChanged) {
job.measureUnit = false;
}
this.updateUnits(job.measureUnit);
this.job = job;
this.validateUnits();
if (this.mode === MODE.CLONE) {
this.cloneId = job._id;
this.resetJob();
} else if (this.mode === MODE.EDIT) {
this.checkImportingStatus();
}
});
// // Populate dropdown items and subcribe to auto refresh the list if changed
this.sub$.add(this.store.select(fromEntity.getAllProducts).subscribe(products => {
this.grpedProds = [];
if (products) {
const prods = products.slice(0).map(it => {
if (!it['type']) {
it['type'] = 1;
}
return it;
});
if (!Utils.isEmptyArray(prods)) {
const grItems = Utils.groupBy(prods, p => p.type);
for (const key in grItems) {
this.grpedProds.push({ label: this.prodTypePipe.transform(key), items: grItems[key].map(i => ({ label: i['name'], value: i })) });
}
}
if (this.grpedProds[0]?.items && this.grpedProds[0].items.length) {
this.onProductChanged(this.grpedProds[0].items[0].value);
}
}
}));
this.sub$.add(this.store.select(fromEntity.getAllPilots).subscribe(pilots => {
this.pilots = [];
if (pilots) {
pilots.forEach(item => {
this.pilots.push(<SelectItem>{ label: item.name, value: item });
});
}
}));
this.sub$.add(this.store.select(fromEntity.getAllVehicles).subscribe(vehicles => {
this.vehicles = [];
if (!Utils.isEmptyArray(vehicles)) {
vehicles.forEach(item => {
this.vehicles.push(<SelectItem>{ label: item.name, value: item });
});
}
}));
this.sub$.add(this.store.select(fromEntity.getAllCrops).subscribe(crops => {
this.crops = [];
if (crops) {
this.crops = crops;
}
}));
this.sub$.add(this.store.select(fromCostingItems.getAllCostingItems).subscribe(costingItems => {
this.costingItems = (costingItems || [])
.filter(item => item)
.map(item => ({
label: `${item.name} - ${this.costingItemUnitPipe.transform(item.unit)} - ${this.invoiceSvc.defaultSetting.currency} ${item.price}`,
value: item,
disabled: false
}));
this.disableExistingCostingItems();
}));
this.sub$.add(this.appActions.ofType(productActions.CREATE_SUCCESS)
.subscribe((action) => {
this.curProduct = action['payload'];
this.onProductChanged(this.curProduct);
this.addingNewProduct = false;
}));
this.sub$.add(this.appActions.ofType(pilotActions.CREATE_SUCCESS).subscribe((action) => {
this.cancelNewPilot();
this.selectedItem.operator = action['payload'];
}));
this.sub$.add(this.appActions.ofType(vehicleActions.CREATE_SUCCESS).subscribe((action) => {
this.cancelNewVehicle();
this.selectedItem.vehicle = action['payload'];
}));
this.sub$.add(this.appActions.ofType(jobActions.CREATE_SUCCESS).subscribe((action) => {
const job = action['payload'];
this.editJobMap(job._id);
}));
this.sub$.add(this.appActions.ofType(jobActions.UPDATE_SUCCESS).subscribe((action) => {
const job = action['payload'];
this.job = { ...this.job, ...job };
this.resetNewEntities();
this.checkOKDl();
}));
this.sub$.add(this.appActions.ofType(jobActions.ASSIGN_SUCCESS).subscribe((action) => {
this._job['dlOp'] = this.selectedItem.dlOp;
}));
this.sub$.add(this.store.select(selectLimit(SubType.PACKAGE)).pipe(take(1)).subscribe((pkg) => {
this.acre = pkg[this.authSvc.getCurLookupKey(SubType.PACKAGE)]?.acre;
if (this.acre?.overLimit) {
return this.updateOps.unshift({ label: $localize`:Append to existing jobs items with what found in the file@@append:Append`, value: '2', disabled: true });
}
this.updateOps.unshift({ label: $localize`:Append to existing jobs items with what found in the file@@append:Append`, value: '2' });
}));
}
ngAfterViewInit(): void {
const timer = setInterval(() => {
if (this.selectedItem && StringUtils.isEmpty(this.selectedItem.name)) {
if (this.jobName.nativeElement) {
this.jobName.nativeElement.focus();
clearInterval(timer);
}
} else {
clearInterval(timer);
}
}, 500);
setTimeout(() => {
clearInterval(timer);
}, 1500);
setTimeout(() => {
if (!this.curProduct && (!Utils.isEmptyArray(this.grpedProds) && !Utils.isEmptyArray(this.grpedProds[0].items))) {
this.curProduct = this.grpedProds[0].items[0].value;
}
if (this.isEdit) {
this.getUploadedFiles();
this.getLogs();
if (this.isPlanner) {
this.getAssignments();
}
}
}, 500);
if (this.prodUnit === undefined) {
this.prodUnit = this.job.measureUnit ? Units.GAL : Units.LIT;
}
}
private validateUnits() {
if (!this.selectedItem) {
return;
}
const ok = ((this.selectedItem.measureUnit === true && [0, 1, 2].indexOf(this.selectedItem.appRateUnit) != -1)
|| (this.selectedItem.measureUnit === false && [3, 4].indexOf(this.selectedItem.appRateUnit) != -1));
if (!ok) {
this.selectedItem.appRateUnit = this.defaultRateUnit;
}
}
private checkOKDl() {
this.okDl = (this.selectedItem && this.selectedItem.status >= 1) && this.selectedItem.hasItems;
}
private getUploadedFiles() {
this.jobSvc.getUploadedFiles({ 'jobId': this.job._id }).subscribe((res) => {
this.uploadedFiles.length = 0;
if (!(res instanceof Array) || res.length === 0) {
return;
}
for (const item of res) {
this.addAppFile(item);
}
this.updateTotalCoverage();
});
}
private getLogs(type: number = 2) {
this.jobSvc.getJobLogs({ 'jobId': this.job._id }).subscribe((res) => {
this.dlLogs = res;
});
}
private getAssignments() {
this.jobSvc.getAssignments({ 'jobId': this.job._id }).subscribe((res) => {
if (res) {
this.srcUsers = !Utils.isEmptyArray(res.avUsers) ? res.avUsers.filter(u => u.pkgActive) : [];
this.tarUsers = res.asUsers;
}
});
}
private getAppRateUnits(isUS: boolean) {
if (isUS) {
this.rateUnits = [
{ label: globals.ozPerAc, value: 0 },
{ label: globals.galPerAc, value: 1 },
{ label: globals.lbPerAc, value: 2 },
];
} else {
this.rateUnits = [
{ label: globals.litPerHa, value: 3 },
{ label: globals.kgPerHa, value: 4 }
];
}
}
get isEdit(): boolean {
return (this.selectedItem && this.selectedItem._id > 0);
}
get canEdit(): boolean {
return this.authSvc.hasRole([RoleIds.APP, RoleIds.APP_ADM]);
}
isValid(): boolean {
let valid = true;
if (this.addingNewProduct) {
valid = false;
}
if (this.addingNewPilot && this.newPilot.nativeElement) {
if (StringUtils.isEmpty(this.newPilot.nativeElement.value)) {
if (valid) {
this.newPilot.nativeElement.focus();
}
valid = false;
}
}
if (this.addingNewVehicle && this.newVehicle.nativeElement) {
if (StringUtils.isEmpty(this.newVehicle.nativeElement.value)) {
if (valid) {
this.newVehicle.nativeElement.focus();
}
valid = false;
}
}
if ((this.selectedItem.startDate && this.selectedItem.endDate)
&& this.selectedItem.endDate < this.selectedItem.startDate) {
valid = false;
this.selectedItem.endDate = this.selectedItem.startDate;
}
return valid;
}
onMoveToActiveList(items) {
if (items && items.length) {
const inactiveACList = items.filter(i => i.active === false);
if (inactiveACList.length) {
this.tarUsers = [...this.tarUsers, ...inactiveACList];
this.srcUsers = this.srcUsers.filter(u => u.active === true);
let errMsg = $localize`:@@cannotUnAssignInactiveVehicles:Cannot unassign inactive Aircraft`;
errMsg += ':[ ' + (inactiveACList.map(u => u.name)).join(',') + ' ]';
this.msgSvc.addFailedMsg(errMsg);
}
}
}
getUserToolTip(user) {
if (Utils.isEmptyObj(user)) return '';
let userTT = user.username;
!user.active && (userTT += `, ${globals.inactive}`);
return userTT;
}
setNewRefEnties() {
if (this.addingNewPilot) {
const newPilot = { _id: 'TMP', name: this.newPilot.nativeElement.value.trim() };
this.pilots.push({ label: newPilot.name, value: newPilot });
this.selectedItem.operator = newPilot;
}
if (this.addingNewProduct) {
const newProduct = { _id: 'TMP', name: this.newProduct.nativeElement.value.trim() };
this.curProduct = newProduct;
}
}
createJob() {
if (!this.selectedItem || !this.isValid()) {
return;
}
this.setNewRefEnties();
const job = this.toJobWithCostings(this.selectedItem);
if (this.mode === MODE.CLONE) {
job['cloneId'] = this.cloneId;
}
this.store.dispatch(new jobActions.Create(job));
}
saveJob() {
if (!this.selectedItem || !this.isValid() || (Utils.deepEquals(this._job, this.selectedItem) && !this.useDefRate)) {
return;
}
if (this.job.invoiceStatus == jobInvoiceStatus.INVOICED && this.job.costings?.currency != this.selectedItem.costings.currency) {
this.msgSvc.addFailedMsg($localize`:@@jobInvoiceCurrencyInvalid:This job is already invoiced in #currency# currency! Please void the invoice first or create new jobs.`.replace('#currency#', this.job.costings.currency));
return;
}
const job = this.toJobWithCostings(this.selectedItem);
this.setNewRefEnties();
this.store.dispatch(new jobActions.Update(<jobActions.UpdateJobOps>{ job, updateItems: false, useDefRate: this.useDefRate }));
this.resetNewEntities();
}
private toJobWithCostings(job) {
const hasCostingItems = job?.costings?.items?.length > 0
if (!hasCostingItems) {
delete job.costings?.currency;
}
return job;
}
addNewProduct() {
this.addingNewProduct = true;
}
onNewProduct(product) {
if (!product) {
this.addingNewProduct = false;
return;
}
const newProd = createNewProduct(this.authSvc.byPUserId);
Object.assign(newProd, product);
this.store.dispatch(new productActions.Create(newProd));
this.addingNewProduct = false;
}
addNewPilot() {
this.addingNewPilot = true;
setTimeout(() => {
if (this.newPilot) {
this.newPilot.nativeElement.focus();
}
}, 500);
}
cancelNewPilot() {
this.addingNewPilot = false;
}
saveNewPilot() {
const pilotName = this.newPilot.nativeElement.value;
if (StringUtils.isEmpty(pilotName)) {
return;
}
const _pilot = createNewPilot(this.authSvc.byPUserId);
_pilot.name = pilotName;
this.store.dispatch(new pilotActions.Create(_pilot));
}
addNewVehicle() {
this.addingNewVehicle = true;
setTimeout(() => {
if (this.newVehicle) {
this.newVehicle.nativeElement.focus();
}
}, 500);
}
cancelNewVehicle() {
this.addingNewVehicle = false;
}
saveNewVehicle() {
const vehicleName = this.newVehicle.nativeElement.value;
if (StringUtils.isEmpty(vehicleName)) {
return;
}
const _vehicle = createNewVehicle(this.authSvc.byPUserId);
_vehicle.name = vehicleName;
this.store.dispatch(new vehicleActions.Create(_vehicle));
}
onStatusChanged(event) {
if (this.isEndStatus(event.value)) {
if (!this.selectedItem.endDate) {
this.selectedItem.endDate = new Date();
}
} else if (this.isBillableStatus(event.value)) {
this.selectedItem.costings.billableArea ?? this.selectedItem.ttSprArea;
} else {
this.selectedItem.costings.billableArea = 0;
}
}
private isEndStatus(status: number): boolean {
return status === this.status.length - 1;
}
private isBillableStatus(status: number): boolean {
return [1, 2, 3].includes(status) && this.selectedItem.status > 0;
}
onUnitChanged(event) {
if (event) {
this.updateUnits(this.selectedItem.measureUnit);
this.selectedItem.appRateUnit = this.defaultRateUnit;
this.onProductChanged(this.curProduct);
}
}
onProductChanged(prod) {
if (!prod || !prod.rate) {
return;
}
// Check the value and unit to determine the conversion before update the Job's AppRate with the new value
const newRate = UnitUtils.toRateUnit(prod.rate.value, prod.rate.unit, this.selectedItem.measureUnit);
if (newRate.unit) {
this.prodRate = Number(newRate.value.roundToDecimalPlace(2));
this.prodUnit = newRate.unit;
}
}
private updateUnits(measureUnit) {
this.getAppRateUnits(measureUnit);
this.prodUnit = measureUnit ? Units.GAL : Units.LIT;
this.byAreaUnitL = measureUnit ? globals.acre : globals.hectare;
this.byAreaUnit = measureUnit ? globals.ac : globals.ha;
}
onUpdateOpChanged(event) {
if (!Utils.equals(this.uploadSettings, this.appConf.settings.upload)) {
this.appConf.settings.upload = Object.assign({}, this.uploadSettings);
this.appConf.save({ upload: this.appConf.settings.upload }, true); // Save settings to BE
}
}
downLoadJob(type: number) {
this.doDownLoadJob(type);
}
private doDownLoadJob(type) {
// TODO: Need to be handled in effects ???
this.jobSvc.downloadJob({ jobId: this.selectedItem._id, type: type }).subscribe(
(res) => {
try {
saveAs(res, `${this.selectedItem.name}_${this.selectedItem._id}.zip`);
} catch (error) {
alert('Sorry. Your browser does not support this feature !');
}
this.getLogs();
});
}
editJobMap(id?: number) {
this.router.navigate(
[
`./jobs/${id ? id : this.job._id}/editMap`,
{ flag: (this.mode === MODE.NEW || this.mode === MODE.CLONE) ? 1 : 2 }
]);
}
onSelectUpload(event) {
this.uploadErrorMsg = '';
if (this.uploader.hasFiles()) {
this.uploader.upload();
}
}
onBeforeUpload(event) {
this.curAppId = '';
if (event && event.formData) {
event.formData.append('jobId', this.job._id);
event.formData.append('updateOp', this.uploadSettings.updateOp);
}
}
onUpload(event) {
const resp = event && event.originalEvent ? event.originalEvent : null;
if (resp) {
this.uploadErrorMsg = '';
const res = resp.body;
if (res && res['_id']) {
this.curAppId = res['_id'];
this.checkImportStatus(this.curAppId);
this.gaSvc.gaEvent('JOBS', 'DATA', 'U');
}
}
}
onUploadError(event) {
if (event && event.error) {
const resp = event.error;
const status = resp.status;
if (status === 401) {
this.store.dispatch(new authActions.Logout);
} else if (status > 400) {
this.importStatus = -1;
this.uploadErrorMsg = this.uploader.files[0].name + ': ' + globals.apiErrorMsg(resp.error['error']['.tag'] || '');
}
this.uploader.clear();
}
}
private addAppFile(item: AppFile) {
const newItem = {
id: item._id,
name: item.fileName,
savedName: item.savedFilename,
size: Utils.formatBytes(item.fileSize, 2),
when: item.createdDate,
totalSprayed: item.totalSprayed,
markedDelete: item.markedDelete
};
if (!this.uploadedFiles || this.uploadedFiles.length === 0) {
this.uploadedFiles = [newItem];
} else if (this.uploadedFiles.indexOf(newItem) === -1) {
this.uploadedFiles = this.uploadedFiles.concat(newItem);
}
}
private checkImportStatus(appId) {
this.importStatus = 0;
this.pollingImport = true;
this.cdRef.detectChanges(); // Workaround to force Angular detects change in *ngIf vars template
this.sub$.add(this.pollImportStatus(appId).subscribe(
data => {
this.curAppId = data ? data['_id'] : '';
if (!data) {
this.stoppedImportPoll.next(true);
this.pollingImport = false;
return;
}
this.importStatus = data.status;
if (data.status === 0 || data.status === 3) {
this.stoppedImportPoll.next(true);
this.pollingImport = false;
if (data.status === 3) {
if (data.warnMsg) {
this.showDupResult(data.warnMsg['dup']);
}
this.getUploadedFiles();
} else {
this.importStatus = -1;
if (data.errorMsg) {
this.uploadErrorMsg = data.fileName + ': ' + globals.apiErrorMsg(data.errorMsg);
}
this.uploader.clear();
}
if (data['proStatus'] >= 10) {
// TODO: new Action and update current Job when success ???
this.jobSvc.getJob(this.job._id, false).subscribe(
data => {
if (data && this.job !== data) {
this.job = data;
}
});
}
}
},
error => {
this.importStatus = 0;
this.pollingImport = false;
}));
}
private checkImportingStatus() {
this.ngZone.run(() => {
this.importStatus = 0;
this.pollingImport = true;
});
const ob$ = this.pollImportingStatus().subscribe(
data => {
this.curAppId = data && data.length ? data[0]['_id'] : '';
if (!data || !data.length) {
this.stoppedImportPoll.next(true);
this.pollingImport = false;
this.getUploadedFiles();
}
},
error => {
// TODO: Handle error here possible after couple of attempts not just once??
this.stoppedImportPoll.next(true);
this.importStatus = 0;
this.pollingImport = false;
});
if (this.sub$) {
this.sub$.add(ob$);
} else {
this.sub$ = ob$;
}
}
private pollImportStatus(appId, checkMs: number = 3000): Observable<AppFile> {
return timer(0, checkMs).pipe(
switchMap(() => this.jobSvc.getImportStatus(appId)),
takeUntil(this.stoppedImportPoll),
retryWhen(errors =>
errors.pipe(
delayWhen(val => timer(15 * 1000)) // if error, retry in 15 seconds
))
);
}
private pollImportingStatus(checkMs: number = 3000): Observable<AppFile[]> {
return timer(500, checkMs).pipe(
switchMap(() => this.jobSvc.getImportingStatus({ jobId: this.job._id })),
takeUntil(this.stoppedImportPoll));
}
cancelProcessFile() {
if (!this.curAppId) {
return;
}
this.jobSvc.cancelImporting([this.curAppId]).subscribe((data) => {
this.ngZone.run(() => {
this.stoppedImportPoll.next(true);
this.pollingImport = false;
});
this.getUploadedFiles();
},
error => {
this.msgSvc.addFailedMsg($localize`:@@cancelFileFailed#Failed to cancel processing uploaded file:Cannot cancel file`);
});
}
private updateTotalCoverage() {
let totalCoverage = 0;
for (const fi of this.uploadedFiles) {
if (fi.totalSprayed) {
totalCoverage += fi.totalSprayed;
}
}
this.totalCoverage = totalCoverage;
}
deleteFile(appFile) {
this.confirmSvc.confirm({
message: globals.confirmDeleteThing.replace('#thing#', $localize`:@@uploadedFile:Uploaded File`),
icon: 'ui-icon-warning',
accept: () => {
this.jobSvc.deleteAppFile({ appId: appFile.id }).subscribe((data) => {
if (data['appId']) {
this.uploadedFiles = this.uploadedFiles.filter(it => it.id !== data['appId']);
}
this.updateTotalCoverage();
});
}
});
}
assignJob() {
if (!this.job) {
return;
}
const assignment = <jobActions.AssignInfo>{
jobId: this.job._id,
dlOp: this.selectedItem.dlOp,
avUsers: this.srcUsers,
asUsers: this.tarUsers
};
this.store.dispatch(new jobActions.Assign(assignment));
}
downloadAppfile(data) {
this.jobSvc.downloadAppFile(data.savedName).subscribe(
(res) => {
try {
saveAs(res, data.name);
} catch (error) {
alert('Sorry. Your browser does not support this feature !');
}
});
}
canDeactivate(): boolean | Promise<boolean> {
if (this.mode === MODE.NEW || this.mode === MODE.CLONE) {
return true;
}
let skipOps;
if (!this.selectedItem.loadOp.capacity) {
skipOps = ['loadOp'];
}
const changed = !(Utils.deepEquals(this._job, this.selectedItem, skipOps));
if (changed) {
return new Promise((resolve, reject) => {
this.confirmSvc.confirm({
message: globals.confirmSaveJobMsg,
accept: () => {
this.store.dispatch(new jobActions.Update(<jobActions.UpdateJobOps>{ job: this.selectedItem, updateItems: false }));
resolve(true);
},
reject: () => {
resolve(true);
}
});
});
} else {
return true;
}
}
goBackToList() {
this.route.queryParams.subscribe(params => {
const previous = params['previous'];
if (previous && previous == 'invoice') {
this.location.back();
} else {
this.router.navigate(['../', { id: this.job._id }]);
}
});
}
private resetJob() {
this.selectedItem._id = 0;
this.selectedItem.name = '';
this.selectedItem.status = 0;
this.selectedItem.endDate = null;
if (this.selectedItem.rptOp) {
this.selectedItem.rptOp.coverage = 0;
}
}
private resetNewEntities() {
this.addingNewPilot = false;
this.addingNewProduct = false;
this.addingNewVehicle = false;
}
deleteAppProduct(prod) {
if (Utils.isEmptyArray(this.selectedItem.products)) {
return;
}
this.selectedItem.products = this.selectedItem.products.filter(c => c.product._id !== prod.product._id);
}
addAppProduct() {
if (!this.curProduct) {
return;
}
if (!this.prodRate) {
this.prodRate = 10;
}
if (!Utils.isEmptyArray(this.selectedItem.products)) {
const items = this.selectedItem.products.filter(p => p.product._id === this.curProduct._id);
if (items && items.length && (items[0].product != this.curProduct || items[0].rate != this.prodRate || items[0].unit != this.prodUnit)) {
items[0].product = cloneDeep(this.curProduct);
items[0].rate = this.prodRate;
items[0].unit = this.prodUnit;
return; // Update the added product only
}
}
const newAppProd = <AppProduct>{
product: cloneDeep(this.curProduct),
rate: NumUtils.round(this.prodRate, 2),
unit: this.prodUnit
};
this.selectedItem.products = Utils.isEmptyArray(this.selectedItem.products) ? [newAppProd] : [...this.selectedItem.products, newAppProd];
}
validunit(unit) {
return this.prodUnits && this.prodUnits.length ? this.prodUnits.filter(u => u.value === unit).length : true;
}
private showDupResult(sameNum: number) {
if (sameNum) {
this.confirmSvc.confirm({
header: globals.addResult,
key: 'okOnly',
message: `${sameNum} ${sameNum > 1 ? globals.dupAreasMsg : globals.dupAreaMsg}`,
});
}
}
showLoadRptDlg() {
this.loadSheetDlgOn = !this.loadSheetDlgOn;
const job = this.selectedItem;
const area = +job.loadOp.area ? +job.loadOp.area : UnitUtils.haToArea(job.ttSprArea, job.measureUnit);
job.loadOp.area = NumUtils.fixedTo(area, 2);
this.updateLoads();
}
ttAreaChanged(val) {
this.selectedItem.loadOp.area = NumUtils.fixedTo(val, 2);
this.updateLoads();
}
afterTTAreaChange(val) {
if (val <= 0.0) {
this.selectedItem.loadOp.area = NumUtils.fixedTo(UnitUtils.haToArea(this.selectedItem.ttSprArea, this.selectedItem.measureUnit), 2);
}
}
ttCapacityChanged(val) {
this.selectedItem.loadOp.capacity = NumUtils.fixedTo(val, 2);
this.updateLoads();
}
loadTypeChanged(e) {
this.updateLoads();
}
private updateLoads() {
const job = this.selectedItem;
job.loadOp.loads = this.computeLoads(+job.appRate, +job.appRateUnit, +job.loadOp.area, +job.loadOp.capacity);
}
private computeLoads(appRate: number, rateUnit: number, area: number, capacity: number): number {
if (!appRate || !area || !capacity) {
return 0;
}
const _apprate = rateUnit === 0 ? appRate * 0.0078125 : appRate;
return Math.ceil(area / (capacity / _apprate));
}
canPreviewLoadRpt() {
let ok = true;
if (!this.selectedItem.loadOp || !this.selectedItem.loadOp.date || !+this.selectedItem.loadOp.area || !+this.selectedItem.loadOp.capacity) {
ok = false;
}
return ok;
}
preViewLoadRpt() {
if (!this.selectedItem.loadOp.area) {
this.selectedItem.loadOp.area = NumUtils.fixedTo(UnitUtils.haToArea(this.selectedItem.ttSprArea, this.selectedItem.measureUnit), 2);
}
const options = {
jobId: this.job._id,
loadOp: <LoadOption>this.selectedItem.loadOp,
lang: this.authSvc.locale
};
this.jobSvc.preLoadReport(options).subscribe(
res => {
window.open(`/#report?rid=${res['rid']}&p=${res['path']}&c=${res['c']}&lang=${this.authSvc.locale}`, `LoadSheet-${this.job.name}`);
this._job['loadOp'] = this.selectedItem.loadOp;
},
null,
() => {
this.ngZone.run(() => this.loadSheetDlgOn = false);
this.gaSvc.gaEvent('REPORT', 'LOAD', 'V');
}
);
}
// invoice costing section
addCostingItem() {
if (!this.canAddCostingItem()) {
return;
}
const newItem = this.createCostingItem();
this.selectedItem.costings.items = [...this.selectedItem.costings.items, newItem];
this.calculateBillableAmount();
this.disableExistingCostingItems();
}
private canAddCostingItem(): boolean {
if (this.isCostingItemsEmpty()) {
this.setDefaultCurrency();
} else if (this.isCurrencyMismatch()) {
this.showCurrencyMismatchError();
return false;
}
return this.isCostingItemSelected();
}
private isCostingItemsEmpty(): boolean {
return this.selectedItem.costings.items.length === 0;
}
private setDefaultCurrency() {
this.selectedItem.costings.currency = this.invoiceSvc.defaultSetting.currency;
this.currencyUnit = this.invoiceSvc.defaultSetting.currency;
}
private isCurrencyMismatch(): boolean {
return this.selectedItem.costings.currency != this.invoiceSvc.defaultSetting.currency;
}
private showCurrencyMismatchError() {
this.costingErrorMsg = $localize`:@@jobCurrencyNotMatchSettingErr:This job's currency does not matched with invoice currency setting.`;
}
private createCostingItem() {
return {
item: this.costingItemForm.selCostingItem._id,
name: this.costingItemForm.selCostingItem.name,
price: this.costingItemForm.selCostingItem.price,
quantity: this.costingItemForm.quantity,
unit: this.costingItemForm.selCostingItem.unit,
};
}
private isCostingItemSelected(): boolean {
return this.costingItemForm.selCostingItem && this.costingItemForm.selCostingItem._id;
}
isAddCostingItemBtnDisabled(): boolean {
return this.costingItemForm.quantity <= 0 || !this.canEditCosting || (this.costingItems?.length === 0) || this.isAllCostingItemsDisabled();
}
deleteCostingItem(costingItem) {
this.selectedItem.costings.items = this.selectedItem.costings?.items?.filter(item => item.item != costingItem.item) || [];
this.calculateBillableAmount();
this.disableExistingCostingItems();
}
calculateBillableAmount() {
this.selectedItem.costings.billableAmount = Utils.arraySum(this.selectedItem.costings.items.map(item => (+item.price * +item.quantity))).roundToDecimalPlace(2);
}
createInvoice() {
if (this.isCurrencyMismatch()) {
this.showCurrencyMismatchError();
return;
}
this.store.dispatch(new jobActions.Select(this.job));
this.router.navigate(['/invoices/edit/0']);
}
viewInvoice() {
this.router.navigate([`/invoices/detail/${this.selectedItem.invoiceId}`]);
}
assignQuantity(evt) {
const value = evt.value;
if (value > this.MAX_VALUE) {
this.costingItemForm.quantity = this.MAX_VALUE;
} else if (value < this.MIN_VALUE) {
this.costingItemForm.quantity = this.MIN_VALUE;
} else {
this.costingItemForm.quantity = value;
}
}
isAllCostingItemsDisabled(): boolean {
return this.costingItems?.every(costingItem => costingItem.disabled);
}
private disableExistingCostingItems() {
this.costingItems = this.costingItems.map(costingItem => ({
...costingItem,
disabled: this.selectedItem.costings?.items?.some(item => item.item === costingItem.value._id)
}));
this.costingItemForm.selCostingItem = this.getFirstEnabledCostingItem();
}
private getFirstEnabledCostingItem() {
return this.costingItems?.find(costingItem => !costingItem.disabled)?.value;
}
isAddingNew() {
return this.addingNewPilot || this.addingNewVehicle || this.addingNewProduct || this.addingNewCropJob;
}
onAddingNewCropJobEvt(evt) {
this.addingNewCropJob = evt == 1;
}
ngOnDestroy(): void {
super.ngOnDestroy();
}
}