aufgabenbearbeitung optimiert

This commit is contained in:
Codex Agent
2025-12-01 12:04:25 +01:00
parent 96f8c5d63c
commit b8e515a03c
2 changed files with 666 additions and 88 deletions

View File

@@ -18,13 +18,16 @@ import {
createTask,
deleteTask,
getTasks,
getEmotions,
PaginationMeta,
TenantTask,
TenantEmotion,
TaskPayload,
updateTask,
} from '../api';
import { isAuthError } from '../auth/tokens';
import { buildEngagementTabPath } from '../constants';
import { filterEmotionsByEventType } from '../lib/emotions';
type TaskFormState = {
title: string;
@@ -56,6 +59,10 @@ export function TasksSection({ embedded = false, onNavigateToCollections }: Task
const [meta, setMeta] = React.useState<PaginationMeta | null>(null);
const [page, setPage] = React.useState(1);
const [search, setSearch] = React.useState('');
const [eventTypeFilter, setEventTypeFilter] = React.useState<string>('all');
const [ownershipFilter, setOwnershipFilter] = React.useState<'all' | 'custom' | 'global'>('all');
const [emotionFilter, setEmotionFilter] = React.useState<number | null>(null);
const [emotions, setEmotions] = React.useState<TenantEmotion[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [dialogOpen, setDialogOpen] = React.useState(false);
@@ -68,7 +75,7 @@ export function TasksSection({ embedded = false, onNavigateToCollections }: Task
setLoading(true);
setError(null);
getTasks({ page, search: search.trim() || undefined })
getTasks({ page, per_page: 200, search: search.trim() || undefined })
.then((result) => {
if (cancelled) return;
setTasks(result.data);
@@ -90,6 +97,45 @@ export function TasksSection({ embedded = false, onNavigateToCollections }: Task
};
}, [page, search, t]);
React.useEffect(() => {
let cancelled = false;
getEmotions()
.then((list) => {
if (!cancelled) {
setEmotions(list);
}
})
.catch(() => undefined);
return () => {
cancelled = true;
};
}, []);
const eventTypeOptions = React.useMemo(() => {
const options = new Map<string, string>();
tasks.forEach((task) => {
const slug = task.event_type?.slug ?? 'none';
const name = task.event_type?.name ?? 'Allgemein';
options.set(slug, name);
});
return Array.from(options.entries());
}, [tasks]);
const filteredTasks = React.useMemo(() => {
return tasks.filter((task) => {
if (eventTypeFilter !== 'all') {
const slug = task.event_type?.slug ?? 'none';
if (slug !== eventTypeFilter) return false;
}
if (ownershipFilter === 'custom' && task.tenant_id === null) return false;
if (ownershipFilter === 'global' && task.tenant_id !== null) return false;
if (emotionFilter !== null) {
if (task.emotion_id !== emotionFilter) return false;
}
return true;
});
}, [tasks, eventTypeFilter, ownershipFilter, emotionFilter]);
const openCreate = React.useCallback(() => {
setEditingTask(null);
setForm(INITIAL_FORM);
@@ -222,24 +268,83 @@ export function TasksSection({ embedded = false, onNavigateToCollections }: Task
}}
className="max-w-sm"
/>
{meta && meta.total > 0 ? (
<div className="flex flex-wrap gap-2 text-xs text-slate-500">
<div className="flex items-center gap-2">
<span className="font-semibold text-slate-700">{t('filters.eventType', 'Event-Typ')}</span>
<Select value={eventTypeFilter} onValueChange={(value) => setEventTypeFilter(value)}>
<SelectTrigger className="h-8 w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('filters.eventTypeAll', 'Alle')}</SelectItem>
{eventTypeOptions.map(([slug, name]) => (
<SelectItem key={slug} value={slug}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold text-slate-700">{t('filters.ownership', 'Quelle')}</span>
<div className="flex gap-1 rounded-lg border border-slate-200 p-1">
{(['all', 'custom', 'global'] as const).map((key) => (
<Button
key={key}
size="sm"
variant={ownershipFilter === key ? 'default' : 'ghost'}
className="h-8"
onClick={() => setOwnershipFilter(key)}
>
{key === 'all'
? t('filters.ownershipAll', 'Alle')
: key === 'custom'
? t('filters.ownershipCustom', 'Selbst angelegt')
: t('filters.ownershipGlobal', 'Standard')}
</Button>
))}
</div>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold text-slate-700">{t('filters.emotion', 'Emotion')}</span>
<Select
value={emotionFilter ? String(emotionFilter) : 'all'}
onValueChange={(value) => setEmotionFilter(value === 'all' ? null : Number(value))}
>
<SelectTrigger className="h-8 w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('filters.emotionAll', 'Alle')}</SelectItem>
{emotions.map((emotion) => (
<SelectItem key={emotion.id} value={String(emotion.id)}>
{emotion.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-xs text-slate-500">
{t('pagination.page', {
current: meta.current_page,
total: meta.last_page,
count: meta.total,
current: meta?.current_page ?? 1,
total: meta?.last_page ?? 1,
count: filteredTasks.length,
})}
</div>
) : null}
</div>
</div>
{loading ? (
<TasksSkeleton />
) : tasks.length === 0 ? (
<EmptyState onCreate={openCreate} />
) : filteredTasks.length === 0 ? (
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50/70 p-6 text-sm text-slate-600">
{t('filters.noResults', 'Keine Aufgaben zu den Filtern gefunden.')}
</div>
) : (
<div className="grid gap-3">
{tasks.map((task) => (
{filteredTasks.map((task) => (
<TaskRow
key={task.id}
task={task}