Admin: allow topic rename/delete and instant photo tagging

Add topic editing and deletion flows in the topics admin section with nesting safeguards for two-level hierarchy. Make photo topic assignment immediate on select change so tags are attached without extra confirmation clicks, while keeping inline detach actions.
This commit is contained in:
Alexander Andreev 2026-02-21 12:22:38 +03:00
parent 3f80f3fbb7
commit f9b949a7bf
2 changed files with 93 additions and 17 deletions

View File

@ -98,6 +98,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = 'Тематика создана'; $message = 'Тематика создана';
} }
if ($action === 'update_topic') {
$topicId = (int)($_POST['topic_id'] ?? 0);
$name = trim((string)($_POST['name'] ?? ''));
$sort = (int)($_POST['sort_order'] ?? 1000);
$parentId = (int)($_POST['parent_id'] ?? 0);
if ($topicId < 1) throw new RuntimeException('Некорректная тематика');
if ($name === '') throw new RuntimeException('Название тематики пустое');
$topic = topicById($topicId);
if (!$topic) throw new RuntimeException('Тематика не найдена');
if ($parentId === $topicId) {
throw new RuntimeException('Тематика не может быть родителем самой себя');
}
if ($parentId > 0) {
$parent = topicById($parentId);
if (!$parent) throw new RuntimeException('Родительская тематика не найдена');
if (!empty($parent['parent_id'])) {
throw new RuntimeException('Разрешено только 2 уровня вложенности тематик');
}
if (topicChildrenCount($topicId) > 0) {
throw new RuntimeException('Тематика с дочерними элементами должна оставаться в верхнем уровне');
}
}
topicUpdate($topicId, $name, $parentId > 0 ? $parentId : null, $sort);
$message = 'Тематика обновлена';
}
if ($action === 'delete_topic') {
$topicId = (int)($_POST['topic_id'] ?? 0);
if ($topicId < 1) throw new RuntimeException('Некорректная тематика');
if (!topicById($topicId)) throw new RuntimeException('Тематика не найдена');
topicDelete($topicId);
$message = 'Тематика удалена';
}
if ($action === 'delete_section') { if ($action === 'delete_section') {
$sectionId = (int)($_POST['section_id'] ?? 0); $sectionId = (int)($_POST['section_id'] ?? 0);
if ($sectionId < 1) throw new RuntimeException('Некорректный раздел'); if ($sectionId < 1) throw new RuntimeException('Некорректный раздел');
@ -819,11 +859,33 @@ function nextUniqueCodeName(string $base): string
<p class="small">Тематик пока нет.</p> <p class="small">Тематик пока нет.</p>
<?php else: ?> <?php else: ?>
<table class="tbl"> <table class="tbl">
<tr><th>Тематика</th><th>Уровень</th></tr> <tr><th>Тематика</th><th>Уровень</th><th>Действия</th></tr>
<?php foreach($topics as $topic): ?> <?php foreach($topics as $topic): ?>
<tr> <tr>
<td><?= h((string)$topic['full_name']) ?></td> <td>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=topics" style="display:grid;gap:8px;min-width:320px">
<input type="hidden" name="action" value="update_topic"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="topic_id" value="<?= (int)$topic['id'] ?>">
<input class="in" type="text" name="name" value="<?= h((string)$topic['name']) ?>" required>
<div style="display:grid;grid-template-columns:1fr 110px;gap:8px">
<select class="in" name="parent_id">
<option value="0" <?= empty($topic['parent_id']) ? 'selected' : '' ?>>Без родителя</option>
<?php foreach($topicRoots as $root): ?>
<?php if ((int)$root['id'] === (int)$topic['id']) continue; ?>
<option value="<?= (int)$root['id'] ?>" <?= (int)$topic['parent_id'] === (int)$root['id'] ? 'selected' : '' ?>>Внутри: <?= h((string)$root['name']) ?></option>
<?php endforeach; ?>
</select>
<input class="in" type="number" name="sort_order" value="<?= (int)$topic['sort_order'] ?>">
</div>
<button class="btn btn-secondary" type="submit">Сохранить</button>
</form>
</td>
<td><?= (int)$topic['level'] === 0 ? '1' : '2' ?></td> <td><?= (int)$topic['level'] === 0 ? '1' : '2' ?></td>
<td>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=topics" onsubmit="return confirm('Удалить тематику? Дочерние тематики и привязки к фото тоже удалятся.')">
<input type="hidden" name="action" value="delete_topic"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="topic_id" value="<?= (int)$topic['id'] ?>">
<button class="btn btn-danger" type="submit">Удалить</button>
</form>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</table> </table>
@ -924,12 +986,11 @@ function nextUniqueCodeName(string $base): string
<div class="topic-controls"> <div class="topic-controls">
<select class="in js-topic-select" <?= $topics === [] ? 'disabled' : '' ?>> <select class="in js-topic-select" <?= $topics === [] ? 'disabled' : '' ?>>
<option value="">Выбери тематику</option> <option value="">Выбери тематику (добавится сразу)</option>
<?php foreach($topics as $topic): ?> <?php foreach($topics as $topic): ?>
<option value="<?= (int)$topic['id'] ?>"><?= (int)$topic['level'] === 1 ? '— ' : '' ?><?= h((string)$topic['full_name']) ?></option> <option value="<?= (int)$topic['id'] ?>"><?= (int)$topic['level'] === 1 ? '— ' : '' ?><?= h((string)$topic['full_name']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<button class="btn btn-secondary btn-xs js-topic-add" type="button" <?= $topics === [] ? 'disabled' : '' ?>>Добавить</button>
</div> </div>
<div class="topic-status js-topic-status"></div> <div class="topic-status js-topic-status"></div>
</div> </div>
@ -1380,9 +1441,7 @@ function nextUniqueCodeName(string $base): string
throw new Error('Некорректные параметры'); throw new Error('Некорректные параметры');
} }
const addBtn = editor.querySelector('.js-topic-add');
const select = editor.querySelector('.js-topic-select'); const select = editor.querySelector('.js-topic-select');
if (addBtn) addBtn.disabled = true;
if (select) select.disabled = true; if (select) select.disabled = true;
const fd = new FormData(); const fd = new FormData();
@ -1419,28 +1478,28 @@ function nextUniqueCodeName(string $base): string
select.value = ''; select.value = '';
} }
} finally { } finally {
if (addBtn) addBtn.disabled = false;
if (select) select.disabled = false; if (select) select.disabled = false;
} }
}; };
document.addEventListener('click', (e) => { document.querySelectorAll('.js-topic-editor').forEach((editor) => {
const addBtn = e.target.closest('.js-topic-add'); const select = editor.querySelector('.js-topic-select');
if (addBtn) { if (!select) {
const editor = addBtn.closest('.js-topic-editor'); return;
if (!editor) return; }
const select = editor.querySelector('.js-topic-select');
const topicId = Number(select?.value || 0); select.addEventListener('change', () => {
const topicId = Number(select.value || 0);
if (topicId < 1) { if (topicId < 1) {
setTopicStatus(editor, 'Сначала выбери тематику', true);
return; return;
} }
postTopicAction(editor, 'attach_photo_topic', topicId) postTopicAction(editor, 'attach_photo_topic', topicId)
.catch((err) => setTopicStatus(editor, err?.message || 'Ошибка добавления', true)); .catch((err) => setTopicStatus(editor, err?.message || 'Ошибка добавления', true));
return; });
} });
document.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.js-topic-remove'); const removeBtn = e.target.closest('.js-topic-remove');
if (removeBtn) { if (removeBtn) {
const editor = removeBtn.closest('.js-topic-editor'); const editor = removeBtn.closest('.js-topic-editor');

View File

@ -109,6 +109,23 @@ function topicCreate(string $name, ?int $parentId, int $sortOrder = 1000): int
return (int)db()->lastInsertId(); return (int)db()->lastInsertId();
} }
function topicUpdate(int $topicId, string $name, ?int $parentId, int $sortOrder = 1000): void
{
$st = db()->prepare('UPDATE topics SET parent_id=:pid, name=:name, sort_order=:sort WHERE id=:id');
$st->bindValue('id', $topicId, PDO::PARAM_INT);
$st->bindValue('pid', $parentId, $parentId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$st->bindValue('name', $name, PDO::PARAM_STR);
$st->bindValue('sort', $sortOrder, PDO::PARAM_INT);
$st->execute();
}
function topicChildrenCount(int $topicId): int
{
$st = db()->prepare('SELECT COUNT(*) FROM topics WHERE parent_id=:id');
$st->execute(['id' => $topicId]);
return (int)$st->fetchColumn();
}
function topicDelete(int $topicId): void function topicDelete(int $topicId): void
{ {
$st = db()->prepare('DELETE FROM topics WHERE id=:id'); $st = db()->prepare('DELETE FROM topics WHERE id=:id');