feat: make assignedTo field accept multiple values for all collections

This commit is contained in:
2025-11-18 13:32:47 +05:30
parent b7f63479a6
commit 8f5c0a1827
12 changed files with 202 additions and 108 deletions

View File

@@ -1,4 +1,4 @@
import mongoose from "mongoose"; import mongoose, { Schema } from "mongoose";
import { z } from "zod"; import { z } from "zod";
import { files } from "../file/file.schema"; import { files } from "../file/file.schema";
import { buildJsonSchemas } from "fastify-zod"; import { buildJsonSchemas } from "fastify-zod";
@@ -25,7 +25,7 @@ const taskSchema = new mongoose.Schema({
ref: "user", ref: "user",
}, },
assignedTo: { assignedTo: {
type: mongoose.Types.ObjectId, type: [Schema.Types.ObjectId],
ref: "user", ref: "user",
}, },
taggedUsers: Array, taggedUsers: Array,
@@ -41,7 +41,7 @@ const createTaskInput = z.object({
title: z.string(), title: z.string(),
dueDate: z.date().optional(), dueDate: z.date().optional(),
files: z.array(files).optional(), files: z.array(files).optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
labels: z.array(z.string()).optional(), labels: z.array(z.string()).optional(),
priority: z.string().optional(), priority: z.string().optional(),
stage: z stage: z
@@ -64,7 +64,7 @@ const updateTaskInput = z.object({
title: z.string().optional(), title: z.string().optional(),
dueDate: z.date().optional(), dueDate: z.date().optional(),
files: z.array(files).optional(), files: z.array(files).optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
labels: z.array(z.string()).optional(), labels: z.array(z.string()).optional(),
priority: z.string().optional(), priority: z.string().optional(),
stage: z stage: z

View File

@@ -163,12 +163,14 @@ export async function listTasks(
}, },
}, },
assignedTo: { assignedTo: {
$let: { $map: {
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } }, input: "$assignedTo",
as: "user",
in: { in: {
_id: "$$assignedTo._id", _id: "$$user._id",
pid: "$$assignedTo.pid", pid: "$$user.pid",
name: "$$assignedTo.name", name: "$$user.name",
avatar: "$$user.avatar",
}, },
}, },
}, },
@@ -272,12 +274,14 @@ export async function searchTasks(
}, },
}, },
assignedTo: { assignedTo: {
$let: { $map: {
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } }, input: "$assignedTo",
as: "user",
in: { in: {
_id: "$$assignedTo._id", _id: "$$user._id",
pid: "$$assignedTo.pid", pid: "$$user.pid",
name: "$$assignedTo.name", name: "$$user.name",
avatar: "$$user.avatar",
}, },
}, },
}, },

View File

@@ -46,7 +46,10 @@ export async function createNote(
if (!obj.taggedUsers) { if (!obj.taggedUsers) {
await model.updateOne( await model.updateOne(
{ pid: resourceId }, { pid: resourceId },
{ $set: { taggedUsers: taggedUsers, assignedTo: userIds[0] } } {
$set: { taggedUsers: taggedUsers },
$addToSet: { assignedTo: userIds[0] },
}
); );
} else { } else {
for (const user of taggedUsers) { for (const user of taggedUsers) {
@@ -57,7 +60,11 @@ export async function createNote(
else obj.taggedUsers.push(user); else obj.taggedUsers.push(user);
} }
obj.assignedTo = userIds[0]; const assignee = obj.assignedTo.find(
(item) => item.toString() == userIds[0]
);
if (!assignee) obj.assignedTo.push(userIds[0]);
obj.markModified("taggedUsers", "assignedTo"); obj.markModified("taggedUsers", "assignedTo");
await obj.save(); await obj.save();

View File

@@ -1,5 +1,5 @@
import { buildJsonSchemas } from "fastify-zod"; import { buildJsonSchemas } from "fastify-zod";
import mongoose from "mongoose"; import mongoose, { Schema } from "mongoose";
import { z } from "zod"; import { z } from "zod";
import { pageQueryParams } from "../pagination"; import { pageQueryParams } from "../pagination";
@@ -27,7 +27,7 @@ const notificationSchema = new mongoose.Schema({
createdAt: Date, createdAt: Date,
updatedAt: Date, updatedAt: Date,
assignedTo: { assignedTo: {
type: mongoose.Types.ObjectId, type: [Schema.Types.ObjectId],
ref: "user", ref: "user",
}, },
taggedUsers: Array, taggedUsers: Array,
@@ -52,12 +52,12 @@ const createNotificationInput = z.object({
county: z.object({}).passthrough(), county: z.object({}).passthrough(),
client: z.string(), client: z.string(),
clientData: z.object({}).passthrough(), clientData: z.object({}).passthrough(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
}); });
const updateNotificationInput = z.object({ const updateNotificationInput = z.object({
status: z.string().optional(), status: z.string().optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
}); });
export type CreateNotificationInput = z.infer<typeof createNotificationInput>; export type CreateNotificationInput = z.infer<typeof createNotificationInput>;

View File

@@ -17,6 +17,7 @@ import {
import { getUser } from "../user/user.service"; import { getUser } from "../user/user.service";
import { createNote } from "../note/note.service"; import { createNote } from "../note/note.service";
import { createAlert } from "../alert/alert.service"; import { createAlert } from "../alert/alert.service";
import { arrayDiff } from "../utils/diff";
export async function createNotification( export async function createNotification(
input: CreateNotificationInput, input: CreateNotificationInput,
@@ -51,6 +52,11 @@ export async function updateNotification(
input: UpdateNotificationInput, input: UpdateNotificationInput,
user: AuthenticatedUser user: AuthenticatedUser
) { ) {
const oldNotification = await notificationModel.findOne(
{ pid: notifId },
{ assignedTo: 1 }
);
const updateNotificationResult = await notificationModel const updateNotificationResult = await notificationModel
.findOneAndUpdate( .findOneAndUpdate(
{ $and: [{ tenantId: user.tenantId }, { pid: notifId }] }, { $and: [{ tenantId: user.tenantId }, { pid: notifId }] },
@@ -64,16 +70,11 @@ export async function updateNotification(
if (updateNotificationResult) { if (updateNotificationResult) {
for (const key in input) { for (const key in input) {
if (["status", "assignedTo"].includes(key)) { if (["status"].includes(key)) {
let msg = ""; let msg = "";
if (input[key] === null) { if (input[key] === null) {
msg = `Cleared ${key}`; msg = `Cleared ${key}`;
} else if (key == "assignedTo") {
const user = await getUser(input.assignedTo);
if (!user) continue;
msg = `Assigned to ${user.firstName + " " + user.lastName}`;
} else { } else {
msg = `Updated ${key} to '${input[key]}'`; msg = `Updated ${key} to '${input[key]}'`;
} }
@@ -86,23 +87,39 @@ export async function updateNotification(
"notifications", "notifications",
user user
); );
} else if (key == "assignedTo") {
const newAssignees = arrayDiff(
updateNotificationResult.assignedTo.map((item) => item._id),
oldNotification.assignedTo
);
if (newAssignees.length > 0) {
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 ${updateNotificationResult.permitNumber}`,
"user",
assignee,
updateNotificationResult.pid,
"permits"
);
}
if (key === "assignedTo") {
await createNote( await createNote(
{ content: msg }, {
updateNotificationResult.permitId, content: msg,
},
notifId,
"notifications", "notifications",
user user
); );
await createAlert(
user.tenantId,
`You are assigned to ${updateNotificationResult.permitNumber}`,
"user",
input.assignedTo,
updateNotificationResult.permitId,
"permits"
);
} }
} }
} }
@@ -190,13 +207,14 @@ export async function listNotifications(
updatedAt: 1, updatedAt: 1,
taggedUsers: 1, taggedUsers: 1,
assignedTo: { assignedTo: {
$let: { $map: {
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } }, input: "$assignedTo",
as: "user",
in: { in: {
_id: "$$assignedTo._id", _id: "$$user._id",
pid: "$$assignedTo.pid", pid: "$$user.pid",
name: "$$assignedTo.name", name: "$$user.name",
avatar: "$$assignedTo.avatar", avatar: "$$user.avatar",
}, },
}, },
}, },

View File

@@ -208,7 +208,7 @@ const permitCore = {
recordId: z.string().optional(), recordId: z.string().optional(),
relatedRecords: z relatedRecords: z
.object({ .object({
custon_id: z.string(), custom_id: z.string(),
relationship: z.string(), relationship: z.string(),
type_text: z.string(), type_text: z.string(),
}) })

View File

@@ -1,5 +1,5 @@
import z from "zod"; import z from "zod";
import mongoose from "mongoose"; import mongoose, { Schema } from "mongoose";
import { pageQueryParams } from "../pagination"; import { pageQueryParams } from "../pagination";
import { buildJsonSchemas } from "fastify-zod"; import { buildJsonSchemas } from "fastify-zod";
@@ -38,7 +38,7 @@ const processedSchema = new mongoose.Schema({
permitType: String, permitType: String,
utility: String, utility: String,
assignedTo: { assignedTo: {
type: mongoose.Types.ObjectId, type: [Schema.Types.ObjectId],
ref: "user", ref: "user",
}, },
link: String, link: String,
@@ -131,13 +131,13 @@ const updateProcessedInput = z.object({
block: z.string().nullable().optional(), block: z.string().nullable().optional(),
jobNumber: z.string().nullable().optional(), jobNumber: z.string().nullable().optional(),
startDate: z.date().nullable().optional(), startDate: z.date().nullable().optional(),
assignedTo: z.string().nullable().optional(), assignedTo: z.array(z.string()).nullable().optional(),
noc: z.string().optional(), noc: z.string().optional(),
deed: z.string().optional(), deed: z.string().optional(),
requests: z.array(z.string()).optional(), requests: z.array(z.string()).optional(),
relatedRecords: z relatedRecords: z
.object({ .object({
custon_id: z.string(), custom_id: z.string(),
relationship: z.string(), relationship: z.string(),
type_text: z.string(), type_text: z.string(),
}) })

View File

@@ -15,6 +15,7 @@ import { createNote } from "../note/note.service";
import { createAlert } from "../alert/alert.service"; import { createAlert } from "../alert/alert.service";
import { getUser } from "../user/user.service"; import { getUser } from "../user/user.service";
import { orgModel } from "../organization/organization.schema"; import { orgModel } from "../organization/organization.schema";
import { arrayDiff } from "../utils/diff";
export async function getProcessedPermit( export async function getProcessedPermit(
permitId: String, permitId: String,
@@ -38,6 +39,10 @@ export async function updateProcessed(
permitId: string, permitId: string,
user: AuthenticatedUser user: AuthenticatedUser
) { ) {
const oldPermitResult = await processedModel.findOne(
{ pid: permitId },
{ assignedTo: 1 }
);
const updateProcessedResult = await processedModel const updateProcessedResult = await processedModel
.findOneAndUpdate( .findOneAndUpdate(
{ {
@@ -52,16 +57,11 @@ export async function updateProcessed(
if (updateProcessedResult) { if (updateProcessedResult) {
for (const key in input) { for (const key in input) {
if (["manualStatus", "utility", "assignedTo"].includes(key)) { if (["manualStatus", "utility"].includes(key)) {
let msg = ""; let msg = "";
if (input[key] === null) { if (input[key] === null) {
msg = `Cleared ${key}`; msg = `Cleared ${key}`;
} else if (key == "assignedTo") {
const user = await getUser(input.assignedTo);
if (!user) continue;
msg = `Assigned to ${user.firstName + " " + user.lastName}`;
} else { } else {
msg = `Updated ${key} to '${input[key]}'`; msg = `Updated ${key} to '${input[key]}'`;
} }
@@ -74,17 +74,6 @@ export async function updateProcessed(
"processed", "processed",
user user
); );
if (key == "assignedTo" && input[key] != null) {
await createAlert(
user.tenantId,
`You are assigned to ${updateProcessedResult.permitNumber}`,
"user",
input.assignedTo,
updateProcessedResult.pid,
"processed"
);
}
} else if (key == "client") { } else if (key == "client") {
const orgInDb = await orgModel.findById(input.client); const orgInDb = await orgModel.findById(input.client);
if (orgInDb) { if (orgInDb) {
@@ -98,6 +87,40 @@ export async function updateProcessed(
updateProcessedResult.markModified("clientData"); updateProcessedResult.markModified("clientData");
await updateProcessedResult.save(); await updateProcessedResult.save();
} }
} else if (key == "assignedTo") {
const newAssignees = arrayDiff(
updateProcessedResult.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 ${updateProcessedResult.permitNumber}`,
"user",
assignee,
updateProcessedResult.pid,
"processed"
);
}
await createNote(
{
content: msg,
},
permitId,
"processed",
user
);
} }
} }
@@ -152,7 +175,7 @@ export async function listProcessedPermits(
from: "users", from: "users",
localField: "assignedTo", localField: "assignedTo",
foreignField: "_id", foreignField: "_id",
as: "assignedRec", as: "assignedTo",
}, },
}, },
{ {
@@ -205,13 +228,14 @@ export async function listProcessedPermits(
deed: 1, deed: 1,
requests: 1, requests: 1,
assignedTo: { assignedTo: {
$let: { $map: {
vars: { assigned: { $arrayElemAt: ["$assignedRec", 0] } }, input: "$assignedTo",
as: "user",
in: { in: {
_id: "$$assigned._id", _id: "$$user._id",
pid: "$$assigned.pid", pid: "$$user.pid",
name: "$$assigned.name", name: "$$user.name",
avatar: "$$assigned.avatar", avatar: "$$user.avatar",
}, },
}, },
}, },

View File

@@ -1,5 +1,5 @@
import { buildJsonSchemas } from "fastify-zod"; import { buildJsonSchemas } from "fastify-zod";
import mongoose from "mongoose"; import mongoose, { Schema } from "mongoose";
import z from "zod"; import z from "zod";
import { files } from "../file/file.schema"; import { files } from "../file/file.schema";
import { pageQueryParams } from "../pagination"; import { pageQueryParams } from "../pagination";
@@ -51,7 +51,7 @@ const rtsSchema = new mongoose.Schema({
ref: "user", ref: "user",
}, },
assignedTo: { assignedTo: {
type: mongoose.Types.ObjectId, type: [Schema.Types.ObjectId],
ref: "user", ref: "user",
}, },
taggedUsers: Array, taggedUsers: Array,
@@ -85,7 +85,7 @@ const rtsCreateInput = z.object({
currentStage: z.number(), currentStage: z.number(),
}) })
.optional(), .optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
status: z.string().optional(), status: z.string().optional(),
}); });
@@ -109,7 +109,7 @@ const rtsUpdateInput = z.object({
currentStage: z.number(), currentStage: z.number(),
}) })
.optional(), .optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
status: z.string().optional(), status: z.string().optional(),
fileValidationStatus: z.string().optional(), fileValidationStatus: z.string().optional(),
}); });

View File

@@ -13,11 +13,12 @@ import {
getTaggedUsersFilter, getTaggedUsersFilter,
PageQueryParams, PageQueryParams,
} from "../pagination"; } from "../pagination";
import { getUserWithoutPopulate } from "../user/user.service"; import { getUser, getUserWithoutPopulate } from "../user/user.service";
import mongoose from "mongoose"; import mongoose from "mongoose";
import { rtsPipeline } from "../utils/pipeline"; import { rtsPipeline } from "../utils/pipeline";
import { createAlert } from "../alert/alert.service"; import { createAlert } from "../alert/alert.service";
import { createNote } from "../note/note.service"; import { createNote } from "../note/note.service";
import { arrayDiff } from "../utils/diff";
export async function createRts( export async function createRts(
input: CreateRtsInput, input: CreateRtsInput,
@@ -126,7 +127,7 @@ export async function listRts(
from: "users", from: "users",
localField: "assignedTo", localField: "assignedTo",
foreignField: "_id", foreignField: "_id",
as: "assignedToRec", as: "assignedTo",
}, },
}, },
{ {
@@ -180,13 +181,14 @@ export async function listRts(
}, },
}, },
assignedTo: { assignedTo: {
$let: { $map: {
vars: { assignedTo: { $arrayElemAt: ["$assignedToRec", 0] } }, input: "$assignedTo",
as: "user",
in: { in: {
_id: "$$assignedTo._id", _id: "$$user._id",
pid: "$$assignedTo.pid", pid: "$$user.pid",
name: "$$assignedTo.name", name: "$$user.name",
avatar: "$$assignedTo.avatar", avatar: "$$user.avatar",
}, },
}, },
}, },
@@ -222,6 +224,7 @@ export async function updateRts(
input: UpdateRtsInput, input: UpdateRtsInput,
user: AuthenticatedUser user: AuthenticatedUser
) { ) {
const oldRts = await rtsModel.findOne({ pid: id }, { assignedTo: 1 });
const updatedRts = await rtsModel const updatedRts = await rtsModel
.findOneAndUpdate({ pid: id, tenantId: user.tenantId }, input, { .findOneAndUpdate({ pid: id, tenantId: user.tenantId }, input, {
new: true, new: true,
@@ -241,14 +244,39 @@ export async function updateRts(
} }
if (updatedRts && input.assignedTo) { if (updatedRts && input.assignedTo) {
await createAlert( const newAssignees = arrayDiff(
user.tenantId, updatedRts.assignedTo.map((item) => item._id),
`You are assigned to RTS`, oldRts.assignedTo
"user",
input.assignedTo,
updatedRts.pid,
"rts"
); );
if (newAssignees.length > 0) {
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 RTS`,
"user",
assignee,
updatedRts.pid,
"rts"
);
}
await createNote(
{
content: msg,
},
id,
"rts",
user
);
}
} }
return updatedRts; return updatedRts;

View File

@@ -1,4 +1,4 @@
import mongoose from "mongoose"; import mongoose, { Schema } from "mongoose";
import { z } from "zod"; import { z } from "zod";
import { files } from "../file/file.schema"; import { files } from "../file/file.schema";
import { buildJsonSchemas } from "fastify-zod"; import { buildJsonSchemas } from "fastify-zod";
@@ -37,7 +37,7 @@ const taskSchema = new mongoose.Schema({
ref: "user", ref: "user",
}, },
assignedTo: { assignedTo: {
type: mongoose.Types.ObjectId, type: [Schema.Types.ObjectId],
ref: "user", ref: "user",
}, },
taggedUsers: Array, taggedUsers: Array,
@@ -53,7 +53,7 @@ const createTaskInput = z.object({
title: z.string(), title: z.string(),
dueDate: z.date().optional(), dueDate: z.date().optional(),
files: z.array(files).optional(), files: z.array(files).optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
labels: z.array(z.string()).optional(), labels: z.array(z.string()).optional(),
priority: z.string().optional(), priority: z.string().optional(),
stage: z stage: z
@@ -76,7 +76,7 @@ const updateTaskInput = z.object({
title: z.string().optional(), title: z.string().optional(),
dueDate: z.date().optional(), dueDate: z.date().optional(),
files: z.array(files).optional(), files: z.array(files).optional(),
assignedTo: z.string().optional(), assignedTo: z.array(z.string()).optional(),
labels: z.array(z.string()).optional(), labels: z.array(z.string()).optional(),
priority: z.string().optional(), priority: z.string().optional(),
stage: z stage: z

View File

@@ -7,6 +7,7 @@ import {
getTaggedUsersFilter, getTaggedUsersFilter,
PageQueryParams, PageQueryParams,
} from "../pagination"; } from "../pagination";
import { arrayDiff } from "../utils/diff";
import { generateId } from "../utils/id"; import { generateId } from "../utils/id";
import { taskPipeline } from "../utils/pipeline"; import { taskPipeline } from "../utils/pipeline";
import { import {
@@ -58,6 +59,7 @@ export async function updateTask(
input: UpdateTaskInput, input: UpdateTaskInput,
user: AuthenticatedUser user: AuthenticatedUser
) { ) {
const oldTask = await taskModel.findOne({ pid: taskId }, { assignedTo: 1 });
const updatedTask = await taskModel const updatedTask = await taskModel
.findOneAndUpdate({ tenantId: user.tenantId, pid: taskId }, input, { .findOneAndUpdate({ tenantId: user.tenantId, pid: taskId }, input, {
new: true, new: true,
@@ -66,14 +68,23 @@ export async function updateTask(
.populate({ path: "assignedTo", select: "pid name avatar" }); .populate({ path: "assignedTo", select: "pid name avatar" });
if (updatedTask && input.assignedTo) { if (updatedTask && input.assignedTo) {
await createAlert( const newAssignees = arrayDiff(
user.tenantId, updatedTask.assignedTo.map((item) => item._id),
`You are assigned to ${updatedTask.title}`, oldTask.assignedTo
"user",
input.assignedTo,
updatedTask.pid,
"tasks"
); );
if (newAssignees.length > 0) {
for (const assignee of newAssignees) {
await createAlert(
user.tenantId,
`You are assigned to ${updatedTask.title}`,
"user",
assignee,
updatedTask.pid,
"tasks"
);
}
}
} }
return updatedTask; return updatedTask;
@@ -146,12 +157,14 @@ export async function listTasks(
}, },
}, },
assignedTo: { assignedTo: {
$let: { $map: {
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } }, input: "$assignedTo",
as: "user",
in: { in: {
_id: "$$assignedTo._id", _id: "$$user._id",
pid: "$$assignedTo.pid", pid: "$$user.pid",
name: "$$assignedTo.name", name: "$$user.name",
avatar: "$$user.avatar",
}, },
}, },
}, },