feat: implement notifications system with API endpoints for fetching and marking notifications as read

This commit is contained in:
2025-12-02 11:06:31 +01:00
parent fae7615818
commit 9b84c6b9e8
8 changed files with 694 additions and 13 deletions

319
src/lib/notifications.js Normal file
View File

@@ -0,0 +1,319 @@
/**
* Notification types - standardized notification types
*/
export const NOTIFICATION_TYPES = {
// Task notifications
TASK_ASSIGNED: "task_assigned",
TASK_STATUS_CHANGED: "task_status_changed",
// Project notifications
PROJECT_UPDATED: "project_updated",
// System notifications
DUE_DATE_REMINDER: "due_date_reminder",
SYSTEM_ANNOUNCEMENT: "system_announcement",
MENTION: "mention",
};
/**
* Notification priorities
*/
export const NOTIFICATION_PRIORITIES = {
LOW: "low",
NORMAL: "normal",
HIGH: "high",
URGENT: "urgent",
};
/**
* Create a notification
* @param {Object} params - Notification parameters
* @param {string} params.userId - User to receive the notification
* @param {string} params.type - Notification type (use NOTIFICATION_TYPES constants)
* @param {string} params.title - Notification title
* @param {string} params.message - Notification message
* @param {string} [params.resourceType] - Type of related resource
* @param {string} [params.resourceId] - ID of the related resource
* @param {string} [params.priority] - Priority level (default: normal)
* @param {string} [params.actionUrl] - URL to navigate to when clicked
* @param {string} [params.expiresAt] - When the notification expires
*/
export async function createNotification({
userId,
type,
title,
message,
resourceType = null,
resourceId = null,
priority = NOTIFICATION_PRIORITIES.NORMAL,
actionUrl = null,
expiresAt = null,
}) {
try {
// Check if we're in Edge Runtime - if so, skip database operations
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
`[Notification - Edge Runtime] ${type} notification for user ${userId}: ${title}`
);
return;
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
const stmt = db.prepare(`
INSERT INTO notifications (
user_id, type, title, message, resource_type, resource_id,
priority, action_url, expires_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
userId,
type,
title,
message,
resourceType,
resourceId,
priority,
actionUrl,
expiresAt
);
console.log(
`Notification created: ${type} for user ${userId} - ${title}`
);
return result.lastInsertRowid;
} catch (error) {
console.error("Failed to create notification:", error);
// Don't throw error to avoid breaking the main application flow
}
}
/**
* Get notifications for a user
* @param {string} userId - User ID
* @param {Object} options - Query options
* @param {boolean} [options.includeRead] - Include read notifications (default: false)
* @param {number} [options.limit] - Maximum number of notifications to return
* @param {number} [options.offset] - Number of notifications to skip
* @returns {Array} Array of notifications
*/
export async function getUserNotifications(
userId,
{ includeRead = false, limit = 50, offset = 0 } = {}
) {
try {
// Check if we're in Edge Runtime - if so, return empty array
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
"[Notification - Edge Runtime] Cannot query notifications in Edge Runtime"
);
return [];
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
let query = `
SELECT * FROM notifications
WHERE user_id = ?
`;
const params = [userId];
if (!includeRead) {
query += " AND is_read = 0";
}
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
params.push(limit, offset);
const stmt = db.prepare(query);
const notifications = stmt.all(...params);
return notifications;
} catch (error) {
console.error("Failed to get user notifications:", error);
return [];
}
}
/**
* Mark notifications as read
* @param {string} userId - User ID
* @param {Array<number>} [notificationIds] - Specific notification IDs to mark as read (if not provided, marks all as read)
*/
export async function markNotificationsAsRead(userId, notificationIds = null) {
try {
// Check if we're in Edge Runtime - if so, skip database operations
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
`[Notification - Edge Runtime] Cannot mark notifications as read in Edge Runtime`
);
return;
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
let query;
let params;
if (notificationIds && notificationIds.length > 0) {
// Mark specific notifications as read
const placeholders = notificationIds.map(() => "?").join(",");
query = `
UPDATE notifications
SET is_read = 1
WHERE user_id = ? AND id IN (${placeholders})
`;
params = [userId, ...notificationIds];
} else {
// Mark all notifications as read
query = `
UPDATE notifications
SET is_read = 1
WHERE user_id = ?
`;
params = [userId];
}
const stmt = db.prepare(query);
stmt.run(...params);
console.log(`Marked notifications as read for user ${userId}`);
} catch (error) {
console.error("Failed to mark notifications as read:", error);
}
}
/**
* Get unread notification count for a user
* @param {string} userId - User ID
* @returns {number} Number of unread notifications
*/
export async function getUnreadNotificationCount(userId) {
try {
// Check if we're in Edge Runtime - if so, return 0
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
return 0;
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
const stmt = db.prepare(`
SELECT COUNT(*) as count
FROM notifications
WHERE user_id = ? AND is_read = 0
`);
const result = stmt.get(userId);
return result.count || 0;
} catch (error) {
console.error("Failed to get unread notification count:", error);
return 0;
}
}
/**
* Delete old notifications (cleanup function)
* @param {number} daysOld - Delete notifications older than this many days
*/
export async function cleanupOldNotifications(daysOld = 30) {
try {
// Check if we're in Edge Runtime - if so, skip database operations
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
`[Notification - Edge Runtime] Cannot cleanup notifications in Edge Runtime`
);
return;
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const cutoffIso = cutoffDate.toISOString();
const stmt = db.prepare(`
DELETE FROM notifications
WHERE created_at < ? AND is_read = 1
`);
const result = stmt.run(cutoffIso);
console.log(`Cleaned up ${result.changes} old notifications`);
} catch (error) {
console.error("Failed to cleanup old notifications:", error);
}
}
/**
* Create notification from audit event (helper function)
* @param {Object} auditEvent - Audit event data
* @param {string} targetUserId - User to notify
* @param {string} notificationType - Type of notification
* @param {string} title - Notification title
* @param {string} message - Notification message
*/
export async function createNotificationFromAuditEvent(
auditEvent,
targetUserId,
notificationType,
title,
message
) {
// Don't notify the user who performed the action
if (auditEvent.userId === targetUserId) {
return;
}
await createNotification({
userId: targetUserId,
type: notificationType,
title,
message,
resourceType: auditEvent.resourceType,
resourceId: auditEvent.resourceId,
actionUrl: getActionUrl(auditEvent.resourceType, auditEvent.resourceId),
});
}
/**
* Generate action URL for notification
* @param {string} resourceType - Type of resource
* @param {string} resourceId - Resource ID
* @returns {string} Action URL
*/
function getActionUrl(resourceType, resourceId) {
switch (resourceType) {
case "project":
return `/projects/${resourceId}`;
case "project_task":
return `/project-tasks/${resourceId}`;
case "task":
return `/tasks/${resourceId}`;
case "contract":
return `/contracts/${resourceId}`;
default:
return null;
}
}