feat(modules): soft delete with restore and active filter

This commit is contained in:
dev
2026-09-18 20:04:02 +03:00
parent d3dd922e32
commit d7d4cc1133
8 changed files with 57 additions and 15 deletions
+16 -5
View File
@@ -670,8 +670,10 @@ async function ensureModulesTable() {
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL UNIQUE,
lessons_count INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now()
)`);
await pool.query('ALTER TABLE modules ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true');
await pool.query('ALTER TABLE entries ADD COLUMN IF NOT EXISTS module_id INT REFERENCES modules(id) ON DELETE SET NULL');
await pool.query('CREATE INDEX IF NOT EXISTS idx_entries_module_id ON entries(module_id)');
}
@@ -1346,6 +1348,7 @@ function normalizeRestoreData(data) {
id: reqInt(x.id),
name: reqStr(x.name, 200),
lessons_count: optInt(x.lessons_count, 0, 10000) ?? 0,
is_active: x.is_active !== false,
created_at: optTs(x.created_at),
}));
const entry_photos = (data.entry_photos || []).map(x => ({
@@ -1529,8 +1532,8 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
}
for (const x of ndata.modules) {
await client.query(
'INSERT INTO modules (id, name, lessons_count, created_at) VALUES ($1,$2,$3,$4)',
[x.id, x.name, x.lessons_count, x.created_at]
'INSERT INTO modules (id, name, lessons_count, is_active, created_at) VALUES ($1,$2,$3,$4,$5)',
[x.id, x.name, x.lessons_count, x.is_active, x.created_at]
);
}
for (const x of ndata.entries) {
@@ -2291,17 +2294,18 @@ app.put('/api/groups/:id/photos/:photoId/cover', requireAuth, async (req, res) =
// --- Modules (темы модулей) ---
app.get('/api/modules', apiLimiter, async (req, res) => {
const { limit, offset, search } = req.query;
const { limit, offset, search, active } = req.query;
const conditions = [];
const params = [];
if (search?.trim()) { params.push(`%${search.trim()}%`); conditions.push(`m.name ILIKE $${params.length}`); }
if (active === '1' || active === 'true') conditions.push('m.is_active = true');
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
const { rows: crows } = await pool.query(`SELECT count(*)::int AS n FROM modules m${where}`, params);
const total = crows[0].n;
let q = `SELECT m.*, count(e.id)::int AS entries_count
FROM modules m
LEFT JOIN entries e ON e.module_id = m.id${where}
GROUP BY m.id ORDER BY m.id`;
GROUP BY m.id ORDER BY m.is_active DESC, m.id`;
const qparams = params.slice();
const lim = parseInt(limit, 10);
if (lim > 0) { qparams.push(lim); q += ` LIMIT $${qparams.length}`; }
@@ -2358,12 +2362,19 @@ app.put('/api/modules/:id', requireAdmin, async (req, res) => {
});
app.delete('/api/modules/:id', requireAdmin, async (req, res) => {
const { rows } = await pool.query('DELETE FROM modules WHERE id = $1 RETURNING id', [req.params.id]);
const { rows } = await pool.query('UPDATE modules SET is_active = false WHERE id = $1 RETURNING id', [req.params.id]);
if (!rows.length) return res.status(404).json({ error: 'Не найдено' });
await logAudit(req, 'module.delete', { id: req.params.id });
res.json({ ok: true });
});
app.put('/api/modules/:id/restore', requireAdmin, async (req, res) => {
const { rows } = await pool.query('UPDATE modules SET is_active = true WHERE id = $1 RETURNING *', [req.params.id]);
if (!rows.length) return res.status(404).json({ error: 'Не найдено' });
await logAudit(req, 'module.restore', { id: req.params.id });
res.json(rows[0]);
});
// --- Students CRUD ---
app.get('/api/students', apiLimiter, optionalAuth, async (req, res) => {
const rows = await cacheWrap('students:list:' + scopeKey(req.user), PUBLIC_TTL_MS, async () => {