Initial commit MKD fixes
This commit is contained in:
201
backend/notificationService.js
Executable file
201
backend/notificationService.js
Executable file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Сервис уведомлений: создание записей только для затронутых пользователей (user_id).
|
||||
* Не рассылаем «всем по роли» — только конкретным userId из сущности.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Создать одно уведомление для пользователя.
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {object} opts - { userId, type, title, body?, entityType?, entityId?, payload? }
|
||||
* @returns {Promise<{ id: number }>}
|
||||
*/
|
||||
async function createNotification(pool, opts) {
|
||||
const { userId, type, title, body = null, entityType = null, entityId = null, payload = null } = opts;
|
||||
if (!userId || !type || !title) {
|
||||
throw new Error('notificationService.createNotification: userId, type, title required');
|
||||
}
|
||||
const r = await pool.query(
|
||||
`INSERT INTO notifications (user_id, type, title, body, entity_type, entity_id, payload, read_at, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, NOW())
|
||||
RETURNING id`,
|
||||
[userId, type, title, body, entityType, entityId != null ? String(entityId) : null, payload ? JSON.stringify(payload) : null]
|
||||
);
|
||||
return { id: r.rows[0].id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать уведомления для нескольких пользователей (один и тот же текст).
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {number[]} userIds - portal_users.id (без дубликатов)
|
||||
* @param {object} opts - { type, title, body?, entityType?, entityId?, payload? }
|
||||
* @returns {Promise<number>} количество созданных записей
|
||||
*/
|
||||
async function createNotificationForUserIds(pool, userIds, opts) {
|
||||
if (!userIds || userIds.length === 0) return 0;
|
||||
const unique = [...new Set(userIds)].filter(Boolean);
|
||||
let count = 0;
|
||||
for (const uid of unique) {
|
||||
await createNotification(pool, { ...opts, userId: uid });
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Резолв employee_id(ы) в portal_users.id.
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string[]} employeeIds - employees.id
|
||||
* @returns {Promise<number[]>} portal_users.id
|
||||
*/
|
||||
async function resolveEmployeeIdsToUserIds(pool, employeeIds) {
|
||||
if (!employeeIds || employeeIds.length === 0) return [];
|
||||
const unique = [...new Set(employeeIds)].filter(Boolean);
|
||||
const r = await pool.query(
|
||||
`SELECT id FROM portal_users WHERE employee_id = ANY($1::text[])`,
|
||||
[unique]
|
||||
);
|
||||
return r.rows.map(row => Number(row.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Резолв одного employee_id в portal_users.id (или null).
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string} employeeId - employees.id
|
||||
* @returns {Promise<number|null>}
|
||||
*/
|
||||
async function resolveEmployeeIdToUserId(pool, employeeId) {
|
||||
if (!employeeId) return null;
|
||||
const r = await pool.query(
|
||||
`SELECT id FROM portal_users WHERE employee_id = $1 LIMIT 1`,
|
||||
[employeeId]
|
||||
);
|
||||
return r.rows.length ? Number(r.rows[0].id) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Резолв имён сотрудников (performer_name, responsible_name, assigned_to) в portal_users.id.
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string[]} names - массив имён (например ['Иванов И.И.', 'Петров П.П.'])
|
||||
* @returns {Promise<number[]>} portal_users.id (без дубликатов)
|
||||
*/
|
||||
async function resolveEmployeeNamesToUserIds(pool, names) {
|
||||
if (!names || names.length === 0) return [];
|
||||
const trimmed = [...new Set(names.map(n => (n && String(n).trim())).filter(Boolean))];
|
||||
if (trimmed.length === 0) return [];
|
||||
const r = await pool.query(
|
||||
`SELECT DISTINCT pu.id FROM portal_users pu
|
||||
JOIN employees e ON e.id = pu.employee_id
|
||||
WHERE e.name = ANY($1::text[])`,
|
||||
[trimmed]
|
||||
);
|
||||
return r.rows.map(row => Number(row.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать уведомления для сотрудников (по employee_id); только тем, у кого есть portal_users.
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string[]} employeeIds - employees.id
|
||||
* @param {object} opts - { type, title, body?, entityType?, entityId?, payload? }
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function createNotificationForEmployeeIds(pool, employeeIds, opts) {
|
||||
const userIds = await resolveEmployeeIdsToUserIds(pool, employeeIds);
|
||||
return createNotificationForUserIds(pool, userIds, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить portal_users.id сотрудников, ответственных за зону (section + sub_id).
|
||||
* Используется для целевых уведомлений по зонам ответственности.
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string} section - раздел (hr, finance, requests, pr, legal, development, office, admin)
|
||||
* @param {string} subId - подраздел (employees, hiring, invoices, ...)
|
||||
* @returns {Promise<number[]>} portal_users.id
|
||||
*/
|
||||
async function getResponsibleUserIdsForZone(pool, section, subId) {
|
||||
if (!section || !subId) return [];
|
||||
const r = await pool.query(
|
||||
`SELECT DISTINCT pu.id FROM portal_users pu
|
||||
JOIN employee_responsibility er ON er.employee_id = pu.employee_id
|
||||
WHERE er.section = $1 AND er.sub_id = $2`,
|
||||
[section, subId]
|
||||
);
|
||||
return r.rows.map(row => Number(row.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать уведомления для ответственных за зону (section + sub_id).
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string} section - раздел
|
||||
* @param {string} subId - подраздел
|
||||
* @param {object} opts - { type, title, body?, entityType?, entityId?, payload? }
|
||||
* @returns {Promise<number>} количество созданных уведомлений
|
||||
*/
|
||||
async function createNotificationForResponsibleZone(pool, section, subId, opts) {
|
||||
const userIds = await getResponsibleUserIdsForZone(pool, section, subId);
|
||||
return createNotificationForUserIds(pool, userIds, opts);
|
||||
}
|
||||
|
||||
const SECTION_IDS = ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'];
|
||||
const ROLE_ACCESS = {
|
||||
DIRECTOR: ['all'],
|
||||
ENGINEER: ['dashboard', 'objects', 'requests', 'office', 'development'],
|
||||
MASTER: ['objects', 'requests'],
|
||||
LAWYER: ['dashboard', 'legal', 'objects', 'requests'],
|
||||
FINANCIER: ['dashboard', 'finance', 'office', 'objects'],
|
||||
HR_MANAGER: ['dashboard', 'hr', 'office'],
|
||||
PR_MANAGER: ['dashboard', 'pr', 'requests']
|
||||
};
|
||||
|
||||
function allowedSectionsFromPermissions(permissions) {
|
||||
if (!permissions || !Array.isArray(permissions) || permissions.length === 0) return null;
|
||||
if (permissions.includes('all')) return SECTION_IDS;
|
||||
const set = new Set();
|
||||
for (const p of permissions) {
|
||||
if (SECTION_IDS.includes(p)) set.add(p);
|
||||
else if (typeof p === 'string' && p.includes('_')) {
|
||||
const section = p.split('_')[0];
|
||||
if (SECTION_IDS.includes(section)) set.add(section);
|
||||
}
|
||||
}
|
||||
return Array.from(set);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить portal_users.id пользователей, у которых есть доступ к любому из указанных разделов (по permissions или роли).
|
||||
* @param {object} pool - pg Pool
|
||||
* @param {string[]} sections - разделы (dashboard, pr, finance, legal, development, hr, office, ...)
|
||||
* @returns {Promise<number[]>} portal_users.id
|
||||
*/
|
||||
async function getPortalUserIdsBySections(pool, sections) {
|
||||
if (!sections || sections.length === 0) return [];
|
||||
const r = await pool.query(
|
||||
`SELECT id, role, permissions FROM portal_users`
|
||||
);
|
||||
const wanted = new Set(sections);
|
||||
const userIds = [];
|
||||
for (const row of r.rows) {
|
||||
let allowed;
|
||||
if (row.permissions && Array.isArray(row.permissions) && row.permissions.length > 0) {
|
||||
allowed = allowedSectionsFromPermissions(row.permissions);
|
||||
} else {
|
||||
const roleSections = ROLE_ACCESS[row.role];
|
||||
allowed = roleSections && roleSections.includes('all') ? SECTION_IDS : (roleSections || []);
|
||||
}
|
||||
if (allowed && allowed.some(s => wanted.has(s))) {
|
||||
userIds.push(Number(row.id));
|
||||
}
|
||||
}
|
||||
return userIds;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createNotification,
|
||||
createNotificationForUserIds,
|
||||
createNotificationForEmployeeIds,
|
||||
createNotificationForResponsibleZone,
|
||||
resolveEmployeeIdsToUserIds,
|
||||
resolveEmployeeIdToUserId,
|
||||
resolveEmployeeNamesToUserIds,
|
||||
getResponsibleUserIdsForZone,
|
||||
getPortalUserIdsBySections,
|
||||
};
|
||||
Reference in New Issue
Block a user