'use strict'; module.exports = function (locals) { const async = require('async'), util = require('util'), debug = require('debug')('agm:job'), ObjectId = require('mongodb').ObjectId, { Job, JobLog, App, AppFile, AppDetail, Customer, JobAssign, Vehicle, Pilot, RptVar } = require('../model'), Currencies = require('../helpers/currencies'), path = require('path'), fs = require('fs-extra'), moment = require('moment'), turf = require('@turf/turf'), uniqid = require('uniqid'), utils = require('../helpers/utils'), jobUtil = require('../helpers/job_util'), geoUtil = require('../helpers/geo_util'), polyUtil = require('../helpers/poly_util'), polylabel = require('polylabel'), cloneDeep = require('clone-deep'), simplify = require('simplify-path'), webUtil = require('../helpers/web_util'), { JobStatus, JobInvoiceStatus } = require('../helpers/job_constants'), { Units, Errors, DEFAULT_LANG, CostingItemType } = require('../helpers/constants'), { AppParamError, AppError, AppAuthError, AppInputError } = require('../helpers/app_error'), env = require('../helpers/env'), Joi = require('joi'); Joi.objectId = require('joi-objectid')(Joi); async function getJobs_get(req, res) { const userInfo = req.userInfo; if (!userInfo) AppAuthError.throw(); const clientId = req.query['clientId']; const ops = utils.isObjectId(clientId) ? { client: ObjectId(clientId) } : { byPuid: ObjectId(userInfo.puid) }; ops.markedDelete = { $in: [null, false] }; const jobsByPilot = utils.stringToBoolean(req.query['jpo']); if (jobsByPilot) { const pilot = await Pilot.findById(ObjectId(req.uid), '_id', { lean: true }); if (!pilot) AppError.throw(Errors.PILOT_NOT_EXIST); ops['operator'] = pilot._id; } const pipeline = [ { $match: ops }, { $project: { _id: 1, orderNumber: 1, name: 1, startDate: 1, endDate: 1, status: 1, client: 1, costings: 1, invoiceStatus: 1, invoiceId: 1 } }, { $lookup: { from: 'users', let: { client: "$client" }, pipeline: [ { $match: { $expr: { $and: [{ $eq: ["$_id", "$$client"] }] } } }, { $project: { name: 1, kind: 1 } } ], as: 'client' } }, { $unwind: "$client" }, { $lookup: { from: 'job_logs', let: { job_id: "$_id" }, pipeline: [ { $match: { $expr: { $and: [{ $eq: ["$job", "$$job_id"] }, { "$type": 2 }] } } }, { $project: { date: 1, user: 1 } }, { $group: { _id: "$user", date: { $max: "$date" } } }, { $lookup: { from: 'users', let: { user_id: "$_id" }, pipeline: [ { $match: { $expr: { $and: [{ $eq: ["$_id", "$$user_id"] }] } } }, { $project: { username: 1, _id: 0 } } ], as: 'userD' } }, { $unwind: "$userD" }, { $project: { _id: 0, date: 1, "user": "$userD.username" } }, { $sort: { date: -1, user: 1 } } ], as: 'by' } }, { $project: { _id: 1, orderNumber: 1, name: 1, startDate: 1, endDate: 1, status: 1, client: 1, costings: 1, invoiceId: 1, invoiceStatus: { $ifNull: ['$invoiceStatus', JobInvoiceStatus.NONE] }, by: { $cond: { if: { $and: [{ $gt: [{ $size: "$by" }, 0] }, { $eq: ["$status", JobStatus.DOWNLOWNED] }] }, then: "$by", else: "$$REMOVE" } }, } } ]; const jobs = await Job.aggregate(pipeline); res.json(jobs); } async function createJob_post(req, res) { const _job = req.body; if (!_job) AppParamError.throw(); if (req.body.costings) { req.body.costings = handleCostingItems(req.body.costings) } if (_job._id === 0) { _job.cloneId = +_job.cloneId; delete _job.rptOp; } delete _job._id; const job = new Job(req.body); // To assign properties quickly // If it is a cloned Job, find the original job then copy items to the new job. if (!!(_job.cloneId)) { const orgJob = await Job.findById(_job.cloneId, 'sprayAreas excludedAreas bufs waypoints places byPuid').lean(); if (orgJob) { for (let p in orgJob) { if (p != "_id") job[p] = orgJob[p]; } // Make sure appRate consistent between job and its spray areas if (!utils.isEmptyArray(job.sprayAreas)) { for (let i = 0; i < job.sprayAreas.length; i++) { if (job.sprayAreas[i].properties.appRate != job.appRate) { job.sprayAreas[i].properties.appRate = job.appRate; } } } } job.set('createdAt', undefined, { strict: false }); job.set('updatedAt', undefined, { strict: false }); } const userInfo = req.userInfo; if (!userInfo) AppAuthError.throw(); job.byPuid = userInfo.puid; const savedJob = await job.save(); const insertedJob = savedJob.toObject(); insertedJob.client = _job.client; insertedJob.product = _job.product; insertedJob.operator = _job.operator; insertedJob.vehicle = _job.vehicle; res.json(insertedJob); } async function getJob_get(req, res) { const jobId = req.params.job_id; const withItems = req.query['withItems'] == "true" ? true : false; const withLines = req.query['withLines'] == "true" ? true : false; let exludeFields = withItems ? '' : '-excludedAreas -bufs -waypoints -places -heading -masterPoint'; if (!withLines) exludeFields += '-lines'; if (!Number(jobId) || !utils.isNumber(Number(jobId))) AppParamError.throw(); const job = await Job.findById(jobId) .select(exludeFields) .populate({ path: 'client', select: 'name' }) .populate({ path: 'operator', select: 'name' }) .populate({ path: 'vehicle', select: { 'name': 1 } }) .populate({ path: 'crop', select: 'name', skipInvalidIds: true }) .populate(withItems ? '' : 'products.product') .populate(withItems ? 'sprayAreas.properties.crop' : '', 'name'); if (!job) AppError.throw(Errors.JOB_NOT_FOUND); let _job = job.toObject(); _job.hasItems = !(utils.isEmptyArray(_job.sprayAreas)); if (!withItems) delete _job.sprayAreas; let jobwSumAreas; // Get sum of all spray areas if the area was not set for the job if (!withItems && (!_job.loadOp || !_job.loadOp.area)) { jobwSumAreas = await Job.aggregate( [ { $match: { _id: _job._id } }, { $unwind: { path: "$sprayAreas" } }, { $group: { _id: null, totalArea: { $sum: "$sprayAreas.properties.area" }, } } ] ); } if (!withItems) { if (jobwSumAreas && jobwSumAreas.length && jobwSumAreas[0].totalArea) { _job.loadOp = jobUtil.defLoadOp(_job.loadOp); _job.loadOp.area = Number(utils.toArea(jobwSumAreas[0].totalArea, _job.measureUnit, false).toFixed(1)); // sqm2 to ha or acre } } res.json(_job); } function handleCostingItems(inputCostings) { const costingItemsSchema = Joi.object().keys({ billableArea: Joi.number().min(0).optional().default(0), billableAmount: Joi.number().min(0).required().default(0), currency: Joi.string().valid(...Object.keys(Currencies)).optional(), items: Joi.array().items({ item: Joi.objectId().required(), name: Joi.string().allow('').required(), price: Joi.number().min(0).required(), quantity: Joi.number().min(0).required(), type: Joi.number().valid(...Object.values(CostingItemType)), unit: Joi.number().valid(...Object.values(Units)), }).required() }); const { error, value } = costingItemsSchema.options({ stripUnknown: true }).validate(inputCostings); if (error) AppInputError.throw(error.details[0].message) const { items, billableArea, billableAmount, currency } = value; return { currency, billableArea, billableAmount: utils.toFixedNumber(billableAmount), items }; } async function updateJob_put(req, res) { /* Param Object: { job: Job, updateItems: boolean, updateStatus?: boolean, delSprItems: any[], useDefRate } */ if (!req.params.job_id || !req.body.job) AppParamError.throw(); delete req.body.job._id; let upJob = {}; const updateItems = req.body.updateItems || false; const useDefRate = req.body.useDefRate || false; const inJob = req.body.job; if (!updateItems) { upJob = inJob; delete upJob.sprayAreas; delete upJob.excludedAreas; delete upJob.waypoints; delete upJob.bufs; delete upJob.places; delete upJob.rptOp; // Not overwrite report options, only update after Prreview report delete upJob.weatherInfo; // Not overwrite weather info, only update after Preview report delete upJob.invoiceStatus; delete upJob.invoiceId; if (upJob.client && upJob.client._id) upJob.client = upJob.client._id; if (upJob.product && upJob.product._id) upJob.product = upJob.product._id; if (upJob.operator && upJob.operator._id) upJob.operator = upJob.operator._id; if (upJob.vehicle && upJob.vehicle._id) upJob.vehicle = upJob.vehicle._id; } else { upJob.ttSprArea = inJob.ttSprArea; } if (updateItems) { if (req.body.updateStatus) upJob.status = inJob.status; const [areas, xcls, waypoints, places] = await Promise.all([ jobUtil.cleanAreasAsync(inJob.sprayAreas), jobUtil.cleanAreasAsync(inJob.excludedAreas), jobUtil.cleanGeoPointsAsync(inJob.waypoints), jobUtil.cleanGeoPointsAsync(inJob.places) ]); upJob.sprayAreas = areas; upJob.excludedAreas = xcls; upJob.waypoints = waypoints; upJob.places = places; upJob.bufs = inJob.bufs; } else if (useDefRate && inJob['appRate']) { await Job.updateMany( { _id: req.params.job_id, "sprayAreas.properties": { $exists: true } }, { $set: { "sprayAreas.$[].properties.appRate": inJob.appRate } }); } if (inJob.costings && !utils.isEmptyArray(inJob.costings.items)) { upJob.costings = handleCostingItems(inJob.costings) } const job = await Job.findOneAndUpdate({ _id: req.params.job_id }, upJob, { new: true }) // NOTES: disable validation for now .populate({ path: 'client', select: 'name' }) .populate({ path: 'operator', select: 'name' }) .populate({ path: 'vehicle', select: 'name' }) .populate('crop', 'name') .populate(updateItems ? '' : 'products.product') .populate(updateItems ? 'sprayAreas.properties.crop' : '', 'name'); if (!job) AppError.throw(Errors.JOB_NOT_FOUND); const retJob = job.toObject(); if (!updateItems) { retJob.hasItems = (retJob.sprayAreas.length > 0); delete retJob.sprayAreas; delete retJob.excludedAreas; delete retJob.bufs; delete retJob.waypoints; delete retJob.places; } else { // Delete gridlines of deleted spray areas const delSprItems = req.body.delSprItems; if (!utils.isEmptyArray(delSprItems)) await jobUtil.deleteAreaLines(delSprItems.map(a => ObjectId(a))); } res.json(retJob); } async function deleteJob(req, res) { const job = await Job.findById(req.params.job_id); if (job) await job.removeFull(); res.json({ ok: true }).end(); } /** * Apply drift, checking and skipping or splitting into spray segments * RULES: If the deposit location is NOT inside any XCLs, and if drift position is on an XCL, don’t plot or paint spray on */ function setDriftSegs(seg, xclZones, refUTM) { if (utils.isEmptyArray(seg)) return seg; const llUTM = new locals.LatLonUTM(0, 0), utmBack = locals.UTM.newInstance(refUTM.zone, refUTM.hemisphere, 0, 0); let cur = 0, orgUtmPnt, driftedLL, depLL, segs = [], skip; let _seg = cloneDeep(seg); // Clone the seg for adding drifts while (cur <= (_seg.length - 1)) { skip = false; llUTM.lat = _seg[cur].lat, llUTM.lon = _seg[cur].lon; orgUtmPnt = llUTM.toUtm(refUTM.zone, refUTM.hemisphere); // Current ll to UTM if (utils.isNumber(_seg[cur].driftX) && utils.isNumber(_seg[cur].driftY) && (_seg[cur].driftX !== 0.0 || _seg[cur].driftY !== 0.0)) { utmBack.easting = orgUtmPnt.easting + _seg[cur].driftX; utmBack.northing = orgUtmPnt.northing + _seg[cur].driftY; driftedLL = utmBack.toLatLon(); _seg[cur].lat = driftedLL.lat, _seg[cur].lon = driftedLL.lon; } if (!utils.isEmptyArray(xclZones) && utils.isNumber(_seg[cur].depositX) && utils.isNumber(_seg[cur].depositX)) { if (polyUtil.isPointinPolys(_seg[cur].lat, _seg[cur].lon, xclZones)) { utmBack.easting = orgUtmPnt.easting + _seg[cur].depositX; utmBack.northing = orgUtmPnt.northing + _seg[cur].depositY; depLL = utmBack.toLatLon(); // Scan until get excluded point => add the previous point to end a segment if (!polyUtil.isPointinPolys(depLL.lat, depLL.lng, xclZones)) { // Skip the point or split seg here if (segs.length && segs[segs.length - 1].length && cur != segs.length - 1) segs.push([]); skip = true; } } } if (!skip) { if (!segs.length) segs[0] = [_seg[cur]]; else segs[segs.length - 1].push(_seg[cur]); } cur++; } return segs; } function applyDrifts(segs, xclZones, refUTM) { let _segs = [], appliedSegs; for (let i = 0; i < segs.length; i++) { appliedSegs = setDriftSegs(segs[i], xclZones, refUTM); appliedSegs && (_segs = [..._segs, ...appliedSegs]); } return _segs; } /** * Get Job Data by Id * @param {*} jobId * @param {*} selectFields * @param {*} Ops Query Options with * { * wApps: with application info or not true/false, * dataOp 0: spray inside paths only, 1: spray paths, 2: flight paths only, 3: both spray-in and flight paths, 4: spray and flight paths, * wFileId: true/false whether to include fileId or not * } * @returns wApps true ? { jobId: job._id, measureUnit: job.measureUnit, apps: [], fileIds: [], data: [appFileData] } : [appFileData] * appFileData { id:(appfile Id), file: filename, data: (arrays of sprayed segments) [[[lat, lon]]] } **/ async function getAppDataByJobId(jobId, selectFields, { wApps = false, dataOp = 0, wFileId = false, withJob = false }) { const job = await Job.findById(jobId).lean(); if (!job) AppError.throw(Errors.JOB_NOT_FOUND); const retData = wApps ? withJob ? ({ job: job, apps: [], fileIds: [], data: [] }) : ({ jobId: job._id, measureUnit: job.measureUnit, apps: [], fileIds: [], data: [] }) : []; const projection = { _id: 1, fileName: 1, totalSprayed: 1, startDateTime: 1, endDateTime: 1, totalFlightTime: 1, totalSprayTime: 1, totalTurnTime: 1 }; const _apps = await App.find({ 'jobId': jobId, 'status': 3, 'totalSprayed': { $ne: null }, markedDelete: { $ne: true } }, projection).sort({ 'startDateTime': 1 }).lean(); if (utils.isEmptyArray(_apps)) return retData; retData.apps = _apps; let afSels = '_id name totalSprayed'; if (job.status === JobStatus.ARCHIVED) afSels += ' data'; const appFiles = await AppFile.find({ appId: { $in: _apps.map(it => it._id) } }, afSels).sort("agn").lean(); if (job.status === JobStatus.ARCHIVED) { appFiles.map(af => { const afData = { file: af.name, data: af.data }; if (wApps) { retData.fileIds.push(af._id); retData.data.push(afData) } else retData.push(afData); }); return retData; } if (!utils.isEmptyArray(appFiles)) { let refUTM; if (dataOp != 2) { // Calculate reference UTM Zone on job's areas let allAreas = [...job.sprayAreas || [], ...job.excludedAreas || []]; for (let zone of allAreas) zone.type = 'Feature'; if (!utils.isEmptyArray(allAreas)) { const centerP = turf.center({ type: "FeatureCollection", features: allAreas }); if (centerP) { const point = turf.getCoord(centerP); refUTM = new locals.LatLonUTM(point[1], point[0]).toUtm(); } } } let afDetails; for await (const appFile of appFiles) { if (wApps) retData.fileIds.push(appFile._id); // .sort('gpsTime') // No need, to avoid pass midnight reset gpsTime data issue afDetails = await AppDetail.find({ 'fileId': appFile._id }).select(selectFields + ' driftX driftY depositX depositY').lean(); if (!utils.isEmptyArray(afDetails)) { // Handle the worst case if job has no areas. if (!refUTM) refUTM = new locals.LatLonUTM(afDetails[0].lat, afDetails[0].lon).toUtm(); let segs = [], fsegs = [], sprayOp = dataOp < 2 ? dataOp : (dataOp - 2) - 1, fileData; if (dataOp != 2) { segs = !(/^.*.asc$/i.test(appFile.name)) && sprayOp < 1 ? getSprayOnSegments(afDetails, true) : getSprayOnSegments(afDetails, false); segs = applyDrifts(segs, job.excludedAreas, refUTM); for (let k = 0; k < segs.length; k++) { if (segs[k].length > 2) { const lonlats = segs[k].map(it => [it.lon, it.lat]); const simplified = simplify(lonlats, 0.00001); segs[k] = simplified.map(it => [it[1], it[0]]); } else segs[k] = segs[k].map(it => [it.lat, it.lon]); } } fileData = { file: appFile.name, data: segs }; if (wFileId) fileData.id = appFile._id; if (dataOp >= 2) { fsegs = getFlightSegments(afDetails); for (let k = 0; k < fsegs.length; k++) { if (!fsegs[k].length) continue; if (fsegs[k].length > 2) { const lonlats = fsegs[k].map(it => [it.lon, it.lat]); const simplified = simplify(lonlats, 0.000019); fsegs[k] = simplified.map(it => [it[1], it[0]]); } else fsegs[k] = fsegs[k].map(it => [it.lat, it.lon]); } fileData['fdata'] = fsegs; } if (wApps) retData.data.push(fileData); else retData.push(fileData); } } } return retData; } function getFlightSegments(data) { if (!data || utils.isEmptyArray(data)) return []; let start = 0, cur = 1, seg = [], segs = []; const MAX_DIST_MET = 100; while (cur < data.length) { if ((geoUtil.distance([data[cur - 1].lat, data[cur - 1].lon], [data[cur].lat, data[cur].lon]) >= MAX_DIST_MET) || cur === data.length - 1) { seg = data.slice(start, (cur + 1)); if (seg.length > 1) { segs.push(seg); } seg = []; start = cur; } cur++; } return segs; } /** * Get Spray-On segments * @param {*} data * @param {*} sprayInOnly */ function getSprayOnSegments(data, sprayInOnly) { if (!data || utils.isEmptyArray(data)) return []; let llnum = data[0].llnum; let start = 0, cur = 1, seg = [], segs = []; while (cur < data.length) { if (data[cur - 1].sprayStat == 0 && data[cur].sprayStat == 0 || data[cur - 1].sprayStat == 0 && data[cur].sprayStat != 0) { llnum = data[cur].llnum; start++; cur++; continue; } if (llnum !== data[cur].llnum || data[cur].sprayStat === 3 || endSegChecker(data[cur].sprayStat, data[cur - 1].sprayStat) || (geoUtil.distance([data[cur - 1].lat, data[cur - 1].lon], [data[cur].lat, data[cur].lon]) >= 1) || (sprayInOnly && (data[cur].sprayStat - data[cur - 1].sprayStat) > 99) || cur === data.length - 1) { seg = data.slice(start, cur); if (seg.length > 1) { if (sprayInOnly) { // Trim ouside points at beginning or end while (seg.length && (seg[0].satsIn < 99 || seg[0].sprayStat == 0)) seg.shift(); let g = seg.length - 1; while (g > 0) { if (seg[g].satsIn < 99) { seg.splice(g, 1); g--; } else break; } } if (seg.length > 1) segs.push(seg); } seg = []; start = cur; } llnum = data[cur].llnum; cur++; } return segs; } function endSegChecker(cur, prev) { return (cur === 0 && prev === 1) || (cur === 0 && prev === 10) || (cur === 3 && prev === 0) || (cur === 0 && prev === 3) || (cur <= 0 && prev > 0 || (cur === 3 && prev === 1)); } async function getData_post(req, res) { const jobId = req.body.jobId; if (!jobId) AppParamError.throw(); const wApps = (req.body.inside != undefined); let dataOp = req.body.dataOp || 0; if (wApps) dataOp = req.body.inside == 0 ? 1 : 0; // '-_id lat lon head alt grSpeed sprayStat lminApp timeAdv swath gpsTime' const jobAppData = await getAppDataByJobId(jobId, '-_id lat lon sprayStat llnum gpsTime satsIn stdHdop', { wApps, dataOp }); let result = { jobId: jobId, data: jobAppData ? (wApps ? jobAppData['data'] : jobAppData) : [] }; // Get the aggregated weather info if (wApps && jobAppData && !utils.isEmptyArray(jobAppData.fileIds)) { const wi = await jobUtil.getDataWeatherInfo(jobAppData.fileIds); if (wi && wi.length) result['weatherInfo'] = ({ windSpd: utils.mpSecToKnot(wi[0]['avgWindSpd']).toFixed(1), windDir: utils.deg2Compass(wi[0]['avgWindDir']), temp: utils.inCorF(wi[0]['avgTemp'], jobAppData.measureUnit, false), humid: utils.truncR(wi[0]['avgHumid'], 0) }); } res.json(result ? result : []); } /** * Get Report Setting values for the job. * @returns If both coverage and actualVol values exist, return them to the client's request as is otherwise gather from imported files. * Notes: the values's units are based on the job's measurement system */ async function getReportOps_get(req, res) { const jobId = req.body.jobId; if (!jobId) return res.json(null).end(); let cvrVal = 0, actVol = 0; const _job = await Job.findById(jobId, { rptOp: 1, measureUnit: 1, appRate: 1, appRateUnit: 1, swathWidth: 1 }).lean(); if (!_job) return res.json(null).end(); // Ignore error for now even if the job is being or was just deleted const numOfApps = await App.countDocuments({ jobId: _job._id }); if (numOfApps) { if (_job.rptOp) { if (_job.rptOp.coverage) cvrVal = utils.toArea(_job.rptOp.coverage, _job.measureUnit, true); if (_job.rptOp.actualVol) actVol = utils.toVolume(_job.rptOp.actualVol, (_job.appRateUnit !== 2 && _job.appRateUnit !== 4), _job.measureUnit); } if (!cvrVal || !actVol) { const results = await App.aggregate([ { $match: { jobId: jobId } }, { $group: { _id: null, coverage: { $sum: "$totalSprayed" }, totalLength: { $sum: "$totalSprLength" }, actualVol: { $sum: "$totalSprayMat" } } }]); // Total Coverage = Total Coverage from AgNav data + Total Coverage by Length (Non-AgNav data) if (results && results.length > 0) { if (!cvrVal) { if (results[0].coverage) cvrVal = utils.toArea(results[0].coverage, _job.measureUnit, true); if (results[0].totalLength && _job.swathWidth) cvrVal += utils.toArea((results[0].totalLength * utils.toMeter(_job.swathWidth, _job.measureUnit)) * 1e-4, _job.measureUnit, true); } if (!actVol) { if (results[0].actualVol) actVol = utils.toVolume(results[0].actualVol, (_job.appRateUnit !== 2 && _job.appRateUnit !== 4), _job.measureUnit); } } } } const rptOp = { areaSize: _job.rptOp && _job.rptOp.areaSize ? utils.toArea(_job.rptOp.areaSize, _job.measureUnit, true) : 0, printArea: (_job.rptOp && _job.rptOp['printArea'] !== undefined) ? _job.rptOp['printArea'] : true, coverage: cvrVal, appRate: _job.appRate, useActualVol: (_job.rptOp && _job.rptOp['useActualVol'] !== undefined) ? _job.rptOp['useActualVol'] : false, actualVol: actVol }; res.json(rptOp); } async function preAppReport_post(req, res) { const input = req.body; const jobId = input.jobId; let job, sprayData, hasData = false, lang = input.lang || DEFAULT_LANG, rptDS; const theJob = await Job.findById(jobId) .populate({ path: 'client', select: '-password' }) .populate({ path: 'operator', select: '-password' }) .populate({ path: 'vehicle', select: '-password' }) .populate('products.product', 'name type restricted epaReg') .populate('crop', 'name'); if (!theJob) AppError.throw(Errors.JOB_NOT_FOUND); job = theJob.toObject(); // Save report settings to the corresponding job. let updateVars = {}; if (input.rptOp) { const rptOp = Object.assign({}, input.rptOp); // Check and convert report settings's values to Metric if the job is in US Measurement if (job.measureUnit) { rptOp.coverage = utils.acreToHa(rptOp.coverage); rptOp.areaSize = utils.acreToHa(rptOp.areaSize); rptOp.actualVol = utils.toMetricVolume(rptOp.actualVol, (job.appRateUnit !== Units.LB && job.appRateUnit !== Units.KG), job.measureUnit); } updateVars["rptOp"] = rptOp; } updateVars["useCustWI"] = input.useCustWI; updateVars["weatherInfo"] = input.weatherInfo; if (Object.keys(updateVars).length) { // Update the custom weather info to the job. const updatedJob = await Job.findOneAndUpdate({ _id: jobId }, { $set: updateVars }, { new: true, lean: true }); if (!updatedJob) AppError.throw(Errors.JOB_NOT_FOUND); job.useCustWI = updatedJob.useCustWI; job.rptOp = updatedJob.rptOp; job.weatherInfo = updatedJob.weatherInfo; } const genFolder = uniqid(`app_${jobId}_`); const tempFolder = path.join(env.TEMP_DIR, 'report', genFolder); const targetFolder = path.join(env.REPORT_DIR, 'dat', genFolder); let reportWebTempPath = `${req.protocol}://${req.hostname}/report/${genFolder}/`; job.sprayAreas = await setPolysWCenter(job.sprayAreas); job.excludedAreas = await setPolysWCenter(job.excludedAreas); const customer = await Customer.findOne({ '_id': job.byPuid }, '-password', { lean: true }); let dataOp = 2; if (input.showSprayed) { if (input.showFlights) dataOp = input.sprOp.dataOp == 0 ? 4 : 3; else dataOp = input.sprOp.dataOp == 0 ? 1 : 0; } if (input.showSprayed || input.showFlights) { sprayData = await getAppDataByJobId(jobId, '-_id lat lon sprayStat llnum gpsTime xTrack satsIn', { wApps: true, dataOp }); hasData = !!(sprayData && !utils.isEmptyArray(sprayData.data)); } // Copy template file to temp folder await fs.copy(path.join(process.cwd(), 'public/sprayMap.html'), path.join(tempFolder, 'sprayMap.html')); // Create data json file const data = { premium: (customer && customer.premium || 0), job: job, params: input.params, data: hasData ? sprayData.data : null, sprOp: input.sprOp, obs: input.obs || [], colors: input.colors || { sprayZone: 'blue', fpColor: 'lime' } }; await fs.writeFile(path.join(tempFolder, 'spraydata.js'), 'var req=' + JSON.stringify(data, null, 2) + ';', 'utf-8'); await fs.ensureDir(path.join(targetFolder, 'map')); // Capture the picture of the area with spray paths in a map webpage await webUtil.webShot({ url: reportWebTempPath + 'sprayMap.html', type: 'jpeg', quality: 90, width: input.params.width, height: input.params.height, path: path.join(targetFolder, 'map') + '.jpg' }); // Consolidate the Dataset and related data for the report so the client then can render the report in Report Viewer const numOfApps = sprayData && sprayData.apps ? sprayData.apps.length : 0; const startApp = numOfApps ? sprayData.apps[0] : null; let endApp; if (numOfApps > 0) { endApp = numOfApps === 1 ? startApp : sprayData.apps[sprayData.apps.length - 1]; } // TODO: Handle the case the actVol is preferred or input manually const totalSprayedArea = Number(input.rptOp.coverage); const appRate = input.rptOp && input.rptOp.appRate ? Number(input.rptOp.appRate) : job.appRate; let totalVolume = totalSprayedArea * appRate; let totalRateUnit = job.appRateUnit; if (job.measureUnit && job.appRateUnit === 0) { totalVolume = utils.ozToGal(totalVolume); totalRateUnit = 1; // gals } let actVolAdj = 1; // Actual volume different adjustment factor if (input.rptOp.useActualVol && input.rptOp.actualVol > 0 && input.rptOp.actualVol !== totalVolume) { actVolAdj += (input.rptOp.actualVol - totalVolume) / totalVolume; totalVolume = totalVolume * actVolAdj; } moment.locale(lang); const planStartDate = moment(job.startDate); const planEndDate = moment(job.endDate); const actStartDate = moment.utc(startApp ? startApp.startDateTime : null); const actEndDate = moment.utc(endApp ? endApp.endDateTime : null); // Total area coverage used to calculate the Total Vol used of products const totalCoverage = utils.roundTo(!hasData ? input.totalArea : totalSprayedArea, 1); rptDS = makeJobAppDataSource(job, customer, `https://${req.hostname}/reports/dat/${genFolder}/map.jpg`, lang); let app = { appId: "app1", jobId: jobId, orderNum: job.orderNum || '', planStart: planStartDate.isValid() ? planStartDate.format("MMM DD, YYYY") : '', planEnd: planEndDate.isValid() ? planEndDate.format("MMM DD, YYYY") : '', appRate: '', actStart: '', actEnd: '', actStartTime: '', actEndTime: '', totalArea: '', totalSprayed: '', totalVolume: '', dataFile: '', windSpd: "", windDir: "", temp: "", humid: "", }; if (numOfApps) { rptDS.reports.type = 1; // Application Report app.appRate = utils.toLocaleStr(appRate, 2, lang) + ' ' + utils.rateUnitString(job.appRateUnit, true); app.actStart = actStartDate.isValid() ? actStartDate.format("MMM DD, YYYY") : ''; app.actEnd = actEndDate.isValid() ? actEndDate.format("MMM DD, YYYY") : ''; app.actStartTime = actStartDate.isValid() ? actStartDate.format('HH:mm:ss') : ''; app.actEndTime = actEndDate.isValid() ? actEndDate.format('HH:mm:ss') : ''; app.totalSprayed = totalSprayedArea ? utils.toLocaleStr(totalSprayedArea, 1, lang) + ' ' + utils.areaUnitString(job.measureUnit, true) : ''; // Total used volume, estimated = total applied area * appRate app.totalVolume = totalVolume ? utils.toLocaleStr(totalVolume, 1, lang) + ' ' + utils.rateUnitString(totalRateUnit, true, 1) : ''; app.dataFile = sprayData.apps.map(i => i.fileName).join(','); let totalFlightTime = 0, totalSprayTime = 0, totalTurnTime = 0; for (let i = 0; i < sprayData.apps.length; i++) { const app = sprayData.apps[i]; if (app.totalSprayTime) totalSprayTime += app.totalSprayTime; if (app.totalTurnTime) totalTurnTime += app.totalTurnTime; if (app.totalFlightTime) totalFlightTime += app.totalFlightTime; } app.totalFlightTime = utils.secondsToHMS(totalFlightTime, 2); app.totalSprayTime = utils.secondsToHMS(totalSprayTime, 2); app.totalTurnTime = utils.secondsToHMS(totalTurnTime, 2); } if (input.rptOp && input.rptOp.printArea && utils.isNumber(input.rptOp.areaSize)) { app.totalArea = `${utils.toLocaleStr(Number(input.rptOp.areaSize), 1, lang)} ${utils.areaUnitString(job.measureUnit, true)}`; } rptDS.apps.push(app); if (!utils.isEmptyArray(job.products)) { rptDS.products = []; let rate, unit, prod; for (let i = 0; i < job.products.length; i++) { rate = job.products[i].rate; unit = job.products[i].unit; prod = { id: job.products[i]._id, jobId: jobId, name: job.products[i].product.name, type: job.products[i].product.type, epaReg: job.products[i].product.epaReg || '', restricted: job.products[i].product.restricted || false, rateStr: utils.toLocaleStr(rate, 2, lang) + ' ' + utils.getProdUnit(unit) }; rate = (rate * totalCoverage) * actVolAdj; if (unit === Units.OZ) { unit = Units.GAL; rate = utils.ozToGal(rate); } prod["totalRateStr"] = utils.toLocaleStr(rate, 2, lang) + ' ' + utils.getProdUnit(unit); rptDS.products.push(prod); } } if (hasData) { // Use custom weather info if (job.useCustWI && job.weatherInfo) { rptDS.apps[0].windSpd = `${job.weatherInfo.windSpd} kt`; rptDS.apps[0].windDir = job.weatherInfo.windDir; rptDS.apps[0].temp = `${utils.truncR(job.weatherInfo.temp, 0)} ${(job.measureUnit ? "°F" : "°C")}`; rptDS.apps[0].humid = `${utils.truncR(job.weatherInfo.humid, 0)} %`; } else { // Use aggregated weather info from data const result = await jobUtil.getDataWeatherInfo(sprayData.fileIds); if (result && result.length > 0) { const windDir = (result[0].maxWindDir - result[0].minWindDir) > 22.5 ? utils.deg2Compass(result[0].minWindDir) + ' - ' + utils.deg2Compass(result[0].maxWindDir) : utils.deg2Compass(result[0].minWindDir); const windSpd = result[0].maxWindSpd > result[0].minWindSpd ? utils.toLocaleStr(utils.mpSecToKnot(result[0].minWindSpd), 1, lang) + ' - ' + utils.toLocaleStr(utils.mpSecToKnot(result[0].maxWindSpd), 1, lang) : utils.toLocaleStr(utils.mpSecToKnot(result[0].minWindSpd), 1, lang); rptDS.apps[0].windSpd = `${windSpd} kt`; rptDS.apps[0].windDir = windDir; rptDS.apps[0].temp = utils.inCorF(result[0].avgTemp, job.measureUnit, true); rptDS.apps[0].humid = `${utils.truncR(result[0].avgHumid, 0)} %`; } } } // Write the report datasource file to the temp folder await fs.writeFile(path.join(targetFolder, 'rptDS.json'), JSON.stringify(rptDS, null, 2), 'utf-8'); const sApplicatorId = customer && customer._id.toHexString(); const existed = await fs.pathExists(path.join(env.REPORT_DIR, `app_${sApplicatorId}.mrt`)); const reportId = existed ? `app_${sApplicatorId}` : 'app'; res.json({ rid: reportId, path: genFolder, c: reportId === 'app' ? 0 : 1 // Whether it is a customized report }); /* Notes: BC we want to keep created temporary files for a while so the customer can still view them in the report. Thus, they are will be deleted by the maintenainer app periodically */ } async function getRptVars_post(req, res) { const input = req.body; const vars = await RptVar.find({ $and: [{ user: ObjectId(req.uid) }, { rpt: input.rpt }] }); // TODO: Check with lean:true ops later res.json(!utils.isEmptyArray(vars) ? vars[0].toObject() : null); } async function setRptVars_post(req, res) { const ops = req.body; if (!ops['rpt']) AppParamError.throw(); await RptVar.deleteMany({ rpt: ops.rpt, user: ObjectId(req.uid) }); if (ops.vars) { await RptVar.insertMany({ rpt: String(ops.rpt), user: ObjectId(req.uid), vars: ops.vars }); } res.json({ ok: true }); } async function saveReport_post(req, res) { const ops = req.body; await fs.writeFile(path.join(env.REPORT_DIR, ops.rid + '.mrt'), ops.content); return res.json({ ok: true }); } //TODO: Make this as async fnc ??? function preLoadReport_post(req, res, next) { if (!req.body || !req.body.jobId || !req.body.loadOp || !req.body.loadOp.area) return next(AppParamError.create()); const input = req.body, jobId = input.jobId; input.loadOp.area = +input.loadOp.area; input.loadOp.loads = +input.loadOp.loads; let _job, rptDS; const lang = input.lang || DEFAULT_LANG; async.series([ function (callback) { Job.findById(jobId) .populate({ path: 'client', select: '-password' }) .populate({ path: 'operator', select: '-password' }) .populate({ path: 'vehicle', select: '-password' }) .populate('products.product', 'name type rate unit') .lean() .then(job => { if (!job) { callback(AppError.create(Errors.JOB_NOT_FOUND)); return; } _job = job; callback(); }) .catch(err => { callback(err); }); }, function (callback) { // Save report settings to the corresponding job. Job.findOneAndUpdate({ _id: jobId }, { $set: { "loadOp": Object.assign({}, input.loadOp) } }, { new: true, lean: true }, (err, data) => { if (err) throw err; if (!data) return callback(AppError.create(Errors.JOB_NOT_FOUND)); callback(); }); } ], function (err) { if (err) { return next(err); } const genFolder = uniqid(`loadsheet_${jobId}_`); const targetFolder = path.join(env.REPORT_DIR, 'dat', genFolder); rptDS = makeJobAppDataSource(_job, null, '', lang, input.loadOp); async.series([ function (callback) { moment.locale(lang); rptDS.reports = { date: moment(input.loadOp.date).format("MMM DD, YYYY") }; if (!utils.isEmptyArray(_job.products)) { rptDS.products = []; rptDS.loads = []; rptDS.load_details = []; let numLoads = input.loadOp.loads, totalLoad = _job.appRate * input.loadOp.area, areaPerLoad = (input.loadOp.area / numLoads); if (input.loadOp.loadType != 0 || totalLoad % input.loadOp.capacity == 0 || numLoads == 1) { rptDS.loads.push({ id: 1, loads: `1 - ${numLoads}`, areaPL: areaPerLoad, area: `${utils.toLocaleStr(areaPerLoad, 1, lang)}`, totalLoadStr: `${utils.toLocaleStr((totalLoad / numLoads), 1, lang)} ${utils.getProdUnit(_job.appRateUnit)}` }); } else { areaPerLoad = (input.loadOp.capacity / _job.appRate); rptDS.loads.push({ id: 1, loads: `1 - ${numLoads - 1}`, areaPL: areaPerLoad, area: `${utils.toLocaleStr(areaPerLoad, 1, lang)}`, totalLoadStr: `${utils.toLocaleStr((areaPerLoad * _job.appRate), 1, lang)} ${utils.getProdUnit(_job.appRateUnit)}` }); areaPerLoad = input.loadOp.area - (areaPerLoad * (input.loadOp.loads - 1)); rptDS.loads.push({ id: 2, loads: numLoads.toString(), areaPL: areaPerLoad, area: `${utils.toLocaleStr(areaPerLoad, 1, lang)}`, totalLoadStr: `${utils.toLocaleStr((areaPerLoad * _job.appRate), 1, lang)} ${utils.getProdUnit(_job.appRateUnit)}` }); } // Products let prdRate, totalRate, unit, prod, loadDetail; for (let i = 0; i < _job.products.length; i++) { prdRate = _job.products[i].rate; unit = _job.products[i].unit; totalRate = prdRate * input.loadOp.area; prod = { id: _job.products[i].product._id.toHexString(), name: _job.products[i].product.name, type: _job.products[i].product.type, rate: utils.toLocaleStr(prdRate, 2, lang), unit: utils.getProdUnit(unit), totalStr: `${utils.toLocaleStr(prdRate * input.loadOp.area, 2, lang)} ${utils.getProdUnit(unit)}` }; if (unit === Units.OZ) { prod.totalStr += '\n' + `${utils.toLocaleStr(utils.ozToGal(totalRate), 2, lang)} gal`; } rptDS.products.push(prod); // Load Details for (let j = 0; j < rptDS.loads.length; j++) { const load = rptDS.loads[j]; loadDetail = { loadId: load.id, prodId: prod.id, totalProdStr: `${utils.toLocaleStr((load.areaPL * prdRate), 1, lang)} ${prod.unit}` }; if (unit === Units.OZ) { loadDetail.totalProdStr += '\n' + `${utils.toLocaleStr(utils.ozToGal((load.areaPL * prdRate)), 1, lang)} gal`; } rptDS.load_details.push(loadDetail); } } } callback(); }, function (callback) { fs.ensureDir(targetFolder, err => { if (err) return callback(err); callback(); }) }, function (callback) { // Write the report datasource file to the temp folder fs.writeFile(path.join(targetFolder, 'rptDS.json'), JSON.stringify(rptDS, null, 2), 'utf-8', function (err) { if (err) return callback(err); callback(); }); } ], function (err) { if (err) { debug(err); return next(err); } res.json({ rid: 'loadsheet', path: genFolder, c: 0 }); // Temporary files to be deleted in the maintenainer app periodically }); }); } function makeJobAppDataSource(job, applicator, genImgPath, lang, loadOp) { const appDS = { reports: { type: 0 }, clients: [{ // The applicator is a client of its customers in report model id: applicator && applicator._id.toHexString(), name: utils.getField(applicator, "name"), address: utils.getField(applicator, "address") }], customers: [{ id: job.client._id.toHexString(), name: job.client.name || '', address: job.client.address || '', contact: job.client.contact || '', phone: job.client.phone || '' }], pilots: [ { id: utils.getField(job.operator, "_id", "0"), name: utils.getField(job.operator, "name"), address: utils.getField(job.operator, "address"), }], products: [{ // default empty product id: "0", jobId: job._id, name: "", epaReg: "", restricted: null, type: 1, rate: 0, unit: "", rateStr: "", totalRateStr: "" }], vehicles: [{ id: utils.getField(job.vehicle, "_id", "0"), name: utils.getField(job.vehicle, "name"), model: utils.getField(job.vehicle, "model") }], apps: [] }; appDS["jobs"] = [{ id: job._id, orderNum: job.orderNumber || "", clientId: applicator && applicator._id, vehicleId: job.vehicle ? job.vehicle._id.toHexString() : "0", customerId: job.client ? job.client._id.toHexString() : "0", pilotId: job.operator ? job.operator._id.toHexString() : "0", name: job.name || '', appRate: `${utils.toLocaleStr(job.appRate, 2, lang)} ${utils.rateUnitString(job.appRateUnit, true)}`, flight: job.flightNumber || '', farm: job.farm || '', crop: ((job.crop && job.crop['_id']) ? job.crop['name'] : job.crop) || '', remark: job.remark || '', measureUnit: job.measureUnit, mapfile: genImgPath, sysPsi: job.sysPsi, appType: job.appType, missionTime: job.missionTime }]; if (loadOp) { appDS.jobs[0].areaSize = loadOp.area; appDS.jobs[0].capacity = loadOp.capacity; appDS.jobs[0].loadType = loadOp.loadType; appDS.jobs[0].totalLoads = loadOp.loads; } return appDS; } // function updatePolyCenter(geoPolys) { // if (geoPolys === undefined || utils.isEmptyArray(geoPolys)) // return false; // try { // let geoP; // for (let i = 0; i < geoPolys.length; i++) { // geoP = polylabel(geoPolys[i].geometry.coordinates, 1.0); // if (geoP) // geoPolys[i].properties["center"] = [geoP[1], geoP[0]]; // } // } catch (error) { // debug('updatePolyCenter()', error); // return false; // } // } async function setPolyWCenter(item) { return new Promise(resolve => { try { const itemWPL = polylabel(item.geometry.coordinates, 1.0); if (itemWPL) item.properties["center"] = [itemWPL[1], itemWPL[0]]; } catch (error) { debug(error); } finally { resolve(item); } }); } async function setPolysWCenter(geoPolys) { return Promise.all(geoPolys.map(async item => await setPolyWCenter(item))); } async function getUploadedFiles_post(req, res) { const jobId = req.body.jobId; const job = await Job.findById(jobId, { '_id': 1 }); if (!job) AppError.throw(Errors.JOB_NOT_FOUND); const apps = await App.find({ 'jobId': job._id, 'status': 3, markedDelete: { $ne: true } }) .sort('-updateDate') .select('_id fileName savedFilename fileSize status proStatus totalSprayed createdDate') .lean(); return res.json(apps || []); } async function importStatus_post(req, res) { const appId = req.body.appId; if (!utils.isObjectId(appId)) AppParamError.throw(); const app = await App.findById(ObjectId(appId)) .select('_id fileName savedFilename fileSize status proStatus errorMsg warnMsg markedDelete') .lean(); res.json(app); } async function importingStatus_post(req, res) { let filterOps = {}; if (req.body.jobId) { filterOps = { jobId: req.body.jobId, $or: [{ status: 1 }, { status: 2 }] }; } else { filterOps = { byUser: ObjectId(req.uid) }; if (!utils.isEmptyArray(req.body.appIds)) { const appIds = req.body.appIds.map(it => ObjectId(it)); filterOps._id = { $in: appIds }; } filterOps['byImport'] = true; } const apps = await App.aggregate( [ { $match: filterOps }, { $lookup: { from: "jobs", localField: "jobId", foreignField: "_id", as: "appjob" } }, { $unwind: { path: "$appjob", "preserveNullAndEmptyArrays": true } }, { $lookup: { from: "users", localField: "appjob.client", foreignField: "_id", as: "jobclient" } }, { $unwind: { path: "$jobclient", "preserveNullAndEmptyArrays": true } }, { $project: { fileName: 1, fileSize: 1, status: 1, proStatus: 1, errorMsg: 1, jobId: 1, createdDate: 1, cid: '$appjob.client', cname: '$jobclient.name' } }, { $sort: { createdDate: - 1 } }, { $limit: 500 } ]); res.json(apps || []).end(); } async function deleteAppFile_post(req, res) { const _appId = req.body.appId; await jobUtil.deleteAppById(_appId, true); res.json({ appId: _appId }); } async function getJobLogs_post(req, res) { const jobId = req.body.jobId; const type = req.body.type || 2; const logs = await JobLog.find({ job: jobId, type: type }, 'date user') .limit(30) .sort('-date') .populate('user', '-_id username') .lean(); res.json(logs ? logs : []); } async function assign_post(req, res) { const _params = req.body; if (!_params || !_params.jobId || !_params.dlOp || !_params.asUsers) AppParamError.throw(); let _doneIds = [], _avIds = []; const job = await Job.findById(_params.jobId).select('dlOp'); if (!job) AppError.throw(Errors.JOB_NOT_FOUND); if (_params.dlOp.type !== job.dlOp.type) await Job.updateOne({ _id: _params.jobId }, { $set: { "dlOp.type": _params.dlOp.type } }); if (!utils.isEmptyArray(_params.avUsers)) { for (const it of _params.avUsers) { if (ObjectId.isValid(it.uid)) _avIds.push(ObjectId(it.uid)); } } if (_avIds.length) await JobAssign.deleteMany({ $or: [{ job: _params.jobId, status: 0 }, { job: _params.jobId, user: { $in: _avIds } }] }); else await JobAssign.deleteMany({ job: _params.jobId, status: 0 }); let doneJUs; if (!utils.isEmptyArray(_params.asUsers)) { const asUIds = []; for (const it of _params.asUsers) { if (ObjectId.isValid(it.uid)) asUIds.push(ObjectId(it.uid)); } doneJUs = await JobAssign.find({ job: _params.jobId, user: { $in: asUIds }, status: { $gt: 0 } }, 'user').lean(); } if (!utils.isEmptyArray(_params.asUsers)) { if (!utils.isEmptyArray(doneJUs)) _doneIds = doneJUs.map(it => it.user); const newItems = []; for (const it of _params.asUsers) { if (!utils.objectIdIn(_doneIds, it.uid)) newItems.push({ user: it.uid, job: _params.jobId, status: 0 }); } if (newItems.length) await JobAssign.insertMany(newItems, { rawResult: true }); } res.json({ ok: true }); } async function assignments_post(req, res) { const _jobId = Number(req.body.jobId); if (!_jobId) AppParamError.throw(Errors.JOB_NOT_FOUND); let _assignedUserIds; const assigment = { avUsers: [], asUsers: [] }; const job = await Job.findById(_jobId, '_id byPuid', { lean: true }); if (!job) AppError.throw(Errors.JOB_NOT_FOUND); const assignedUsers = await JobAssign.find({ job: _jobId }, '-_id user', { lean: true }); if (!utils.isEmptyArray(assignedUsers)) _assignedUserIds = assignedUsers.map(it => it.user); const _availableUsers = await Vehicle.aggregate([ { $match: { markedDelete: { $ne: true }, parent: job.byPuid, username: { $nin: [null, ''] } } }, { $project: { _id: 0, uid: "$_id", name: 1, username: 1, active: 1 } }, ]); let avUsers = [], asUsers = []; if (!utils.isEmptyArray(_assignedUserIds)) { for (let i in _availableUsers) { if (utils.objectIdIn(_assignedUserIds, _availableUsers[i].uid)) asUsers.push(_availableUsers[i]); else if (_availableUsers[i].active === true) avUsers.push(_availableUsers[i]); } } else if (!utils.isEmptyArray(_availableUsers)) { avUsers = _availableUsers.slice(0); } if (!utils.isEmptyArray(avUsers)) assigment.avUsers = avUsers; if (!utils.isEmptyArray(asUsers)) assigment.asUsers = asUsers; res.json(assigment); } async function countByClient_post(req, res) { const clientId = req.body.clientId; if (!utils.isObjectId(clientId)) AppParamError.throw(); const count = await Job.countDocuments({ client: ObjectId(clientId) }); res.json(count); // Notes: Can not use res.json(number) or express error and the response will fail. } async function saveMapOps_post(req, res) { const mapOps = req.body.mapOps; if (!req.body.jobId || !mapOps || !mapOps.width || !mapOps.height || !mapOps.center || !mapOps.zoom) AppParamError.throw(); const data = await Job.findOneAndUpdate({ _id: req.body.jobId }, { $set: { "dlOp.mapOp": mapOps } }, { new: true, lean: true }); return res.json(data ? data.dlOp.mapOp : null); // Ingore job_not_found } async function searchJobs_post(req, res) { // { byPuid: this.authSvc.byPUserId, clientId: this.fClient?.value, nameId: this.fNameId?.trim(), status: this.fStatus?.value } const params = req.body; // console.log('params:', params); if (!params || !params.byPuid) AppParamError.throw(); const ops = { byPuid: ObjectId(params.byPuid) }; if (params.clientId && ObjectId.isValid(params.clientId)) ops['client'] = ObjectId(params.clientId); if (params.nameId) { if (!utils.isNumber(Number(params.nameId))) ops['name'] = { $regex: new RegExp(`${params.nameId}`, 'i') }; else ops['_id'] = Number(params.nameId); } if (utils.isNumber(params.status)) ops['status'] = params.status; const jobs = await Job.find(ops, '_id name sprayAreas excludedAreas measureUnit appRate appRateUnit status invoiceStatus').sort('_id'); res.json(jobs); } /** * Get list of uploaded files given the jobId */ async function appFiles_post(req, res) { const params = req.body; if (!params || !params.jobId) AppParamError.throw(); const jobs = await App.aggregate([ { $match: { jobId: params.jobId, markedDelete: { $ne: true } } }, { $lookup: { from: "appfiles", localField: "_id", foreignField: "appId", as: "appfiles" } }, { $unwind: { path: '$appfiles' } }, { $project: { fid: '$appfiles._id', name: '$appfiles.name', agn: '$appfiles.agn', meta: '$appfiles.meta' } }, { $sort: { agn: 1 } }]); res.json(jobs); } /** * Get spray data records given the fileIds list */ async function filesdata_post(req, res) { const params = req.body; if (!params || !params.fileIds) AppParamError.throw(); let filesData = []; for (let i = 0; i < params.fileIds.length; i++) { const filewData = { fid: params.fileIds[i] }; filewData.data = await AppDetail.find({ fileId: params.fileIds[i] }, null, { lean: true }); filesData.push(filewData); } res.json(filesData); } return { getJobs_get, createJob_post, getJob_get, updateJob_put, deleteJob, getData_post, getReportOps_get, preAppReport_post, getRptVars_post, setRptVars_post, saveReport_post, preLoadReport_post, getUploadedFiles_post, importStatus_post, importingStatus_post, deleteAppFile_post, getJobLogs_post, assign_post, assignments_post, countByClient_post, saveMapOps_post, searchJobs_post, appFiles_post, filesdata_post, getAppDataByJobId } }