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:
parent
3f80f3fbb7
commit
f9b949a7bf
93
admin.php
93
admin.php
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user