aufgabenbearbeitung optimiert
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user