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 (
{/* Header */}

📚 Wissensdatenbank

{tab === 'artikel' ? `${articles.length} Artikel · durchsuchbare Lösungen` : `${tickets.length} geschlossene Tickets`}

{tab === 'artikel' && canEdit && (
)}
{/* Tabs */}
{[ { key: 'artikel', label: '📖 Artikel', count: articles.length }, { key: 'tickets', label: '🎫 Geschlossene Tickets', count: ticketsLoaded ? tickets.length : null }, ].map(t => ( ))}
{/* ── ARTIKEL TAB ── */} {tab === 'artikel' && ( <> {/* Filter bar */}
setSearchArt(e.target.value)} />
{['Alle', ...CATEGORIES].map(c => ( ))}
{loadingArt ? (
Lade…
) : filteredArticles.length === 0 ? (
📭
{articles.length === 0 ? 'Noch keine Artikel. Erstelle den ersten!' : 'Keine Treffer.'}
) : (
{filteredArticles.map(art => (
setExpanded(expanded === art.id ? null : art.id)}> {/* Header row */}
{art.problem}
{art.category} {art.tags && art.tags.split(',').map(tag => ( #{tag.trim()} ))} {art.auto_generated ? 🤖 KI : null} {art.source_ticket_id ? Ticket #{art.source_ticket_id} : null} 📅 {formatDate(art.created_at)} {art.created_by_username && von {art.created_by_username}}
{canEdit && ( <> )} {expanded === art.id ? '▲' : '▼'}
{/* Expanded solution */} {expanded === art.id && (
Lösung
{/* Images */} {(() => { const imgs = (() => { try { return JSON.parse(art.images || '[]'); } catch { return []; } })(); return (imgs.length > 0 || canEdit) && (
Bilder
{imgs.map(img => (
{img} { e.stopPropagation(); window.open(`/uploads/kb/${img}`, '_blank'); }} /> {canEdit && ( )}
))} {canEdit && ( )}
); })()}
)}
))}
)} )} {/* ── TICKETS TAB ── */} {tab === 'tickets' && ( <>
setSearchTickets(e.target.value)} />
{ticketCats.map(c => ( ))}
{loadingTickets ? (
Lade…
) : filteredTickets.length === 0 ? (
📭
Keine geschlossenen Tickets gefunden
) : (
{filteredTickets.map(t => (
e.currentTarget.style.borderColor = 'var(--primary)'} onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border-color)'}>
{t.ticket_number} {t.title}
{t.requester_name && 👤 {t.requester_name}} {t.assigned_to_username && 🛠 {t.assigned_to_first_name || ''} {t.assigned_to_last_name || t.assigned_to_username}}
{t.category} {t.satisfaction_rating === 'gut' && ( 👍 )} {t.satisfaction_rating === 'schlecht' && ( 👎 )}
✅ {formatDate(t.closed_at || t.updated_at)}
{getDuration(t.created_at, t.closed_at || t.updated_at) && (
⏱ {getDuration(t.created_at, t.closed_at || t.updated_at)}
)}
))}
)} )}
{/* ── Import Modal ── */} {showImport && (
setShowImport(false)}>
e.stopPropagation()}>

📥 In Wissensdatenbank importieren

{/* Sub-tabs */}
{[{ key: 'text', label: '📝 Text einfügen' }, { key: 'file', label: '📎 Datei hochladen' }, { key: 'url', label: '🌐 URL importieren' }].map(t => ( ))}
{importTab === 'text' ? ( <>

Füge beliebigen Text ein (Handbuch, Anleitung, E-Mail-Inhalt usw.).
Die KI extrahiert automatisch Problem/Lösungs-Paare.