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({ 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({ 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({ 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 { 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 { 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 = { 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 { 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({ 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 = { 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: 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(); } }