diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5e0aa9b..678c9c7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,9 +5,10 @@ import { TextArea } from '@heroui/react'; import { - Activity, AudioLines, Bot, Cable, CalendarDays, ChevronRight, Command, - Home, LifeBuoy, Logs, MessageSquare, Puzzle, RadioTower, ScanSearch, - Settings, Shield, Sparkles, Tag, Ticket, UserRound, Users, Wrench + Activity, AudioLines, Bot, Cable, CalendarDays, ChevronRight, ClipboardList, + Command, Home, LifeBuoy, LogIn, Logs, MessageSquare, Music, Pencil, + Puzzle, RadioTower, ScanSearch, Settings, Shield, Sparkles, Tag, + Ticket, Trash2, UserRound, Users, Wrench } from 'lucide-react'; import { apiFetch } from './utils/api'; import { formatDate, guildIconUrl } from './utils/formatters'; @@ -22,7 +23,9 @@ import { ActivityTile } from './components/shared/ActivityTile'; import { LoadingSkeleton } from './components/shared/LoadingSkeleton'; import type { AppConfig, User, Guild, NavKey, TicketRecord, StatusService, - EventItem, ReactionRoleSet, ModuleItem, LogEntry, SettingsState + EventItem, ReactionRoleSet, ModuleItem, LogEntry, SettingsState, + SupportLoginConfig, SupportLoginStatus, RegisterForm, RegisterFormField, + RegisterApplication, MusicSession } from './types'; const appConfig: AppConfig = (window as any).__PAPO__ || {}; @@ -59,6 +62,20 @@ function App() { const [statsDraft, setStatsDraft] = useState(null); const [reactionDraft, setReactionDraft] = useState({ title: '', channelId: '', entries: '' }); + const [supportLogin, setSupportLogin] = useState<{ config: SupportLoginConfig; status: SupportLoginStatus; supportRoleId?: string } | null>(null); + const [registerForms, setRegisterForms] = useState([]); + const [registerApps, setRegisterApps] = useState([]); + const [formDraft, setFormDraft] = useState({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' }); + const [editingFormId, setEditingFormId] = useState(null); + const [registerTab, setRegisterTab] = useState('forms'); + const [musicStatus, setMusicStatus] = useState<{ activeGuilds: number; sessions: MusicSession[] }>({ activeGuilds: 0, sessions: [] }); + const [kbEditDraft, setKbEditDraft] = useState<{ id: string; title: string; keywords: string; content: string } | null>(null); + const [automationEditDraft, setAutomationEditDraft] = useState<{ id: string; name: string; conditionValue: string; actionValue: string } | null>(null); + const [statusServiceDraft, setStatusServiceDraft] = useState<{ id?: string; name: string; url: string; status: string }>({ name: '', url: '', status: 'unknown' }); + const [statsItemDraft, setStatsItemDraft] = useState<{ id?: string; label: string; type: string }>({ label: '', type: 'members' }); + const [ticketDetail, setTicketDetail] = useState(null); + const [ticketMessages, setTicketMessages] = useState([]); + useEffect(() => { const hash = window.location.hash.replace('#', '') as NavKey; if (navItems.some((item) => item.key === hash)) setSection(hash); @@ -82,7 +99,8 @@ function App() { setStatusMessage('Lade Daten...'); try { const [guildInfoRes, overviewRes, activityRes, logsRes, settingsRes, modulesRes, - birthdayRes, reactionRes, statusRes, statsRes, eventsRes] = await Promise.all([ + birthdayRes, reactionRes, statusRes, statsRes, eventsRes, supportLoginRes, + registerFormsRes, registerAppsRes] = await Promise.all([ apiFetch(`/guild/info?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/overview?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/guild/activity?guildId=${encodeURIComponent(guildId)}`), @@ -93,10 +111,14 @@ function App() { apiFetch(`/reactionroles?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/statuspage?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/server-stats?guildId=${encodeURIComponent(guildId)}`), - apiFetch(`/events?guildId=${encodeURIComponent(guildId)}`) + apiFetch(`/events?guildId=${encodeURIComponent(guildId)}`), + apiFetch(`/tickets/support-login?guildId=${encodeURIComponent(guildId)}`), + apiFetch(`/register/forms?guildId=${encodeURIComponent(guildId)}`), + apiFetch(`/register/apps?guildId=${encodeURIComponent(guildId)}`) ]); setGuildInfo(guildInfoRes.guild || null); setOverview(overviewRes); + setMusicStatus(overviewRes.music || { activeGuilds: 0, sessions: [] }); setActivity(activityRes.activity || {}); setLogs(logsRes.logs || []); setSettings(settingsRes.settings || {}); @@ -108,6 +130,9 @@ function App() { setStatsDraft(statsRes.config || { items: [] }); setStatusDraft(statusRes.config || { services: [] }); setEvents(eventsRes.events || []); + setSupportLogin(supportLoginRes); + setRegisterForms(registerFormsRes.forms || []); + setRegisterApps(registerAppsRes.applications || []); setReactionDraft({ title: '', channelId: '', entries: '' }); await Promise.all([loadTicketData(guildId), loadAdminData()]); setStatusMessage(''); @@ -228,6 +253,136 @@ function App() { await loadTicketData(currentGuildId); } + async function updateKbArticle(id: string) { + if (!kbEditDraft) return; + await apiFetch(`/kb/${id}`, { + method: 'PUT', + body: JSON.stringify({ guildId: currentGuildId, title: kbEditDraft.title, keywords: kbEditDraft.keywords, content: kbEditDraft.content }) + }); + setKbEditDraft(null); + setStatusMessage('KB-Artikel aktualisiert'); + await loadTicketData(currentGuildId); + } + + async function deleteKbArticle(id: string) { + await apiFetch(`/kb/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) }); + setStatusMessage('KB-Artikel gelöscht'); + await loadTicketData(currentGuildId); + } + + async function updateAutomation(id: string) { + if (!automationEditDraft) return; + await apiFetch(`/automations/${id}`, { + method: 'PUT', + body: JSON.stringify({ guildId: currentGuildId, name: automationEditDraft.name, condition: { category: automationEditDraft.conditionValue }, action: { type: 'reminder', message: automationEditDraft.actionValue }, active: true }) + }); + setAutomationEditDraft(null); + setStatusMessage('Automation aktualisiert'); + await loadTicketData(currentGuildId); + } + + async function deleteAutomation(id: string) { + await apiFetch(`/automations/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) }); + setStatusMessage('Automation gelöscht'); + await loadTicketData(currentGuildId); + } + + async function saveSupportLogin() { + if (!supportLogin) return; + await apiFetch('/tickets/support-login', { + method: 'POST', + body: JSON.stringify({ guildId: currentGuildId, ...supportLogin.config }) + }); + setStatusMessage('Support Login gespeichert'); + await loadGuildData(currentGuildId); + } + + async function saveForm() { + const fields: RegisterFormField[] = formDraft.fields.split('\n').filter(Boolean).map((line) => { + const parts = line.split('|').map((s) => s.trim()); + return { label: parts[0] || 'Feld', type: (parts[1] || 'text') as any, required: parts[2] === 'required', options: parts[3] ? parts[3].split(',').map((s) => s.trim()) : undefined }; + }); + const body: any = { guildId: currentGuildId, name: formDraft.name, description: formDraft.description, reviewChannelId: formDraft.reviewChannelId || undefined, notifyRoleIds: formDraft.notifyRoleIds.split(',').map((s) => s.trim()).filter(Boolean), fields, isActive: true }; + if (editingFormId) { + await apiFetch(`/register/forms/${editingFormId}`, { method: 'PUT', body: JSON.stringify(body) }); + setStatusMessage('Formular aktualisiert'); + } else { + await apiFetch('/register/forms', { method: 'POST', body: JSON.stringify(body) }); + setStatusMessage('Formular erstellt'); + } + setFormDraft({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' }); + setEditingFormId(null); + await loadGuildData(currentGuildId); + } + + async function deleteForm(id: string) { + await apiFetch(`/register/forms/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) }); + setStatusMessage('Formular gelöscht'); + await loadGuildData(currentGuildId); + } + + async function sendFormPanel(formId: string) { + if (!supportLogin?.config?.panelChannelId) { setStatusMessage('Bitte zuerst Support Login konfigurieren'); return; } + await apiFetch(`/register/forms/${formId}/panel`, { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, channelId: supportLogin.config.panelChannelId }) }); + setStatusMessage('Panel gesendet'); + } + + async function addStatusService() { + if (!statusServiceDraft.name) return; + await apiFetch('/statuspage/service', { + method: 'POST', + body: JSON.stringify({ guildId: currentGuildId, name: statusServiceDraft.name, url: statusServiceDraft.url, status: statusServiceDraft.status }) + }); + setStatusServiceDraft({ name: '', url: '', status: 'unknown' }); + setStatusMessage('Service hinzugefügt'); + await loadGuildData(currentGuildId); + } + + async function deleteStatusService(id: string) { + await apiFetch(`/statuspage/service/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) }); + setStatusMessage('Service entfernt'); + await loadGuildData(currentGuildId); + } + + async function addStatsItem() { + if (!statsItemDraft.label) return; + const draft = statsDraft || { enabled: true, categoryName: '', refreshMinutes: 10, items: [] }; + const items = [...(draft.items || []), { key: statsItemDraft.label.toLowerCase().replace(/\s+/g, '_'), label: statsItemDraft.label, type: statsItemDraft.type }]; + const updated = { ...draft, items }; + setStatsDraft(updated); + await apiFetch('/server-stats', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: updated }) }); + setStatsItemDraft({ label: '', type: 'members' }); + setStatusMessage('Stat-Item hinzugefügt'); + await loadGuildData(currentGuildId); + } + + async function deleteStatsItem(index: number) { + const draft = statsDraft || { enabled: true, categoryName: '', refreshMinutes: 10, items: [] }; + const items = (draft.items || []).filter((_: any, i: number) => i !== index); + const updated = { ...draft, items }; + setStatsDraft(updated); + await apiFetch('/server-stats', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: updated }) }); + setStatusMessage('Stat-Item entfernt'); + await loadGuildData(currentGuildId); + } + + async function loadTicketMessages(ticketId: string) { + const res = await apiFetch(`/tickets/${ticketId}/messages`); + setTicketMessages(res.messages || []); + } + + async function updateTicketStatus(ticketId: string, status: string) { + await apiFetch(`/tickets/${ticketId}/status`, { method: 'POST', body: JSON.stringify({ status }) }); + setStatusMessage('Status aktualisiert'); + await loadGuildData(currentGuildId); + } + + async function closeTicket(ticketId: string) { + await apiFetch(`/tickets/${ticketId}/close`, { method: 'POST', body: JSON.stringify({ guildId: currentGuildId }) }); + setStatusMessage('Ticket geschlossen'); + await loadGuildData(currentGuildId); + } + const selectedGuild = guilds.find((g) => g.id === currentGuildId) || null; const moduleFlags = guildInfo?.modules || {}; @@ -418,7 +573,35 @@ function App() {
{tickets.length ? tickets.map((t, i) => ( -
{t.topic || 'Ticket'} · {t.status || 'open'}
+ + +
+
{t.topic || 'Ticket'}
+ + {t.status || 'open'} + +
+
+ {t.category || 'Allgemein'} · {formatDate(t.createdAt)} + {t.claimedById ? ` · Claimed` : ''} +
+
+ + + +
+
+
)) :

Keine Tickets

}
@@ -426,6 +609,31 @@ function App() {
{t.category || 'Allgemein'} · {formatDate(t.createdAt)}
)) :

Keine Daten

}
+ {ticketDetail && ( + + +

{ticketDetail.topic || 'Ticket-Details'}

+ +
+ +
+
Status: {ticketDetail.status}
+
Priorität: {ticketDetail.priority || 'normal'}
+
Kategorie: {ticketDetail.category || '-'}
+
+ +

Nachrichten ({ticketMessages.length})

+
+ {ticketMessages.length ? ticketMessages.map((msg, i) => ( +
+ {msg.author?.username || msg.authorId}: + {msg.content || '(Embed)'} +
+ )) :

Keine Nachrichten geladen

} +
+
+
+ )}
)} @@ -434,7 +642,16 @@ function App() { {['neu', 'in_bearbeitung', 'warten_auf_user', 'erledigt'].map((state) => ( {(pipeline[state] || []).length ? (pipeline[state] || []).map((t, i) => ( -
{t.topic || t.id}
+ + +
{t.topic || t.id}
+
+ +
+
+
)) :

Keine Tickets

}
))} @@ -464,17 +681,44 @@ function App() {
{automations.length ? automations.map((rule, i) => ( -
- {rule.name || 'Automation'} · {rule.active === false ? 'inaktiv' : 'aktiv'} -
+ + +
+
{rule.name || 'Automation'}
+ + {rule.active !== false ? 'Aktiv' : 'Inaktiv'} + +
+
+ + +
+
+
)) :

Keine Regeln

}
- - setAutomationDraft((s) => ({ ...s, name: v }))} /> - setAutomationDraft((s) => ({ ...s, conditionValue: v }))} /> -