feat: vollständiges Dashboard-Redesign mit HeroUI - monolithische App.tsx aufgelöst, 16 Seiten, Context-API, collapsible Sidebar, neues Dashboard-Layout
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled
This commit is contained in:
137
frontend/src/pages/Register.tsx
Normal file
137
frontend/src/pages/Register.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Tabs, Tab, Separator } from '@heroui/react';
|
||||
import { ClipboardList, Pencil, Trash2, Send, Plus, FileText } from 'lucide-react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { SectionCard } from '../components/shared/SectionCard';
|
||||
import { formatDate } from '../utils/formatters';
|
||||
|
||||
export function Register() {
|
||||
const {
|
||||
registerForms, registerApps, registerTab, setRegisterTab,
|
||||
formDraft, setFormDraft, editingFormId, setEditingFormId,
|
||||
saveForm, deleteForm, sendFormPanel
|
||||
} = useApp();
|
||||
|
||||
return (
|
||||
<SectionCard title="Registrierungsformulare" subtitle="Bewerbungs-Formulare und eingegangene Antr<74>ge verwalten.">
|
||||
<Tabs aria-label="Register Tabs" color="primary" selectedKey={registerTab} variant="bordered" onSelectionChange={(key) => setRegisterTab(String(key))}>
|
||||
<Tab key="forms" title="Formulare" />
|
||||
<Tab key="apps" title="Antr<74>ge" />
|
||||
</Tabs>
|
||||
|
||||
{registerTab === 'forms' && (
|
||||
<div className="mt-5 grid gap-5 xl:grid-cols-[1fr_420px]">
|
||||
<div>
|
||||
<h3 className="mb-3 text-base font-semibold">Bestehende Formulare ({registerForms.length})</h3>
|
||||
<div className="space-y-3">
|
||||
{registerForms.length ? registerForms.map((f) => (
|
||||
<Card key={f.id} className="border border-default-100 bg-default-50/20">
|
||||
<CardContent className="flex flex-col gap-3 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<FileText size={16} className="text-primary-400 shrink-0" />
|
||||
<span className="font-semibold text-small truncate">{f.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button isIconOnly size="sm" variant="light" onPress={() => {
|
||||
setFormDraft({
|
||||
name: f.name, description: f.description || '',
|
||||
reviewChannelId: f.reviewChannelId || '',
|
||||
notifyRoleIds: (f.notifyRoleIds || []).join(', '),
|
||||
fields: (f.fields || []).map((fd) =>
|
||||
fd.label + '|' + fd.type + (fd.required ? '|required' : '') + (fd.options ? '|' + fd.options.join(',') : '')
|
||||
).join('\n')
|
||||
});
|
||||
setEditingFormId(f.id);
|
||||
}}>
|
||||
<Pencil size={14} />
|
||||
</Button>
|
||||
<Button isIconOnly size="sm" variant="light" color="danger" onPress={() => deleteForm(f.id)}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-small text-default-400">{f.description || 'Keine Beschreibung'}</p>
|
||||
<div className="flex items-center gap-2 text-tiny">
|
||||
<Chip size="sm" variant="flat" color={f.isActive ? 'success' : 'default'}>
|
||||
{f.isActive ? 'Aktiv' : 'Inaktiv'}
|
||||
</Chip>
|
||||
<span className="text-default-400">{f.fields?.length || 0} Felder</span>
|
||||
<Button size="sm" variant="flat" startContent={<Send size={12} />} onPress={() => sendFormPanel(f.id)}>
|
||||
Panel senden
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)) : (
|
||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400">
|
||||
<ClipboardList size={24} />
|
||||
Keine Formulare
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="border border-default-100 bg-default-50/20">
|
||||
<CardHeader className="px-5 pt-5 pb-0">
|
||||
<h3 className="text-base font-semibold">{editingFormId ? 'Formular bearbeiten' : 'Neues Formular'}</h3>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 p-5">
|
||||
<Input label="Name" value={formDraft.name} onValueChange={(v) => setFormDraft((s) => ({ ...s, name: v }))} />
|
||||
<Input label="Beschreibung" value={formDraft.description} onValueChange={(v) => setFormDraft((s) => ({ ...s, description: v }))} />
|
||||
<Input label="Review Channel ID" value={formDraft.reviewChannelId} onValueChange={(v) => setFormDraft((s) => ({ ...s, reviewChannelId: v }))} />
|
||||
<Input label="Benachrichtigungs-Rollen (Komma-getrennt)" value={formDraft.notifyRoleIds} onValueChange={(v) => setFormDraft((s) => ({ ...s, notifyRoleIds: v }))} />
|
||||
<TextArea label="Felder (label|type|required|options)" minRows={6} value={formDraft.fields} onValueChange={(v) => setFormDraft((s) => ({ ...s, fields: v }))} />
|
||||
<p className="text-tiny text-default-400">
|
||||
Pro Zeile: label | type (text/paragraph/select/multi) | required | option1,option2
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" onPress={saveForm}>{editingFormId ? 'Aktualisieren' : 'Erstellen'}</Button>
|
||||
{editingFormId && (
|
||||
<Button variant="flat" onPress={() => { setEditingFormId(null); setFormDraft({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' }); }}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{registerTab === 'apps' && (
|
||||
<div className="mt-5">
|
||||
<h3 className="mb-3 text-base font-semibold">Eingegangene Antr<EFBFBD>ge ({registerApps.length})</h3>
|
||||
<div className="space-y-3">
|
||||
{registerApps.length ? registerApps.map((app) => (
|
||||
<Card key={app.id} className="border border-default-100 bg-default-50/20">
|
||||
<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="flat" color={app.status === 'approved' ? 'success' : app.status === 'rejected' ? 'danger' : 'warning'}>
|
||||
{app.status}
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="text-tiny text-default-400">{formatDate(app.createdAt)}</div>
|
||||
{app.answers?.length ? (
|
||||
<div className="space-y-1">
|
||||
{app.answers.map((a, i) => (
|
||||
<div key={i} className="text-small">
|
||||
<span className="text-default-500">{a.label || 'Frage'}: </span>
|
||||
{a.value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)) : (
|
||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400">
|
||||
<ClipboardList size={24} />
|
||||
Keine Antr<EFBFBD>ge
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user