feat: add team alerts

This commit is contained in:
2026-01-19 11:56:32 +05:30
parent 46c07e23ad
commit c10b3629fc
9 changed files with 226 additions and 70 deletions

View File

@@ -57,14 +57,11 @@ export async function getUserAlerts(
const filters: Array<object> = [
{ recipientType: "user", recipientId: user.userId },
];
if (user.role == "client")
filters.push({
{
recipientType: "team",
recipientId: { $in: [...user.orgId] },
});
else filters.push({ recipientType: "team" });
},
];
const alerts = await alertsModel
.find({

View File

@@ -30,6 +30,7 @@ const notificationSchema = new mongoose.Schema({
type: [Schema.Types.ObjectId],
ref: "user",
},
assignedToOrg: String,
taggedUsers: Array,
});
@@ -53,11 +54,13 @@ const createNotificationInput = z.object({
client: z.string(),
clientData: z.object({}).passthrough(),
assignedTo: z.array(z.string()).optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
});
const updateNotificationInput = z.object({
status: z.string().optional(),
assignedTo: z.array(z.string()).optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
});
export type CreateNotificationInput = z.infer<typeof createNotificationInput>;

View File

@@ -21,7 +21,7 @@ import { arrayDiff } from "../utils/diff";
export async function createNotification(
input: CreateNotificationInput,
tenantId: string
tenantId: string,
) {
const notification = await notificationModel.create({
...input,
@@ -30,6 +30,9 @@ export async function createNotification(
createdAt: new Date(),
});
if (input.assignedToOrg) {
}
return await notificationModel
.findOne({ pid: notification.pid })
.populate({ path: "assignedTo", select: "pid name avatar" });
@@ -37,7 +40,7 @@ export async function createNotification(
export async function getNotification(
notifId: string,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
return await notificationModel
.findOne({
@@ -50,11 +53,19 @@ export async function getNotification(
export async function updateNotification(
notifId: string,
input: UpdateNotificationInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
}
const oldNotification = await notificationModel.findOne(
{ pid: notifId },
{ assignedTo: 1 }
{ assignedTo: 1, assignedToOrg: 1 },
);
const updateNotificationResult = await notificationModel
@@ -64,7 +75,7 @@ export async function updateNotification(
...input,
updatedAt: new Date(),
},
{ new: true }
{ new: true },
)
.populate({ path: "assignedTo", select: "pid name avatar" });
@@ -85,12 +96,12 @@ export async function updateNotification(
},
notifId,
"notifications",
user
user,
);
} else if (key == "assignedTo") {
const newAssignees = arrayDiff(
updateNotificationResult.assignedTo.map((item) => item._id),
oldNotification.assignedTo
oldNotification.assignedTo,
);
if (newAssignees.length > 0) {
@@ -112,7 +123,7 @@ export async function updateNotification(
{
client: updateNotificationResult.client.toString(),
county: updateNotificationResult.county.id.toString(),
}
},
);
}
@@ -122,9 +133,35 @@ export async function updateNotification(
},
notifId,
"notifications",
user
user,
);
}
} else if (key == "assignedToOrg") {
if (
oldNotification.assignedToOrg ==
updateNotificationResult.assignedToOrg
)
continue;
const orgName =
input.assignedToOrg == "agent"
? "Suncoast"
: updateNotificationResult.clientData.name;
await createAlert(
user.tenantId,
`${orgName} assigned to ${updateNotificationResult.permitNumber}`,
"team",
input.assignedToOrg == "client"
? updateNotificationResult.client.toString()
: process.env.SUNCOAST_ID,
updateNotificationResult.pid,
"permits",
{
client: updateNotificationResult.client.toString(),
county: updateNotificationResult.county.id.toString(),
},
);
}
}
}
@@ -134,7 +171,7 @@ export async function updateNotification(
export async function listNotifications(
params: PageQueryParams,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
@@ -173,7 +210,7 @@ export async function listNotifications(
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
@@ -246,7 +283,7 @@ export async function listNotifications(
],
},
},
]
],
);
const notifications = await notificationModel.aggregate(pipeline);

View File

@@ -41,6 +41,7 @@ const permitSchema = new mongoose.Schema({
type: [Schema.Types.ObjectId],
ref: "user",
},
assignedToOrg: String,
link: String,
address: Object,
recordType: String,
@@ -160,6 +161,7 @@ const permitCore = {
permitType: z.string().optional(),
utility: z.string().nullable().optional(),
assignedTo: z.array(z.string()).nullable().optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
link: z.string().optional(),
address: z
.object({
@@ -272,6 +274,7 @@ const updatePermitInput = z.object({
manualStatus: z.string().nullable().optional(),
utility: z.string().nullable().optional(),
assignedTo: z.string().nullable().optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
newPayment: z.array(z.any()).optional(),
communityName: z.string().nullable().optional(),
lot: z.string().nullable().optional(),

View File

@@ -24,7 +24,7 @@ import { arrayDiff } from "../utils/diff";
export async function createPermit(
input: CreatePermitInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
if (!input.stage) {
input.stage = {
@@ -63,7 +63,7 @@ export async function createPermit(
orgId: permit.client.toString(),
document: permit,
} as ChangeEvent,
["permit:read"]
["permit:read"],
);
return await permit.populate({
@@ -88,7 +88,7 @@ export async function createPermit(
orgId: permit.client.toString(),
document: permit,
} as ChangeEvent,
["permit:read"]
["permit:read"],
);
return await permit.populate({
@@ -130,7 +130,7 @@ export async function getPermit(permitId: string, user: AuthenticatedUser) {
export async function listPermits(
params: PageQueryParams,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
@@ -155,7 +155,7 @@ export async function listPermits(
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
@@ -264,11 +264,19 @@ export async function listPermits(
export async function updatePermit(
input: CreatePermitInput,
permitId: string,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
}
const oldPermitResult = await permitModel.findOne(
{ pid: permitId },
{ assignedTo: 1 }
{ assignedTo: 1, assignedToOrg: 1 },
);
const updatePermitResult = await permitModel
.findOneAndUpdate(
@@ -276,7 +284,7 @@ export async function updatePermit(
$and: [{ tenantId: user.tenantId }, { pid: permitId }],
},
{ ...input, lastUpdateDate: new Date() },
{ new: true }
{ new: true },
)
.populate({ path: "assignedTo", select: "pid name avatar" })
.populate({ path: "createdBy", select: "pid name avatar" });
@@ -300,7 +308,7 @@ export async function updatePermit(
},
permitId,
"permits",
user
user,
);
if (key == "requests" && input[key] != null) {
@@ -323,7 +331,7 @@ export async function updatePermit(
client: updatePermitResult.client.toString(),
county: updatePermitResult.county.id.toString(),
address: updatePermitResult.address,
}
},
);
}
}
@@ -346,13 +354,13 @@ export async function updatePermit(
},
permitId,
"permits",
user
user,
);
}
} else if (key == "assignedTo") {
const newAssignees = arrayDiff(
updatePermitResult.assignedTo.map((item) => item._id),
oldPermitResult.assignedTo
oldPermitResult.assignedTo,
);
if (newAssignees.length == 0) continue;
@@ -376,7 +384,7 @@ export async function updatePermit(
client: updatePermitResult.client.toString(),
county: updatePermitResult.county.id.toString(),
address: updatePermitResult.address.full_address,
}
},
);
}
@@ -386,7 +394,34 @@ export async function updatePermit(
},
permitId,
"permits",
user
user,
);
} else if (key == "assignedToOrg") {
if (oldPermitResult.assignedToOrg == updatePermitResult.assignedToOrg)
continue;
console.log(oldPermitResult.assignedToOrg);
console.log(updatePermitResult.assignedToOrg);
const orgName =
input.assignedToOrg == "agent"
? "Suncoast"
: updatePermitResult.clientData.name;
await createAlert(
user.tenantId,
`${orgName} assigned to ${updatePermitResult.permitNumber}`,
"team",
input.assignedToOrg == "client"
? updatePermitResult.client.toString()
: process.env.SUNCOAST_ID,
updatePermitResult.pid,
"permits",
{
client: updatePermitResult.client.toString(),
county: updatePermitResult.county.id.toString(),
address: updatePermitResult.address.full_address,
},
);
}
}
@@ -401,7 +436,7 @@ export async function updatePermit(
orgId: updatePermitResult.client._id.toString(),
document: updatePermitResult,
} as ChangeEvent,
["permit:read"]
["permit:read"],
);
}
@@ -423,7 +458,7 @@ export async function deletePermit(permitId: string, tenantId: string) {
pid: permitId,
},
} as ChangeEvent,
["permit:read"]
["permit:read"],
);
return res;
@@ -431,7 +466,7 @@ export async function deletePermit(permitId: string, tenantId: string) {
export async function searchPermit(
params: PageQueryParams,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
@@ -629,7 +664,7 @@ export async function bulkImport(csvData: any[], user: AuthenticatedUser) {
clientCache[clientName] = clientData;
} else {
errors.push(
"Client not found. The value in Client column must exactly match the client name in Quicker Permits"
"Client not found. The value in Client column must exactly match the client name in Quicker Permits",
);
}
}
@@ -650,7 +685,7 @@ export async function bulkImport(csvData: any[], user: AuthenticatedUser) {
countyCache[countyName] = countyData;
} else {
errors.push(
"County not found. The value in County column must exactly match the county name in Quicker Permits"
"County not found. The value in County column must exactly match the county name in Quicker Permits",
);
}
}

View File

@@ -30,7 +30,7 @@ const processedSchema = new mongoose.Schema({
pipeline: Array,
currentStage: Number,
},
{ _id: false }
{ _id: false },
),
status: String,
manualStatus: String,
@@ -41,6 +41,7 @@ const processedSchema = new mongoose.Schema({
type: [Schema.Types.ObjectId],
ref: "user",
},
assignedToOrg: String,
link: String,
address: Object,
recordType: String,
@@ -113,13 +114,13 @@ const processedSchema = new mongoose.Schema({
}).index({ tenantId: 1, permitNumber: 1 }, { unique: true });
export const processedFields = Object.keys(processedSchema.paths).filter(
(path) => path !== "__v"
(path) => path !== "__v",
);
export const processedModel = mongoose.model(
"processed",
processedSchema,
"processed"
"processed",
);
const updateProcessedInput = z.object({
@@ -133,6 +134,7 @@ const updateProcessedInput = z.object({
jobNumber: z.string().nullable().optional(),
startDate: z.date().nullable().optional(),
assignedTo: z.array(z.string()).nullable().optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
noc: z.string().optional(),
deed: z.string().optional(),
requests: z.array(z.string()).optional(),
@@ -152,5 +154,5 @@ export const { schemas: processedSchemas, $ref: $processed } = buildJsonSchemas(
pageQueryParams,
updateProcessedInput,
},
{ $id: "processed" }
{ $id: "processed" },
);

View File

@@ -19,7 +19,7 @@ import { arrayDiff } from "../utils/diff";
export async function getProcessedPermit(
permitId: String,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const permit = await processedModel
.findOne({
@@ -41,11 +41,19 @@ export async function getProcessedPermit(
export async function updateProcessed(
input: UpdateProcessedInput,
permitId: string,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
}
const oldPermitResult = await processedModel.findOne(
{ pid: permitId },
{ assignedTo: 1 }
{ assignedTo: 1, assignedToOrg: 1 },
);
const updateProcessedResult = await processedModel
.findOneAndUpdate(
@@ -53,7 +61,7 @@ export async function updateProcessed(
$and: [{ tenantId: user.tenantId }, { pid: permitId }],
},
{ ...input, lastUpdateDate: new Date() },
{ new: true }
{ new: true },
)
.populate({ path: "county", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" })
@@ -76,7 +84,7 @@ export async function updateProcessed(
},
permitId,
"processed",
user
user,
);
} else if (key == "client") {
const orgInDb = await orgModel.findById(input.client);
@@ -97,13 +105,13 @@ export async function updateProcessed(
},
permitId,
"permits",
user
user,
);
}
} else if (key == "assignedTo") {
const newAssignees = arrayDiff(
updateProcessedResult.assignedTo.map((item) => item._id),
oldPermitResult.assignedTo
oldPermitResult.assignedTo,
);
if (newAssignees.length == 0) continue;
@@ -127,7 +135,7 @@ export async function updateProcessed(
client: updateProcessedResult.client.toString(),
county: updateProcessedResult.county.id.toString(),
address: updateProcessedResult.address.full_address,
}
},
);
}
@@ -137,7 +145,33 @@ export async function updateProcessed(
},
permitId,
"processed",
user
user,
);
} else if (key == "assignedToOrg") {
if (
oldPermitResult.assignedToOrg == updateProcessedResult.assignedToOrg
)
continue;
const orgName =
input.assignedToOrg == "agent"
? "Suncoast"
: updateProcessedResult.clientData.name;
await createAlert(
user.tenantId,
`${orgName} assigned to ${updateProcessedResult.permitNumber}`,
"team",
input.assignedToOrg == "client"
? updateProcessedResult.client.toString()
: process.env.SUNCOAST_ID,
updateProcessedResult.pid,
"permits",
{
client: updateProcessedResult.client.toString(),
county: updateProcessedResult.county.id.toString(),
address: updateProcessedResult.address.full_address,
},
);
}
}
@@ -148,7 +182,7 @@ export async function updateProcessed(
export async function listProcessedPermits(
params: PageQueryParams,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
@@ -173,7 +207,7 @@ export async function listProcessedPermits(
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
@@ -281,7 +315,7 @@ export async function listProcessedPermits(
],
},
},
]
],
);
const permitsList = await processedModel.aggregate(pipeline);

View File

@@ -22,7 +22,7 @@ const rtsSchema = new mongoose.Schema({
ref: "user",
},
},
{ _id: false }
{ _id: false },
),
],
county: {
@@ -39,7 +39,7 @@ const rtsSchema = new mongoose.Schema({
pipeline: Array,
currentStage: Number,
},
{ _id: false }
{ _id: false },
),
status: String,
labels: [String],
@@ -54,6 +54,7 @@ const rtsSchema = new mongoose.Schema({
type: [Schema.Types.ObjectId],
ref: "user",
},
assignedToOrg: String,
taggedUsers: Array,
fileValidationStatus: String,
permitNumber: [String],
@@ -61,7 +62,7 @@ const rtsSchema = new mongoose.Schema({
});
export const rtsFields = Object.keys(rtsSchema.paths).filter(
(path) => path !== "__v"
(path) => path !== "__v",
);
export const rtsModel = mongoose.model("rts", rtsSchema, "rts");
@@ -82,12 +83,13 @@ const rtsCreateInput = z.object({
date: z.date().nullable().optional(),
description: z.string().optional(),
comment: z.string().optional(),
})
}),
),
currentStage: z.number(),
})
.optional(),
assignedTo: z.array(z.string()).optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
status: z.string().optional(),
permitNumber: z.array(z.string()).optional(),
lot: z.array(z.string()).optional(),
@@ -108,12 +110,13 @@ const rtsUpdateInput = z.object({
date: z.date().nullable().optional(),
description: z.string().optional(),
comment: z.string().optional(),
})
}),
),
currentStage: z.number(),
})
.optional(),
assignedTo: z.array(z.string()).optional(),
assignedToOrg: z.enum(["client", "agent"]).nullable().optional(),
status: z.string().optional(),
fileValidationStatus: z.string().optional(),
permitNumber: z.array(z.string()).optional(),
@@ -135,5 +138,5 @@ export const { schemas: rtsSchemas, $ref: $rts } = buildJsonSchemas(
rtsNewUpload,
pageQueryParams,
},
{ $id: "rts" }
{ $id: "rts" },
);

View File

@@ -19,10 +19,11 @@ import { rtsPipeline } from "../utils/pipeline";
import { createAlert } from "../alert/alert.service";
import { createNote } from "../note/note.service";
import { arrayDiff } from "../utils/diff";
import { getOrg } from "../organization/organization.service";
export async function createRts(
input: CreateRtsInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
let defaultClient = input.client;
const userInDb = await getUserWithoutPopulate(user.userId);
@@ -75,7 +76,7 @@ export async function getRts(id: string, tenantId: string) {
export async function listRts(
params: PageQueryParams,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
@@ -100,7 +101,7 @@ export async function listRts(
let { taggedFilter, taggedUserFilterIndex } = getTaggedUsersFilter(
filterObj,
sortObj
sortObj,
);
if (taggedUserFilterIndex != -1) filterObj.splice(taggedUserFilterIndex, 1);
@@ -236,9 +237,21 @@ export async function listRts(
export async function updateRts(
id: string,
input: UpdateRtsInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
const oldRts = await rtsModel.findOne({ pid: id }, { assignedTo: 1 });
if (input.assignedToOrg && input.assignedTo) {
input.assignedTo = [];
} else if (input.assignedToOrg) {
input.assignedTo = [];
} else if (input.assignedTo) {
input.assignedToOrg = null;
}
const oldRts = await rtsModel.findOne(
{ pid: id },
{ assignedTo: 1, assignedToOrg: 1 },
);
const updatedRts = await rtsModel
.findOneAndUpdate({ pid: id, tenantId: user.tenantId }, input, {
new: true,
@@ -253,14 +266,14 @@ export async function updateRts(
{ content: `Updated type to '${input.permitType}'` },
id,
"rts",
user
user,
);
}
if (updatedRts && input.assignedTo) {
const newAssignees = arrayDiff(
updatedRts.assignedTo.map((item) => item._id),
oldRts.assignedTo
oldRts.assignedTo,
);
if (newAssignees.length > 0) {
@@ -285,7 +298,7 @@ export async function updateRts(
//@ts-ignore
county: updatedRts.county._id.toString(),
permitType: updatedRts.permitType,
}
},
);
}
@@ -295,11 +308,40 @@ export async function updateRts(
},
id,
"rts",
user
user,
);
}
}
if (
updatedRts &&
input.assignedToOrg &&
oldRts.assignedToOrg != updatedRts.assignedToOrg
) {
const orgName =
//@ts-ignore
input.assignedToOrg == "agent" ? "Suncoast" : updatedRts.client.name;
await createAlert(
user.tenantId,
`${orgName} assigned to RTS`,
"team",
input.assignedToOrg == "client"
? //@ts-ignore
updatedRts.client._id.toString()
: process.env.SUNCOAST_ID,
updatedRts.pid,
"permits",
{
//@ts-ignore
client: updatedRts.client._id.toString(),
//@ts-ignore
county: updatedRts.county._id.toString(),
permitType: updatedRts.permitType,
},
);
}
return updatedRts;
}
@@ -310,7 +352,7 @@ export async function deleteRts(id: string, tenantId: string) {
export async function newUpload(
id: string,
newUpload: UploadRtsInput,
user: AuthenticatedUser
user: AuthenticatedUser,
) {
return await rtsModel.findOneAndUpdate(
{ pid: id, tenantId: user.tenantId },
@@ -322,6 +364,6 @@ export async function newUpload(
createdBy: user.userId,
},
},
}
},
);
}