294 lines
16 KiB
PHP
294 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* AegisOne Engineering — Управление документацией (только владелец)
|
|
*
|
|
* Позволяет:
|
|
* - Видеть все документы из service/docs/
|
|
* - Переименовывать документы (обновляет файл + permissions)
|
|
* - Управлять порядком сортировки
|
|
* - Включать/отключать доступ инженеров (view/edit/cancel)
|
|
* - Автоматически обнаруживать новые .md-файлы
|
|
*/
|
|
require_once __DIR__ . '/../../inc/auth.php';
|
|
require_once __DIR__ . '/../../inc/functions.php';
|
|
require_once __DIR__ . '/../../inc/docs_functions.php';
|
|
|
|
$session = auth_require('owner');
|
|
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
// Обработка действий
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|
$csrf = $_POST['csrf_token'] ?? '';
|
|
if (!auth_csrf_verify($csrf)) {
|
|
$message = 'Ошибка безопасности.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$slug = trim($_POST['slug'] ?? '');
|
|
$permissions = load_permissions();
|
|
|
|
if ($_POST['action'] === 'rename' && $slug && !empty($permissions[$slug])) {
|
|
$newTitle = trim($_POST['new_title'] ?? '');
|
|
$newSort = (int)($_POST['new_sort'] ?? 0);
|
|
if ($newTitle !== '') {
|
|
$permissions[$slug]['title'] = $newTitle;
|
|
$permissions[$slug]['sort_order'] = $newSort;
|
|
save_permissions($permissions);
|
|
$message = 'Документ обновлён.';
|
|
$messageType = 'success';
|
|
}
|
|
} elseif ($_POST['action'] === 'toggle_view' && $slug) {
|
|
if (!isset($permissions[$slug]['permissions'])) {
|
|
$permissions[$slug]['permissions'] = ['engineer' => ['view' => false, 'edit' => false, 'cancel' => false]];
|
|
}
|
|
$current = !empty($permissions[$slug]['permissions']['engineer']['view']);
|
|
$permissions[$slug]['permissions']['engineer']['view'] = !$current;
|
|
save_permissions($permissions);
|
|
$message = 'Доступ обновлён.';
|
|
$messageType = 'success';
|
|
} elseif ($_POST['action'] === 'toggle_edit' && $slug) {
|
|
if (!isset($permissions[$slug]['permissions'])) {
|
|
$permissions[$slug]['permissions'] = ['engineer' => ['view' => false, 'edit' => false, 'cancel' => false]];
|
|
}
|
|
$current = !empty($permissions[$slug]['permissions']['engineer']['edit']);
|
|
$permissions[$slug]['permissions']['engineer']['edit'] = !$current;
|
|
if ($permissions[$slug]['permissions']['engineer']['edit']) {
|
|
$permissions[$slug]['permissions']['engineer']['view'] = true;
|
|
}
|
|
save_permissions($permissions);
|
|
$message = 'Доступ обновлён.';
|
|
$messageType = 'success';
|
|
} elseif ($_POST['action'] === 'toggle_cancel' && $slug) {
|
|
if (!isset($permissions[$slug]['permissions'])) {
|
|
$permissions[$slug]['permissions'] = ['engineer' => ['view' => false, 'edit' => false, 'cancel' => false]];
|
|
}
|
|
$current = !empty($permissions[$slug]['permissions']['engineer']['cancel']);
|
|
$permissions[$slug]['permissions']['engineer']['cancel'] = !$current;
|
|
if ($permissions[$slug]['permissions']['engineer']['cancel']) {
|
|
$permissions[$slug]['permissions']['engineer']['view'] = true;
|
|
}
|
|
save_permissions($permissions);
|
|
$message = 'Доступ обновлён.';
|
|
$messageType = 'success';
|
|
} elseif ($_POST['action'] === 'sort_up' || $_POST['action'] === 'sort_down') {
|
|
$docs = get_docs_list();
|
|
$idx = -1;
|
|
foreach ($docs as $i => $d) {
|
|
if ($d['slug'] === $slug) { $idx = $i; break; }
|
|
}
|
|
if ($idx >= 0) {
|
|
$swapIdx = $_POST['action'] === 'sort_up' ? $idx - 1 : $idx + 1;
|
|
if ($swapIdx >= 0 && $swapIdx < count($docs)) {
|
|
$aSlug = $docs[$idx]['slug'];
|
|
$bSlug = $docs[$swapIdx]['slug'];
|
|
$aSort = $permissions[$aSlug]['sort_order'] ?? $idx;
|
|
$bSort = $permissions[$bSlug]['sort_order'] ?? $swapIdx;
|
|
$permissions[$aSlug]['sort_order'] = $bSort;
|
|
$permissions[$bSlug]['sort_order'] = $aSort;
|
|
save_permissions($permissions);
|
|
$message = 'Порядок изменён.';
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$permissions = load_permissions();
|
|
$docs = get_docs_list();
|
|
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
require __DIR__ . '/../../inc/header.php';
|
|
?>
|
|
|
|
<div class="docs-admin">
|
|
<div class="page-header">
|
|
<h1>Управление документацией</h1>
|
|
<p class="page-subtitle">
|
|
Добавьте .md-файлы в папку <code>service/docs/</code> — они появятся в списке автоматически.<br>
|
|
Управляйте правами инженеров: просмотр, редактирование, отмена.
|
|
</p>
|
|
</div>
|
|
|
|
<?php if ($message): ?>
|
|
<div class="alert alert-<?= htmlspecialchars($messageType) ?>"><?= htmlspecialchars($message) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:30px">↕</th>
|
|
<th>Документ</th>
|
|
<th>Файл</th>
|
|
<th class="text-center">Просмотр</th>
|
|
<th class="text-center">Редакт.</th>
|
|
<th class="text-center">Отмена</th>
|
|
<th class="text-center">Действия</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($docs)): ?>
|
|
<tr>
|
|
<td colspan="7" class="text-center" style="color:var(--text-muted);padding:32px;">
|
|
Нет документов. Добавьте .md-файлы в <code>service/docs/</code>
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach ($docs as $idx => $doc):
|
|
$p = $permissions[$doc['slug']] ?? [];
|
|
$engPerms = $p['permissions']['engineer'] ?? ['view' => false, 'edit' => false, 'cancel' => false];
|
|
// Backward compat: old boolean 'engineer' field
|
|
if (!isset($p['permissions']) && !empty($p['engineer'])) {
|
|
$engPerms = ['view' => true, 'edit' => false, 'cancel' => false];
|
|
}
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<div style="display:flex;flex-direction:column;gap:2px;">
|
|
<form method="post" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
|
|
<input type="hidden" name="action" value="sort_up">
|
|
<button type="submit" class="btn-sort <?= $idx === 0 ? 'disabled' : '' ?>" data-tooltip="Вверх">▲</button>
|
|
</form>
|
|
<form method="post" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
|
|
<input type="hidden" name="action" value="sort_down">
|
|
<button type="submit" class="btn-sort <?= $idx === count($docs) - 1 ? 'disabled' : '' ?>" data-tooltip="Вниз">▼</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<a href="/service/pages/shared/docs.php?view=<?= urlencode($doc['slug']) ?>" class="doc-link">
|
|
<?= htmlspecialchars($doc['title']) ?>
|
|
</a>
|
|
</td>
|
|
<td><code><?= htmlspecialchars($doc['filename']) ?></code></td>
|
|
<td class="text-center">
|
|
<form method="post" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<input type="hidden" name="action" value="toggle_view">
|
|
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
|
|
<button type="submit" class="btn-perm <?= $engPerms['view'] ? 'on' : 'off' ?>" data-tooltip="Просмотр"><?= $engPerms['view'] ? '✓' : '✗' ?></button>
|
|
</form>
|
|
</td>
|
|
<td class="text-center">
|
|
<form method="post" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<input type="hidden" name="action" value="toggle_edit">
|
|
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
|
|
<button type="submit" class="btn-perm <?= $engPerms['edit'] ? 'on' : 'off' ?>" data-tooltip="Редактирование"><?= $engPerms['edit'] ? '✓' : '✗' ?></button>
|
|
</form>
|
|
</td>
|
|
<td class="text-center">
|
|
<form method="post" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<input type="hidden" name="action" value="toggle_cancel">
|
|
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
|
|
<button type="submit" class="btn-perm <?= $engPerms['cancel'] ? 'on' : 'off' ?>" data-tooltip="Отмена"><?= $engPerms['cancel'] ? '✓' : '✗' ?></button>
|
|
</form>
|
|
</td>
|
|
<td class="text-center">
|
|
<button type="button" class="btn btn-sm btn-secondary btn-rename" data-tooltip="Переименовать" data-slug="<?= htmlspecialchars($doc['slug']) ?>" data-title="<?= htmlspecialchars($doc['title']) ?>" data-sort="<?= $p['sort_order'] ?? $idx ?>">✎</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="docs-admin__info">
|
|
<h3>Как это работает</h3>
|
|
<ul>
|
|
<li>Все .md-файлы из <code>service/docs/</code> автоматически обнаруживаются.</li>
|
|
<li>Стрелки ▲▼ меняют порядок отображения.</li>
|
|
<li><strong>Просмотр</strong> — инженер видит документ в списке.</li>
|
|
<li><strong>Редакт.</strong> — инженер может изменить содержимое (автоматически включает просмотр).</li>
|
|
<li><strong>Отмена</strong> — инженер может отменить документ без удаления.</li>
|
|
<li>Кнопка ✎ открывает переименование и настройку порядка.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rename Modal -->
|
|
<div class="modal-overlay" id="renameModal">
|
|
<div class="modal">
|
|
<button class="modal-close" onclick="closeRename()">×</button>
|
|
<h2>Переименовать документ</h2>
|
|
<form method="post">
|
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
|
<input type="hidden" name="action" value="rename">
|
|
<input type="hidden" name="slug" id="rename-slug">
|
|
<div class="form-group">
|
|
<label>Название</label>
|
|
<input type="text" id="rename-title" name="new_title" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Порядок (число, по возрастанию)</label>
|
|
<input type="number" id="rename-sort" name="new_sort" value="0" min="0">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
|
<button type="button" class="btn btn-secondary" onclick="closeRename()">Отмена</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.docs-admin { padding: 24px; }
|
|
.page-header { margin-bottom: 24px; }
|
|
.page-header h1 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
|
|
.page-subtitle { font-size: 13px; color: var(--text-muted); line-height: 1.6; }
|
|
.page-subtitle code { background: var(--bg-input); padding: 2px 6px; border-radius: 4px; font-size: 12px; color: var(--accent); }
|
|
.table-responsive { overflow-x: auto; }
|
|
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.table th, .table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); color: var(--text-primary); }
|
|
.table th { font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; background: var(--bg-card); }
|
|
.table tbody tr:hover { background: rgba(0,173,239,0.03); }
|
|
.text-center { text-align: center; }
|
|
.doc-link { color: var(--accent); text-decoration: none; font-weight: 500; }
|
|
.doc-link:hover { text-decoration: underline; }
|
|
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
|
|
.alert-success { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
|
|
.alert-danger { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
|
|
.btn-sm { padding: 6px 14px; font-size: 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
|
|
.btn-primary { background: var(--accent); color: #fff; }
|
|
.btn-secondary { background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border); }
|
|
.btn-sort { background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:10px; padding:2px 4px; transition:color 0.2s; }
|
|
.btn-sort:hover:not(.disabled) { color:var(--accent); }
|
|
.btn-sort.disabled { opacity:0.2; cursor:default; }
|
|
.btn-perm { width:28px; height:28px; border:none; border-radius:6px; cursor:pointer; font-size:14px; font-weight:700; transition:all 0.2s; }
|
|
.btn-perm.on { background:rgba(34,197,94,0.2); color:var(--success); }
|
|
.btn-perm.off { background:rgba(136,153,187,0.15); color:var(--text-muted); }
|
|
.btn-perm:hover { transform:scale(1.1); }
|
|
.btn-rename { padding:4px 8px; }
|
|
.docs-admin__info { margin-top: 32px; padding: 24px; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--border); }
|
|
.docs-admin__info h3 { font-size: 14px; color: var(--text-primary); margin-bottom: 12px; }
|
|
.docs-admin__info ul { list-style: none; padding: 0; }
|
|
.docs-admin__info li { padding: 4px 0; font-size: 13px; color: var(--text-secondary); line-height: 1.5; }
|
|
.docs-admin__info li::before { content: '• '; color: var(--accent); font-weight: bold; }
|
|
</style>
|
|
|
|
<script>
|
|
(function(){
|
|
var modal = document.getElementById('renameModal');
|
|
window.closeRename = function(){ modal.classList.remove('open'); };
|
|
document.querySelectorAll('.btn-rename').forEach(function(btn){
|
|
btn.addEventListener('click', function(){
|
|
document.getElementById('rename-slug').value = btn.getAttribute('data-slug');
|
|
document.getElementById('rename-title').value = btn.getAttribute('data-title');
|
|
document.getElementById('rename-sort').value = btn.getAttribute('data-sort');
|
|
modal.classList.add('open');
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/../../inc/footer.php'; ?>
|