rearranged tenant admin layout, invite layouts now visible and manageable
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
||||
updateTask,
|
||||
} from '../api';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
import { ADMIN_EVENTS_PATH, ADMIN_TASK_COLLECTIONS_PATH } from '../constants';
|
||||
import { buildEngagementTabPath } from '../constants';
|
||||
|
||||
type TaskFormState = {
|
||||
title: string;
|
||||
@@ -42,9 +42,15 @@ const INITIAL_FORM: TaskFormState = {
|
||||
is_completed: false,
|
||||
};
|
||||
|
||||
export default function TasksPage() {
|
||||
export type TasksSectionProps = {
|
||||
embedded?: boolean;
|
||||
onNavigateToCollections?: () => void;
|
||||
};
|
||||
|
||||
export function TasksSection({ embedded = false, onNavigateToCollections }: TasksSectionProps): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const [tasks, setTasks] = React.useState<TenantTask[]>([]);
|
||||
const [meta, setMeta] = React.useState<PaginationMeta | null>(null);
|
||||
const [page, setPage] = React.useState(1);
|
||||
@@ -83,13 +89,13 @@ export default function TasksPage() {
|
||||
};
|
||||
}, [page, search]);
|
||||
|
||||
function openCreate() {
|
||||
const openCreate = React.useCallback(() => {
|
||||
setEditingTask(null);
|
||||
setForm(INITIAL_FORM);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function openEdit(task: TenantTask) {
|
||||
const openEdit = React.useCallback((task: TenantTask) => {
|
||||
setEditingTask(task);
|
||||
setForm({
|
||||
title: task.title,
|
||||
@@ -99,7 +105,15 @@ export default function TasksPage() {
|
||||
is_completed: task.is_completed,
|
||||
});
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigateToCollections = React.useCallback(() => {
|
||||
if (onNavigateToCollections) {
|
||||
onNavigateToCollections();
|
||||
return;
|
||||
}
|
||||
navigate(buildEngagementTabPath('collections'));
|
||||
}, [navigate, onNavigateToCollections]);
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
@@ -137,7 +151,7 @@ export default function TasksPage() {
|
||||
}
|
||||
|
||||
async function handleDelete(taskId: number) {
|
||||
if (!window.confirm('Task wirklich loeschen?')) {
|
||||
if (!window.confirm('Task wirklich löschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +160,7 @@ export default function TasksPage() {
|
||||
setTasks((prev) => prev.filter((task) => task.id !== taskId));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
setError('Task konnte nicht geloescht werden.');
|
||||
setError('Task konnte nicht gelöscht werden.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,25 +179,13 @@ export default function TasksPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const title = embedded ? 'Aufgaben' : 'Task Bibliothek';
|
||||
const subtitle = embedded
|
||||
? 'Plane Aufgaben, Aktionen und Highlights für deine Gäste.'
|
||||
: 'Weise Aufgaben zu und tracke Fortschritt rund um deine Events.';
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
title="Task Bibliothek"
|
||||
subtitle="Weise Aufgaben zu und tracke Fortschritt rund um deine Events."
|
||||
actions={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_TASK_COLLECTIONS_PATH)}>
|
||||
{t('navigation.collections')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-lg shadow-pink-500/20"
|
||||
onClick={openCreate}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Neu
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Fehler</AlertTitle>
|
||||
@@ -192,76 +194,169 @@ export default function TasksPage() {
|
||||
)}
|
||||
|
||||
<Card className="border-0 bg-white/85 shadow-xl shadow-pink-100/60">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-slate-900">Tasks verwalten</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
Erstelle Aufgaben und ordne sie deinen Events zu.
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl text-slate-900">{title}</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">{subtitle}</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" onClick={handleNavigateToCollections}>
|
||||
{t('navigation.collections')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-lg shadow-pink-500/20"
|
||||
onClick={openCreate}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Neu
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Input
|
||||
placeholder="Nach Tasks suchen..."
|
||||
placeholder="Nach Aufgaben suchen ..."
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
className="sm:max-w-sm"
|
||||
onChange={(event) => {
|
||||
setPage(1);
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_EVENTS_PATH)}>
|
||||
Events oeffnen
|
||||
</Button>
|
||||
{meta && meta.total > 0 ? (
|
||||
<div className="text-xs text-slate-500">
|
||||
Seite {meta.current_page} von {meta.last_page} · {meta.total} Einträge
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<TaskSkeleton />
|
||||
<TasksSkeleton />
|
||||
) : tasks.length === 0 ? (
|
||||
<EmptyTasksState onCreate={openCreate} />
|
||||
<EmptyState onCreate={openCreate} />
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3">
|
||||
{tasks.map((task) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
onToggle={() => void toggleCompletion(task)}
|
||||
onToggle={() => toggleCompletion(task)}
|
||||
onEdit={() => openEdit(task)}
|
||||
onDelete={() => void handleDelete(task.id)}
|
||||
onDelete={() => handleDelete(task.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{meta && meta.last_page > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
Zurueck
|
||||
</Button>
|
||||
<span className="text-xs text-slate-500">
|
||||
Seite {meta.current_page} von {meta.last_page}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={page >= meta.last_page}
|
||||
onClick={() => setPage((prev) => Math.min(prev + 1, meta.last_page))}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
{meta && meta.last_page > 1 ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-100 pt-4 text-sm">
|
||||
<div className="text-slate-500">
|
||||
Insgesamt {meta.total} Aufgaben · Seite {meta.current_page} von {meta.last_page}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setPage((page) => Math.max(page - 1, 1))} disabled={meta.current_page <= 1}>
|
||||
Zurück
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((page) => Math.min(page + 1, meta.last_page ?? page + 1))}
|
||||
disabled={meta.current_page >= (meta.last_page ?? 1)}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<TaskDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
onSubmit={handleSubmit}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
saving={saving}
|
||||
isEditing={Boolean(editingTask)}
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingTask ? 'Task bearbeiten' : 'Neue Task erstellen'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-title">Titel</Label>
|
||||
<Input
|
||||
id="task-title"
|
||||
value={form.title}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, title: event.target.value }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-description">Beschreibung</Label>
|
||||
<Input
|
||||
id="task-description"
|
||||
value={form.description}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, description: event.target.value }))}
|
||||
placeholder="Was sollen Gäste machen?"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-priority">Priorität</Label>
|
||||
<Select
|
||||
value={form.priority ?? 'medium'}
|
||||
onValueChange={(value: TaskPayload['priority']) => setForm((prev) => ({ ...prev, priority: value }))}
|
||||
>
|
||||
<SelectTrigger id="task-priority">
|
||||
<SelectValue placeholder="Priorität wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Niedrig</SelectItem>
|
||||
<SelectItem value="medium">Mittel</SelectItem>
|
||||
<SelectItem value="high">Hoch</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-due-date">Fälligkeitsdatum</Label>
|
||||
<Input
|
||||
id="task-due-date"
|
||||
type="date"
|
||||
value={form.due_date}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, due_date: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50/50 p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700">Bereits erledigt?</p>
|
||||
<p className="text-xs text-slate-500">Markiere Aufgaben als abgeschlossen, wenn sie nicht mehr sichtbar sein sollen.</p>
|
||||
</div>
|
||||
<Switch checked={form.is_completed} onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_completed: checked }))} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving} className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white">
|
||||
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TasksPage(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('management');
|
||||
const { t: tc } = useTranslation('common');
|
||||
return (
|
||||
<AdminLayout
|
||||
title={tc('navigation.tasks')}
|
||||
subtitle="Weise Aufgaben zu und tracke Fortschritt rund um deine Events."
|
||||
>
|
||||
<TasksSection onNavigateToCollections={() => navigate(buildEngagementTabPath('collections'))} />
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
@@ -277,207 +372,68 @@ function TaskRow({
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const assignedCount = task.assigned_events_count ?? task.assigned_events?.length ?? 0;
|
||||
const completed = task.is_completed;
|
||||
const isGlobal = task.tenant_id === null;
|
||||
|
||||
const isCompleted = task.is_completed;
|
||||
const statusIcon = isCompleted ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <Circle className="h-4 w-4 text-slate-300" />;
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-2xl border border-slate-100 bg-white/90 p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-1 items-start gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={isGlobal ? undefined : onToggle}
|
||||
aria-disabled={isGlobal}
|
||||
className={`rounded-full border border-pink-200 bg-white/80 p-2 text-pink-600 shadow-sm transition ${
|
||||
isGlobal ? 'opacity-50 cursor-not-allowed' : 'hover:bg-pink-50'
|
||||
}`}
|
||||
>
|
||||
{completed ? <CheckCircle2 className="h-5 w-5" /> : <Circle className="h-5 w-5" />}
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-slate-200/70 bg-white/80 p-4 shadow-sm shadow-pink-100/20 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<button type="button" onClick={onToggle} className="mt-1 text-slate-500 transition-colors hover:text-emerald-500">
|
||||
{statusIcon}
|
||||
</button>
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className={`text-sm font-semibold ${completed ? 'text-slate-500 line-through' : 'text-slate-900'}`}>
|
||||
{task.title}
|
||||
</p>
|
||||
<Badge variant="outline" className="border-pink-200 text-pink-600">
|
||||
{mapPriority(task.priority)}
|
||||
</Badge>
|
||||
{isGlobal && (
|
||||
<Badge variant="secondary" className="bg-slate-100 text-slate-600">
|
||||
Global
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{task.description && <p className="text-xs text-slate-600">{task.description}</p>}
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-500">
|
||||
{task.due_date && <span>Faellig: {formatDate(task.due_date)}</span>}
|
||||
<span>Zugeordnet: {assignedCount}</span>
|
||||
<span className="text-sm font-medium text-slate-900">{task.title}</span>
|
||||
{task.priority ? <PriorityBadge priority={task.priority} /> : null}
|
||||
{task.collection_id ? <Badge variant="secondary">Vorlage #{task.collection_id}</Badge> : null}
|
||||
</div>
|
||||
{task.description ? <p className="text-xs text-slate-500">{task.description}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={onEdit} disabled={isGlobal} className={isGlobal ? 'opacity-50' : ''}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={onEdit}>
|
||||
<Pencil className="mr-1 h-4 w-4" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
className={`text-rose-600 ${isGlobal ? 'opacity-50' : ''}`}
|
||||
disabled={isGlobal}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Button variant="ghost" size="sm" onClick={onDelete} className="text-slate-500 hover:text-rose-500">
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
Löschen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
form,
|
||||
setForm,
|
||||
saving,
|
||||
isEditing,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
form: TaskFormState;
|
||||
setForm: React.Dispatch<React.SetStateAction<TaskFormState>>;
|
||||
saving: boolean;
|
||||
isEditing: boolean;
|
||||
}) {
|
||||
const handleFormSubmit = React.useCallback((event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
onSubmit(event);
|
||||
}, [onSubmit]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? 'Task bearbeiten' : 'Neuen Task anlegen'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="space-y-4" onSubmit={handleFormSubmit}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-title">Titel</Label>
|
||||
<Input
|
||||
id="task-title"
|
||||
value={form.title}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, title: event.target.value }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-description">Beschreibung</Label>
|
||||
<textarea
|
||||
id="task-description"
|
||||
value={form.description}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, description: event.target.value }))}
|
||||
className="min-h-[80px] w-full rounded-md border border-slate-200 bg-white/80 p-2 text-sm shadow-sm focus:border-pink-300 focus:outline-none focus:ring-2 focus:ring-pink-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-priority">Prioritaet</Label>
|
||||
<Select
|
||||
value={form.priority ?? 'medium'}
|
||||
onValueChange={(value) => setForm((prev) => ({ ...prev, priority: value as TaskPayload['priority'] }))}
|
||||
>
|
||||
<SelectTrigger id="task-priority">
|
||||
<SelectValue placeholder="Prioritaet waehlen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Niedrig</SelectItem>
|
||||
<SelectItem value="medium">Mittel</SelectItem>
|
||||
<SelectItem value="high">Hoch</SelectItem>
|
||||
<SelectItem value="urgent">Dringend</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-due-date">Faellig am</Label>
|
||||
<Input
|
||||
id="task-due-date"
|
||||
type="date"
|
||||
value={form.due_date}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, due_date: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-pink-100 bg-pink-50/60 p-3">
|
||||
<div>
|
||||
<Label htmlFor="task-completed" className="text-sm font-medium text-slate-800">
|
||||
Bereits erledigt
|
||||
</Label>
|
||||
<p className="text-xs text-slate-500">Markiere Task als abgeschlossen.</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="task-completed"
|
||||
checked={form.is_completed}
|
||||
onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_completed: Boolean(checked) }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving}>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Speichern'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
function PriorityBadge({ priority }: { priority: NonNullable<TaskPayload['priority']> }) {
|
||||
const mapping: Record<NonNullable<TaskPayload['priority']>, { label: string; className: string }> = {
|
||||
low: { label: 'Niedrig', className: 'bg-emerald-50 text-emerald-600' },
|
||||
medium: { label: 'Mittel', className: 'bg-amber-50 text-amber-600' },
|
||||
high: { label: 'Hoch', className: 'bg-rose-50 text-rose-600' },
|
||||
};
|
||||
const { label, className } = mapping[priority];
|
||||
return <Badge className={`border-none ${className}`}>{label}</Badge>;
|
||||
}
|
||||
|
||||
function EmptyTasksState({ onCreate }: { onCreate: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-slate-200 bg-white/70 p-10 text-center">
|
||||
<div className="rounded-full bg-pink-100 p-3 text-pink-600 shadow-inner shadow-pink-200/80">
|
||||
<Plus className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">Noch keine Tasks angelegt. Lege deinen ersten Task an.</p>
|
||||
<Button onClick={onCreate}>Task erstellen</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskSkeleton() {
|
||||
function TasksSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className="h-24 animate-pulse rounded-2xl bg-gradient-to-r from-white/40 via-white/60 to-white/40" />
|
||||
<div key={`task-skeleton-${index}`} className="h-16 animate-pulse rounded-xl bg-gradient-to-r from-white/40 via-white/60 to-white/40" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function mapPriority(priority: TenantTask['priority']): string {
|
||||
switch (priority) {
|
||||
case 'low':
|
||||
return 'Niedrig';
|
||||
case 'high':
|
||||
return 'Hoch';
|
||||
case 'urgent':
|
||||
return 'Dringend';
|
||||
default:
|
||||
return 'Mittel';
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined): string {
|
||||
if (!value) return '--';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '--';
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
function EmptyState({ onCreate }: { onCreate: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-slate-200 bg-slate-50/60 p-10 text-center">
|
||||
<h3 className="text-base font-semibold text-slate-800">Noch keine Tasks angelegt</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
Starte mit einer neuen Aufgabe oder importiere Aufgabenvorlagen, um deine Gäste zu inspirieren.
|
||||
</p>
|
||||
<Button onClick={onCreate} className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white">
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
Erste Task erstellen
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user