tududi/backend/models/notification.js
Chris 542be2c1e9
Fix bug 366 (#764)
* Optimize DB

* Clean up names

* fixup! Clean up names

* fixup! fixup! Clean up names
2026-01-07 18:18:07 +02:00

334 lines
8.9 KiB
JavaScript

const { DataTypes } = require('sequelize');
const { v4: uuid } = require('uuid');
module.exports = (sequelize) => {
const Notification = sequelize.define(
'Notification',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
uid: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
defaultValue: () => uuid(),
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id',
},
},
type: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isIn: [
[
'task_assigned',
'task_completed',
'task_due_soon',
'task_overdue',
'comment_added',
'mention',
'reminder',
'system',
'project_due_soon',
'project_overdue',
],
],
},
},
level: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'info',
validate: {
isIn: [['info', 'warning', 'error', 'success']],
},
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
message: {
type: DataTypes.TEXT,
allowNull: true,
},
data: {
type: DataTypes.JSON,
allowNull: true,
},
sources: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: [],
validate: {
isValidSources(value) {
if (!Array.isArray(value)) {
throw new Error('Sources must be an array');
}
const validSources = ['telegram', 'mobile', 'email'];
const invalidSources = value.filter(
(s) => !validSources.includes(s)
);
if (invalidSources.length > 0) {
throw new Error(
`Invalid sources: ${invalidSources.join(', ')}`
);
}
},
},
},
read_at: {
type: DataTypes.DATE,
allowNull: true,
},
sent_at: {
type: DataTypes.DATE,
allowNull: true,
},
dismissed_at: {
type: DataTypes.DATE,
allowNull: true,
},
},
{
tableName: 'notifications',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['user_id'],
},
{
fields: ['read_at'],
},
{
fields: ['created_at'],
},
{
fields: ['user_id', 'read_at'],
},
{
fields: ['dismissed_at'],
},
{
fields: ['user_id', 'dismissed_at'],
},
],
}
);
Notification.associate = function (models) {
Notification.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'User',
});
};
Notification.createNotification = async function ({
userId,
type,
title,
message,
data = null,
sources = [],
sentAt = null,
level = 'info',
}) {
const notification = await Notification.create({
user_id: userId,
type,
title,
message,
data,
sources,
level,
sent_at: sentAt || new Date(),
});
if (sources.includes('email')) {
await sendEmailNotification(userId, title, message, Notification);
}
if (sources.includes('telegram')) {
await sendTelegramNotification(
userId,
title,
message,
data,
Notification
);
}
return notification;
};
async function sendEmailNotification(
userId,
title,
message,
NotificationModel
) {
try {
const {
sendEmail,
isEmailEnabled,
} = require('../services/emailService');
if (!isEmailEnabled() || !message) {
return;
}
const UserModel = NotificationModel.sequelize.models.User;
const user = await UserModel.findByPk(userId, {
attributes: ['email', 'name'],
});
if (user?.email) {
await sendEmail({
to: user.email,
subject: title,
text: message,
});
}
} catch (error) {
console.error('Failed to send email notification:', error);
}
}
async function sendTelegramNotification(
userId,
title,
message,
data,
NotificationModel
) {
try {
const telegramService = require('../modules/telegram/telegramNotificationService');
if (!message) {
return;
}
const UserModel = NotificationModel.sequelize.models.User;
const user = await UserModel.findByPk(userId, {
attributes: [
'id',
'name',
'surname',
'telegram_bot_token',
'telegram_chat_id',
],
});
if (user && telegramService.isTelegramConfigured(user)) {
await telegramService.sendTelegramNotification(user, {
title,
message,
data,
level: 'info',
});
}
} catch (error) {
console.error('Failed to send Telegram notification:', error);
}
}
/**
* Mark a notification as read
*/
Notification.prototype.markAsRead = async function () {
if (!this.read_at) {
this.read_at = new Date();
await this.save();
}
return this;
};
Notification.prototype.markAsUnread = async function () {
this.read_at = null;
await this.save();
return this;
};
Notification.prototype.isRead = function () {
return this.read_at !== null;
};
Notification.prototype.dismiss = async function () {
if (!this.dismissed_at) {
this.dismissed_at = new Date();
await this.save();
}
return this;
};
Notification.prototype.isDismissed = function () {
return this.dismissed_at !== null;
};
Notification.getUserNotifications = async function (userId, options = {}) {
const {
limit = 10,
offset = 0,
includeRead = true,
type = null,
} = options;
const where = {
user_id: userId,
dismissed_at: null,
};
if (!includeRead) {
where.read_at = null;
}
if (type) {
where.type = type;
}
const result = await Notification.findAndCountAll({
where,
order: [['created_at', 'DESC']],
limit,
offset,
});
return {
notifications: result.rows,
total: result.count,
};
};
Notification.getUnreadCount = async function (userId) {
return await Notification.count({
where: {
user_id: userId,
read_at: null,
dismissed_at: null,
},
});
};
Notification.markAllAsRead = async function (userId) {
return await Notification.update(
{ read_at: new Date() },
{
where: {
user_id: userId,
read_at: null,
dismissed_at: null,
},
}
);
};
return Notification;
};