import React, { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import aiService from '../services/aiService'; import ticketService from '../services/ticketService'; import { toast } from 'react-toastify'; import { renderMd } from '../utils/sanitizeMarkdown'; const CATEGORIES = ['Allgemein', 'Software', 'Hardware', 'Netzwerk', 'SelectLine', 'Sonstiges']; const CAT_COLOR = { Software: '#6366f1', Hardware: '#f59e0b', SelectLine: '#10b981', Netzwerk: '#3b82f6', Allgemein: '#64748b', Sonstiges: '#8b5cf6' }; const catColor = (c) => CAT_COLOR[c] || '#64748b'; const EMPTY_FORM = { problem: '', solution: '', category: 'Allgemein', tags: '' }; export default function KnowledgeBasePage() { const { isAdmin, isSuperAdmin } = useAuth(); const canEdit = isAdmin() || isSuperAdmin(); // Tab state const [tab, setTab] = useState('artikel'); // KB Articles state const [articles, setArticles] = useState([]); const [loadingArt, setLoadingArt] = useState(true); const [searchArt, setSearchArt] = useState(''); const [catFilter, setCatFilter] = useState('Alle'); const [expanded, setExpanded] = useState(null); const [showModal, setShowModal] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(EMPTY_FORM); const [saving, setSaving] = useState(false); const [showImport, setShowImport] = useState(false); const [importTab, setImportTab] = useState('text'); // 'text' | 'file' | 'url' const [importText, setImportText] = useState(''); const [importFile, setImportFile] = useState(null); const [importUrl, setImportUrl] = useState(''); const [importCrawl, setImportCrawl] = useState(false); const [importCrawlMax, setImportCrawlMax] = useState(20); const [importing, setImporting] = useState(false); // Closed tickets state const [tickets, setTickets] = useState([]); const [loadingTickets, setLoadingTickets] = useState(false); const [searchTickets, setSearchTickets] = useState(''); const [ticketCat, setTicketCat] = useState('Alle'); const [ticketsLoaded, setTicketsLoaded] = useState(false); const loadArticles = useCallback(async () => { setLoadingArt(true); try { const data = await aiService.getKnowledgeBase(); setArticles(data); } catch { toast.error('Fehler beim Laden der Wissensdatenbank'); } finally { setLoadingArt(false); } }, []); const loadTickets = useCallback(async () => { if (ticketsLoaded) return; setLoadingTickets(true); try { const data = await ticketService.getAll({ status: 'geschlossen' }); setTickets(Array.isArray(data) ? data : []); setTicketsLoaded(true); } catch { setTickets([]); } finally { setLoadingTickets(false); } }, [ticketsLoaded]); useEffect(() => { loadArticles(); }, [loadArticles]); useEffect(() => { if (tab === 'tickets') loadTickets(); }, [tab, loadTickets]); // ── Article handlers ───────────────────────────────────────────────────── const openCreate = () => { setEditingId(null); setForm(EMPTY_FORM); setShowModal(true); }; const openEdit = (art, e) => { e.stopPropagation(); setEditingId(art.id); setForm({ problem: art.problem, solution: art.solution, category: art.category || 'Allgemein', tags: art.tags || '' }); setShowModal(true); }; const handleSave = async (e) => { e.preventDefault(); setSaving(true); try { if (editingId) { await aiService.updateKnowledgeEntry(editingId, form); toast.success('Artikel aktualisiert'); } else { await aiService.addKnowledgeEntry(form); toast.success('Artikel erstellt'); } setShowModal(false); loadArticles(); } catch { toast.error('Fehler beim Speichern'); } finally { setSaving(false); } }; const handleImport = async (e) => { e.preventDefault(); setImporting(true); try { let result; if (importTab === 'file' && importFile) { result = await aiService.importFileToKb(importFile); } else if (importTab === 'url') { result = importCrawl ? await aiService.importCrawlToKb(importUrl, importCrawlMax) : await aiService.importUrlToKb(importUrl); } else { result = await aiService.importTextToKb(importText); } const pagesInfo = result.pages ? ` (${result.pages} Seiten)` : ''; toast.success(`${result.created} Artikel erstellt${pagesInfo}`); setShowImport(false); setImportText(''); setImportFile(null); setImportUrl(''); setImportCrawl(false); setImportCrawlMax(20); setImportTab('text'); loadArticles(); } catch (err) { toast.error(err?.response?.data?.message || err?.message || 'Fehler beim Importieren'); } finally { setImporting(false); } }; const handleDelete = async (id, e) => { e.stopPropagation(); if (!window.confirm('Artikel wirklich löschen?')) return; try { await aiService.deleteKnowledgeEntry(id); toast.success('Artikel gelöscht'); setExpanded(null); loadArticles(); } catch { toast.error('Fehler beim Löschen'); } }; const handleImageUpload = async (artId, e) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { toast.error('Max. 5 MB'); return; } try { await aiService.uploadKbImage(artId, file); toast.success('Bild hochgeladen'); loadArticles(); } catch { toast.error('Fehler beim Hochladen'); } e.target.value = ''; }; const handleImageDelete = async (artId, filename, e) => { e.stopPropagation(); if (!window.confirm('Bild löschen?')) return; try { await aiService.deleteKbImage(artId, filename); loadArticles(); } catch { toast.error('Fehler beim Löschen'); } }; // ── Filtered data ───────────────────────────────────────────────────────── const filteredArticles = articles.filter(a => { const q = searchArt.toLowerCase(); const matchQ = !q || a.problem?.toLowerCase().includes(q) || a.solution?.toLowerCase().includes(q) || a.tags?.toLowerCase().includes(q); const matchC = catFilter === 'Alle' || a.category === catFilter; return matchQ && matchC; }); const ticketCats = ['Alle', ...Array.from(new Set(tickets.map(t => t.category).filter(Boolean)))]; const filteredTickets = tickets.filter(t => { const q = searchTickets.toLowerCase(); const matchQ = !q || t.title?.toLowerCase().includes(q) || t.ticket_number?.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q); const matchC = ticketCat === 'Alle' || t.category === ticketCat; return matchQ && matchC; }); const formatDate = (d) => d ? new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '—'; const getDuration = (a, b) => { if (!a || !b) return null; const h = Math.floor((new Date(b) - new Date(a)) / 3600000); return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`; }; // ── Render ──────────────────────────────────────────────────────────────── return (
{tab === 'artikel' ? `${articles.length} Artikel · durchsuchbare Lösungen` : `${tickets.length} geschlossene Tickets`}