overhaul automod escalation and register applications, restore orange theme
All checks were successful
Deploy Discord Bot / deploy (push) Successful in -1m12s
SonarQube / sonar (push) Successful in -21s

Automod: per-filter actions (warn/timeout/kick/ban), channel/role exemptions,
regex/severity badword rules, and a strike/escalation system backed by a new
AutomodStrike table. Register: fixes missing answer labels, adds internal
reviewer notes and cross-form application history for a user, and exposes
form/status filters in the dashboard. Dashboard: overrides HeroUI's default
blue accent token with the bot's established orange (#f97316), which was
lost during the HeroUI v3 migration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 00:21:20 +02:00
parent fe41bfdd88
commit e708cac790
15 changed files with 3689 additions and 178 deletions

View File

@@ -1,14 +1,30 @@
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Tabs, Tab, Separator, TextField, Label } from '@heroui/react';
import { ClipboardList, Pencil, Trash2, Send, Plus, FileText } from 'lucide-react';
import { ClipboardList, Pencil, Trash2, Send, Plus, FileText, History, MessageSquare } from 'lucide-react';
import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard';
import { formatDate } from '../utils/formatters';
const STATUS_LABELS: Record<string, string> = {
pending: 'Ausstehend',
accepted: 'Akzeptiert',
invited: 'Zum Gespräch eingeladen',
rejected: 'Abgelehnt',
};
const STATUS_COLORS: Record<string, 'success' | 'danger' | 'warning' | 'accent' | 'default'> = {
pending: 'warning',
accepted: 'success',
invited: 'accent',
rejected: 'danger',
};
export function Register() {
const {
registerForms, registerApps, registerTab, setRegisterTab,
formDraft, setFormDraft, editingFormId, setEditingFormId,
saveForm, deleteForm, sendFormPanel
saveForm, deleteForm, sendFormPanel,
registerStatusFilter, registerFormFilter, setRegisterStatusFilter, setRegisterFormFilter, loadRegisterApps,
selectedAppId, openAppDetail, appHistory, appNotes, noteDraft, setNoteDraft, addAppNote
} = useApp();
return (
@@ -118,17 +134,47 @@ export function Register() {
{registerTab === 'apps' && (
<div className="mt-5">
<div className="mb-4 flex flex-wrap items-end gap-2">
<TextField className="w-56">
<Label className="text-xs">Formular/Position</Label>
<select
className="w-full rounded-xl px-3 py-2 text-sm"
value={registerFormFilter}
onChange={(e) => { setRegisterFormFilter(e.target.value); loadRegisterApps({ formId: e.target.value }); }}
>
<option value="">Alle</option>
{registerForms.map((f) => <option key={f.id} value={f.id}>{f.name}</option>)}
</select>
</TextField>
<TextField className="w-48">
<Label className="text-xs">Status</Label>
<select
className="w-full rounded-xl px-3 py-2 text-sm"
value={registerStatusFilter}
onChange={(e) => { setRegisterStatusFilter(e.target.value); loadRegisterApps({ status: e.target.value }); }}
>
<option value="">Alle</option>
{Object.entries(STATUS_LABELS).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</TextField>
</div>
<h3 className="mb-3 text-base font-semibold">Eingegangene Anträge ({registerApps.length})</h3>
<div className="space-y-3">
{registerApps.length ? registerApps.map((app) => (
<Card key={app.id}>
<CardContent className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between">
<div className="font-semibold">{app.username || app.userId}</div>
<Chip size="sm" variant="soft" color={app.status === 'approved' ? 'success' : app.status === 'rejected' ? 'danger' : 'warning'}>
{app.status}
<button type="button" className="flex items-center justify-between gap-2 text-left" onClick={() => openAppDetail(app.id)}>
<div className="min-w-0">
<div className="font-semibold truncate">{app.username || app.userId}</div>
<div className="text-xs text-muted truncate">{app.form?.name || 'Formular'}</div>
</div>
<Chip size="sm" variant="soft" color={STATUS_COLORS[app.status] || 'default'}>
{STATUS_LABELS[app.status] || app.status}
</Chip>
</div>
</button>
<div className="text-xs text-muted">{formatDate(app.createdAt)}</div>
{app.answers?.length ? (
<div className="space-y-1">
@@ -140,6 +186,45 @@ export function Register() {
))}
</div>
) : null}
{selectedAppId === app.id && (
<div className="mt-2 grid gap-4 border-t border-border pt-3 sm:grid-cols-2">
<div>
<h4 className="mb-2 flex items-center gap-1.5 text-sm font-semibold"><History size={14} /> Bisherige Bewerbungen</h4>
{appHistory.length ? (
<div className="space-y-1.5">
{appHistory.map((h) => (
<div key={h.id} className="flex items-center justify-between gap-2 text-xs">
<span className="truncate">{h.form?.name || 'Formular'} · {formatDate(h.createdAt)}</span>
<Chip size="sm" variant="soft" color={STATUS_COLORS[h.status] || 'default'}>{STATUS_LABELS[h.status] || h.status}</Chip>
</div>
))}
</div>
) : <p className="text-xs text-muted">Keine weiteren Bewerbungen dieses Nutzers</p>}
</div>
<div>
<h4 className="mb-2 flex items-center gap-1.5 text-sm font-semibold"><MessageSquare size={14} /> Interne Notizen</h4>
<div className="space-y-2">
{appNotes.length ? appNotes.map((n) => (
<div key={n.id} className="rounded-lg bg-surface-secondary p-2 text-xs">
<div className="mb-1 flex items-center justify-between text-muted">
<span className="font-medium text-default">{n.authorTag}</span>
<span>{formatDate(n.createdAt)}</span>
</div>
{n.body}
</div>
)) : <p className="text-xs text-muted">Keine Notizen</p>}
</div>
<TextArea
className="mt-2" rows={2} placeholder="Interne Notiz hinzufügen..."
value={noteDraft} onChange={(e) => setNoteDraft(e.target.value)}
/>
<Button size="sm" variant="tertiary" className="mt-2" isDisabled={!noteDraft.trim()} onPress={addAppNote}>
Notiz speichern
</Button>
</div>
</div>
)}
</CardContent>
</Card>
)) : (