diff --git a/src/ctask/ctask.route.ts b/src/ctask/ctask.route.ts index 08a6992..117d9fd 100644 --- a/src/ctask/ctask.route.ts +++ b/src/ctask/ctask.route.ts @@ -115,7 +115,7 @@ export async function ctaskRoutes(fastify: FastifyInstance) { const { field } = req.params as { field: string }; try { - const uniqueValues = await getUniqueFields(field, "task", req.user); + const uniqueValues = await getUniqueFields(field, "ctasks", req.user); return res.code(200).send(uniqueValues); } catch (err) { return err; diff --git a/src/ctask/ctask.schema.ts b/src/ctask/ctask.schema.ts index 226044c..a217683 100644 --- a/src/ctask/ctask.schema.ts +++ b/src/ctask/ctask.schema.ts @@ -28,6 +28,7 @@ const taskSchema = new mongoose.Schema({ type: mongoose.Types.ObjectId, ref: "user", }, + taggedUsers: Array, }); export const taskFields = Object.keys(taskSchema.paths).filter( diff --git a/src/ctask/ctask.service.ts b/src/ctask/ctask.service.ts index 0242156..c7ee7df 100644 --- a/src/ctask/ctask.service.ts +++ b/src/ctask/ctask.service.ts @@ -37,6 +37,7 @@ export async function createTask( content: input.note, }, task.pid, + "ctasks", user ); } @@ -80,6 +81,25 @@ export async function listTasks( const sortObj = getSortObject(params, taskFields); const filterObj = getFilterObject(params) || []; + let taggedFilter = []; + if (sortObj.taggedUsers) { + taggedFilter = [ + { + $addFields: { + taggedUsers: { + $filter: { + input: "$taggedUsers", + as: "user", + cond: { $eq: ["$$user.userId", user.userId] }, + }, + }, + }, + }, + { $match: { "taggedUsers.0": { $exists: true } } }, + { $sort: { "taggedUsers.taggedAt": sortObj.taggedUsers } }, + ]; + } + const taskList = await taskModel.aggregate([ { $match: { @@ -90,6 +110,7 @@ export async function listTasks( ], }, }, + ...taggedFilter, { $lookup: { from: "users", diff --git a/src/note/note.controller.ts b/src/note/note.controller.ts index 3ba778f..49ff3c5 100644 --- a/src/note/note.controller.ts +++ b/src/note/note.controller.ts @@ -8,9 +8,10 @@ export async function createNoteHandler( ) { const { resourceId } = req.params as { resourceId: string }; const input = req.body as CreateNoteInput; + const resourceType = req.originalUrl.split("/")[3]; try { - const note = await createNote(input, resourceId, req.user); + const note = await createNote(input, resourceId, resourceType, req.user); return res.code(201).send(note); } catch (err) { return err; diff --git a/src/note/note.service.ts b/src/note/note.service.ts index 1590228..a77a613 100644 --- a/src/note/note.service.ts +++ b/src/note/note.service.ts @@ -1,12 +1,24 @@ +import { createAlert } from "../alert/alert.service"; import { AuthenticatedUser } from "../auth"; import { generateId } from "../utils/id"; +import { extractExpressions, modelMap } from "../utils/tags"; import { CreateNoteInput, noteModel } from "./note.schema"; export async function createNote( input: CreateNoteInput, resourceId: string, + resourceType: string, 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(), @@ -16,6 +28,56 @@ export async function createNote( createdBy: user.userId, }); + if ( + userIds.length > 0 && + [ + "permits", + "processed", + "rts", + "tasks", + "ctasks", + "payments", + "notifications", + ].includes(resourceType) + ) { + const model = modelMap[resourceType]; + const obj = await model.findOne({ pid: resourceId }); + + console.log(obj.taggedUsers); + + if (!obj.taggedUsers) { + await model.updateOne( + { pid: resourceId }, + { $set: { taggedUsers: taggedUsers } } + ); + } else { + for (const user of taggedUsers) { + const userIndex = obj.taggedUsers.findIndex( + (item) => item.userId == user.userId + ); + console.log(userIndex); + if (userIndex != -1) obj.taggedUsers[userIndex].taggedAt = new Date(); + else obj.taggedUsers.push(user); + } + + obj.markModified("taggedUsers"); + await obj.save(); + } + + for (const id of userIds) { + if (id == user.userId) continue; + + await createAlert( + user.tenantId, + "You are tagged in a note", + "user", + id, + resourceId, + resourceType + ); + } + } + return newNote.populate({ path: "createdBy", select: "pid name avatar" }); } diff --git a/src/notification/notification.schema.ts b/src/notification/notification.schema.ts index cf43a06..3bb393c 100644 --- a/src/notification/notification.schema.ts +++ b/src/notification/notification.schema.ts @@ -25,6 +25,7 @@ const notificationSchema = new mongoose.Schema({ type: mongoose.Types.ObjectId, ref: "user", }, + taggedUsers: Array, }); export const notificationFields = Object.keys(notificationSchema.paths).filter( diff --git a/src/notification/notification.service.ts b/src/notification/notification.service.ts index dcb5b82..309ffe9 100644 --- a/src/notification/notification.service.ts +++ b/src/notification/notification.service.ts @@ -77,6 +77,7 @@ export async function updateNotification( content: msg, }, notifId, + "notifications", user ); @@ -84,6 +85,7 @@ export async function updateNotification( await createNote( { content: msg }, updateNotificationResult.permitId, + "notifications", user ); @@ -123,6 +125,25 @@ export async function listNotifications( filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); } + let taggedFilter = []; + if (sortObj.taggedUsers) { + taggedFilter = [ + { + $addFields: { + taggedUsers: { + $filter: { + input: "$taggedUsers", + as: "user", + cond: { $eq: ["$$user.userId", user.userId] }, + }, + }, + }, + }, + { $match: { "taggedUsers.0": { $exists: true } } }, + { $sort: { "taggedUsers.taggedAt": sortObj.taggedUsers } }, + ]; + } + const pipeline: any = [ { $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, @@ -140,6 +161,7 @@ export async function listNotifications( pipeline.push( ...[ + ...taggedFilter, { $lookup: { from: "users", diff --git a/src/payments/payment.route.ts b/src/payments/payment.route.ts index d1d639a..ca77e26 100644 --- a/src/payments/payment.route.ts +++ b/src/payments/payment.route.ts @@ -62,7 +62,7 @@ export async function paymentRoutes(fastify: FastifyInstance) { const { field } = req.params as { field: string }; try { - const uniqueValues = await getUniqueFields(field, "payment", req.user); + const uniqueValues = await getUniqueFields(field, "payments", req.user); return res.code(200).send(uniqueValues); } catch (err) { return err; diff --git a/src/permit/permit.schema.ts b/src/permit/permit.schema.ts index 32ae7fa..959ff11 100644 --- a/src/permit/permit.schema.ts +++ b/src/permit/permit.schema.ts @@ -1,7 +1,7 @@ -import { z } from 'zod'; -import mongoose from 'mongoose'; -import { buildJsonSchemas } from 'fastify-zod'; -import { pageMetadata, pageQueryParams } from '../pagination'; +import { z } from "zod"; +import mongoose from "mongoose"; +import { buildJsonSchemas } from "fastify-zod"; +import { pageMetadata, pageQueryParams } from "../pagination"; const permitSchema = new mongoose.Schema({ tenantId: { @@ -16,7 +16,7 @@ const permitSchema = new mongoose.Schema({ county: Object, client: { type: mongoose.Types.ObjectId, - ref: 'organization', + ref: "organization", }, clientData: Object, permitDate: Date, @@ -34,7 +34,7 @@ const permitSchema = new mongoose.Schema({ utility: String, assignedTo: { type: mongoose.Types.ObjectId, - ref: 'user', + ref: "user", }, link: String, address: Object, @@ -52,7 +52,7 @@ const permitSchema = new mongoose.Schema({ createdAt: Date, createdBy: { type: mongoose.Types.ObjectId, - ref: 'user', + ref: "user", }, newProcessingStatus: Array, newPayment: Array, @@ -71,12 +71,13 @@ const permitSchema = new mongoose.Schema({ jobNumber: String, startDate: Date, history: Array, + taggedUsers: Array, }).index({ tenantId: 1, permitNumber: 1 }, { unique: true }); export const permitFields = Object.keys(permitSchema.paths).filter( - (path) => path !== '__v' + (path) => path !== "__v" ); -export const permitModel = mongoose.model('permit', permitSchema); +export const permitModel = mongoose.model("permit", permitSchema); const permitCore = { permitNumber: z.string(), @@ -161,5 +162,5 @@ export const { schemas: permitSchemas, $ref: $permit } = buildJsonSchemas( updatePermitInput, pageQueryParams, }, - { $id: 'permit' } + { $id: "permit" } ); diff --git a/src/permit/permit.service.ts b/src/permit/permit.service.ts index facfa11..845eb29 100644 --- a/src/permit/permit.service.ts +++ b/src/permit/permit.service.ts @@ -72,10 +72,30 @@ export async function listPermits( filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); } + let taggedFilter = []; + if (sortObj.taggedUsers) { + taggedFilter = [ + { + $addFields: { + taggedUsers: { + $filter: { + input: "$taggedUsers", + as: "user", + cond: { $eq: ["$$user.userId", user.userId] }, + }, + }, + }, + }, + { $match: { "taggedUsers.0": { $exists: true } } }, + { $sort: { "taggedUsers.taggedAt": sortObj.taggedUsers } }, + ]; + } + const permitsList = await permitModel.aggregate([ { $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, }, + ...taggedFilter, { $lookup: { from: "users", @@ -206,6 +226,7 @@ export async function updatePermit( content: msg, }, permitId, + "permits", user ); diff --git a/src/processed/processed.schema.ts b/src/processed/processed.schema.ts index e2ff1e7..69e44a5 100644 --- a/src/processed/processed.schema.ts +++ b/src/processed/processed.schema.ts @@ -1,7 +1,7 @@ -import z from 'zod'; -import mongoose from 'mongoose'; -import { pageQueryParams } from '../pagination'; -import { buildJsonSchemas } from 'fastify-zod'; +import z from "zod"; +import mongoose from "mongoose"; +import { pageQueryParams } from "../pagination"; +import { buildJsonSchemas } from "fastify-zod"; const processedSchema = new mongoose.Schema({ tenantId: { @@ -16,7 +16,7 @@ const processedSchema = new mongoose.Schema({ county: Object, client: { type: mongoose.Types.ObjectId, - ref: 'organization', + ref: "organization", }, clientData: Object, permitDate: Date, @@ -34,7 +34,7 @@ const processedSchema = new mongoose.Schema({ utility: String, assignedTo: { type: mongoose.Types.ObjectId, - ref: 'user', + ref: "user", }, link: String, address: Object, @@ -52,7 +52,7 @@ const processedSchema = new mongoose.Schema({ createdAt: Date, createdBy: { type: mongoose.Types.ObjectId, - ref: 'user', + ref: "user", }, newProcessingStatus: Array, newPayment: Array, @@ -71,16 +71,17 @@ const processedSchema = new mongoose.Schema({ jobNumber: String, transferDate: Date, history: Array, + taggedUsers: Array, }).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', + "processed", processedSchema, - 'processed' + "processed" ); const updateProcessedInput = z.object({ @@ -100,5 +101,5 @@ export const { schemas: processedSchemas, $ref: $processed } = buildJsonSchemas( pageQueryParams, updateProcessedInput, }, - { $id: 'processed' } + { $id: "processed" } ); diff --git a/src/processed/processed.service.ts b/src/processed/processed.service.ts index 7147d75..2f609d2 100644 --- a/src/processed/processed.service.ts +++ b/src/processed/processed.service.ts @@ -48,6 +48,7 @@ export async function updateProcessed( content: msg, }, permitId, + "processed", user ); } @@ -70,6 +71,25 @@ export async function listProcessedPermits( filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); } + let taggedFilter = []; + if (sortObj.taggedUsers) { + taggedFilter = [ + { + $addFields: { + taggedUsers: { + $filter: { + input: "$taggedUsers", + as: "user", + cond: { $eq: ["$$user.userId", user.userId] }, + }, + }, + }, + }, + { $match: { "taggedUsers.0": { $exists: true } } }, + { $sort: { "taggedUsers.taggedAt": sortObj.taggedUsers } }, + ]; + } + const pipeline: any = [ { $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, @@ -91,6 +111,7 @@ export async function listProcessedPermits( pipeline.push( ...[ + ...taggedFilter, { $lookup: { from: "users", diff --git a/src/rts/rts.schema.ts b/src/rts/rts.schema.ts index 9162946..4bc3f37 100644 --- a/src/rts/rts.schema.ts +++ b/src/rts/rts.schema.ts @@ -53,6 +53,7 @@ const rtsSchema = new mongoose.Schema({ type: mongoose.Types.ObjectId, ref: "user", }, + taggedUsers: Array, }); export const rtsFields = Object.keys(rtsSchema.paths).filter( diff --git a/src/rts/rts.service.ts b/src/rts/rts.service.ts index 08e12c0..93f8c51 100644 --- a/src/rts/rts.service.ts +++ b/src/rts/rts.service.ts @@ -34,7 +34,7 @@ export async function createRts( ...input, tenantId: user.tenantId, pid: generateId(), - client: defaultClient, + client: user.role == "client" ? defaultClient : input.client, createdAt: new Date(), createdBy: user.userId ?? null, }); @@ -78,10 +78,30 @@ export async function listRts( filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) }); } + let taggedFilter = []; + if (sortObj.taggedUsers) { + taggedFilter = [ + { + $addFields: { + taggedUsers: { + $filter: { + input: "$taggedUsers", + as: "user", + cond: { $eq: ["$$user.userId", user.userId] }, + }, + }, + }, + }, + { $match: { "taggedUsers.0": { $exists: true } } }, + { $sort: { "taggedUsers.taggedAt": sortObj.taggedUsers } }, + ]; + } + const rtsList = await rtsModel.aggregate([ { $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, }, + ...taggedFilter, { $lookup: { from: "organizations", diff --git a/src/task/task.controller.ts b/src/task/task.controller.ts index 34084e3..9406f07 100644 --- a/src/task/task.controller.ts +++ b/src/task/task.controller.ts @@ -47,7 +47,7 @@ export async function listTaskHandler(req: FastifyRequest, res: FastifyReply) { const queryParams = req.query as PageQueryParams; try { - const taskList = await listTasks(queryParams, req.user.tenantId); + const taskList = await listTasks(queryParams, req.user); return res.code(200).send(taskList); } catch (err) { return err; diff --git a/src/task/task.route.ts b/src/task/task.route.ts index d6a17de..0282659 100644 --- a/src/task/task.route.ts +++ b/src/task/task.route.ts @@ -128,7 +128,7 @@ export async function taskRoutes(fastify: FastifyInstance) { const { field } = req.params as { field: string }; try { - const uniqueValues = await getUniqueFields(field, "task", req.user); + const uniqueValues = await getUniqueFields(field, "tasks", req.user); return res.code(200).send(uniqueValues); } catch (err) { return err; diff --git a/src/task/task.schema.ts b/src/task/task.schema.ts index 28b2cba..b492f1b 100644 --- a/src/task/task.schema.ts +++ b/src/task/task.schema.ts @@ -40,6 +40,7 @@ const taskSchema = new mongoose.Schema({ type: mongoose.Types.ObjectId, ref: "user", }, + taggedUsers: Array, }); export const taskFields = Object.keys(taskSchema.paths).filter( diff --git a/src/task/task.service.ts b/src/task/task.service.ts index 56b099f..e657682 100644 --- a/src/task/task.service.ts +++ b/src/task/task.service.ts @@ -37,6 +37,7 @@ export async function createTask( content: input.note, }, task.pid, + "tasks", user ); } @@ -80,16 +81,39 @@ export async function getTask(taskId: string, tenantId: string) { .populate({ path: "assignedTo", select: "pid name avatar" }); } -export async function listTasks(params: PageQueryParams, tenantId: string) { +export async function listTasks( + params: PageQueryParams, + user: AuthenticatedUser +) { const page = params.page || 1; const pageSize = params.pageSize || 10; const sortObj = getSortObject(params, taskFields); const filterObj = getFilterObject(params) || []; + let taggedFilter = []; + if (sortObj.taggedUsers) { + taggedFilter = [ + { + $addFields: { + taggedUsers: { + $filter: { + input: "$taggedUsers", + as: "user", + cond: { $eq: ["$$user.userId", user.userId] }, + }, + }, + }, + }, + { $match: { "taggedUsers.0": { $exists: true } } }, + { $sort: { "taggedUsers.taggedAt": sortObj.taggedUsers } }, + ]; + } + const taskList = await taskModel.aggregate([ { - $match: { $and: [{ tenantId: tenantId }, ...filterObj] }, + $match: { $and: [{ tenantId: user.tenantId }, ...filterObj] }, }, + ...taggedFilter, { $lookup: { from: "users", diff --git a/src/unique.ts b/src/unique.ts index b6f139c..67525e0 100644 --- a/src/unique.ts +++ b/src/unique.ts @@ -1,33 +1,18 @@ import { orgModel } from "./organization/organization.schema"; import { userModel } from "./user/user.schema"; -import { permitModel } from "./permit/permit.schema"; -import { processedModel } from "./processed/processed.schema"; -import { notificationModel } from "./notification/notification.schema"; -import { rtsModel } from "./rts/rts.schema"; -import { taskModel } from "./task/task.schema"; import { AuthenticatedUser } from "./auth"; -import { paymentModel } from "./payments/payment.schema"; +import { modelMap } from "./utils/tags"; type Collection = | "users" | "permits" - | "organizations" + | "orgs" | "processed" | "notifications" | "rts" - | "task" - | "payment"; - -const modelMap = { - users: userModel, - permits: permitModel, - organizations: orgModel, - processed: processedModel, - notifications: notificationModel, - rts: rtsModel, - task: taskModel, - payment: paymentModel, -}; + | "tasks" + | "ctasks" + | "payments"; export async function getUniqueFields( field: string, diff --git a/src/utils/tags.ts b/src/utils/tags.ts new file mode 100644 index 0000000..543c144 --- /dev/null +++ b/src/utils/tags.ts @@ -0,0 +1,28 @@ +import mongoose from "mongoose"; +import { userModel } from "../user/user.schema"; +import { permitModel } from "../permit/permit.schema"; +import { orgModel } from "../organization/organization.schema"; +import { processedModel } from "../processed/processed.schema"; +import { notificationModel } from "../notification/notification.schema"; +import { rtsModel } from "../rts/rts.schema"; +import { taskModel } from "../task/task.schema"; +import { taskModel as ctaskModel } from "../ctask/ctask.schema"; +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)); +} + +export const modelMap = { + users: userModel, + permits: permitModel, + orgs: orgModel, + processed: processedModel, + notifications: notificationModel, + rts: rtsModel, + ctasks: ctaskModel, + tasks: taskModel, + payments: paymentModel, +};