weitere optimierungen für mobile
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft, Layers, Loader2, PlusCircle, Search, Sparkles, Pencil, Check, X } from 'lucide-react';
|
||||
import { ArrowLeft, Layers, Loader2, PlusCircle, Search, Sparkles, Pencil, Check, X, CircleOff } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -95,7 +95,8 @@ export default function EventTasksPage() {
|
||||
const [batchSaving, setBatchSaving] = React.useState(false);
|
||||
const [inlineSavingId, setInlineSavingId] = React.useState<number | null>(null);
|
||||
const [emotionFilterOpen, setEmotionFilterOpen] = React.useState(false);
|
||||
const libraryRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [libraryOpen, setLibraryOpen] = React.useState(false);
|
||||
const [librarySearch, setLibrarySearch] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const handle = window.setTimeout(() => setDebouncedTaskSearch(taskSearch.trim().toLowerCase()), 180);
|
||||
return () => window.clearTimeout(handle);
|
||||
@@ -451,7 +452,7 @@ export default function EventTasksPage() {
|
||||
return mode !== 'photo_only';
|
||||
}, [event?.engagement_mode, event?.settings]);
|
||||
|
||||
const hasSelection = selectedAssignedIds.length > 0 || selectedAvailableIds.length > 0;
|
||||
const hasSelection = selectedAssignedIds.length > 0;
|
||||
const tasksFirst = assignedTasks.length > 0;
|
||||
const tabOrder: Array<'tasks' | 'packs' | 'emotions'> = tasksFirst ? ['tasks', 'packs', 'emotions'] : ['packs', 'tasks', 'emotions'];
|
||||
const prevAssignedRef = React.useRef(assignedTasks.length);
|
||||
@@ -471,11 +472,18 @@ export default function EventTasksPage() {
|
||||
|
||||
try {
|
||||
const nextMode = checked ? 'tasks' : 'photo_only';
|
||||
const updated = await updateEvent(slug, {
|
||||
const payload = {
|
||||
name: event.name,
|
||||
slug: event.slug,
|
||||
event_type_id: event.event_type_id ?? event.event_type?.id,
|
||||
event_date: event.event_date ?? undefined,
|
||||
settings: {
|
||||
...(event.settings ?? {}),
|
||||
engagement_mode: nextMode,
|
||||
},
|
||||
};
|
||||
const updated = await updateEvent(slug, {
|
||||
...payload,
|
||||
});
|
||||
setEvent((prev) => ({
|
||||
...(prev ?? updated),
|
||||
@@ -513,6 +521,7 @@ export default function EventTasksPage() {
|
||||
setAssignedTasks((prev) => [...prev, ...move]);
|
||||
setAvailableTasks((prev) => prev.filter((task) => !nextAvailableSet.has(task.id)));
|
||||
setSelectedAvailableIds([]);
|
||||
setLibraryOpen(false);
|
||||
setBatchSaving(true);
|
||||
try {
|
||||
await assignTasksToEvent(event.id, ids);
|
||||
@@ -557,7 +566,7 @@ export default function EventTasksPage() {
|
||||
}, [event, selectedAssignedIds, assignedTasks, availableTasks, t]);
|
||||
|
||||
const handleInlineUpdate = React.useCallback(
|
||||
async (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | '' }) => {
|
||||
async (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | ''; emotion_id?: number | null }) => {
|
||||
if (!event) return;
|
||||
|
||||
const prevAssigned = assignedTasks;
|
||||
@@ -573,13 +582,25 @@ export default function EventTasksPage() {
|
||||
...payload,
|
||||
title: payload.title ?? optimistic.title,
|
||||
difficulty: payload.difficulty || null,
|
||||
emotion_id: payload.emotion_id ?? optimistic.emotion_id ?? null,
|
||||
} as Partial<TenantTask>;
|
||||
|
||||
const nextEmotion =
|
||||
typeof payload.emotion_id === 'number'
|
||||
? emotions.find((emotion) => emotion.id === payload.emotion_id) ?? null
|
||||
: payload.emotion_id === null
|
||||
? null
|
||||
: optimistic.emotion ?? null;
|
||||
|
||||
if (existingAssigned) {
|
||||
setAssignedTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...patch } : task)));
|
||||
setAssignedTasks((prev) =>
|
||||
prev.map((task) => (task.id === taskId ? { ...task, ...patch, emotion: nextEmotion } : task)),
|
||||
);
|
||||
}
|
||||
if (existingAvailable) {
|
||||
setAvailableTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...patch } : task)));
|
||||
setAvailableTasks((prev) =>
|
||||
prev.map((task) => (task.id === taskId ? { ...task, ...patch, emotion: nextEmotion } : task)),
|
||||
);
|
||||
}
|
||||
|
||||
setInlineSavingId(taskId);
|
||||
@@ -610,14 +631,22 @@ export default function EventTasksPage() {
|
||||
difficulty: payload.difficulty || undefined,
|
||||
description: optimistic.description ?? undefined,
|
||||
priority: optimistic.priority ?? undefined,
|
||||
emotion_id: optimistic.emotion_id ?? undefined,
|
||||
emotion_id: payload.emotion_id ?? optimistic.emotion_id ?? undefined,
|
||||
});
|
||||
|
||||
if (existingAssigned) {
|
||||
setAssignedTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updated } : task)));
|
||||
setAssignedTasks((prev) =>
|
||||
prev.map((task) =>
|
||||
task.id === taskId ? { ...task, ...updated, emotion: nextEmotion ?? updated.emotion ?? null } : task,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (existingAvailable) {
|
||||
setAvailableTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updated } : task)));
|
||||
setAvailableTasks((prev) =>
|
||||
prev.map((task) =>
|
||||
task.id === taskId ? { ...task, ...updated, emotion: nextEmotion ?? updated.emotion ?? null } : task,
|
||||
),
|
||||
);
|
||||
}
|
||||
toast.success(t('actions.updated', 'Task aktualisiert.'));
|
||||
}
|
||||
@@ -666,25 +695,29 @@ export default function EventTasksPage() {
|
||||
) : (
|
||||
<>
|
||||
<Tabs value={tab} onValueChange={(value) => setTab(value as 'tasks' | 'packs' | 'emotions')} className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<TabsList className="grid flex-1 gap-2 rounded-2xl bg-slate-100/80 p-1 sm:grid-cols-3">
|
||||
{tabOrder.map((key) => (
|
||||
<TabsTrigger key={key} value={key}>
|
||||
{key === 'packs'
|
||||
? t('tabs.packs', 'Vorlagen / Aufgaben-Bundles')
|
||||
: key === 'tasks'
|
||||
? t('tabs.tasks', 'Aufgaben')
|
||||
: t('tabs.emotions', 'Emotionen')}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-emerald-200 text-emerald-700"
|
||||
onClick={() => navigate(buildEngagementTabPath('tasks'))}
|
||||
>
|
||||
{t('library.open', 'Aufgaben-Bibliothek öffnen')}
|
||||
</Button>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-emerald-200 text-emerald-700"
|
||||
onClick={() => navigate(buildEngagementTabPath('tasks'))}
|
||||
>
|
||||
{t('library.open', 'Aufgaben-Bibliothek öffnen')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<TabsList className="inline-flex min-w-fit gap-2 rounded-2xl bg-slate-100/80 p-1">
|
||||
{tabOrder.map((key) => (
|
||||
<TabsTrigger key={key} value={key} className="px-3 py-1.5 text-sm sm:text-base">
|
||||
{key === 'packs'
|
||||
? t('tabs.packs', 'Aufgaben-Sets')
|
||||
: key === 'tasks'
|
||||
? t('tabs.tasks', 'Aufgaben')
|
||||
: t('tabs.emotions', 'Emotionen')}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="tasks" className="space-y-6">
|
||||
<Card className="border-0 bg-white/85 shadow-xl shadow-pink-100/60">
|
||||
@@ -747,6 +780,34 @@ export default function EventTasksPage() {
|
||||
{t('actions.addCustom', 'Eigene Aufgabe hinzufügen')}
|
||||
</Button>
|
||||
</div>
|
||||
{!tasksEnabled ? (
|
||||
<div className="flex flex-col gap-2 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<CircleOff className="h-5 w-5" />
|
||||
<div>
|
||||
<p className="font-semibold">{t('modes.disabledTitle', 'Aufgabenmodus ist aus')}</p>
|
||||
<p className="text-xs text-amber-800">
|
||||
{t('modes.disabledCopy', 'Gäste sehen keine Mission Cards. Aktivieren, um Aufgaben sichtbar zu machen.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 text-white hover:bg-amber-700"
|
||||
onClick={() => void handleModeChange(true)}
|
||||
disabled={modeSaving || assignedTasks.length === 0}
|
||||
>
|
||||
{modeSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : t('modes.enable', 'Aufgaben aktivieren')}
|
||||
</Button>
|
||||
{assignedTasks.length === 0 ? (
|
||||
<p className="text-[11px] text-amber-800">
|
||||
{t('modes.needTasks', 'Aktiviere Aufgaben, sobald mindestens eine Aufgabe zugewiesen ist.')}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<DndContext
|
||||
@@ -755,7 +816,26 @@ export default function EventTasksPage() {
|
||||
onDragStart={(event) => setDraggingId(Number(event.active.id))}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<CardContent className="grid gap-4 lg:grid-cols-2">
|
||||
<CardContent className="relative grid gap-4">
|
||||
{!tasksEnabled ? (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-2xl bg-white/80 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-2 text-center text-sm text-slate-700">
|
||||
<CircleOff className="h-5 w-5 text-amber-700" />
|
||||
<p className="font-semibold">{t('modes.disabledTitle', 'Aufgabenmodus ist aus')}</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
{t('modes.disabledOverlay', 'Aktiviere Aufgaben, um Listen und Aktionen zu nutzen.')}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 text-white hover:bg-amber-700"
|
||||
onClick={() => void handleModeChange(true)}
|
||||
disabled={modeSaving || assignedTasks.length === 0}
|
||||
>
|
||||
{t('modes.enable', 'Aufgaben aktivieren')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
@@ -866,17 +946,18 @@ export default function EventTasksPage() {
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
className="border-emerald-200 text-emerald-700"
|
||||
variant="outline"
|
||||
onClick={() => void handleDetachSelected()}
|
||||
disabled={selectedAssignedIds.length === 0 || batchSaving || saving}
|
||||
onClick={() => setLibraryOpen(true)}
|
||||
>
|
||||
{t('actions.removeSelected', 'Auswahl entfernen')}
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{t('actions.openLibrary', 'Aus Bibliothek hinzufügen')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{emotionChips.length > 0 && emotionFilterOpen ? (
|
||||
<div className="flex flex-wrap gap-2 rounded-xl border border-slate-200 bg-white/80 p-2">
|
||||
{emotionChips.length > 0 && emotionFilterOpen ? (
|
||||
<div className="flex flex-wrap gap-2 rounded-xl border border-slate-200 bg-white/80 p-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={emotionFilter.length === 0 ? 'default' : 'outline'}
|
||||
@@ -943,95 +1024,13 @@ export default function EventTasksPage() {
|
||||
disabled={batchSaving || saving}
|
||||
onInlineUpdate={handleInlineUpdate}
|
||||
inlineSaving={inlineSavingId === task.id}
|
||||
emotions={relevantEmotions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DropZone>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2" ref={libraryRef} id="library-section">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold text-slate-900">
|
||||
<PlusCircle className="h-4 w-4 text-emerald-500" />
|
||||
{t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
{t('sections.library.helper', 'Suche, filtere und füge einzelne Aufgaben hinzu. Eigene Aufgaben legst du über den Dialog an.')}
|
||||
</p>
|
||||
<DropZone id="library-dropzone">
|
||||
<div className="space-y-2 max-h-80 overflow-y-auto">
|
||||
{availableTasks.length === 0 ? (
|
||||
<EmptyState message={t('sections.library.empty', 'Keine Tasks in der Bibliothek gefunden.')} />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2 pb-1">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-700">
|
||||
<Checkbox
|
||||
checked={
|
||||
availableTasks.length > 0 && availableTasks.every((task) => selectedAvailableIds.includes(task.id))
|
||||
? true
|
||||
: selectedAvailableIds.some((id) => availableTasks.some((task) => task.id === id))
|
||||
? 'indeterminate'
|
||||
: false
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedAvailableIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
availableTasks.forEach((task) => next.add(task.id));
|
||||
return Array.from(next);
|
||||
});
|
||||
} else {
|
||||
setSelectedAvailableIds([]);
|
||||
}
|
||||
}}
|
||||
aria-label={t('sections.library.selectAll', 'Alle Bibliotheks-Tasks auswählen')}
|
||||
/>
|
||||
<span>
|
||||
{t('sections.library.selectedCount', {
|
||||
defaultValue: '{{count}} ausgewählt',
|
||||
count: selectedAvailableIds.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleAssignSelected()}
|
||||
disabled={selectedAvailableIds.length === 0 || !tasksEnabled || batchSaving || saving}
|
||||
>
|
||||
{t('actions.assignSelected', 'Auswahl zuweisen')}
|
||||
</Button>
|
||||
</div>
|
||||
{availableTasks.map((task) => (
|
||||
<DraggableTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
origin="library"
|
||||
onAdd={() => void handleAssignSingle(task.id)}
|
||||
disabled={!tasksEnabled || saving || batchSaving}
|
||||
showCheckbox
|
||||
checked={selectedAvailableIds.includes(task.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
setSelectedAvailableIds((prev) => {
|
||||
if (checked) {
|
||||
if (prev.includes(task.id)) return prev;
|
||||
return [...prev, task.id];
|
||||
}
|
||||
return prev.filter((id) => id !== task.id);
|
||||
});
|
||||
}}
|
||||
onInlineUpdate={handleInlineUpdate}
|
||||
inlineSaving={inlineSavingId === task.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropZone>
|
||||
</section>
|
||||
</CardContent>
|
||||
<DragOverlay>
|
||||
{draggingId ? (
|
||||
@@ -1053,36 +1052,21 @@ export default function EventTasksPage() {
|
||||
count: selectedAssignedIds.length,
|
||||
})}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-slate-200 text-slate-700">
|
||||
{t('sections.bulk.librarySelected', {
|
||||
defaultValue: '{{count}} ausgewählt (Bibliothek)',
|
||||
count: selectedAvailableIds.length,
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleAssignSelected()}
|
||||
disabled={selectedAvailableIds.length === 0 || !tasksEnabled || batchSaving || saving}
|
||||
>
|
||||
{t('actions.assignSelected', 'Auswahl zuweisen')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleDetachSelected()}
|
||||
disabled={selectedAssignedIds.length === 0 || batchSaving || saving}
|
||||
>
|
||||
{t('actions.removeSelected', 'Auswahl entfernen')}
|
||||
{t('actions.removeSelected', 'Ausgewählte Aufgaben entfernen')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedAssignedIds([]);
|
||||
setSelectedAvailableIds([]);
|
||||
}}
|
||||
>
|
||||
{t('sections.bulk.clear', 'Auswahl aufheben')}
|
||||
@@ -1114,11 +1098,11 @@ export default function EventTasksPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog open={quickAddOpen} onOpenChange={setQuickAddOpen}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('sections.library.quickCreate', 'Eigene Aufgabe hinzufügen')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Dialog open={quickAddOpen} onOpenChange={setQuickAddOpen}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('sections.library.quickCreate', 'Eigene Aufgabe hinzufügen')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-slate-600">
|
||||
{t('sections.library.quickHelper', 'Titel eingeben, optional beschreiben und sofort zum Event zuweisen.')}
|
||||
@@ -1190,6 +1174,104 @@ export default function EventTasksPage() {
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={libraryOpen} onOpenChange={(open) => {
|
||||
setLibraryOpen(open);
|
||||
if (!open) {
|
||||
setSelectedAvailableIds([]);
|
||||
setLibrarySearch('');
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex w-full items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2">
|
||||
<Search className="h-4 w-4 text-slate-500" />
|
||||
<Input
|
||||
value={librarySearch}
|
||||
onChange={(e) => setLibrarySearch(e.target.value)}
|
||||
placeholder={t('sections.library.search', 'Aufgaben suchen...')}
|
||||
className="h-8 border-0 bg-transparent p-0 text-sm focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs text-slate-600">
|
||||
<Badge variant="outline" className="border-slate-200">
|
||||
{t('sections.library.selectedCount', { defaultValue: '{{count}} ausgewählt', count: selectedAvailableIds.length })}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-slate-200">
|
||||
{t('summary.library', 'Bibliothek')} · {availableTasks.length}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-[55vh] overflow-y-auto">
|
||||
{availableTasks.length === 0 ? (
|
||||
<EmptyState message={t('sections.library.empty', 'Keine Tasks in der Bibliothek gefunden.')} />
|
||||
) : (
|
||||
availableTasks
|
||||
.filter((task) =>
|
||||
librarySearch.trim()
|
||||
? `${task.title ?? ''} ${task.description ?? ''}`.toLowerCase().includes(librarySearch.toLowerCase().trim())
|
||||
: true,
|
||||
)
|
||||
.map((task) => (
|
||||
<DraggableTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
origin="library"
|
||||
onAdd={() => void handleAssignSingle(task.id)}
|
||||
disabled={!tasksEnabled || saving || batchSaving}
|
||||
showCheckbox
|
||||
checked={selectedAvailableIds.includes(task.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
setSelectedAvailableIds((prev) => {
|
||||
if (checked) {
|
||||
if (prev.includes(task.id)) return prev;
|
||||
return [...prev, task.id];
|
||||
}
|
||||
return prev.filter((id) => id !== task.id);
|
||||
});
|
||||
}}
|
||||
onInlineUpdate={handleInlineUpdate}
|
||||
inlineSaving={inlineSavingId === task.id}
|
||||
emotions={relevantEmotions}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedAvailableIds.length > 0 ? (
|
||||
<div className="sticky bottom-0 flex flex-wrap items-center gap-2 rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-lg">
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2 text-sm text-slate-700">
|
||||
<Badge variant="outline" className="border-slate-200 text-slate-700">
|
||||
{t('sections.library.selectedCount', { defaultValue: '{{count}} ausgewählt', count: selectedAvailableIds.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleAssignSelected()}
|
||||
disabled={selectedAvailableIds.length === 0 || !tasksEnabled || batchSaving || saving}
|
||||
>
|
||||
{t('actions.assignSelected', 'Auswahl zuweisen')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setSelectedAvailableIds([])}
|
||||
>
|
||||
{t('sections.bulk.clear', 'Auswahl aufheben')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={emotionsModalOpen} onOpenChange={setEmotionsModalOpen}>
|
||||
@@ -1247,6 +1329,7 @@ function DraggableTaskCard({
|
||||
onCheckedChange,
|
||||
onInlineUpdate,
|
||||
inlineSaving,
|
||||
emotions,
|
||||
}: {
|
||||
task: TenantTask;
|
||||
origin: 'assigned' | 'library';
|
||||
@@ -1256,39 +1339,30 @@ function DraggableTaskCard({
|
||||
showCheckbox?: boolean;
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
onInlineUpdate?: (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | '' }) => void;
|
||||
onInlineUpdate?: (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | ''; emotion_id?: number | null }) => void;
|
||||
inlineSaving?: boolean;
|
||||
emotions?: TenantEmotion[];
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useDraggable({
|
||||
id: task.id,
|
||||
data: { list: origin },
|
||||
});
|
||||
|
||||
const [editing, setEditing] = React.useState(false);
|
||||
const [titleValue, setTitleValue] = React.useState(task.title ?? '');
|
||||
const [difficultyValue, setDifficultyValue] = React.useState<TenantTask['difficulty'] | ''>(task.difficulty ?? '');
|
||||
const [emotionIdValue, setEmotionIdValue] = React.useState<number | ''>(task.emotion_id ?? '');
|
||||
|
||||
React.useEffect(() => {
|
||||
setTitleValue(task.title ?? '');
|
||||
setDifficultyValue(task.difficulty ?? '');
|
||||
}, [task.title, task.difficulty]);
|
||||
|
||||
const style = {
|
||||
transform: transform ? CSS.Translate.toString(transform) : undefined,
|
||||
transition: transition || undefined,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
};
|
||||
setEmotionIdValue(task.emotion_id ?? '');
|
||||
}, [task.title, task.difficulty, task.emotion_id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="rounded-2xl border border-slate-200 bg-white/90 px-4 py-3 shadow-sm"
|
||||
className="rounded-xl border border-slate-200 bg-white/90 px-3 py-2 shadow-sm"
|
||||
onDoubleClick={() => setEditing(true)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex min-w-[32px] flex-col items-center gap-1 pt-1">
|
||||
{showCheckbox ? (
|
||||
<Checkbox
|
||||
checked={checked ?? false}
|
||||
@@ -1296,17 +1370,43 @@ function DraggableTaskCard({
|
||||
aria-label={t('actions.selectTask', 'Task auswählen')}
|
||||
/>
|
||||
) : null}
|
||||
<button
|
||||
className="h-7 w-7 rounded-md border border-slate-200 bg-white text-slate-500 transition hover:bg-slate-50 disabled:opacity-50"
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
disabled={disabled}
|
||||
aria-label={t('library.dragHandle', 'Task verschieben')}
|
||||
>
|
||||
⋮⋮
|
||||
</button>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setEditing((prev) => !prev)}
|
||||
aria-label={t('actions.edit', 'Bearbeiten')}
|
||||
disabled={inlineSaving}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
{editing ? <X className="h-4 w-4" /> : <Pencil className="h-4 w-4 text-slate-500" />}
|
||||
</Button>
|
||||
{origin === 'assigned' ? (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={onRemove}
|
||||
disabled={disabled}
|
||||
aria-label={t('actions.remove', 'Vom Event entfernen')}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-slate-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={onAdd}
|
||||
disabled={disabled}
|
||||
aria-label={t('actions.assign', 'Zum Event hinzufügen')}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4 text-emerald-600" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 min-w-0">
|
||||
<div className="flex-1 space-y-1 min-w-0">
|
||||
<div className="space-y-1 min-w-0 max-w-full">
|
||||
{editing ? (
|
||||
<div className="space-y-2">
|
||||
@@ -1327,6 +1427,21 @@ function DraggableTaskCard({
|
||||
<option value="medium">{t('sections.library.difficulty.medium', 'Mittel')}</option>
|
||||
<option value="hard">{t('sections.library.difficulty.hard', 'Schwer')}</option>
|
||||
</select>
|
||||
{emotions && emotions.length > 0 ? (
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-slate-200 bg-white px-2 text-sm"
|
||||
value={emotionIdValue}
|
||||
onChange={(e) => setEmotionIdValue(e.target.value ? Number(e.target.value) : '')}
|
||||
disabled={inlineSaving}
|
||||
>
|
||||
<option value="">{t('sections.library.quickEmotionNone', 'Keine')}</option>
|
||||
{emotions.map((emotion) => (
|
||||
<option key={emotion.id} value={emotion.id}>
|
||||
{emotion.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -1335,7 +1450,11 @@ function DraggableTaskCard({
|
||||
disabled={!titleValue.trim() || inlineSaving}
|
||||
onClick={() => {
|
||||
if (!onInlineUpdate) return;
|
||||
onInlineUpdate(task.id, { title: titleValue.trim(), difficulty: difficultyValue });
|
||||
onInlineUpdate(task.id, {
|
||||
title: titleValue.trim(),
|
||||
difficulty: difficultyValue,
|
||||
emotion_id: emotionIdValue === '' ? null : Number(emotionIdValue),
|
||||
});
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
@@ -1363,7 +1482,7 @@ function DraggableTaskCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{!editing && task.emotion ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -1387,36 +1506,6 @@ function DraggableTaskCard({
|
||||
{t('tasks.customBadge', 'Eigene Aufgabe')}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setEditing((prev) => !prev)}
|
||||
aria-label={t('actions.edit', 'Bearbeiten')}
|
||||
disabled={inlineSaving}
|
||||
>
|
||||
{editing ? <X className="h-4 w-4" /> : <Pencil className="h-4 w-4 text-slate-500" />}
|
||||
</Button>
|
||||
{origin === 'assigned' ? (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={onRemove}
|
||||
disabled={disabled}
|
||||
aria-label={t('actions.remove', 'Vom Event entfernen')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-slate-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={onAdd}
|
||||
disabled={disabled}
|
||||
aria-label={t('actions.assign', 'Zum Event hinzufügen')}
|
||||
>
|
||||
<PlusCircle className="h-4 w-4 text-emerald-600" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user