admin-profiles.html
This commit is contained in:
563
admin-doc.js
Normal file
563
admin-doc.js
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
// admin-doc.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Middleware для проверки прав администратора
|
||||||
|
const requireAdmin = (req, res, next) => {
|
||||||
|
if (!req.session.user || req.session.user.role !== 'admin') {
|
||||||
|
return res.status(403).json({ error: 'Недостаточно прав' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Настройка загрузки файлов для шаблонов документов
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const uploadDir = path.join(__dirname, 'data', 'uploads', 'templates');
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, uploadDir);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
|
||||||
|
cb(null, uniqueName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
const allowedTypes = ['.doc', '.docx', '.pdf', '.txt', '.rtf'];
|
||||||
|
const ext = path.extname(file.originalname).toLowerCase();
|
||||||
|
if (allowedTypes.includes(ext)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Недопустимый тип файла. Разрешены: .doc, .docx, .pdf, .txt, .rtf'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение всех типов документов
|
||||||
|
router.get('/admin/doc/types', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT dt.*,
|
||||||
|
COUNT(DISTINCT a.id) as approval_stages_count,
|
||||||
|
COUNT(DISTINCT d.id) as documents_count
|
||||||
|
FROM document_types dt
|
||||||
|
LEFT JOIN approval_stages a ON dt.id = a.document_type_id
|
||||||
|
LEFT JOIN documents d ON dt.id = d.document_type_id
|
||||||
|
GROUP BY dt.id
|
||||||
|
ORDER BY dt.name
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [], (err, types) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения типов документов:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
res.json(types);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение конкретного типа документа
|
||||||
|
router.get('/admin/doc/types/:id', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.id;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT dt.*
|
||||||
|
FROM document_types dt
|
||||||
|
WHERE dt.id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.get(query, [typeId], (err, docType) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!docType) {
|
||||||
|
return res.status(404).json({ error: 'Тип документа не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(docType);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание нового типа документа
|
||||||
|
router.post('/admin/doc/types', requireAdmin, upload.single('template'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const { name, description } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Название типа документа обязательно' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли тип с таким названием
|
||||||
|
db.get("SELECT id FROM document_types WHERE name = ?", [name], (err, existing) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.status(400).json({ error: 'Тип документа с таким названием уже существует' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatePath = req.file ? req.file.path : null;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO document_types (name, description, template_path)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(query, [name, description || '', templatePath], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка создания типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Тип документа создан',
|
||||||
|
typeId: this.lastID
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновление типа документа
|
||||||
|
router.put('/admin/doc/types/:id', requireAdmin, upload.single('template'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.id;
|
||||||
|
const { name, description } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Название типа документа обязательно' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли другой тип с таким названием
|
||||||
|
db.get("SELECT id FROM document_types WHERE name = ? AND id != ?", [name, typeId], (err, existing) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.status(400).json({ error: 'Тип документа с таким названием уже существует' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущий тип для удаления старого файла
|
||||||
|
db.get("SELECT template_path FROM document_types WHERE id = ?", [typeId], (err, currentType) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let templatePath = currentType?.template_path;
|
||||||
|
|
||||||
|
// Если загружен новый файл
|
||||||
|
if (req.file) {
|
||||||
|
// Удаляем старый файл, если он существует
|
||||||
|
if (currentType?.template_path && fs.existsSync(currentType.template_path)) {
|
||||||
|
fs.unlinkSync(currentType.template_path);
|
||||||
|
}
|
||||||
|
templatePath = req.file.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
UPDATE document_types
|
||||||
|
SET name = ?, description = ?, template_path = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(query, [name, description || '', templatePath, typeId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
return res.status(404).json({ error: 'Тип документа не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Тип документа обновлен' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление типа документа
|
||||||
|
router.delete('/admin/doc/types/:id', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.id;
|
||||||
|
|
||||||
|
// Проверяем, есть ли документы этого типа
|
||||||
|
db.get("SELECT COUNT(*) as count FROM documents WHERE document_type_id = ?", [typeId], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки документов:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.count > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Нельзя удалить тип документа, так как есть документы этого типа'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем путь к файлу шаблона
|
||||||
|
db.get("SELECT template_path FROM document_types WHERE id = ?", [typeId], (err, docType) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файл шаблона, если он существует
|
||||||
|
if (docType?.template_path && fs.existsSync(docType.template_path)) {
|
||||||
|
fs.unlinkSync(docType.template_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сначала удаляем этапы согласования
|
||||||
|
db.run("DELETE FROM approval_stages WHERE document_type_id = ?", [typeId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка удаления этапов согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Затем удаляем тип документа
|
||||||
|
db.run("DELETE FROM document_types WHERE id = ?", [typeId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка удаления типа документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
return res.status(404).json({ error: 'Тип документа не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Тип документа удален' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение этапов согласования для типа документа
|
||||||
|
router.get('/admin/doc/types/:typeId/stages', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.typeId;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT a.*, u.name as approver_name
|
||||||
|
FROM approval_stages a
|
||||||
|
LEFT JOIN users u ON a.approver_user_id = u.id
|
||||||
|
WHERE a.document_type_id = ?
|
||||||
|
ORDER BY a.stage_number
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [typeId], (err, stages) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения этапов согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(stages || []);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавление этапа согласования
|
||||||
|
router.post('/admin/doc/types/:typeId/stages', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.typeId;
|
||||||
|
const {
|
||||||
|
stage_number,
|
||||||
|
stage_name,
|
||||||
|
approver_role,
|
||||||
|
approver_user_id,
|
||||||
|
is_required = true,
|
||||||
|
can_edit = false,
|
||||||
|
can_comment = true,
|
||||||
|
deadline_days
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!stage_number || !stage_name) {
|
||||||
|
return res.status(400).json({ error: 'Номер и название этапа обязательны' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!approver_role && !approver_user_id) {
|
||||||
|
return res.status(400).json({ error: 'Укажите роль или конкретного пользователя для согласования' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли этап с таким номером
|
||||||
|
db.get("SELECT id FROM approval_stages WHERE document_type_id = ? AND stage_number = ?",
|
||||||
|
[typeId, stage_number], (err, existing) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки этапа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.status(400).json({ error: 'Этап с таким номером уже существует' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO approval_stages
|
||||||
|
(document_type_id, stage_number, stage_name, approver_role,
|
||||||
|
approver_user_id, is_required, can_edit, can_comment, deadline_days)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(query, [
|
||||||
|
typeId, stage_number, stage_name, approver_role || null,
|
||||||
|
approver_user_id || null, is_required ? 1 : 0,
|
||||||
|
can_edit ? 1 : 0, can_comment ? 1 : 0, deadline_days
|
||||||
|
], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка создания этапа согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Этап согласования добавлен',
|
||||||
|
stageId: this.lastID
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновление этапа согласования
|
||||||
|
router.put('/admin/doc/types/:typeId/stages/:stageId', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.typeId;
|
||||||
|
const stageId = req.params.stageId;
|
||||||
|
const {
|
||||||
|
stage_number,
|
||||||
|
stage_name,
|
||||||
|
approver_role,
|
||||||
|
approver_user_id,
|
||||||
|
is_required,
|
||||||
|
can_edit,
|
||||||
|
can_comment,
|
||||||
|
deadline_days
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!stage_number || !stage_name) {
|
||||||
|
return res.status(400).json({ error: 'Номер и название этапа обязательны' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли другой этап с таким номером
|
||||||
|
db.get("SELECT id FROM approval_stages WHERE document_type_id = ? AND stage_number = ? AND id != ?",
|
||||||
|
[typeId, stage_number, stageId], (err, existing) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки этапа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return res.status(400).json({ error: 'Этап с таким номером уже существует' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
UPDATE approval_stages
|
||||||
|
SET stage_number = ?, stage_name = ?, approver_role = ?,
|
||||||
|
approver_user_id = ?, is_required = ?, can_edit = ?,
|
||||||
|
can_comment = ?, deadline_days = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND document_type_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(query, [
|
||||||
|
stage_number, stage_name, approver_role || null,
|
||||||
|
approver_user_id || null, is_required ? 1 : 0,
|
||||||
|
can_edit ? 1 : 0, can_comment ? 1 : 0, deadline_days,
|
||||||
|
stageId, typeId
|
||||||
|
], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления этапа согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
return res.status(404).json({ error: 'Этап согласования не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Этап согласования обновлен' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление этапа согласования
|
||||||
|
router.delete('/admin/doc/types/:typeId/stages/:stageId', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const typeId = req.params.typeId;
|
||||||
|
const stageId = req.params.stageId;
|
||||||
|
|
||||||
|
db.run("DELETE FROM approval_stages WHERE id = ? AND document_type_id = ?",
|
||||||
|
[stageId, typeId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка удаления этапа согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
return res.status(404).json({ error: 'Этап согласования не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Этап согласования удален' });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение всех пользователей для выбора согласующих
|
||||||
|
router.get('/admin/doc/approvers', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT id, name, login, email, role
|
||||||
|
FROM users
|
||||||
|
WHERE role IN ('admin', 'teacher')
|
||||||
|
ORDER BY name
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [], (err, users) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения пользователей:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
res.json(users);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение статистики по документам
|
||||||
|
router.get('/admin/doc/stats', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const stats = await new Promise((resolve, reject) => {
|
||||||
|
db.get(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_documents,
|
||||||
|
COUNT(CASE WHEN status = 'draft' THEN 1 END) as draft_documents,
|
||||||
|
COUNT(CASE WHEN status = 'in_review' THEN 1 END) as in_review_documents,
|
||||||
|
COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_documents,
|
||||||
|
COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_documents,
|
||||||
|
COUNT(DISTINCT created_by) as unique_authors,
|
||||||
|
COUNT(DISTINCT document_type_id) as types_used
|
||||||
|
FROM documents
|
||||||
|
`, [], (err, row) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(row || {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentDocuments = await new Promise((resolve, reject) => {
|
||||||
|
db.all(`
|
||||||
|
SELECT d.id, d.title, d.status, d.created_at,
|
||||||
|
u.name as author_name, dt.name as type_name
|
||||||
|
FROM documents d
|
||||||
|
LEFT JOIN users u ON d.created_by = u.id
|
||||||
|
LEFT JOIN document_types dt ON d.document_type_id = dt.id
|
||||||
|
ORDER BY d.created_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
`, [], (err, rows) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeStats = await new Promise((resolve, reject) => {
|
||||||
|
db.all(`
|
||||||
|
SELECT dt.name,
|
||||||
|
COUNT(d.id) as document_count,
|
||||||
|
COUNT(CASE WHEN d.status = 'approved' THEN 1 END) as approved_count,
|
||||||
|
COUNT(CASE WHEN d.status = 'rejected' THEN 1 END) as rejected_count
|
||||||
|
FROM document_types dt
|
||||||
|
LEFT JOIN documents d ON dt.id = d.document_type_id
|
||||||
|
GROUP BY dt.id
|
||||||
|
ORDER BY document_count DESC
|
||||||
|
`, [], (err, rows) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
stats,
|
||||||
|
recentDocuments,
|
||||||
|
typeStats,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения статистики:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка получения статистики' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
104
database.js
104
database.js
@@ -194,6 +194,110 @@ function createSQLiteTables() {
|
|||||||
|
|
||||||
console.log('✅ Таблица для пользовательских настроек инициализирована');
|
console.log('✅ Таблица для пользовательских настроек инициализирована');
|
||||||
|
|
||||||
|
// Таблица для типов документов
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS document_types (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
template_path TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для документов
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS documents (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
document_type_id INTEGER NOT NULL,
|
||||||
|
status TEXT DEFAULT 'draft',
|
||||||
|
created_by INTEGER NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
approved_at DATETIME,
|
||||||
|
approved_by INTEGER,
|
||||||
|
rejected_at DATETIME,
|
||||||
|
rejected_by INTEGER,
|
||||||
|
rejection_reason TEXT,
|
||||||
|
file_path TEXT,
|
||||||
|
file_name TEXT,
|
||||||
|
file_size INTEGER,
|
||||||
|
version INTEGER DEFAULT 1,
|
||||||
|
parent_document_id INTEGER,
|
||||||
|
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (approved_by) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (rejected_by) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (parent_document_id) REFERENCES documents (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для этапов согласования
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
document_type_id INTEGER NOT NULL,
|
||||||
|
stage_number INTEGER NOT NULL,
|
||||||
|
stage_name TEXT NOT NULL,
|
||||||
|
approver_role TEXT,
|
||||||
|
approver_user_id INTEGER,
|
||||||
|
is_required BOOLEAN DEFAULT true,
|
||||||
|
can_edit BOOLEAN DEFAULT false,
|
||||||
|
can_comment BOOLEAN DEFAULT true,
|
||||||
|
deadline_days INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(document_type_id, stage_number),
|
||||||
|
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
|
||||||
|
FOREIGN KEY (approver_user_id) REFERENCES users (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для согласования документов
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
document_id INTEGER NOT NULL,
|
||||||
|
stage_id INTEGER NOT NULL,
|
||||||
|
approver_user_id INTEGER NOT NULL,
|
||||||
|
status TEXT DEFAULT 'pending',
|
||||||
|
comments TEXT,
|
||||||
|
approved_at DATETIME,
|
||||||
|
rejected_at DATETIME,
|
||||||
|
deadline DATETIME,
|
||||||
|
notified_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(document_id, stage_id, approver_user_id),
|
||||||
|
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (stage_id) REFERENCES approval_stages (id),
|
||||||
|
FOREIGN KEY (approver_user_id) REFERENCES users (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для комментариев к документам
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
document_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
comment TEXT NOT NULL,
|
||||||
|
is_internal BOOLEAN DEFAULT false,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для истории изменений документов
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS document_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
document_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
changes TEXT,
|
||||||
|
version INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
console.log('✅ Таблицы для согласования документов созданы');
|
||||||
|
|
||||||
// Запускаем проверку и обновление структуры таблиц
|
// Запускаем проверку и обновление структуры таблиц
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkAndUpdateTableStructure();
|
checkAndUpdateTableStructure();
|
||||||
|
|||||||
953
doc-endpoints.js
Normal file
953
doc-endpoints.js
Normal file
@@ -0,0 +1,953 @@
|
|||||||
|
// doc-endpoints.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Middleware для проверки авторизации
|
||||||
|
const requireAuth = (req, res, next) => {
|
||||||
|
if (!req.session.user) {
|
||||||
|
return res.status(401).json({ error: 'Требуется аутентификация' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Настройка загрузки файлов для документов
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const uploadDir = path.join(__dirname, 'data', 'uploads', 'documents');
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, uploadDir);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
|
||||||
|
cb(null, uniqueName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
const allowedTypes = ['.doc', '.docx', '.pdf', '.txt', '.rtf', '.xls', '.xlsx', '.ppt', '.pptx', '.jpg', '.jpeg', '.png'];
|
||||||
|
const ext = path.extname(file.originalname).toLowerCase();
|
||||||
|
if (allowedTypes.includes(ext)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Недопустимый тип файла'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Вспомогательная функция для логирования действий с документами
|
||||||
|
function logDocumentActivity(db, documentId, userId, action, changes = '') {
|
||||||
|
db.run(
|
||||||
|
"INSERT INTO document_history (document_id, user_id, action, changes) VALUES (?, ?, ?, ?)",
|
||||||
|
[documentId, userId, action, changes]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение доступных типов документов для создания
|
||||||
|
router.get('/api/doc/types', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT dt.*,
|
||||||
|
COUNT(DISTINCT a.id) as stages_count
|
||||||
|
FROM document_types dt
|
||||||
|
LEFT JOIN approval_stages a ON dt.id = a.document_type_id
|
||||||
|
GROUP BY dt.id
|
||||||
|
ORDER BY dt.name
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [], (err, types) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения типов документов:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
res.json(types);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание нового документа
|
||||||
|
router.post('/api/documents', requireAuth, upload.single('file'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const { title, description, document_type_id } = req.body;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
if (!title || !document_type_id) {
|
||||||
|
return res.status(400).json({ error: 'Название и тип документа обязательны' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'Файл документа обязателен' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем существование типа документа
|
||||||
|
db.get("SELECT id FROM document_types WHERE id = ?", [document_type_id], (err, docType) => {
|
||||||
|
if (err || !docType) {
|
||||||
|
return res.status(400).json({ error: 'Тип документа не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
// Создаем документ
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO documents
|
||||||
|
(title, description, document_type_id, status, created_by,
|
||||||
|
file_path, file_name, file_size)
|
||||||
|
VALUES (?, ?, ?, 'draft', ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
title,
|
||||||
|
description || '',
|
||||||
|
document_type_id,
|
||||||
|
userId,
|
||||||
|
req.file.path,
|
||||||
|
req.file.originalname,
|
||||||
|
req.file.size
|
||||||
|
],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка создания документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentId = this.lastID;
|
||||||
|
|
||||||
|
// Логируем создание
|
||||||
|
logDocumentActivity(db, documentId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`);
|
||||||
|
|
||||||
|
// Получаем этапы согласования для этого типа документа
|
||||||
|
db.all(
|
||||||
|
"SELECT * FROM approval_stages WHERE document_type_id = ? ORDER BY stage_number",
|
||||||
|
[document_type_id],
|
||||||
|
(err, stages) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения этапов согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем записи для согласования
|
||||||
|
if (stages && stages.length > 0) {
|
||||||
|
stages.forEach(stage => {
|
||||||
|
// Определяем пользователя для согласования
|
||||||
|
let approverUserId = stage.approver_user_id;
|
||||||
|
|
||||||
|
// Если указана роль, находим пользователя с этой ролью
|
||||||
|
if (stage.approver_role && !approverUserId) {
|
||||||
|
db.get(
|
||||||
|
"SELECT id FROM users WHERE role = ? LIMIT 1",
|
||||||
|
[stage.approver_role],
|
||||||
|
(err, user) => {
|
||||||
|
if (user) {
|
||||||
|
approverUserId = user.id;
|
||||||
|
}
|
||||||
|
createApprovalRecord();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createApprovalRecord();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApprovalRecord() {
|
||||||
|
if (!approverUserId) {
|
||||||
|
console.log('⚠️ Не указан согласующий для этапа:', stage.stage_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем дедлайн, если указано
|
||||||
|
let deadline = null;
|
||||||
|
if (stage.deadline_days) {
|
||||||
|
deadline = new Date();
|
||||||
|
deadline.setDate(deadline.getDate() + stage.deadline_days);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO document_approvals
|
||||||
|
(document_id, stage_id, approver_user_id, deadline)
|
||||||
|
VALUES (?, ?, ?, ?)`,
|
||||||
|
[documentId, stage.id, approverUserId, deadline]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Документ создан',
|
||||||
|
documentId: documentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение списка документов пользователя
|
||||||
|
router.get('/api/documents', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
const { status, type, search } = req.query;
|
||||||
|
|
||||||
|
let query = `
|
||||||
|
SELECT DISTINCT d.*,
|
||||||
|
u.name as author_name,
|
||||||
|
dt.name as type_name,
|
||||||
|
dt.id as type_id,
|
||||||
|
(SELECT COUNT(*) FROM document_approvals da
|
||||||
|
WHERE da.document_id = d.id AND da.approver_user_id = ? AND da.status = 'pending') as pending_for_me
|
||||||
|
FROM documents d
|
||||||
|
LEFT JOIN users u ON d.created_by = u.id
|
||||||
|
LEFT JOIN document_types dt ON d.document_type_id = dt.id
|
||||||
|
LEFT JOIN document_approvals da ON d.id = da.document_id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const params = [userId];
|
||||||
|
|
||||||
|
// Фильтр по статусу
|
||||||
|
if (status && status !== 'all') {
|
||||||
|
query += ` AND d.status = ?`;
|
||||||
|
params.push(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтр по типу
|
||||||
|
if (type && type !== 'all') {
|
||||||
|
query += ` AND d.document_type_id = ?`;
|
||||||
|
params.push(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск
|
||||||
|
if (search) {
|
||||||
|
query += ` AND (d.title LIKE ? OR d.description LIKE ?)`;
|
||||||
|
const searchPattern = `%${search}%`;
|
||||||
|
params.push(searchPattern, searchPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем документы, которые создал пользователь или которые ему нужно согласовать
|
||||||
|
query += ` AND (d.created_by = ? OR da.approver_user_id = ?)`;
|
||||||
|
params.push(userId, userId);
|
||||||
|
|
||||||
|
query += ` GROUP BY d.id ORDER BY d.created_at DESC`;
|
||||||
|
|
||||||
|
db.all(query, params, async (err, documents) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения документов:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем информацию о согласовании
|
||||||
|
const documentsWithApprovals = await Promise.all(documents.map(async (doc) => {
|
||||||
|
const approvals = await new Promise((resolve) => {
|
||||||
|
db.all(`
|
||||||
|
SELECT da.*, a.stage_number, a.stage_name,
|
||||||
|
u.name as approver_name, u.login as approver_login
|
||||||
|
FROM document_approvals da
|
||||||
|
LEFT JOIN approval_stages a ON da.stage_id = a.id
|
||||||
|
LEFT JOIN users u ON da.approver_user_id = u.id
|
||||||
|
WHERE da.document_id = ?
|
||||||
|
ORDER BY a.stage_number
|
||||||
|
`, [doc.id], (err, rows) => {
|
||||||
|
resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
approvals
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(documentsWithApprovals);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение информации о конкретном документе
|
||||||
|
router.get('/api/documents/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Проверяем доступ к документу
|
||||||
|
db.get(`
|
||||||
|
SELECT 1 FROM documents d
|
||||||
|
LEFT JOIN document_approvals da ON d.id = da.document_id
|
||||||
|
WHERE d.id = ? AND (d.created_by = ? OR da.approver_user_id = ?)
|
||||||
|
LIMIT 1
|
||||||
|
`, [documentId, userId, userId], (err, hasAccess) => {
|
||||||
|
if (err || !hasAccess) {
|
||||||
|
return res.status(403).json({ error: 'Доступ к документу запрещен' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о документе
|
||||||
|
db.get(`
|
||||||
|
SELECT d.*,
|
||||||
|
u.name as author_name, u.login as author_login,
|
||||||
|
dt.name as type_name, dt.description as type_description,
|
||||||
|
au.name as approver_name, ru.name as rejector_name
|
||||||
|
FROM documents d
|
||||||
|
LEFT JOIN users u ON d.created_by = u.id
|
||||||
|
LEFT JOIN document_types dt ON d.document_type_id = dt.id
|
||||||
|
LEFT JOIN users au ON d.approved_by = au.id
|
||||||
|
LEFT JOIN users ru ON d.rejected_by = ru.id
|
||||||
|
WHERE d.id = ?
|
||||||
|
`, [documentId], async (err, document) => {
|
||||||
|
if (err || !document) {
|
||||||
|
return res.status(404).json({ error: 'Документ не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем этапы согласования
|
||||||
|
const approvals = await new Promise((resolve) => {
|
||||||
|
db.all(`
|
||||||
|
SELECT da.*, a.stage_number, a.stage_name, a.can_edit, a.can_comment,
|
||||||
|
u.name as approver_name, u.login as approver_login,
|
||||||
|
CASE
|
||||||
|
WHEN da.approver_user_id = ? THEN 'current'
|
||||||
|
WHEN da.status = 'pending' AND a.stage_number < (
|
||||||
|
SELECT MIN(a2.stage_number)
|
||||||
|
FROM document_approvals da2
|
||||||
|
LEFT JOIN approval_stages a2 ON da2.stage_id = a2.id
|
||||||
|
WHERE da2.document_id = ? AND da2.status = 'pending'
|
||||||
|
) THEN 'completed'
|
||||||
|
ELSE 'future'
|
||||||
|
END as user_relation
|
||||||
|
FROM document_approvals da
|
||||||
|
LEFT JOIN approval_stages a ON da.stage_id = a.id
|
||||||
|
LEFT JOIN users u ON da.approver_user_id = u.id
|
||||||
|
WHERE da.document_id = ?
|
||||||
|
ORDER BY a.stage_number
|
||||||
|
`, [userId, documentId, documentId], (err, rows) => {
|
||||||
|
resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получаем комментарии
|
||||||
|
const comments = await new Promise((resolve) => {
|
||||||
|
db.all(`
|
||||||
|
SELECT dc.*, u.name as user_name, u.login as user_login
|
||||||
|
FROM document_comments dc
|
||||||
|
LEFT JOIN users u ON dc.user_id = u.id
|
||||||
|
WHERE dc.document_id = ?
|
||||||
|
ORDER BY dc.created_at DESC
|
||||||
|
`, [documentId], (err, rows) => {
|
||||||
|
resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получаем историю
|
||||||
|
const history = await new Promise((resolve) => {
|
||||||
|
db.all(`
|
||||||
|
SELECT dh.*, u.name as user_name
|
||||||
|
FROM document_history dh
|
||||||
|
LEFT JOIN users u ON dh.user_id = u.id
|
||||||
|
WHERE dh.document_id = ?
|
||||||
|
ORDER BY dh.created_at DESC
|
||||||
|
LIMIT 20
|
||||||
|
`, [documentId], (err, rows) => {
|
||||||
|
resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Определяем текущий этап для пользователя
|
||||||
|
let currentStage = null;
|
||||||
|
if (document.status === 'in_review') {
|
||||||
|
currentStage = approvals.find(a =>
|
||||||
|
a.approver_user_id === userId && a.status === 'pending'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем права на редактирование
|
||||||
|
let canEdit = false;
|
||||||
|
let canApprove = false;
|
||||||
|
let canComment = true;
|
||||||
|
|
||||||
|
if (document.created_by === userId && document.status === 'draft') {
|
||||||
|
canEdit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStage) {
|
||||||
|
const stageInfo = approvals.find(a => a.id === currentStage.id);
|
||||||
|
canEdit = stageInfo?.can_edit || false;
|
||||||
|
canApprove = true;
|
||||||
|
canComment = stageInfo?.can_comment || true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
document,
|
||||||
|
approvals,
|
||||||
|
comments,
|
||||||
|
history,
|
||||||
|
permissions: {
|
||||||
|
canEdit,
|
||||||
|
canApprove,
|
||||||
|
canComment,
|
||||||
|
canDownload: true,
|
||||||
|
canDelete: document.created_by === userId && document.status === 'draft'
|
||||||
|
},
|
||||||
|
currentStage
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Скачивание документа
|
||||||
|
router.get('/api/documents/:id/download', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Проверяем доступ к документу
|
||||||
|
db.get(`
|
||||||
|
SELECT 1 FROM documents d
|
||||||
|
LEFT JOIN document_approvals da ON d.id = da.document_id
|
||||||
|
WHERE d.id = ? AND (d.created_by = ? OR da.approver_user_id = ?)
|
||||||
|
LIMIT 1
|
||||||
|
`, [documentId, userId, userId], (err, hasAccess) => {
|
||||||
|
if (err || !hasAccess) {
|
||||||
|
return res.status(403).json({ error: 'Доступ к документу запрещен' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о файле
|
||||||
|
db.get("SELECT file_path, file_name FROM documents WHERE id = ?", [documentId], (err, doc) => {
|
||||||
|
if (err || !doc) {
|
||||||
|
return res.status(404).json({ error: 'Документ не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(doc.file_path)) {
|
||||||
|
return res.status(404).json({ error: 'Файл не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для скачивания
|
||||||
|
const encodedFileName = encodeURIComponent(doc.file_name);
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
|
||||||
|
res.setHeader('Content-Type', 'application/octet-stream');
|
||||||
|
|
||||||
|
// Отправляем файл
|
||||||
|
res.sendFile(doc.file_path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправка документа на согласование
|
||||||
|
router.post('/api/documents/:id/submit', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Проверяем, что документ принадлежит пользователю и имеет статус черновика
|
||||||
|
db.get("SELECT status, created_by FROM documents WHERE id = ?", [documentId], (err, document) => {
|
||||||
|
if (err || !document) {
|
||||||
|
return res.status(404).json({ error: 'Документ не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.created_by !== userId) {
|
||||||
|
return res.status(403).json({ error: 'Недостаточно прав' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.status !== 'draft') {
|
||||||
|
return res.status(400).json({ error: 'Документ уже отправлен на согласование' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
// Обновляем статус документа
|
||||||
|
db.run(
|
||||||
|
"UPDATE documents SET status = 'in_review', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||||
|
[documentId],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления статуса:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем действие
|
||||||
|
logDocumentActivity(db, documentId, userId, 'DOCUMENT_SUBMITTED', 'Документ отправлен на согласование');
|
||||||
|
|
||||||
|
// Отправляем уведомления согласующим первого этапа
|
||||||
|
db.all(`
|
||||||
|
SELECT da.approver_user_id, u.email, u.name
|
||||||
|
FROM document_approvals da
|
||||||
|
LEFT JOIN users u ON da.approver_user_id = u.id
|
||||||
|
WHERE da.document_id = ? AND da.status = 'pending'
|
||||||
|
ORDER BY (
|
||||||
|
SELECT stage_number FROM approval_stages WHERE id = da.stage_id
|
||||||
|
) LIMIT 1
|
||||||
|
`, [documentId], (err, approvers) => {
|
||||||
|
if (!err && approvers && approvers.length > 0) {
|
||||||
|
// Здесь можно добавить отправку email уведомлений
|
||||||
|
console.log(`📄 Документ ${documentId} отправлен на согласование`);
|
||||||
|
approvers.forEach(approver => {
|
||||||
|
console.log(` 👤 Уведомить: ${approver.name} (${approver.email})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Документ отправлен на согласование' });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Согласование документа
|
||||||
|
router.post('/api/documents/:id/approve', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
const { comments } = req.body;
|
||||||
|
|
||||||
|
// Проверяем, что пользователь является текущим согласующим
|
||||||
|
db.get(`
|
||||||
|
SELECT da.*, a.stage_number
|
||||||
|
FROM document_approvals da
|
||||||
|
LEFT JOIN approval_stages a ON da.stage_id = a.id
|
||||||
|
WHERE da.document_id = ? AND da.approver_user_id = ? AND da.status = 'pending'
|
||||||
|
ORDER BY a.stage_number
|
||||||
|
LIMIT 1
|
||||||
|
`, [documentId, userId], (err, approval) => {
|
||||||
|
if (err || !approval) {
|
||||||
|
return res.status(403).json({ error: 'Вы не являетесь текущим согласующим' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
// Обновляем запись согласования
|
||||||
|
db.run(
|
||||||
|
`UPDATE document_approvals
|
||||||
|
SET status = 'approved', comments = ?, approved_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`,
|
||||||
|
[comments || '', approval.id],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления согласования:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем действие
|
||||||
|
logDocumentActivity(db, documentId, userId, 'STAGE_APPROVED',
|
||||||
|
`Этап ${approval.stage_number} согласован`);
|
||||||
|
|
||||||
|
// Добавляем комментарий, если он есть
|
||||||
|
if (comments) {
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO document_comments (document_id, user_id, comment, is_internal)
|
||||||
|
VALUES (?, ?, ?, 0)`,
|
||||||
|
[documentId, userId, comments]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, остались ли этапы для согласования
|
||||||
|
db.get(`
|
||||||
|
SELECT COUNT(*) as pending_count
|
||||||
|
FROM document_approvals
|
||||||
|
WHERE document_id = ? AND status = 'pending'
|
||||||
|
`, [documentId], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки оставшихся этапов:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.pending_count === 0) {
|
||||||
|
// Все этапы согласованы
|
||||||
|
db.run(
|
||||||
|
`UPDATE documents
|
||||||
|
SET status = 'approved', approved_at = CURRENT_TIMESTAMP,
|
||||||
|
approved_by = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`,
|
||||||
|
[userId, documentId],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления статуса документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
logDocumentActivity(db, documentId, userId, 'DOCUMENT_APPROVED',
|
||||||
|
'Документ полностью согласован');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Документ полностью согласован',
|
||||||
|
documentStatus: 'approved'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Переходим к следующему этапу
|
||||||
|
db.get(`
|
||||||
|
SELECT da.approver_user_id, u.email, u.name
|
||||||
|
FROM document_approvals da
|
||||||
|
LEFT JOIN users u ON da.approver_user_id = u.id
|
||||||
|
WHERE da.document_id = ? AND da.status = 'pending'
|
||||||
|
ORDER BY (
|
||||||
|
SELECT stage_number FROM approval_stages WHERE id = da.stage_id
|
||||||
|
) LIMIT 1
|
||||||
|
`, [documentId], (err, nextApprover) => {
|
||||||
|
if (!err && nextApprover) {
|
||||||
|
// Здесь можно добавить отправку уведомления следующему согласующему
|
||||||
|
console.log(`📄 Переход к следующему согласующему: ${nextApprover.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Этап согласован',
|
||||||
|
documentStatus: 'in_review'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отклонение документа
|
||||||
|
router.post('/api/documents/:id/reject', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
const { reason } = req.body;
|
||||||
|
|
||||||
|
if (!reason) {
|
||||||
|
return res.status(400).json({ error: 'Укажите причину отклонения' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что пользователь является текущим согласующим или создателем
|
||||||
|
db.get(`
|
||||||
|
SELECT d.created_by, da.*, a.stage_number
|
||||||
|
FROM documents d
|
||||||
|
LEFT JOIN document_approvals da ON d.id = da.document_id AND da.approver_user_id = ? AND da.status = 'pending'
|
||||||
|
LEFT JOIN approval_stages a ON da.stage_id = a.id
|
||||||
|
WHERE d.id = ?
|
||||||
|
`, [userId, documentId], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка проверки прав:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const canReject = result?.created_by === userId || result?.id; // Создатель или текущий согласующий
|
||||||
|
|
||||||
|
if (!canReject) {
|
||||||
|
return res.status(403).json({ error: 'Недостаточно прав для отклонения документа' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
// Если отклоняет согласующий, отмечаем его этап
|
||||||
|
if (result.id) {
|
||||||
|
db.run(
|
||||||
|
`UPDATE document_approvals
|
||||||
|
SET status = 'rejected', comments = ?, rejected_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`,
|
||||||
|
[reason, result.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
logDocumentActivity(db, documentId, userId, 'STAGE_REJECTED',
|
||||||
|
`Этап ${result.stage_number} отклонен: ${reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статус документа
|
||||||
|
db.run(
|
||||||
|
`UPDATE documents
|
||||||
|
SET status = 'rejected', rejected_at = CURRENT_TIMESTAMP,
|
||||||
|
rejected_by = ?, rejection_reason = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`,
|
||||||
|
[userId, reason, documentId],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления статуса документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем комментарий
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO document_comments (document_id, user_id, comment, is_internal)
|
||||||
|
VALUES (?, ?, ?, 0)`,
|
||||||
|
[documentId, userId, `Документ отклонен: ${reason}`]
|
||||||
|
);
|
||||||
|
|
||||||
|
logDocumentActivity(db, documentId, userId, 'DOCUMENT_REJECTED',
|
||||||
|
`Документ отклонен: ${reason}`);
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Документ отклонен' });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавление комментария к документу
|
||||||
|
router.post('/api/documents/:id/comments', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
const { comment, is_internal = false } = req.body;
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
return res.status(400).json({ error: 'Комментарий обязателен' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем доступ к документу
|
||||||
|
db.get(`
|
||||||
|
SELECT 1 FROM documents d
|
||||||
|
LEFT JOIN document_approvals da ON d.id = da.document_id
|
||||||
|
WHERE d.id = ? AND (d.created_by = ? OR da.approver_user_id = ?)
|
||||||
|
LIMIT 1
|
||||||
|
`, [documentId, userId, userId], (err, hasAccess) => {
|
||||||
|
if (err || !hasAccess) {
|
||||||
|
return res.status(403).json({ error: 'Доступ к документу запрещен' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO document_comments (document_id, user_id, comment, is_internal)
|
||||||
|
VALUES (?, ?, ?, ?)`,
|
||||||
|
[documentId, userId, comment, is_internal ? 1 : 0],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка добавления комментария:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
logDocumentActivity(db, documentId, userId, 'COMMENT_ADDED', 'Добавлен комментарий');
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Комментарий добавлен' });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновление документа (новая версия)
|
||||||
|
router.put('/api/documents/:id', requireAuth, upload.single('file'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
const { title, description } = req.body;
|
||||||
|
|
||||||
|
// Проверяем права на редактирование
|
||||||
|
db.get(`
|
||||||
|
SELECT d.*, da.id as approval_id, a.can_edit
|
||||||
|
FROM documents d
|
||||||
|
LEFT JOIN document_approvals da ON d.id = da.document_id AND da.approver_user_id = ? AND da.status = 'pending'
|
||||||
|
LEFT JOIN approval_stages a ON da.stage_id = a.id
|
||||||
|
WHERE d.id = ?
|
||||||
|
`, [userId, documentId], (err, document) => {
|
||||||
|
if (err || !document) {
|
||||||
|
return res.status(404).json({ error: 'Документ не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const canEdit = (document.created_by === userId && document.status === 'draft') ||
|
||||||
|
(document.approval_id && document.can_edit);
|
||||||
|
|
||||||
|
if (!canEdit) {
|
||||||
|
return res.status(403).json({ error: 'Недостаточно прав для редактирования' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
// Создаем новую версию документа
|
||||||
|
const newVersion = document.version + 1;
|
||||||
|
|
||||||
|
const updateFields = [
|
||||||
|
'updated_at = CURRENT_TIMESTAMP',
|
||||||
|
'version = ?'
|
||||||
|
];
|
||||||
|
const params = [newVersion];
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
updateFields.push('title = ?');
|
||||||
|
params.push(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
updateFields.push('description = ?');
|
||||||
|
params.push(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
// Удаляем старый файл, если он существует
|
||||||
|
if (document.file_path && fs.existsSync(document.file_path)) {
|
||||||
|
fs.unlinkSync(document.file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFields.push('file_path = ?', 'file_name = ?', 'file_size = ?');
|
||||||
|
params.push(req.file.path, req.file.originalname, req.file.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(documentId);
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`UPDATE documents SET ${updateFields.join(', ')} WHERE id = ?`,
|
||||||
|
params,
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка обновления документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем действие
|
||||||
|
const changes = [];
|
||||||
|
if (title && title !== document.title) changes.push(`Название: "${document.title}" -> "${title}"`);
|
||||||
|
if (description && description !== document.description) changes.push('Описание изменено');
|
||||||
|
if (req.file) changes.push('Файл обновлен');
|
||||||
|
|
||||||
|
logDocumentActivity(db, documentId, userId, 'DOCUMENT_UPDATED',
|
||||||
|
`Версия ${newVersion}: ${changes.join(', ')}`);
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Документ обновлен', version: newVersion });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление документа
|
||||||
|
router.delete('/api/documents/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const documentId = req.params.id;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
// Проверяем, что документ принадлежит пользователю и имеет статус черновика
|
||||||
|
db.get("SELECT status, created_by, file_path FROM documents WHERE id = ?", [documentId], (err, document) => {
|
||||||
|
if (err || !document) {
|
||||||
|
return res.status(404).json({ error: 'Документ не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.created_by !== userId) {
|
||||||
|
return res.status(403).json({ error: 'Недостаточно прав' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.status !== 'draft') {
|
||||||
|
return res.status(400).json({ error: 'Можно удалять только черновики' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
// Удаляем связанные записи
|
||||||
|
db.run("DELETE FROM document_approvals WHERE document_id = ?", [documentId]);
|
||||||
|
db.run("DELETE FROM document_comments WHERE document_id = ?", [documentId]);
|
||||||
|
db.run("DELETE FROM document_history WHERE document_id = ?", [documentId]);
|
||||||
|
|
||||||
|
// Удаляем файл
|
||||||
|
if (document.file_path && fs.existsSync(document.file_path)) {
|
||||||
|
fs.unlinkSync(document.file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем документ
|
||||||
|
db.run("DELETE FROM documents WHERE id = ?", [documentId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка удаления документа:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Документ удален' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение документов, ожидающих согласования пользователем
|
||||||
|
router.get('/api/documents/pending', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { getDb } = require('./database');
|
||||||
|
const db = getDb();
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT d.*,
|
||||||
|
u.name as author_name,
|
||||||
|
dt.name as type_name,
|
||||||
|
a.stage_number, a.stage_name,
|
||||||
|
da.deadline,
|
||||||
|
julianday(da.deadline) - julianday('now') as days_until_deadline
|
||||||
|
FROM document_approvals da
|
||||||
|
LEFT JOIN documents d ON da.document_id = d.id
|
||||||
|
LEFT JOIN users u ON d.created_by = u.id
|
||||||
|
LEFT JOIN document_types dt ON d.document_type_id = dt.id
|
||||||
|
LEFT JOIN approval_stages a ON da.stage_id = a.id
|
||||||
|
WHERE da.approver_user_id = ?
|
||||||
|
AND da.status = 'pending'
|
||||||
|
AND d.status = 'in_review'
|
||||||
|
ORDER BY da.deadline ASC, d.created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [userId], (err, documents) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка получения документов:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
res.json(documents);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
23
server.js
23
server.js
@@ -14,6 +14,10 @@ const { sendTaskNotifications, checkUpcomingDeadlines, getStatusText } = require
|
|||||||
const { setupUploadMiddleware } = require('./upload-middleware');
|
const { setupUploadMiddleware } = require('./upload-middleware');
|
||||||
const { setupTaskEndpoints } = require('./task-endpoints');
|
const { setupTaskEndpoints } = require('./task-endpoints');
|
||||||
|
|
||||||
|
// admin-doc
|
||||||
|
const adminDocRouter = require('./admin-doc');
|
||||||
|
const docRouter = require('./doc-endpoints');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
@@ -39,6 +43,10 @@ app.use(session({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Подключаем роутеры для документов
|
||||||
|
app.use(adminDocRouter);
|
||||||
|
app.use(docRouter);
|
||||||
|
|
||||||
// Middleware для проверки готовности сервера
|
// Middleware для проверки готовности сервера
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (!serverReady && req.path !== '/health' && req.path !== '/api/health') {
|
if (!serverReady && req.path !== '/health' && req.path !== '/api/health') {
|
||||||
@@ -557,6 +565,21 @@ app.get('/admin/profiles', (req, res) => {
|
|||||||
}
|
}
|
||||||
res.sendFile(path.join(__dirname, 'public/admin-profiles.html'));
|
res.sendFile(path.join(__dirname, 'public/admin-profiles.html'));
|
||||||
});
|
});
|
||||||
|
// Админ панель для документов
|
||||||
|
app.get('/admin-doc', (req, res) => {
|
||||||
|
if (!req.session.user || req.session.user.role !== 'admin') {
|
||||||
|
return res.status(403).send('Доступ запрещен');
|
||||||
|
}
|
||||||
|
res.sendFile(path.join(__dirname, 'public/admin-doc.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Страница согласования документов
|
||||||
|
app.get('/doc', (req, res) => {
|
||||||
|
if (!req.session.user) {
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
res.sendFile(path.join(__dirname, 'public/doc.html'));
|
||||||
|
});
|
||||||
|
|
||||||
// API для получения настроек уведомлений пользователя
|
// API для получения настроек уведомлений пользователя
|
||||||
app.get('/api/user/settings', requireAuth, async (req, res) => {
|
app.get('/api/user/settings', requireAuth, async (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user