Files
site_for_glpi/knowledge.php
T

227 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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()">&times;</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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
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>