Initial commit

This commit is contained in:
2026-05-17 05:22:06 +03:00
commit ca4d00c895
155 changed files with 45216 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$content = $_POST['content'] ?? '';
$excerpt = trim($_POST['excerpt'] ?? '');
$category = $_POST['category'] ?? 'risk-engineering';
$status = $_POST['status'] ?? 'draft';
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif (!$title || !$content) {
$message = 'Заголовок и содержание обязательны.';
} else {
if (!$slug) {
$slug = preg_replace('/[^a-z0-9-]/', '', mb_strtolower(str_replace(' ', '-', $title)));
}
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE blog_posts SET title=?, slug=?, content=?, excerpt=?, category=?, status=? WHERE id=?");
$stmt->execute([$title, $slug, $content, $excerpt, $category, $status, $id]);
$message = 'Пост обновлён.';
} else {
$stmt = $pdo->prepare("INSERT INTO blog_posts (title, slug, content, excerpt, category, author_id, status, published_at) VALUES (?,?,?,?,?,?,?,?)");
$published = $status === 'published' ? date('Y-m-d H:i:s') : null;
$stmt->execute([$title, $slug, $content, $excerpt, $category, $session['user_id'], $status, $published]);
$message = 'Пост создан.';
}
auth_log($session['user_id'], 'blog_save', "Title: $title");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . ($e->getCode() === '23000' ? 'Такой slug уже существует.' : $e->getMessage());
}
}
}
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("DELETE FROM blog_posts WHERE id=?")->execute([$id]);
$message = 'Пост удалён.';
}
}
if (isset($_GET['publish'])) {
$id = (int)$_GET['publish'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("UPDATE blog_posts SET status='published', published_at=NOW() WHERE id=?")->execute([$id]);
$message = 'Пост опубликован.';
}
}
$editPost = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM blog_posts WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editPost = $stmt->fetch();
}
$posts = $pdo->query("SELECT bp.*, u.full_name as author_name FROM blog_posts bp LEFT JOIN users u ON u.id=bp.author_id ORDER BY bp.created_at DESC")->fetchAll();
$csrf = auth_csrf_token();
$categories = [
'audit' => 'Аудит',
'sla' => 'SLA',
'incident' => 'Инциденты',
'supervision' => 'Технадзор',
'documentation' => 'Документация',
'risk' => 'Риск-инжиниринг',
'cases' => 'Кейсы',
];
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>Управление блогом</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / Блог</div>
</div>
<button class="btn btn-primary btn-sm" onclick="openModal()" data-tooltip="Создать новую статью">+ Новая статья</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Заголовок</th>
<th>Категория</th>
<th>Автор</th>
<th>Статус</th>
<th>Дата</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($posts)): ?>
<tr><td colspan="6" style="text-align:center;color:var(--text-muted);">Нет статей</td></tr>
<?php endif; ?>
<?php foreach ($posts as $p): ?>
<tr>
<td><strong><?= htmlspecialchars($p['title']) ?></strong></td>
<td><span class="badge badge-medium"><?= $categories[$p['category']] ?? $p['category'] ?></span></td>
<td><?= htmlspecialchars($p['author_name'] ?: '—') ?></td>
<td><span class="badge badge-<?= $p['status'] === 'published' ? 'done' : ($p['status'] === 'draft' ? 'draft' : 'cancelled') ?>"><?= $p['status'] ?></span></td>
<td><?= $p['published_at'] ? date('d.m.Y', strtotime($p['published_at'])) : date('d.m.Y', strtotime($p['created_at'])) ?></td>
<td class="actions">
<a href="/blog/<?= htmlspecialchars($p['slug']) ?>" class="btn btn-secondary btn-sm" target="_blank" data-tooltip="Просмотр">👁️</a>
<a href="?edit=<?= $p['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
<?php if ($p['status'] !== 'published'): ?>
<a href="?publish=<?= $p['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-success btn-sm" data-tooltip="Опубликовать">📢</a>
<?php endif; ?>
<a href="?delete=<?= $p['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить статью?')" data-tooltip="Удалить">🗑️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editPost ? 'open' : '' ?>" id="blogModal">
<div class="modal" style="max-width:720px;">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editPost ? 'Редактировать' : 'Новая' ?> статья</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editPost['id'] ?? 0 ?>">
<div class="form-group">
<label for="title">Заголовок *</label>
<input type="text" id="title" name="title" value="<?= htmlspecialchars($editPost['title'] ?? '') ?>" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="slug">Slug (оставьте пустым для автогенерации)</label>
<input type="text" id="slug" name="slug" value="<?= htmlspecialchars($editPost['slug'] ?? '') ?>">
</div>
<div class="form-group">
<label for="category">Категория</label>
<select id="category" name="category">
<?php foreach ($categories as $key => $label): ?>
<option value="<?= $key ?>" <?= ($editPost['category'] ?? '') === $key ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="status">Статус</label>
<select id="status" name="status">
<option value="draft" <?= ($editPost['status'] ?? '') === 'draft' ? 'selected' : '' ?>>Черновик</option>
<option value="published" <?= ($editPost['status'] ?? '') === 'published' ? 'selected' : '' ?>>Опубликовать</option>
<option value="archived" <?= ($editPost['status'] ?? '') === 'archived' ? 'selected' : '' ?>>Архив</option>
</select>
</div>
<div class="form-group">
<label for="excerpt">Краткое описание</label>
<input type="text" id="excerpt" name="excerpt" value="<?= htmlspecialchars($editPost['excerpt'] ?? '') ?>">
</div>
</div>
<div class="form-group">
<label for="content">Содержание * (HTML)</label>
<div class="editor-toolbar">
<button type="button" data-insert="bold" title="Жирный"><b>Ж</b></button>
<button type="button" data-insert="italic" title="Курсив"><i>К</i></button>
<button type="button" data-insert="h2" title="Заголовок 2">H2</button>
<button type="button" data-insert="h3" title="Заголовок 3">H3</button>
<button type="button" data-insert="ul" title="Маркированный список">• Список</button>
<button type="button" data-insert="ol" title="Нумерованный список">1. Список</button>
<button type="button" data-insert="link" title="Ссылка">🔗</button>
<button type="button" data-insert="image" title="Изображение">🖼️</button>
</div>
<textarea id="content" name="content" rows="15" style="font-family:monospace;"><?= htmlspecialchars($editPost['content'] ?? '') ?></textarea>
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
</form>
</div>
</div>
<script>
function openModal() { document.getElementById('blogModal').classList.add('open'); }
function closeModal() { document.getElementById('blogModal').classList.remove('open'); window.location.href = 'blog.php'; }
// Slug auto-generation from title
(function(){
var titleInput = document.getElementById('title');
var slugInput = document.getElementById('slug');
if (!titleInput || !slugInput) return;
var slugTouched = slugInput.value !== '';
slugInput.addEventListener('input', function(){ slugTouched = true; });
titleInput.addEventListener('input', function(){
if (slugTouched) return;
var val = titleInput.value.toLowerCase().replace(/[^a-zа-яё0-9\s-]/gi, '').replace(/\s+/g, '-');
// Transliterate Cyrillic
var map = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z','и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'shch','ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya'};
var result = '';
for (var i = 0; i < val.length; i++) {
var ch = val[i];
result += map[ch] !== undefined ? map[ch] : ch;
}
slugInput.value = result.replace(/-+/g, '-').replace(/^-|-$/g, '');
});
})();
// Editor toolbar
(function(){
var textarea = document.getElementById('content');
if (!textarea) return;
document.querySelectorAll('.editor-toolbar button').forEach(function(btn){
btn.addEventListener('click', function(e){
e.preventDefault();
var action = btn.getAttribute('data-insert');
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
var selected = textarea.value.substring(start, end);
var insert = '';
switch(action) {
case 'bold': insert = '<strong>' + (selected || 'текст') + '</strong>'; break;
case 'italic': insert = '<em>' + (selected || 'текст') + '</em>'; break;
case 'h2': insert = '<h2>' + (selected || 'Заголовок') + '</h2>'; break;
case 'h3': insert = '<h3>' + (selected || 'Заголовок') + '</h3>'; break;
case 'ul': insert = '<ul>\n <li>' + (selected || 'Пункт') + '</li>\n</ul>'; break;
case 'ol': insert = '<ol>\n <li>' + (selected || 'Пункт') + '</li>\n</ol>'; break;
case 'link': insert = '<a href="">' + (selected || 'текст ссылки') + '</a>'; break;
case 'image': insert = '<img src="" alt="' + (selected || 'описание') + '">'; break;
}
textarea.value = textarea.value.substring(0, start) + insert + textarea.value.substring(end);
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = start + insert.length;
});
});
})();
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
<style>
.editor-toolbar { display:flex; gap:4px; margin-bottom:8px; flex-wrap:wrap; }
.editor-toolbar button { padding:4px 10px; background:var(--bg-input); border:1px solid var(--border); border-radius:4px; color:var(--text-primary); cursor:pointer; font-size:12px; transition:all 0.2s; }
.editor-toolbar button:hover { border-color:var(--accent); background:var(--accent-dim); }
</style>