import { getFilterObject, getSortObject, getTaggedUsersFilter, PageQueryParams, } from "../pagination"; import { generateId } from "../utils/id"; import { CreatePermitInput, permitFields, permitModel, UpdatePermitInput, } from "./permit.schema"; import { ChangeEvent, dbEvents } from "../realtime"; import { permitPipeline } from "../utils/pipeline"; import { AuthenticatedUser } from "../auth"; import mongoose from "mongoose"; import { getUser } from "../user/user.service"; import { createNote } from "../note/note.service"; import { createAlert } from "../alert/alert.service"; import { processedModel } from "../processed/processed.schema"; import { orgModel } from "../organization/organization.schema"; import { arrayDiff } from "../utils/diff"; export async function createPermit( input: CreatePermitInput, user: AuthenticatedUser ) { if (!input.stage) { input.stage = { pipeline: permitPipeline, currentStage: 0, }; } if (input.client && !input.clientData) { const client = await orgModel.findById(input.client); if (client) { input.clientData = { pid: client.pid, licenseNumber: client.licenseNumber, name: client.name, avatar: client.avatar, }; } } if (input.issued) { const permit = await processedModel.create({ tenantId: user.tenantId, pid: generateId(), createdAt: new Date(), createdBy: user.userId, ...input, }); dbEvents.emit( "change", { tenantId: user.tenantId, type: "insert", collection: "processed", orgId: permit.client.toString(), document: permit, } as ChangeEvent, ["permit:read"] ); return await permit.populate({ path: "assignedTo createdBy", select: "pid name avatar", }); } else { const permit = await permitModel.create({ tenantId: user.tenantId, pid: generateId(), createdAt: new Date(), createdBy: user.userId, ...input, }); dbEvents.emit( "change", { tenantId: user.tenantId, type: "insert", collection: "permits", orgId: permit.client.toString(), document: permit, } as ChangeEvent, ["permit:read"] ); return await permit.populate({ path: "assignedTo createdBy", select: "pid name avatar", }); } } export async function getPermit(permitId: string, user: AuthenticatedUser) { const permit = await permitModel .findOne({ $and: [{ tenantId: user.tenantId }, { pid: permitId }], }) //.populate({ path: "county", select: "pid name avatar" }) //.populate({ path: "client", select: "pid name avatar" }) .populate({ path: "assignedTo", select: "pid name avatar" }) .populate({ path: "createdBy", select: "pid name avatar" }); // Don't return the record if the user doesn't have access to the org if ( permit && user.role == "client" && !user.orgId.includes(permit.client.toString()) ) return null; // Don't return the record if the user doesn't have access to the org if ( permit && user.counties && user.counties.length > 0 && !user.counties.includes(permit.county.id.toString()) ) return null; return permit; } export async function listPermits( params: PageQueryParams, user: AuthenticatedUser ) { const page = params.page || 1; const pageSize = params.pageSize || 10; const sortObj = getSortObject(params, permitFields); let filterObj = getFilterObject(params) || []; if (user.role == "client") { filterObj.push({ client: { $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), }, }); } if (user.counties && user.counties.length > 0) { filterObj.push({ "county.id": { $in: user.counties.map((item) => new mongoose.Types.ObjectId(item)), }, }); } let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter( filterObj, sortObj ); if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1); const permitsList = await permitModel.aggregate([ { $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, }, ...taggedFilter, { $lookup: { from: "users", localField: "assignedTo", foreignField: "_id", as: "assignedTo", }, }, { $project: { _id: 1, pid: 1, permitNumber: 1, permitDate: 1, stage: 1, status: 1, manualStatus: 1, cleanStatus: 1, permitType: 1, utility: 1, link: 1, address: 1, recordType: 1, description: 1, applicationDetails: 1, applicationInfo: 1, applicationInfoTable: 1, conditions: 1, ownerDetails: 1, parcelInfo: 1, paymentData: 1, inspections: 1, newProcessingStatus: 1, newPayment: 1, newConditions: 1, professionals: 1, recordId: 1, relatedRecords: 1, accelaStatus: 1, createdAt: 1, county: 1, client: 1, clientData: 1, openDate: 1, lastUpdateDate: 1, statusUpdated: 1, issuedDate: 1, communityName: 1, lot: 1, block: 1, jobNumber: 1, startDate: 1, history: 1, taggedUsers: 1, noc: 1, deed: 1, requests: 1, assignedTo: { $map: { input: "$assignedTo", as: "user", in: { _id: "$$user._id", pid: "$$user.pid", name: "$$user.name", avatar: "$$user.avatar", }, }, }, }, }, { $facet: { metadata: [{ $count: "count" }], data: [ { $sort: sortObj }, { $skip: (page - 1) * pageSize }, { $limit: pageSize }, ], }, }, ]); if (permitsList[0].data.length === 0) return { permits: [], metadata: { count: 0, page, pageSize } }; return { permits: permitsList[0]?.data, metadata: { count: permitsList[0].metadata[0].count, page, pageSize, }, }; } export async function updatePermit( input: CreatePermitInput, permitId: string, user: AuthenticatedUser ) { const oldPermitResult = await permitModel.findOne( { pid: permitId }, { assignedTo: 1 } ); const updatePermitResult = await permitModel .findOneAndUpdate( { $and: [{ tenantId: user.tenantId }, { pid: permitId }], }, { ...input, lastUpdateDate: new Date() }, { new: true } ) .populate({ path: "assignedTo", select: "pid name avatar" }) .populate({ path: "createdBy", select: "pid name avatar" }); if (updatePermitResult) { for (const key in input) { if (["manualStatus", "utility", "requests"].includes(key)) { let msg = ""; if (input[key] === null) { msg = `Cleared ${key}`; } else if (key == "requests") { msg = `Updated ${key} to '${input[key].join(", ")}'`; } else { msg = `Updated ${key} to '${input[key]}'`; } await createNote( { content: msg, }, permitId, "permits", user ); if (key == "requests" && input[key] != null) { const requestAlertsUsers = [ "6830d9ac46971e8148fda973", //Lucy "67c717024871a88b9ee54f55", //Ashlee "67f53b557b41964444a74095", //Khran "67db2803502dafae0d705519", //Joe ]; for (const userId of requestAlertsUsers) { await createAlert( user.tenantId, `Request updated`, "user", userId, updatePermitResult.pid, "permits", { client: updatePermitResult.client.toString(), county: updatePermitResult.county.id.toString(), address: updatePermitResult.address, } ); } } } else if (key == "client") { const orgInDb = await orgModel.findById(input.client); if (orgInDb) { updatePermitResult.clientData = { pid: orgInDb.pid, licenseNumber: orgInDb.licenseNumber, name: orgInDb.name, avatar: orgInDb.avatar, }; updatePermitResult.markModified("clientData"); await updatePermitResult.save(); } } else if (key == "assignedTo") { const newAssignees = arrayDiff( updatePermitResult.assignedTo.map((item) => item._id), oldPermitResult.assignedTo ); if (newAssignees.length == 0) continue; let msg = "Assigned to:\n\n"; for (const assignee of newAssignees) { const user = await getUser(assignee); if (!user) continue; msg += `${user.firstName + " " + user.lastName}\n`; await createAlert( user.tenantId, `You are assigned to ${updatePermitResult.permitNumber}`, "user", assignee, updatePermitResult.pid, "permits", { client: updatePermitResult.client.toString(), county: updatePermitResult.county.id.toString(), address: updatePermitResult.address.full_address, } ); } await createNote( { content: msg, }, permitId, "permits", user ); } } dbEvents.emit( "change", { tenantId: user.tenantId, type: "update", collection: "permits", //@ts-ignore orgId: updatePermitResult.client._id.toString(), document: updatePermitResult, } as ChangeEvent, ["permit:read"] ); } return updatePermitResult; } export async function deletePermit(permitId: string, tenantId: string) { const res = await permitModel.deleteOne({ $and: [{ tenantId: tenantId }, { pid: permitId }], }); dbEvents.emit( "change", { tenantId: tenantId, type: "delete", collection: "permits", document: { pid: permitId, }, } as ChangeEvent, ["permit:read"] ); return res; } export async function searchPermit( params: PageQueryParams, user: AuthenticatedUser ) { const page = params.page || 1; const pageSize = params.pageSize || 10; const sortObj = getSortObject(params, permitFields); const filterObj = getFilterObject(params) || []; if (user.role == "client") { filterObj.push({ client: { $in: user.orgId.map((item) => new mongoose.Types.ObjectId(item)), }, }); } if (user.counties && user.counties.length > 0) { filterObj.push({ "county.id": { $in: user.counties.map((item) => new mongoose.Types.ObjectId(item)), }, }); } if (!params.searchToken) return { permits: [], metadata: { count: 0, page, pageSize } }; const regex = new RegExp(params.searchToken, "i"); const permitsList = await permitModel.aggregate([ { $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, }, { $match: { $or: [ { permitNumber: { $regex: regex } }, { link: { $regex: regex } }, { "address.full_address": { $regex: regex } }, ], }, }, { $lookup: { from: "users", localField: "assignedTo", foreignField: "_id", as: "assignedTo", }, }, { $project: { _id: 1, pid: 1, permitNumber: 1, permitDate: 1, stage: 1, status: 1, manualStatus: 1, cleanStatus: 1, permitType: 1, utility: 1, link: 1, address: 1, recordType: 1, description: 1, applicationDetails: 1, applicationInfo: 1, applicationInfoTable: 1, conditions: 1, ownerDetails: 1, parcelInfo: 1, paymentData: 1, inspections: 1, newProcessingStatus: 1, newPayment: 1, newConditions: 1, professionals: 1, recordId: 1, relatedRecords: 1, accelaStatus: 1, createdAt: 1, county: 1, client: 1, clientData: 1, openDate: 1, lastUpdateDate: 1, statusUpdated: 1, issuedDate: 1, communityName: 1, lot: 1, block: 1, jobNumber: 1, startDate: 1, history: 1, taggedUsers: 1, noc: 1, deed: 1, requests: 1, assignedTo: { $map: { input: "$assignedTo", as: "user", in: { _id: "$$user._id", pid: "$$user.pid", name: "$$user.name", avatar: "$$user.avatar", }, }, }, }, }, { $facet: { metadata: [{ $count: "count" }], data: [ { $sort: sortObj }, { $skip: (page - 1) * pageSize }, { $limit: pageSize }, ], }, }, ]); if (permitsList[0].data.length === 0) return { permits: [], metadata: { count: 0, page, pageSize } }; return { permits: permitsList[0]?.data, metadata: { count: permitsList[0].metadata[0].count, page, pageSize, }, }; } export async function searchPermitByAddress(address: string) { return await permitModel .find({ $text: { $search: address } }, { score: { $meta: "textScore" } }) .sort({ score: { $meta: "textScore" } }) .limit(1); } export async function bulkImport(csvData: any[], user: AuthenticatedUser) { const allowedFields = [ "Permit Number", "County", "Client", "Address", "Open Date", "County Status", "Record Type", "Lot", "Block", "Job Number", "Community Name", ]; const failed = []; const created = []; const clientCache = {}; const countyCache = {}; // Validation run for (const [index, record] of csvData.entries()) { const errors = []; if (!record["Permit Number"]) { errors.push("Permit Number is empty"); } else if (!record["County"]) { errors.push("County is empty"); } else if (!record["Client"]) { errors.push("Client is empty"); } else if (!record["Address"]) { errors.push("Address is empty"); } if (record["County"] && record["Client"]) { let clientData = clientCache[record["Client"]]; if (!clientData) { const clientInDb = await orgModel.findOne({ name: record["Client"] }); if (clientInDb) { clientData = { id: clientInDb._id, pid: clientInDb.pid, licenseNumber: clientInDb.licenseNumber, name: clientInDb.name, avatar: clientInDb.avatar, }; clientCache[record["Client"]] = clientData; } else { errors.push("Client not found"); } } csvData[index].clientData = clientData; let countyData = countyCache[record["County"]]; if (!countyData) { const countyInDb = await orgModel.findOne({ name: record["County"] }); if (countyInDb) { countyData = { id: countyInDb._id, pid: countyInDb.pid, name: countyInDb.name, avatar: countyInDb.avatar, }; countyCache[record["County"]] = countyData; } else { errors.push("County not found"); } } csvData[index].countyData = countyData; } if (errors.length > 0) failed.push({ rowId: index + 2, errors }); } if (failed.length > 0) return { created, failed, allowedFields }; // Main run for (const [index, record] of csvData.entries()) { try { const permitInDb = await permitModel.findOne({ permitNumber: record["Permit Number"], }); if (!permitInDb) { const newPermit = await permitModel.create({ tenantId: user.tenantId, pid: generateId(), permitNumber: record["Permit Number"], county: record.countyData, client: record.clientData?.id, clientData: record.clientData, cleanStatus: record["County Status"], address: record["Address"], recordType: record["Record Type"], lot: record["Lot"], block: record["Block"], jobNumber: record["Job Number"], communityName: record["Community Name"], createdAt: new Date(), createdBy: user.userId, importFlag: true, }); const populatedPermit = await newPermit.populate({ path: "assignedTo createdBy", select: "pid name avatar", }); created.push(populatedPermit); } else { failed.push({ rowId: index + 2, errors: [ `Permit with this number: ${record["Permit Number"]} already exists`, ], }); } } catch (err) { console.log(err); failed.push({ rowId: index + 2, errors: ["Internal Error"] }); } } return { created, failed, allowedFields }; }