feat: add taggedOrgs feature

This commit is contained in:
2026-01-26 11:16:30 +05:30
parent 9e3f0f8b07
commit ccde4f8356
15 changed files with 237 additions and 112 deletions

View File

@@ -16,6 +16,7 @@ export const alertsModel = mongoose.model(
county: { type: String, ref: "organization" },
permitType: String,
address: String,
createdBy: { type: String, ref: "user" },
},
recipientType: {
type: String,
@@ -28,7 +29,7 @@ export const alertsModel = mongoose.model(
type: [String],
default: [],
},
})
}),
);
const alertResponse = z.object({
@@ -43,6 +44,7 @@ const alertResponse = z.object({
county: z.any().optional(),
permitType: z.string().optional(),
address: z.string().optional(),
createdBy: z.any().optional(),
})
.optional(),
recipientType: z.enum(["user", "team"]),
@@ -64,5 +66,5 @@ export const { schemas: alertSchemas, $ref: $alert } = buildJsonSchemas(
listAlertResponse,
pageQueryParams,
},
{ $id: "alert" }
{ $id: "alert" },
);

View File

@@ -16,7 +16,8 @@ export async function createAlert(
county?: String;
permitType?: String;
address?: String;
}
createdBy?: String;
},
) {
const newAlert = await alertsModel.create({
tenantId,
@@ -44,13 +45,13 @@ export async function createAlert(
createdAt: new Date(),
},
},
["alert:read"]
["alert:read"],
);
}
export async function getUserAlerts(
user: AuthenticatedUser,
params: PageQueryParams
params: PageQueryParams,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
@@ -71,7 +72,8 @@ export async function getUserAlerts(
.limit(pageSize)
.skip((page - 1) * pageSize)
.populate({ path: "metaFields.client", select: "pid name avatar" })
.populate({ path: "metaFields.county", select: "pid name avatar" });
.populate({ path: "metaFields.county", select: "pid name avatar" })
.populate({ path: "metaFields.createdBy", select: "pid name avatar" });
const modifiedAlerts = alerts.map((alert) => {
return {
@@ -89,7 +91,7 @@ export async function markAsRead(alertId: string, user: AuthenticatedUser) {
const updatedAlert = await alertsModel.findOneAndUpdate(
{ tenantId: user.tenantId, pid: alertId },
{ $addToSet: { readBy: user.userId } },
{ new: true }
{ new: true },
);
if (!updatedAlert) return null;
@@ -116,7 +118,7 @@ export async function markAllRead(user: AuthenticatedUser) {
const updatedResult = await alertsModel.updateMany(
{ $or: filters },
{ $addToSet: { readBy: user.userId } }
{ $addToSet: { readBy: user.userId } },
);
return updatedResult;

View File

@@ -8,17 +8,10 @@ export async function createNote(
input: CreateNoteInput,
resourceId: string,
resourceType: string,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const userIds = extractExpressions(input.content);
const taggedUsers = userIds.map((item) => {
return {
userId: item,
taggedAt: new Date(),
};
});
const newNote = await noteModel.create({
tenantId: user.tenantId,
pid: generateId(),
@@ -43,47 +36,89 @@ export async function createNote(
const model = modelMap[resourceType];
const obj = await model.findOne({ pid: resourceId });
if (!obj.taggedUsers) {
await model.updateOne(
{ pid: resourceId },
{
$set: { taggedUsers: taggedUsers },
$addToSet: { assignedTo: userIds[0] },
}
const orgs = [];
userIds.forEach((item) => {
if (item == "client" && obj.client)
orgs.push({ orgId: obj.client.toString(), taggedAt: new Date() });
if (item == "agent")
orgs.push({ orgId: process.env.SUNCOAST_ID, taggedAt: new Date() });
});
const taggedUsers = userIds
.filter((item) => !["client", "agent"].includes(item))
.map((item) => {
return {
userId: item,
taggedAt: new Date(),
};
});
for (const org of orgs) {
if (!obj.taggedOrgs) obj.taggedOrgs = [];
const orgIndex = obj.taggedOrgs.findIndex(
(item) => item.orgId == org.orgId,
);
} else {
if (orgIndex != -1) obj.taggedOrgs[orgIndex].taggedAt = new Date();
else obj.taggedOrgs.push(org);
}
for (const user of taggedUsers) {
if (!obj.taggedUsers) obj.taggedUsers = [];
const userIndex = obj.taggedUsers.findIndex(
(item) => item.userId == user.userId
(item) => item.userId == user.userId,
);
if (userIndex != -1) obj.taggedUsers[userIndex].taggedAt = new Date();
else obj.taggedUsers.push(user);
}
if (taggedUsers.length > 0) {
const assignee = obj.assignedTo.find(
(item) => item.toString() == userIds[0]
(item) => item.toString() == taggedUsers[0].userId,
);
if (!assignee) obj.assignedTo.push(userIds[0]);
obj.markModified("taggedUsers", "assignedTo");
await obj.save();
if (!assignee) obj.assignedTo.push(taggedUsers[0].userId);
}
for (const id of userIds) {
if (id == user.userId) continue;
obj.markModified("taggedUsers", "assignedTo", "taggedOrgs");
await obj.save();
if (taggedUsers.length > 0) {
for (const taggedUser of taggedUsers) {
if (taggedUser.userId == user.userId) continue;
await createAlert(
user.tenantId,
"You are tagged in a note",
"user",
id,
taggedUser.userId,
resourceId,
resourceType
resourceType,
{
createdBy: user.userId,
},
);
}
}
if (orgs.length > 0) {
for (const org of orgs) {
await createAlert(
user.tenantId,
`Your organization is tagged in a note`,
"team",
org.orgId,
resourceId,
resourceType,
{
createdBy: user.userId,
},
);
}
}
}
return newNote.populate({ path: "createdBy", select: "pid name avatar" });
}
@@ -91,7 +126,7 @@ export async function updateNote(
input: CreateNoteInput,
resourceId: string,
noteId: string,
tenantId: string
tenantId: string,
) {
return await noteModel
.findOneAndUpdate(
@@ -103,7 +138,7 @@ export async function updateNote(
],
},
{ ...input },
{ new: true }
{ new: true },
)
.populate({ path: "createdBy", select: "pid name avatar" });
}
@@ -147,7 +182,7 @@ export async function listNotes(resourceId: string, tenantId: string) {
export async function deleteNote(
resourceId: string,
noteId: string,
tenantId: string
tenantId: string,
) {
return await noteModel.deleteOne({
$and: [{ pid: noteId }, { tenantId: tenantId }, { resourceId: resourceId }],

View File

@@ -32,15 +32,16 @@ const notificationSchema = new mongoose.Schema({
},
assignedToOrg: String,
taggedUsers: Array,
taggedOrgs: Array,
});
export const notificationFields = Object.keys(notificationSchema.paths).filter(
(path) => path !== "__v"
(path) => path !== "__v",
);
export const notificationModel = mongoose.model(
"notification",
notificationSchema
notificationSchema,
);
const createNotificationInput = z.object({
@@ -73,5 +74,5 @@ export const { schemas: notificationSchemas, $ref: $notification } =
updateNotificationInput,
pageQueryParams,
},
{ $id: "notification" }
{ $id: "notification" },
);

View File

@@ -4,6 +4,7 @@ import { orgModel } from "../organization/organization.schema";
import {
getFilterObject,
getSortObject,
getTaggedOrgsFilter,
getTaggedUsersFilter,
PageQueryParams,
} from "../pagination";
@@ -56,11 +57,7 @@ export async function updateNotification(
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
delete input.assignedTo;
}
const oldNotification = await notificationModel.findOne(
@@ -208,12 +205,18 @@ export async function listNotifications(
});
}
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
let { taggedUsersFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj,
);
let { taggedOrgsFilter, taggedOrgsFilterIndex } = getTaggedOrgsFilter(
filterObj,
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
if (taggedOrgsFilterIndex != -1) filterObj.splice(taggedOrgsFilterIndex, 1);
const pipeline: any = [
{
@@ -232,7 +235,8 @@ export async function listNotifications(
pipeline.push(
...[
...taggedFilter,
...taggedUsersFilter,
...taggedOrgsFilter,
{
$lookup: {
from: "users",
@@ -259,6 +263,7 @@ export async function listNotifications(
createdAt: 1,
updatedAt: 1,
taggedUsers: 1,
taggedOrgs: 1,
assignedToOrg: 1,
assignedTo: {
$map: {

View File

@@ -19,7 +19,7 @@ export type PageQueryParams = z.infer<typeof pageQueryParams>;
export function getSortObject(
params: PageQueryParams,
validFields: Array<string>
validFields: Array<string>,
) {
const sortObj: Record<string, 1 | -1> = {};
@@ -41,7 +41,7 @@ export function getSortObject(
export function getFilterObject(params: PageQueryParams) {
if (params.filter && params.filter != "") {
const parsedQuery = parse(params.filter.split("|")).filter(
(query) => Object.keys(query).length > 0
(query) => Object.keys(query).length > 0,
);
return parsedQuery;
}
@@ -49,13 +49,13 @@ export function getFilterObject(params: PageQueryParams) {
export function getTaggedUsersFilter(
filterObj: MongoFilter[],
sortObj: Record<string, 1 | -1>
sortObj: Record<string, 1 | -1>,
) {
const taggedUserFilterIndex = filterObj.findIndex((item) =>
Object.keys(item).includes("taggedUsers")
Object.keys(item).includes("taggedUsers"),
);
let taggedFilter = [];
let taggedUsersFilter = [];
if (taggedUserFilterIndex != -1) {
const filterItem = filterObj[taggedUserFilterIndex].taggedUsers;
let filterValues = null;
@@ -75,7 +75,7 @@ export function getTaggedUsersFilter(
const condition = notFlag ? { $exists: false } : { $exists: true };
taggedFilter = [
taggedUsersFilter = [
{
$addFields: {
taggedUsersFilter: {
@@ -92,5 +92,53 @@ export function getTaggedUsersFilter(
];
}
return { taggedFilter, taggedUserFilterIndex };
return { taggedUsersFilter, taggedUserFilterIndex };
}
export function getTaggedOrgsFilter(
filterObj: MongoFilter[],
sortObj: Record<string, 1 | -1>,
) {
const taggedOrgsFilterIndex = filterObj.findIndex((item) =>
Object.keys(item).includes("taggedOrgs"),
);
let taggedOrgsFilter = [];
if (taggedOrgsFilterIndex != -1) {
const filterItem = filterObj[taggedOrgsFilterIndex].taggedOrgs;
let filterValues = null;
let notFlag = false;
if (filterItem["$eq"]) {
filterValues = [filterItem["$eq"]];
} else if (filterItem["$ne"]) {
filterValues = [filterItem["$ne"]];
notFlag = true;
} else if (filterItem["$in"]) {
filterValues = filterItem["$in"];
} else {
filterValues = filterItem["$nin"];
notFlag = true;
}
const condition = notFlag ? { $exists: false } : { $exists: true };
taggedOrgsFilter = [
{
$addFields: {
taggedOrgsFilter: {
$filter: {
input: "$taggedOrgs",
as: "org",
cond: { $in: ["$$org.orgId", filterValues] },
},
},
},
},
{ $match: { "taggedOrgsFilter.0": condition } },
{ $sort: { "taggedOrgsFilter.taggedAt": sortObj.taggedOrgs ?? -1 } },
];
}
return { taggedOrgsFilter, taggedOrgsFilterIndex };
}

View File

@@ -30,7 +30,7 @@ const permitSchema = new mongoose.Schema({
pipeline: Array,
currentStage: Number,
},
{ _id: false }
{ _id: false },
),
status: String,
manualStatus: String,
@@ -103,6 +103,7 @@ const permitSchema = new mongoose.Schema({
startDate: Date,
history: Array,
taggedUsers: Array,
taggedOrgs: Array,
noc: String,
deed: String,
requests: [String],
@@ -118,7 +119,7 @@ permitSchema.index({ tenantId: 1, permitNumber: 1 }, { unique: true });
permitSchema.index({ "address.full_address": "text" });
export const permitFields = Object.keys(permitSchema.paths).filter(
(path) => path !== "__v"
(path) => path !== "__v",
);
export const permitModel = mongoose.model("permit", permitSchema);
@@ -150,7 +151,7 @@ const permitCore = {
date: z.date().nullable().optional(),
description: z.string().optional(),
comment: z.string().optional(),
})
}),
),
currentStage: z.number(),
})
@@ -182,7 +183,7 @@ const permitCore = {
due_date: z.date().optional(),
is_completed: z.string().optional(),
comment: z.string().optional(),
})
}),
)
.optional(),
newPayment: z
@@ -194,7 +195,7 @@ const permitCore = {
balance_due: z.number().optional(),
code_text: z.string().optional(),
status: z.string().optional(),
})
}),
)
.optional(),
newConditions: z
@@ -204,7 +205,7 @@ const permitCore = {
status_value: z.string().optional(),
short_comments: z.string().optional(),
name: z.string().optional(),
})
}),
)
.optional(),
professionals: z.record(z.any()).optional(),
@@ -297,5 +298,5 @@ export const { schemas: permitSchemas, $ref: $permit } = buildJsonSchemas(
updatePermitInput,
pageQueryParams,
},
{ $id: "permit" }
{ $id: "permit" },
);

View File

@@ -1,6 +1,7 @@
import {
getFilterObject,
getSortObject,
getTaggedOrgsFilter,
getTaggedUsersFilter,
PageQueryParams,
} from "../pagination";
@@ -153,18 +154,25 @@ export async function listPermits(
});
}
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
let { taggedUsersFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj,
);
let { taggedOrgsFilter, taggedOrgsFilterIndex } = getTaggedOrgsFilter(
filterObj,
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
if (taggedOrgsFilterIndex != -1) filterObj.splice(taggedOrgsFilterIndex, 1);
const permitsList = await permitModel.aggregate([
{
$match: { $and: [{ tenantId: user.tenantId }, ...filterObj] },
},
...taggedFilter,
...taggedUsersFilter,
...taggedOrgsFilter,
{
$lookup: {
from: "users",
@@ -219,6 +227,7 @@ export async function listPermits(
startDate: 1,
history: 1,
taggedUsers: 1,
taggedOrgs: 1,
noc: 1,
deed: 1,
requests: 1,
@@ -268,11 +277,7 @@ export async function updatePermit(
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
delete input.assignedTo;
}
const oldPermitResult = await permitModel.findOne(
@@ -562,6 +567,7 @@ export async function searchPermit(
startDate: 1,
history: 1,
taggedUsers: 1,
taggedOrgs: 1,
noc: 1,
deed: 1,
requests: 1,

View File

@@ -103,6 +103,7 @@ const processedSchema = new mongoose.Schema({
startDate: Date,
history: Array,
taggedUsers: Array,
taggedOrgs: Array,
noc: String,
deed: String,
requests: [String],

View File

@@ -3,6 +3,7 @@ import { AuthenticatedUser } from "../auth";
import {
getFilterObject,
getSortObject,
getTaggedOrgsFilter,
getTaggedUsersFilter,
PageQueryParams,
} from "../pagination";
@@ -44,11 +45,7 @@ export async function updateProcessed(
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
delete input.assignedTo;
}
const oldPermitResult = await processedModel.findOne(
@@ -205,12 +202,18 @@ export async function listProcessedPermits(
});
}
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
let { taggedUsersFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj,
);
let { taggedOrgsFilter, taggedOrgsFilterIndex } = getTaggedOrgsFilter(
filterObj,
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
if (taggedOrgsFilterIndex != -1) filterObj.splice(taggedOrgsFilterIndex, 1);
const pipeline: any = [
{
@@ -233,7 +236,8 @@ export async function listProcessedPermits(
pipeline.push(
...[
...taggedFilter,
...taggedUsersFilter,
...taggedOrgsFilter,
{
$lookup: {
from: "users",
@@ -288,6 +292,7 @@ export async function listProcessedPermits(
block: 1,
jobNumber: 1,
taggedUsers: 1,
taggedOrgs: 1,
noc: 1,
deed: 1,
requests: 1,

View File

@@ -56,6 +56,7 @@ const rtsSchema = new mongoose.Schema({
},
assignedToOrg: String,
taggedUsers: Array,
taggedOrgs: Array,
fileValidationStatus: String,
permitNumber: [String],
lot: [String],

View File

@@ -10,6 +10,7 @@ import { generateId } from "../utils/id";
import {
getFilterObject,
getSortObject,
getTaggedOrgsFilter,
getTaggedUsersFilter,
PageQueryParams,
} from "../pagination";
@@ -99,18 +100,25 @@ export async function listRts(
});
}
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
let { taggedUsersFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj,
);
let { taggedOrgsFilter, taggedOrgsFilterIndex } = getTaggedOrgsFilter(
filterObj,
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
if (taggedOrgsFilterIndex != -1) filterObj.splice(taggedOrgsFilterIndex, 1);
const rtsList = await rtsModel.aggregate([
{
$match: { $and: [{ tenantId: user.tenantId }, ...filterObj] },
},
...taggedFilter,
...taggedUsersFilter,
...taggedOrgsFilter,
{
$lookup: {
from: "organizations",
@@ -241,11 +249,7 @@ export async function updateRts(
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
delete input.assignedTo;
}
const oldRts = await rtsModel.findOne(

View File

@@ -4,6 +4,7 @@ import { createNote } from "../note/note.service";
import {
getFilterObject,
getSortObject,
getTaggedOrgsFilter,
getTaggedUsersFilter,
PageQueryParams,
} from "../pagination";
@@ -20,7 +21,7 @@ import {
export async function createTask(
input: CreateTaskInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
if (!input.stage) {
input.stage = {
@@ -44,7 +45,7 @@ export async function createTask(
},
task.pid,
"tasks",
user
user,
);
}
@@ -57,7 +58,7 @@ export async function createTask(
export async function updateTask(
taskId: string,
input: UpdateTaskInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const oldTask = await taskModel.findOne({ pid: taskId }, { assignedTo: 1 });
const updatedTask = await taskModel
@@ -70,7 +71,7 @@ export async function updateTask(
if (updatedTask && input.assignedTo) {
const newAssignees = arrayDiff(
updatedTask.assignedTo.map((item) => item._id),
oldTask.assignedTo
oldTask.assignedTo,
);
if (newAssignees.length > 0) {
@@ -81,7 +82,7 @@ export async function updateTask(
"user",
assignee,
updatedTask.pid,
"tasks"
"tasks",
);
}
}
@@ -99,25 +100,32 @@ export async function getTask(taskId: string, tenantId: string) {
export async function listTasks(
params: PageQueryParams,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const sortObj = getSortObject(params, taskFields);
let filterObj = getFilterObject(params) || [];
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
let { taggedUsersFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj
sortObj,
);
let { taggedOrgsFilter, taggedOrgsFilterIndex } = getTaggedOrgsFilter(
filterObj,
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
if (taggedOrgsFilterIndex != -1) filterObj.splice(taggedOrgsFilterIndex, 1);
const taskList = await taskModel.aggregate([
{
$match: { $and: [{ tenantId: user.tenantId }, ...filterObj] },
},
...taggedFilter,
...taggedUsersFilter,
...taggedOrgsFilter,
{
$lookup: {
from: "users",
@@ -146,6 +154,7 @@ export async function listTasks(
stage: 1,
createdAt: 1,
taggedUsers: 1,
taggedOrgs: 1,
createdBy: {
$let: {
vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } },
@@ -244,6 +253,7 @@ export async function searchTasks(params: PageQueryParams, tenantId: string) {
stage: 1,
createdAt: 1,
taggedUsers: 1,
taggedOrgs: 1,
createdBy: {
$let: {
vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } },
@@ -296,7 +306,7 @@ export async function searchTasks(params: PageQueryParams, tenantId: string) {
export async function newUpload(
id: string,
newUpload: UploadTaskInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
return await taskModel.findOneAndUpdate(
{ pid: id, tenantId: user.tenantId },
@@ -308,6 +318,6 @@ export async function newUpload(
createdBy: user.userId,
},
},
}
},
);
}

View File

@@ -2,7 +2,7 @@ import mongoose from "mongoose";
export type MongoFilter = Record<string, any>;
const ignoreObjectIdConversion = ["taggedUsers"];
const ignoreObjectIdConversion = ["taggedUsers", "taggedOrgs"];
const ignoreNumberConversion = ["jobNumber"];
const ignoreDateConversion = ["permitNumber"];
@@ -46,7 +46,7 @@ function formulaToMongoFilter(formula: string): MongoFilter {
// Convert each value to appropriate type
const parsedValues = valueArray.map((value) =>
convertValue(trimmedField, value)
convertValue(trimmedField, value),
);
// If no operator or equals operator, use $in
@@ -98,7 +98,7 @@ function formulaToMongoFilter(formula: string): MongoFilter {
export function parse(formulas: string[]): Array<MongoFilter> {
const parsedQuery: Array<MongoFilter> = formulas.map((formula) =>
formulaToMongoFilter(formula)
formulaToMongoFilter(formula),
);
return parsedQuery;
}

View File

@@ -12,7 +12,11 @@ import { paymentModel } from "../payments/payment.schema";
export function extractExpressions(input: string) {
return [...input.matchAll(/{{(.*?)}}/g)]
.map((match) => match[1].trim())
.filter((item) => mongoose.Types.ObjectId.isValid(item));
.filter(
(item) =>
mongoose.Types.ObjectId.isValid(item) ||
["client", "team"].includes(item),
);
}
export const modelMap = {