diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 36be5f5..d5573bf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ import { MusicPage } from './pages/Music'; import { SettingsPage } from './pages/Settings'; import { ModulesPage } from './pages/Modules'; import { Events } from './pages/Events'; +import { Tasks } from './pages/Tasks'; import { Admin } from './pages/Admin'; function AppContent() { @@ -45,6 +46,7 @@ function AppContent() { case 'settings': return ; case 'modules': return ; case 'events': return ; + case 'tasks': return ; case 'admin': return ; default: return ; } diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 29e044e..0e33464 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -5,7 +5,7 @@ import { } from '@heroui/react'; import { LogOut, PanelLeftClose, PanelLeft, Activity, AudioLines, CalendarDays, - ClipboardList, Home, LogIn, Music, Puzzle, RadioTower, Settings, + ClipboardList, Home, LogIn, ListChecks, Music, Puzzle, RadioTower, Settings, Shield, Sparkles, Tag, Ticket, Wrench } from 'lucide-react'; import { useApp } from '../../context/AppContext'; @@ -39,6 +39,7 @@ const navGroups = [ items: [ { key: 'automod', label: 'Automod', icon: }, { key: 'reactionroles', label: 'Reaction Roles', icon: }, + { key: 'tasks', label: 'Team-Aufgaben', icon: }, ] }, { diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx index 410f370..cb502fe 100644 --- a/frontend/src/context/AppContext.tsx +++ b/frontend/src/context/AppContext.tsx @@ -4,7 +4,7 @@ import type { AppConfig, User, Guild, NavKey, TicketRecord, StatusService, EventItem, ReactionRoleSet, ModuleItem, LogEntry, SettingsState, SupportLoginConfig, SupportLoginStatus, RegisterForm, RegisterFormField, - RegisterApplication, MusicSession + RegisterApplication, MusicSession, StaffTask } from '../types'; const appConfig: AppConfig = (window as any).__PAPO__ || {}; @@ -44,6 +44,8 @@ type AppState = { appNotes: any[]; appHistory: any[]; noteDraft: string; + tasks: StaffTask[]; + taskDraft: { title: string; description: string }; }; type AppContextType = AppState & { @@ -120,6 +122,10 @@ type AppContextType = AppState & { openAppDetail: (id: string) => Promise; setNoteDraft: (v: string) => void; addAppNote: () => Promise; + setTaskDraft: (s: any | ((prev: any) => any)) => void; + createTask: () => Promise; + updateTaskStatus: (id: string, status: string) => Promise; + deleteTask: (id: string) => Promise; }; const AppContext = createContext(null); @@ -175,6 +181,8 @@ export function AppProvider({ children }: { children: ReactNode }) { const [appNotes, setAppNotes] = useState([]); const [appHistory, setAppHistory] = useState([]); const [noteDraft, setNoteDraft] = useState(''); + const [tasks, setTasks] = useState([]); + const [taskDraft, setTaskDraft] = useState({ title: '', description: '' }); const setSection = useCallback((key: NavKey) => { setSectionState(key); @@ -225,7 +233,7 @@ export function AppProvider({ children }: { children: ReactNode }) { try { const [guildInfoRes, overviewRes, activityRes, logsRes, settingsRes, modulesRes, birthdayRes, reactionRes, statusRes, statsRes, eventsRes, supportLoginRes, - registerFormsRes, registerAppsRes] = await Promise.all([ + registerFormsRes, registerAppsRes, tasksRes] = await Promise.all([ apiFetch(`/guild/info?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/overview?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/guild/activity?guildId=${encodeURIComponent(guildId)}`), @@ -239,7 +247,8 @@ export function AppProvider({ children }: { children: ReactNode }) { apiFetch(`/events?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/tickets/support-login?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/register/forms?guildId=${encodeURIComponent(guildId)}`), - apiFetch(`/register/apps?guildId=${encodeURIComponent(guildId)}`) + apiFetch(`/register/apps?guildId=${encodeURIComponent(guildId)}`), + apiFetch(`/tasks?guildId=${encodeURIComponent(guildId)}`) ]); setGuildInfo(guildInfoRes.guild || null); setOverview(overviewRes); @@ -258,6 +267,7 @@ export function AppProvider({ children }: { children: ReactNode }) { setSupportLogin(supportLoginRes); setRegisterForms(registerFormsRes.forms || []); setRegisterApps(registerAppsRes.applications || []); + setTasks(tasksRes.tasks || []); setReactionDraft({ title: '', channelId: '', entries: '' }); await Promise.all([loadTicketData(guildId), loadAdminData()]); setStatusMessage(''); @@ -267,12 +277,12 @@ export function AppProvider({ children }: { children: ReactNode }) { async function loadAdminData() { if (!user?.isAdmin) return; try { - const [overviewRes, , logsRes] = await Promise.all([ + const [overviewRes, activityRes, logsRes] = await Promise.all([ apiFetch('/admin/overview'), apiFetch('/admin/activity'), apiFetch('/admin/logs') ]); - setAdmin({ overview: overviewRes, activity: null, logs: logsRes.logs || [] }); + setAdmin({ overview: overviewRes.overview || {}, activity: activityRes.points || [], logs: logsRes.logs || [] }); } catch {} } @@ -541,6 +551,28 @@ export function AppProvider({ children }: { children: ReactNode }) { setAppNotes(notesRes.notes || []); } + async function createTask() { + if (!taskDraft.title.trim() || !currentGuildId) return; + await apiFetch('/tasks', { + method: 'POST', + body: JSON.stringify({ guildId: currentGuildId, title: taskDraft.title.trim(), description: taskDraft.description.trim() || undefined }) + }); + setTaskDraft({ title: '', description: '' }); + setStatusMessage('Aufgabe erstellt'); + await loadGuildData(currentGuildId); + } + + async function updateTaskStatus(id: string, status: string) { + await apiFetch(`/tasks/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) }); + await loadGuildData(currentGuildId); + } + + async function deleteTask(id: string) { + await apiFetch(`/tasks/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) }); + setStatusMessage('Aufgabe gelöscht'); + await loadGuildData(currentGuildId); + } + const handleLogout = useCallback(() => { window.location.href = `${appConfig.baseAuth || '/auth'}/logout`; }, []); @@ -555,7 +587,7 @@ export function AppProvider({ children }: { children: ReactNode }) { formDraft, editingFormId, registerTab, statusServiceDraft, statsItemDraft, ticketDetail, ticketMessages, kbEditDraft, automationEditDraft, automodStrikes, registerStatusFilter, registerFormFilter, selectedAppId, - appNotes, appHistory, noteDraft, + appNotes, appHistory, noteDraft, tasks, taskDraft, setCurrentGuildId, setSection, setSettings, setBirthday, setSupportLogin, setStatusDraft, setStatsDraft, setStatusMessage, loadGuildData, saveSettingsPayload, saveBirthday, saveStatuspage, saveServerStats, @@ -570,6 +602,7 @@ export function AppProvider({ children }: { children: ReactNode }) { setAutomationEditDraft, loadAutomodStrikes, resetAutomodStrike, setRegisterStatusFilter, setRegisterFormFilter, loadRegisterApps, openAppDetail, setNoteDraft, addAppNote, + setTaskDraft, createTask, updateTaskStatus, deleteTask, }}> {children} diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index 28c14d5..b3b87dc 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -1,43 +1,54 @@ import { Card, CardContent, CardHeader, Chip } from '@heroui/react'; -import { Wrench, Activity, Clock, Server, Terminal } from 'lucide-react'; +import { Activity, Clock, Server, Terminal } from 'lucide-react'; import { useApp } from '../context/AppContext'; import { SectionCard } from '../components/shared/SectionCard'; -import { formatDate } from '../utils/formatters'; +import { StatCard } from '../components/shared/StatCard'; +import { BarComparisonChart } from '../components/shared/BarComparisonChart'; +import { formatDate, formatDuration } from '../utils/formatters'; + +const LEVEL_COLORS: Record = { + INFO: 'accent', + WARN: 'warning', + ERROR: 'danger', +}; export function Admin() { const { user, admin } = useApp(); if (!user?.isAdmin) return null; + const overview = admin.overview || {}; + const activityPoints: { hour: string; count: number }[] = admin.activity || []; + const logs: { timestamp: number; level: string; message: string; guildId?: string; category?: string }[] = admin.logs || []; + + const chartItems = activityPoints.slice(-12).map((p) => ({ + label: new Date(p.hour).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }), + value: p.count + })); + return ( -
+
+ } label="Guilds" value={overview.guildCount ?? '-'} color="accent" /> + } label="Aktive Guilds (24h)" value={overview.activeGuilds24 ?? '-'} color="success" /> + } label="Uptime" value={formatDuration(overview.uptimeMs)} color="warning" /> + } label="Log-Einträge" value={logs.length} /> +
+ +
-

Bot Overview

+

Aktivität (letzte Stunden)

- -
-
- - Guilds + + {chartItems.length ? ( + + ) : ( +
+ + Noch keine Aktivität erfasst
- {admin.overview?.guilds ?? '-'} -
-
-
- - Aktive Guilds (24h) -
- {admin.overview?.activeGuilds ?? '-'} -
-
-
- - Uptime -
- {admin.overview?.uptime ?? '-'} -
+ )} @@ -45,15 +56,19 @@ export function Admin() {

Letzte Admin Logs

- {(admin.logs || []).length} Einträge + {logs.length} Einträge
- - {(admin.logs || []).length ? (admin.logs || []).slice(0, 20).map((log, i) => ( + + {logs.length ? logs.slice(0, 30).map((log, i) => (
-
-

{log.message || '-'}

+
+
+ {log.level} + {log.category && {log.category}} +
+

{log.message || '-'}

{formatDate(log.timestamp)}

diff --git a/frontend/src/pages/Tasks.tsx b/frontend/src/pages/Tasks.tsx new file mode 100644 index 0000000..91e0001 --- /dev/null +++ b/frontend/src/pages/Tasks.tsx @@ -0,0 +1,80 @@ +import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, TextField, Label } from '@heroui/react'; +import { ListChecks, Trash2, Plus } from 'lucide-react'; +import { useApp } from '../context/AppContext'; +import { SectionCard } from '../components/shared/SectionCard'; +import { formatDate } from '../utils/formatters'; + +const STATUS_LABELS: Record = { open: 'Offen', in_progress: 'In Bearbeitung', done: 'Erledigt' }; +const STATUS_COLORS: Record = { open: 'warning', in_progress: 'accent', done: 'success' }; + +export function Tasks() { + const { tasks, taskDraft, setTaskDraft, createTask, updateTaskStatus, deleteTask } = useApp(); + + return ( + +
+
+

Aufgaben ({tasks.length})

+
+ {tasks.length ? tasks.map((t) => ( + + +
+
+
{t.title}
+
von {t.createdByTag} · {formatDate(t.createdAt)}
+
+ +
+ {t.description &&

{t.description}

} +
+ + + {STATUS_LABELS[t.status] || t.status} + + {t.assigneeId && Zugewiesen an User-ID {t.assigneeId}} +
+
+
+ )) : ( +
+ + Keine Aufgaben +
+ )} +
+
+ + + +

Neue Aufgabe

+
+ + + + setTaskDraft((s) => ({ ...s, title: e.target.value }))} /> + + + +