699 lines
17 KiB
TypeScript
699 lines
17 KiB
TypeScript
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 };
|
|
}
|