Первоначальная версия проекта chinilich-1.0
This commit is contained in:
+227
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$user = checkAuth($pdo);
|
||||
if (!$user) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/glpi_api.php';
|
||||
$glpi = new GlpiApi(GLPI_API_URL, GLPI_APP_TOKEN, GLPI_USERNAME, GLPI_PASSWORD);
|
||||
|
||||
// Ручная синхронизация (только для админа)
|
||||
if (isset($_GET['sync']) && $_GET['sync'] === '1' && ($user['role'] === 'admin' || $user['role'] === 'superadmin')) {
|
||||
$log = [];
|
||||
syncKnowledgeBaseIncremental($pdo, $glpi, $log);
|
||||
header('Location: knowledge.php?synced=1');
|
||||
exit;
|
||||
}
|
||||
$synced = isset($_GET['synced']);
|
||||
|
||||
// Сортировка: по дате или по названию
|
||||
$order = $_GET['order'] ?? 'date';
|
||||
$allowedOrders = ['date', 'title'];
|
||||
if (!in_array($order, $allowedOrders)) $order = 'date';
|
||||
$sqlOrder = ($order === 'date') ? 'updated_at DESC' : 'title ASC';
|
||||
$stmt = $pdo->prepare("SELECT * FROM knowledge_base ORDER BY $sqlOrder");
|
||||
$stmt->execute();
|
||||
$articles = $stmt->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>База знаний – Service Desk</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header containers">
|
||||
<div class="container">
|
||||
<h1>🧙♂️ Чинилыч</h1>
|
||||
<div class="user-info">
|
||||
<span><?= htmlspecialchars($user['last_name'] . ' ' . $user['first_name']) ?></span>
|
||||
<button id="themeToggle" class="theme-toggle" aria-label="Тёмная тема">🌙</button>
|
||||
<a href="logout.php" class="btn btn-outline">Выход</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<div class="nav-links">
|
||||
<a href="dashboard.php">📋 Заявки</a>
|
||||
<a href="knowledge.php" class="active">📚 База знаний</a>
|
||||
</div>
|
||||
|
||||
<?php if ($synced): ?>
|
||||
<div class="alert alert-success">Синхронизация успешно завершена.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchInput" placeholder="Поиск по базе знаний...">
|
||||
<button class="btn btn-primary" id="searchBtn">Найти</button>
|
||||
<button class="btn btn-outline" id="resetSearchBtn">Сбросить</button>
|
||||
<div style="flex:1"></div>
|
||||
<div class="sort-buttons">
|
||||
<a href="?order=date" class="btn btn-outline <?= $order === 'date' ? 'active' : '' ?>">📅 По дате</a>
|
||||
<a href="?order=title" class="btn btn-outline <?= $order === 'title' ? 'active' : '' ?>">🔤 По названию</a>
|
||||
</div>
|
||||
<?php if ($user['role'] === 'admin' || $user['role'] === 'superadmin'): ?>
|
||||
<a href="?sync=1" class="btn btn-outline">🔄 Обновить базу</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📚 Статьи</h2>
|
||||
<div id="articlesList">
|
||||
<?php if (empty($articles)): ?>
|
||||
<p>Статьи не найдены. Нажмите "Обновить базу", чтобы загрузить статьи из GLPI.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($articles as $item):
|
||||
$preview = mb_substr(strip_tags($item['content']), 0, 200) . '...';
|
||||
?>
|
||||
<div class="article-card" data-id="<?= $item['id'] ?>">
|
||||
<div class="article-title"><?= htmlspecialchars($item['title']) ?></div>
|
||||
<div class="article-meta">Обновлено: <?= date('d.m.Y', strtotime($item['updated_at'] ?? $item['created_at'])) ?></div>
|
||||
<div class="article-preview"><?= htmlspecialchars($preview) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Модальное окно статьи -->
|
||||
<div class="modal-overlay" id="articleModal">
|
||||
<div class="modal modal-large">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle"></h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalContent">
|
||||
<div class="loading-spinner">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-large { max-width: 900px; width: 95%; }
|
||||
.documents-list { margin-top: 20px; border-top: 1px solid var(--border-light); padding-top: 15px; }
|
||||
.documents-list h4 { margin-bottom: 12px; }
|
||||
.doc-item { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border-light); }
|
||||
.doc-icon { font-size: 1.4rem; }
|
||||
.doc-name { flex: 1; word-break: break-all; }
|
||||
.doc-link { color: var(--primary-dark); text-decoration: none; }
|
||||
.doc-link:hover { text-decoration: underline; }
|
||||
.sort-buttons { display: flex; gap: 8px; }
|
||||
.sort-buttons .btn.active { background: var(--primary); color: white; border-color: var(--primary); }
|
||||
@media (max-width: 640px) {
|
||||
.search-box { flex-direction: column; }
|
||||
.sort-buttons { justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
themeToggle.textContent = '☀️';
|
||||
}
|
||||
themeToggle.addEventListener('click', () => {
|
||||
document.body.classList.toggle('dark');
|
||||
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
|
||||
themeToggle.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
|
||||
});
|
||||
|
||||
const modal = document.getElementById('articleModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalContent = document.getElementById('modalContent');
|
||||
function closeModal() { modal.style.display = 'none'; }
|
||||
window.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
|
||||
|
||||
document.addEventListener('click', async function(e) {
|
||||
const card = e.target.closest('.article-card');
|
||||
if (!card) return;
|
||||
const articleId = card.dataset.id;
|
||||
modal.style.display = 'flex';
|
||||
modalTitle.textContent = 'Загрузка...';
|
||||
modalContent.innerHTML = '<div class="loading-spinner">Загрузка статьи...</div>';
|
||||
try {
|
||||
const response = await fetch(`get_article.php?id=${articleId}`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
modalTitle.textContent = data.title;
|
||||
let html = `<div class="article-content">${data.content || 'Нет текста'}</div>`;
|
||||
if (data.documents && data.documents.length > 0) {
|
||||
html += `<div class="documents-list"><h4>📎 Прикреплённые файлы</h4>`;
|
||||
data.documents.forEach(doc => {
|
||||
const icon = doc.mime.startsWith('image/') ? '🖼️' : (doc.mime === 'application/pdf' ? '📄' : '📎');
|
||||
html += `
|
||||
<div class="doc-item">
|
||||
<span class="doc-icon">${icon}</span>
|
||||
<span class="doc-name">${escapeHtml(doc.name)}</span>
|
||||
<a href="${doc.url}" class="doc-link" target="_blank">Скачать</a>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
modalContent.innerHTML = html;
|
||||
} catch (error) {
|
||||
modalContent.innerHTML = '<div class="error-message">Не удалось загрузить статью.</div>';
|
||||
modalTitle.textContent = 'Ошибка';
|
||||
}
|
||||
});
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
const searchBtn = document.getElementById('searchBtn');
|
||||
const resetBtn = document.getElementById('resetSearchBtn');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
async function performSearch() {
|
||||
const query = searchInput.value.trim();
|
||||
if (!query) { resetSearch(); return; }
|
||||
const articlesDiv = document.getElementById('articlesList');
|
||||
articlesDiv.innerHTML = '<div class="loading-spinner">Поиск...</div>';
|
||||
try {
|
||||
const response = await fetch(`search_knowledge.php?q=${encodeURIComponent(query)}`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.length === 0) {
|
||||
articlesDiv.innerHTML = '<div class="no-results">Ничего не найдено.</div>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
data.forEach(item => {
|
||||
html += `<div class="article-card" data-id="${item.id}">
|
||||
<div class="article-title">${escapeHtml(item.title)}</div>
|
||||
<div class="article-meta">Обновлено: ${item.updated_at}</div>
|
||||
<div class="article-preview">${escapeHtml(item.preview)}</div>
|
||||
</div>`;
|
||||
});
|
||||
articlesDiv.innerHTML = html;
|
||||
} catch(e) {
|
||||
articlesDiv.innerHTML = '<div class="error-message">Ошибка поиска.</div>';
|
||||
}
|
||||
}
|
||||
function resetSearch() { searchInput.value = ''; location.reload(); }
|
||||
searchBtn.addEventListener('click', performSearch);
|
||||
resetBtn.addEventListener('click', resetSearch);
|
||||
searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user