@@ -20,11 +24,15 @@ function createDailyNotification(email, notificationContent) {
STAPLE Daily Notifications
- This email is to notify you about recent updates to your project(s). You can view all notifications on the Notifications page (you may be asked to log in).
- Here are new announcements, tasks, and other project updates:
-
+ This email is to notify you about overdue tasks and recent updates to your project(s).
+ You can view all notifications on the Notifications page.
+
+
+
⏰ Overdue Tasks
+
${overdueContent}
- ${notificationContent}
+
📢 Project Updates
+
${notificationContent}
`
@@ -79,6 +87,85 @@ export async function fetchAndGroupNotifications() {
}, {})
}
+// Function to fetch and group overdue tasks by email and project
+export async function fetchAndGroupOverdueTasks() {
+ const now = new Date()
+
+ const tasks = await db.task.findMany({
+ where: {
+ deadline: { lt: now },
+ },
+ include: {
+ project: { select: { name: true } },
+ assignedMembers: {
+ include: {
+ users: { select: { email: true } },
+ },
+ },
+ taskLogs: {
+ select: {
+ id: true,
+ createdAt: true,
+ assignedToId: true,
+ status: true,
+ completedById: true,
+ completedAs: true,
+ },
+ orderBy: { createdAt: "desc" },
+ },
+ },
+ orderBy: { deadline: "asc" },
+ })
+
+ // A task counts as overdue for a member only if that member has a latest log and it is NOT_COMPLETED.
+ // If there is no log for that member, assume not assigned → do not include.
+ const isUnfinishedLatest = (log) => {
+ if (!log) return false
+ const s = (log.status || "").toString().toUpperCase()
+ return s === "NOT_COMPLETED"
+ }
+
+ // Group as: email -> projectName -> [task rows]
+ return tasks.reduce((acc, task) => {
+ const projectName = task?.project?.name || "No Project"
+ const taskName = task?.name || `Task #${task?.id}`
+ const due = task?.deadline ? fmtDate(task.deadline) : "no due date"
+ const pastDeadline = task?.deadline && task.deadline < now
+
+ // Map latest TaskLog by assignee (assignedToId) — schema note: TaskLog does not have projectmemberId
+ const latestByMember = new Map()
+ for (const log of task.taskLogs || []) {
+ if (!latestByMember.has(log.assignedToId)) {
+ latestByMember.set(log.assignedToId, log)
+ }
+ }
+
+ const members = task?.assignedMembers || []
+ if (members.length === 0) return acc
+
+ for (const m of members) {
+ const latest = latestByMember.get(m.id)
+ const isUnfinished = isUnfinishedLatest(latest)
+
+ if (pastDeadline && isUnfinished) {
+ const line = `${projectName} - ${taskName} - Due: ${due}`
+ const users = m?.users || []
+ for (const u of users) {
+ const email = u?.email
+ if (!email) continue
+ if (!acc[email]) acc[email] = {}
+ if (!acc[email][projectName]) acc[email][projectName] = []
+ acc[email][projectName].push(line)
+ }
+
+ // TODO: If assignment is to a team, add logic here to notify team distribution list or members
+ }
+ }
+
+ return acc
+ }, {})
+}
+
// Function to introduce a delay (in milliseconds)
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
@@ -105,19 +192,43 @@ const checkRateLimit = async () => {
}
}
// Function to send grouped notifications
-export async function sendGroupedNotifications(groupedNotifications) {
+export async function sendGroupedNotifications(groupedNotifications, groupedOverdues) {
const delayTime = 500 // Delay time between each email in milliseconds (e.g., 1 second)
- for (const [email, projects] of Object.entries(groupedNotifications)) {
- const notificationContent = Object.entries(projects)
- .map(([projectName, messages]) => {
- const projectHeader = `