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 = 'Тематика создана';
}
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') {
$sectionId = (int)($_POST['section_id'] ?? 0);
if ($sectionId < 1) throw new RuntimeException('Некорректный раздел');
@ -819,11 +859,33 @@ function nextUniqueCodeName(string $base): string
<p class="small">Тематик пока нет.</p>
<?php else: ?>
<table class="tbl">
<tr><th>Тематика</th><th>Уровень</th></tr>
<tr><th>Тематика</th><th>Уровень</th><th>Действия</th></tr>
<?php foreach($topics as $topic): ?>
<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>
<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>
<?php endforeach; ?>
</table>
@ -924,12 +986,11 @@ function nextUniqueCodeName(string $base): string
<div class="topic-controls">
<select class="in js-topic-select" <?= $topics === [] ? 'disabled' : '' ?>>
<option value="">Выбери тематику</option>
<option value="">Выбери тематику (добавится сразу)</option>
<?php foreach($topics as $topic): ?>
<option value="<?= (int)$topic['id'] ?>"><?= (int)$topic['level'] === 1 ? '— ' : '' ?><?= h((string)$topic['full_name']) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-secondary btn-xs js-topic-add" type="button" <?= $topics === [] ? 'disabled' : '' ?>>Добавить</button>
</div>
<div class="topic-status js-topic-status"></div>
</div>
@ -1380,9 +1441,7 @@ function nextUniqueCodeName(string $base): string
throw new Error('Некорректные параметры');
}
const addBtn = editor.querySelector('.js-topic-add');
const select = editor.querySelector('.js-topic-select');
if (addBtn) addBtn.disabled = true;
if (select) select.disabled = true;
const fd = new FormData();
@ -1419,28 +1478,28 @@ function nextUniqueCodeName(string $base): string
select.value = '';
}
} finally {
if (addBtn) addBtn.disabled = false;
if (select) select.disabled = false;
}
};
document.addEventListener('click', (e) => {
const addBtn = e.target.closest('.js-topic-add');
if (addBtn) {
const editor = addBtn.closest('.js-topic-editor');
if (!editor) return;
const select = editor.querySelector('.js-topic-select');
const topicId = Number(select?.value || 0);
document.querySelectorAll('.js-topic-editor').forEach((editor) => {
const select = editor.querySelector('.js-topic-select');
if (!select) {
return;
}
select.addEventListener('change', () => {
const topicId = Number(select.value || 0);
if (topicId < 1) {
setTopicStatus(editor, 'Сначала выбери тематику', true);
return;
}
postTopicAction(editor, 'attach_photo_topic', topicId)
.catch((err) => setTopicStatus(editor, err?.message || 'Ошибка добавления', true));
return;
}
});
});
document.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.js-topic-remove');
if (removeBtn) {
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();
}
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
{
$st = db()->prepare('DELETE FROM topics WHERE id=:id');