Refactor event tasks tabs
This commit is contained in:
@@ -558,6 +558,12 @@
|
|||||||
"disabled": "Fotoaufgaben deaktiviert",
|
"disabled": "Fotoaufgaben deaktiviert",
|
||||||
"permissionHint": "Du hast keine Berechtigung, Fotoaufgaben zu ändern."
|
"permissionHint": "Du hast keine Berechtigung, Fotoaufgaben zu ändern."
|
||||||
},
|
},
|
||||||
|
"tabs": {
|
||||||
|
"tasks": "Aufgaben",
|
||||||
|
"library": "Aufgabenbibliothek",
|
||||||
|
"emotions": "Emotionen",
|
||||||
|
"collections": "Sammlungen"
|
||||||
|
},
|
||||||
"quickNav": "Schnellzugriff",
|
"quickNav": "Schnellzugriff",
|
||||||
"sections": {
|
"sections": {
|
||||||
"assigned": "Zugewiesen",
|
"assigned": "Zugewiesen",
|
||||||
@@ -606,6 +612,8 @@
|
|||||||
"bulkRemoveTitle": "Auswahl löschen",
|
"bulkRemoveTitle": "Auswahl löschen",
|
||||||
"bulkRemoveBody": "Dies entfernt die ausgewählten Fotoaufgaben aus dem Event.",
|
"bulkRemoveBody": "Dies entfernt die ausgewählten Fotoaufgaben aus dem Event.",
|
||||||
"select": "Fotoaufgabe auswählen",
|
"select": "Fotoaufgabe auswählen",
|
||||||
|
"addEmotion": "Emotion hinzufügen",
|
||||||
|
"emotionsEmpty": "Noch keine Emotionen vorhanden. Füge eine hinzu, um Aufgaben zu kategorisieren.",
|
||||||
"manageEmotions": "Emotionen verwalten",
|
"manageEmotions": "Emotionen verwalten",
|
||||||
"manageEmotionsHint": "Filtere und halte deine Taxonomie gepflegt.",
|
"manageEmotionsHint": "Filtere und halte deine Taxonomie gepflegt.",
|
||||||
"saveEmotion": "Emotion speichern",
|
"saveEmotion": "Emotion speichern",
|
||||||
|
|||||||
@@ -554,6 +554,12 @@
|
|||||||
"disabled": "Photo tasks disabled",
|
"disabled": "Photo tasks disabled",
|
||||||
"permissionHint": "You do not have permission to change photo tasks."
|
"permissionHint": "You do not have permission to change photo tasks."
|
||||||
},
|
},
|
||||||
|
"tabs": {
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"library": "Task Library",
|
||||||
|
"emotions": "Emotions",
|
||||||
|
"collections": "Collections"
|
||||||
|
},
|
||||||
"quickNav": "Quick jump",
|
"quickNav": "Quick jump",
|
||||||
"sections": {
|
"sections": {
|
||||||
"assigned": "Assigned",
|
"assigned": "Assigned",
|
||||||
@@ -602,6 +608,8 @@
|
|||||||
"bulkRemoveTitle": "Delete selection",
|
"bulkRemoveTitle": "Delete selection",
|
||||||
"bulkRemoveBody": "This will remove the selected photo tasks from the event.",
|
"bulkRemoveBody": "This will remove the selected photo tasks from the event.",
|
||||||
"select": "Select photo task",
|
"select": "Select photo task",
|
||||||
|
"addEmotion": "Add emotion",
|
||||||
|
"emotionsEmpty": "No emotions yet. Add one to help categorize tasks.",
|
||||||
"manageEmotions": "Manage emotions",
|
"manageEmotions": "Manage emotions",
|
||||||
"manageEmotionsHint": "Filter and keep your taxonomy tidy.",
|
"manageEmotionsHint": "Filter and keep your taxonomy tidy.",
|
||||||
"saveEmotion": "Save emotion",
|
"saveEmotion": "Save emotion",
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { ListItem } from '@tamagui/list-item';
|
|||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
import { Button } from '@tamagui/button';
|
import { Button } from '@tamagui/button';
|
||||||
import { AlertDialog } from '@tamagui/alert-dialog';
|
import { AlertDialog } from '@tamagui/alert-dialog';
|
||||||
import { ScrollView } from '@tamagui/scroll-view';
|
|
||||||
import { Switch } from '@tamagui/switch';
|
import { Switch } from '@tamagui/switch';
|
||||||
import { Checkbox } from '@tamagui/checkbox';
|
import { Checkbox } from '@tamagui/checkbox';
|
||||||
import { Tabs } from 'tamagui';
|
import { Tabs } from 'tamagui';
|
||||||
@@ -47,8 +46,7 @@ import { Tag } from './components/Tag';
|
|||||||
import { useEventContext } from '../context/EventContext';
|
import { useEventContext } from '../context/EventContext';
|
||||||
import { RadioGroup } from '@tamagui/radio-group';
|
import { RadioGroup } from '@tamagui/radio-group';
|
||||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||||
import { buildTaskSummary } from './lib/taskSummary';
|
import { type TaskSectionKey } from './lib/taskSectionCounts';
|
||||||
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
|
|
||||||
import { withAlpha } from './components/colors';
|
import { withAlpha } from './components/colors';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
import { resolveEngagementMode } from '../lib/events';
|
import { resolveEngagementMode } from '../lib/events';
|
||||||
@@ -97,7 +95,7 @@ export default function MobileEventTasksPage() {
|
|||||||
const [library, setLibrary] = React.useState<TenantTask[]>([]);
|
const [library, setLibrary] = React.useState<TenantTask[]>([]);
|
||||||
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
|
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
|
||||||
const [emotions, setEmotions] = React.useState<TenantEmotion[]>([]);
|
const [emotions, setEmotions] = React.useState<TenantEmotion[]>([]);
|
||||||
const [showCollectionSheet, setShowCollectionSheet] = React.useState(false);
|
const [activeTab, setActiveTab] = React.useState<TaskSectionKey>('assigned');
|
||||||
const [showTaskSheet, setShowTaskSheet] = React.useState(false);
|
const [showTaskSheet, setShowTaskSheet] = React.useState(false);
|
||||||
const [newTask, setNewTask] = React.useState({
|
const [newTask, setNewTask] = React.useState({
|
||||||
id: null as number | null,
|
id: null as number | null,
|
||||||
@@ -131,19 +129,10 @@ export default function MobileEventTasksPage() {
|
|||||||
const [savingEmotion, setSavingEmotion] = React.useState(false);
|
const [savingEmotion, setSavingEmotion] = React.useState(false);
|
||||||
const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false);
|
const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false);
|
||||||
const [showTaskDetails, setShowTaskDetails] = React.useState(false);
|
const [showTaskDetails, setShowTaskDetails] = React.useState(false);
|
||||||
const [quickNavSelection, setQuickNavSelection] = React.useState<TaskSectionKey | ''>('');
|
|
||||||
const [eventRecord, setEventRecord] = React.useState<TenantEvent | null>(null);
|
const [eventRecord, setEventRecord] = React.useState<TenantEvent | null>(null);
|
||||||
const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false);
|
const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false);
|
||||||
const text = textStrong;
|
const text = textStrong;
|
||||||
const assignedRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const libraryRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
|
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
|
||||||
const summary = buildTaskSummary({
|
|
||||||
assigned: assignedTasks.length,
|
|
||||||
library: library.length,
|
|
||||||
collections: collections.length,
|
|
||||||
emotions: emotions.length,
|
|
||||||
});
|
|
||||||
const permissionSource = eventRecord ?? activeEvent;
|
const permissionSource = eventRecord ?? activeEvent;
|
||||||
const memberPermissions = Array.isArray(permissionSource?.member_permissions) ? permissionSource?.member_permissions ?? [] : [];
|
const memberPermissions = Array.isArray(permissionSource?.member_permissions) ? permissionSource?.member_permissions ?? [] : [];
|
||||||
const canManageTasks = React.useMemo(
|
const canManageTasks = React.useMemo(
|
||||||
@@ -151,7 +140,6 @@ export default function MobileEventTasksPage() {
|
|||||||
[isMember, memberPermissions]
|
[isMember, memberPermissions]
|
||||||
);
|
);
|
||||||
const tasksEnabled = resolveEngagementMode(permissionSource ?? null) !== 'photo_only';
|
const tasksEnabled = resolveEngagementMode(permissionSource ?? null) !== 'photo_only';
|
||||||
const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]);
|
|
||||||
const maxTasks = React.useMemo(() => {
|
const maxTasks = React.useMemo(() => {
|
||||||
const limit = eventRecord?.limits?.tasks?.limit;
|
const limit = eventRecord?.limits?.tasks?.limit;
|
||||||
return typeof limit === 'number' && Number.isFinite(limit) ? limit : null;
|
return typeof limit === 'number' && Number.isFinite(limit) ? limit : null;
|
||||||
@@ -187,34 +175,6 @@ export default function MobileEventTasksPage() {
|
|||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
const scrollToSection = (ref: React.RefObject<HTMLDivElement | null>) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuickNav = (key: TaskSectionKey) => {
|
|
||||||
if (key === 'assigned') {
|
|
||||||
scrollToSection(assignedRef);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'library') {
|
|
||||||
scrollToSection(libraryRef);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'collections') {
|
|
||||||
setShowCollectionSheet(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setShowEmotionSheet(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuickNavPress = (key: TaskSectionKey) => {
|
|
||||||
if (quickNavSelection === key) {
|
|
||||||
handleQuickNav(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const load = React.useCallback(async () => {
|
const load = React.useCallback(async () => {
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
try {
|
try {
|
||||||
@@ -654,7 +614,7 @@ export default function MobileEventTasksPage() {
|
|||||||
<CTAButton
|
<CTAButton
|
||||||
label={t('events.tasks.emptyActionPack', 'Import photo task pack')}
|
label={t('events.tasks.emptyActionPack', 'Import photo task pack')}
|
||||||
tone="ghost"
|
tone="ghost"
|
||||||
onPress={() => setShowCollectionSheet(true)}
|
onPress={() => setActiveTab('collections')}
|
||||||
disabled={!canAddTasks}
|
disabled={!canAddTasks}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
/>
|
/>
|
||||||
@@ -708,7 +668,7 @@ export default function MobileEventTasksPage() {
|
|||||||
toast.error(limitReachedMessage);
|
toast.error(limitReachedMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setShowCollectionSheet(true);
|
setActiveTab('collections');
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
@@ -741,7 +701,6 @@ export default function MobileEventTasksPage() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
) : (
|
) : (
|
||||||
<YStack space="$2">
|
<YStack space="$2">
|
||||||
<YStack ref={assignedRef} />
|
|
||||||
<XStack alignItems="center" flexWrap="wrap" space="$2">
|
<XStack alignItems="center" flexWrap="wrap" space="$2">
|
||||||
<Text fontSize="$sm" color={muted}>
|
<Text fontSize="$sm" color={muted}>
|
||||||
{t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })}
|
{t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })}
|
||||||
@@ -845,29 +804,41 @@ export default function MobileEventTasksPage() {
|
|||||||
</YGroup.Item>
|
</YGroup.Item>
|
||||||
))}
|
))}
|
||||||
</YGroup>
|
</YGroup>
|
||||||
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
|
</YStack>
|
||||||
<YStack ref={libraryRef} />
|
);
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
|
||||||
{t('events.tasks.library', 'Weitere Fotoaufgaben')}
|
const libraryPanel = (
|
||||||
|
<YStack space="$2">
|
||||||
|
<XStack alignItems="center" justifyContent="space-between" flexWrap="wrap">
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||||
|
{t('events.tasks.tabs.library', 'Task Library')}
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable onPress={() => setShowCollectionSheet(true)}>
|
<Pressable onPress={() => setActiveTab('collections')}>
|
||||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||||
{t('events.tasks.import', 'Import photo task pack')}
|
{t('events.tasks.import', 'Import photo task pack')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</XStack>
|
</XStack>
|
||||||
<Pressable onPress={() => setExpandedLibrary((prev) => !prev)}>
|
{!canAddTasks ? (
|
||||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
<Text fontSize={12} fontWeight="600" color={dangerText}>
|
||||||
{expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')}
|
{limitReachedMessage}
|
||||||
|
{limitReachedHint ? ` ${limitReachedHint}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
) : null}
|
||||||
|
{library.length > 6 ? (
|
||||||
|
<Pressable onPress={() => setExpandedLibrary((prev) => !prev)}>
|
||||||
|
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||||
|
{expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
{library.length === 0 ? (
|
{library.length === 0 ? (
|
||||||
<Text fontSize={12} fontWeight="500" color={subtle}>
|
<Text fontSize={12} fontWeight="500" color={subtle}>
|
||||||
{t('events.tasks.libraryEmpty', 'Keine weiteren Fotoaufgaben verfügbar.')}
|
{t('events.tasks.libraryEmpty', 'No more photo tasks available.')}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
|
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
|
||||||
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
|
{(expandedLibrary ? library : library.slice(0, 6)).map((task) => (
|
||||||
<YGroup.Item key={`lib-${task.id}`}>
|
<YGroup.Item key={`lib-${task.id}`}>
|
||||||
<ListItem
|
<ListItem
|
||||||
hoverTheme
|
hoverTheme
|
||||||
@@ -915,6 +886,134 @@ export default function MobileEventTasksPage() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const collectionsPanel = (
|
||||||
|
<YStack space="$2">
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{t('events.tasks.importHint', 'Use predefined packs for your event type.')}
|
||||||
|
</Text>
|
||||||
|
{!canAddTasks ? (
|
||||||
|
<Text fontSize={12} fontWeight="600" color={dangerText}>
|
||||||
|
{limitReachedMessage}
|
||||||
|
{limitReachedHint ? ` ${limitReachedHint}` : ''}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{collections.length > 6 ? (
|
||||||
|
<Pressable onPress={() => setExpandedCollections((prev) => !prev)}>
|
||||||
|
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||||
|
{expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
{collections.length === 0 ? (
|
||||||
|
<Text fontSize={13} fontWeight="500" color={muted}>
|
||||||
|
{t('events.tasks.collectionsEmpty', 'No collections available.')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
|
||||||
|
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection) => (
|
||||||
|
<YGroup.Item key={collection.id}>
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
title={
|
||||||
|
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||||
|
{collection.name}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
subTitle={
|
||||||
|
collection.description ? (
|
||||||
|
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||||
|
{collection.description}
|
||||||
|
</Text>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
iconAfter={
|
||||||
|
<XStack space="$1.5" alignItems="center">
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
if (!canAddTasks) {
|
||||||
|
toast.error(limitReachedMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importCollection(collection.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fontSize={12} fontWeight="600" color={canAddTasks ? primary : muted}>
|
||||||
|
{t('events.tasks.import', 'Import')}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
<ChevronRight size={14} color={subtle} />
|
||||||
|
</XStack>
|
||||||
|
}
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
))}
|
||||||
|
</YGroup>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const emotionsPanel = (
|
||||||
|
<YStack space="$2">
|
||||||
|
<XStack alignItems="center" justifyContent="space-between" flexWrap="wrap">
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||||
|
{t('events.tasks.tabs.emotions', 'Emotions')}
|
||||||
|
</Text>
|
||||||
|
<CTAButton
|
||||||
|
label={t('events.tasks.addEmotion', 'Add emotion')}
|
||||||
|
fullWidth={false}
|
||||||
|
onPress={() => {
|
||||||
|
setEditingEmotion(null);
|
||||||
|
setEmotionForm({ name: '', color: border });
|
||||||
|
setShowEmotionSheet(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</XStack>
|
||||||
|
{emotions.length === 0 ? (
|
||||||
|
<Text fontSize={12} fontWeight="500" color={muted}>
|
||||||
|
{t('events.tasks.emotionsEmpty', 'No emotions yet. Add one to help categorize tasks.')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
|
||||||
|
{emotions.map((emotion) => (
|
||||||
|
<YGroup.Item key={`emo-${emotion.id}`}>
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
title={
|
||||||
|
<XStack alignItems="center" space="$2">
|
||||||
|
<Tag label={emotion.name ?? ''} color={emotion.color ?? border} />
|
||||||
|
</XStack>
|
||||||
|
}
|
||||||
|
iconAfter={
|
||||||
|
<XStack space="$2">
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
setEditingEmotion(emotion);
|
||||||
|
setEmotionForm({ name: emotion.name ?? '', color: emotion.color ?? border });
|
||||||
|
setShowEmotionSheet(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil size={14} color={primary} />
|
||||||
|
</Pressable>
|
||||||
|
<Pressable onPress={() => removeEmotion(emotion.id)}>
|
||||||
|
<Trash2 size={14} color={danger} />
|
||||||
|
</Pressable>
|
||||||
|
<ChevronRight size={14} color={subtle} />
|
||||||
|
</XStack>
|
||||||
|
}
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
))}
|
||||||
|
</YGroup>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell
|
<MobileShell
|
||||||
activeTab="tasks"
|
activeTab="tasks"
|
||||||
@@ -1024,212 +1123,80 @@ export default function MobileEventTasksPage() {
|
|||||||
))}
|
))}
|
||||||
</YStack>
|
</YStack>
|
||||||
) : (
|
) : (
|
||||||
<YStack space="$2">
|
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TaskSectionKey)}>
|
||||||
<Card
|
<Tabs.List>
|
||||||
borderRadius={22}
|
<Tabs.Tab value="assigned">{t('events.tasks.tabs.tasks', 'Tasks')}</Tabs.Tab>
|
||||||
borderWidth={2}
|
<Tabs.Tab value="library">{t('events.tasks.tabs.library', 'Task Library')}</Tabs.Tab>
|
||||||
borderColor={border}
|
<Tabs.Tab value="emotions">{t('events.tasks.tabs.emotions', 'Emotions')}</Tabs.Tab>
|
||||||
backgroundColor={surface}
|
<Tabs.Tab value="collections">{t('events.tasks.tabs.collections', 'Collections')}</Tabs.Tab>
|
||||||
padding="$3"
|
</Tabs.List>
|
||||||
>
|
|
||||||
<YStack space="$2.5">
|
<Tabs.Content value="assigned" paddingTop="$2">
|
||||||
<XStack alignItems="center" justifyContent="space-between">
|
<YStack space="$2">
|
||||||
<XStack
|
<YStack className="admin-sticky-toolbar">
|
||||||
alignItems="center"
|
<Card
|
||||||
paddingHorizontal="$3"
|
borderRadius={20}
|
||||||
paddingVertical="$1.5"
|
borderWidth={2}
|
||||||
borderRadius={999}
|
borderColor={stickyBorder}
|
||||||
borderWidth={1}
|
backgroundColor={stickySurface}
|
||||||
borderColor={border}
|
padding="$3"
|
||||||
backgroundColor={surfaceMuted}
|
shadowColor={stickyShadow}
|
||||||
|
shadowOpacity={0.16}
|
||||||
|
shadowRadius={16}
|
||||||
|
shadowOffset={{ width: 0, height: 10 }}
|
||||||
>
|
>
|
||||||
<Text fontSize="$xs" fontWeight="800" color={text}>
|
<XStack alignItems="center" space="$2">
|
||||||
{t('events.tasks.quickNav', 'Quick jump')}
|
<XStack flex={1}>
|
||||||
</Text>
|
<MobileInput
|
||||||
</XStack>
|
type="search"
|
||||||
</XStack>
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
placeholder={t('events.tasks.search', 'Search photo tasks')}
|
||||||
<Tabs
|
compact
|
||||||
value={quickNavSelection}
|
/>
|
||||||
onValueChange={(next) => {
|
</XStack>
|
||||||
const key = next as TaskSectionKey | '';
|
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
|
||||||
if (!key) {
|
<XStack
|
||||||
return;
|
alignItems="center"
|
||||||
}
|
space="$1.5"
|
||||||
setQuickNavSelection(key);
|
paddingVertical="$2"
|
||||||
handleQuickNav(key);
|
paddingHorizontal="$3"
|
||||||
}}
|
borderRadius={14}
|
||||||
orientation="horizontal"
|
borderWidth={1}
|
||||||
>
|
borderColor={border}
|
||||||
<Tabs.List backgroundColor="transparent" borderWidth={0} paddingVertical="$1" gap="$2">
|
backgroundColor={surface}
|
||||||
{sectionCounts.map((section) => {
|
>
|
||||||
const isActive = quickNavSelection === section.key;
|
<Text fontSize={11} fontWeight="700" color={text}>
|
||||||
const activeBorder = withAlpha(primary, 0.45);
|
{t('events.tasks.emotionFilterShort', 'Emotion')}
|
||||||
const activeBackground = withAlpha(primary, 0.16);
|
|
||||||
return (
|
|
||||||
<Tabs.Tab
|
|
||||||
key={section.key}
|
|
||||||
value={section.key}
|
|
||||||
unstyled
|
|
||||||
onPress={() => handleQuickNavPress(section.key)}
|
|
||||||
borderRadius={999}
|
|
||||||
borderWidth={1}
|
|
||||||
borderColor={isActive ? activeBorder : border}
|
|
||||||
backgroundColor={isActive ? activeBackground : surface}
|
|
||||||
paddingHorizontal="$3"
|
|
||||||
paddingVertical="$2"
|
|
||||||
height={36}
|
|
||||||
pressStyle={{ backgroundColor: isActive ? activeBackground : surfaceMuted }}
|
|
||||||
>
|
|
||||||
<XStack alignItems="center" space="$1.5">
|
|
||||||
<Text fontSize="$xs" fontWeight="700" color={textStrong}>
|
|
||||||
{t(`events.tasks.sections.${section.key}`, section.key)}
|
|
||||||
</Text>
|
|
||||||
<XStack
|
|
||||||
paddingHorizontal="$2"
|
|
||||||
paddingVertical="$0.5"
|
|
||||||
borderRadius={999}
|
|
||||||
borderWidth={1}
|
|
||||||
borderColor={isActive ? activeBorder : border}
|
|
||||||
backgroundColor={surfaceMuted}
|
|
||||||
>
|
|
||||||
<Text fontSize={10} fontWeight="800" color={textStrong}>
|
|
||||||
{section.count}
|
|
||||||
</Text>
|
|
||||||
</XStack>
|
|
||||||
</XStack>
|
|
||||||
</Tabs.Tab>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs>
|
|
||||||
</ScrollView>
|
|
||||||
</YStack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="admin-sticky-toolbar">
|
|
||||||
<Card
|
|
||||||
borderRadius={20}
|
|
||||||
borderWidth={2}
|
|
||||||
borderColor={stickyBorder}
|
|
||||||
backgroundColor={stickySurface}
|
|
||||||
padding="$3"
|
|
||||||
shadowColor={stickyShadow}
|
|
||||||
shadowOpacity={0.16}
|
|
||||||
shadowRadius={16}
|
|
||||||
shadowOffset={{ width: 0, height: 10 }}
|
|
||||||
>
|
|
||||||
<XStack alignItems="center" space="$2">
|
|
||||||
<XStack flex={1}>
|
|
||||||
<MobileInput
|
|
||||||
type="search"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
placeholder={t('events.tasks.search', 'Search photo tasks')}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
</XStack>
|
|
||||||
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
|
|
||||||
<XStack
|
|
||||||
alignItems="center"
|
|
||||||
space="$1.5"
|
|
||||||
paddingVertical="$2"
|
|
||||||
paddingHorizontal="$3"
|
|
||||||
borderRadius={14}
|
|
||||||
borderWidth={1}
|
|
||||||
borderColor={border}
|
|
||||||
backgroundColor={surface}
|
|
||||||
>
|
|
||||||
<Text fontSize={11} fontWeight="700" color={text}>
|
|
||||||
{t('events.tasks.emotionFilterShort', 'Emotion')}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize={11} color={muted}>
|
|
||||||
{emotionFilter
|
|
||||||
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
|
|
||||||
: t('events.tasks.allEmotions', 'All')}
|
|
||||||
</Text>
|
|
||||||
<ChevronDown size={14} color={muted} />
|
|
||||||
</XStack>
|
|
||||||
</Pressable>
|
|
||||||
</XStack>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{taskPanel}
|
|
||||||
</YStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MobileSheet
|
|
||||||
open={showCollectionSheet}
|
|
||||||
onClose={() => setShowCollectionSheet(false)}
|
|
||||||
title={t('events.tasks.import', 'Fotoaufgabenpaket importieren')}
|
|
||||||
footer={null}
|
|
||||||
>
|
|
||||||
<YStack space="$2">
|
|
||||||
{!canAddTasks ? (
|
|
||||||
<Text fontSize={12} fontWeight="600" color={dangerText}>
|
|
||||||
{limitReachedMessage}
|
|
||||||
{limitReachedHint ? ` ${limitReachedHint}` : ''}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{collections.length > 6 ? (
|
|
||||||
<Pressable onPress={() => setExpandedCollections((prev) => !prev)}>
|
|
||||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
|
||||||
{expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
|
||||||
{collections.length === 0 ? (
|
|
||||||
<Text fontSize={13} fontWeight="500" color={muted}>
|
|
||||||
{t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
|
|
||||||
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => (
|
|
||||||
<YGroup.Item key={collection.id}>
|
|
||||||
<ListItem
|
|
||||||
hoverTheme
|
|
||||||
pressTheme
|
|
||||||
title={
|
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
|
||||||
{collection.name}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
subTitle={
|
|
||||||
collection.description ? (
|
|
||||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
|
||||||
{collection.description}
|
|
||||||
</Text>
|
</Text>
|
||||||
) : null
|
<Text fontSize={11} color={muted}>
|
||||||
}
|
{emotionFilter
|
||||||
iconAfter={
|
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
|
||||||
<XStack space="$1.5" alignItems="center">
|
: t('events.tasks.allEmotions', 'All')}
|
||||||
<Pressable
|
</Text>
|
||||||
onPress={() => {
|
<ChevronDown size={14} color={muted} />
|
||||||
if (!canAddTasks) {
|
|
||||||
toast.error(limitReachedMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
importCollection(collection.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text fontSize={12} fontWeight="600" color={canAddTasks ? primary : muted}>
|
|
||||||
{t('events.tasks.import', 'Import')}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
<ChevronRight size={14} color={subtle} />
|
|
||||||
</XStack>
|
</XStack>
|
||||||
}
|
</Pressable>
|
||||||
paddingVertical="$2"
|
</XStack>
|
||||||
paddingHorizontal="$3"
|
</Card>
|
||||||
/>
|
</YStack>
|
||||||
</YGroup.Item>
|
{taskPanel}
|
||||||
))}
|
</YStack>
|
||||||
</YGroup>
|
</Tabs.Content>
|
||||||
)}
|
|
||||||
</YStack>
|
<Tabs.Content value="library" paddingTop="$2">
|
||||||
</MobileSheet>
|
{libraryPanel}
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="emotions" paddingTop="$2">
|
||||||
|
{emotionsPanel}
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="collections" paddingTop="$2">
|
||||||
|
{collectionsPanel}
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
|
||||||
<MobileSheet
|
<MobileSheet
|
||||||
open={showTaskSheet}
|
open={showTaskSheet}
|
||||||
@@ -1355,39 +1322,6 @@ export default function MobileEventTasksPage() {
|
|||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
/>
|
/>
|
||||||
</MobileField>
|
</MobileField>
|
||||||
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
|
|
||||||
{emotions.map((em, idx) => (
|
|
||||||
<YGroup.Item key={`emo-${em.id}`}>
|
|
||||||
<ListItem
|
|
||||||
hoverTheme
|
|
||||||
pressTheme
|
|
||||||
title={
|
|
||||||
<XStack alignItems="center" space="$2">
|
|
||||||
<Tag label={em.name ?? ''} color={em.color ?? border} />
|
|
||||||
</XStack>
|
|
||||||
}
|
|
||||||
iconAfter={
|
|
||||||
<XStack space="$2">
|
|
||||||
<Pressable
|
|
||||||
onPress={() => {
|
|
||||||
setEditingEmotion(em);
|
|
||||||
setEmotionForm({ name: em.name ?? '', color: em.color ?? border });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pencil size={14} color={primary} />
|
|
||||||
</Pressable>
|
|
||||||
<Pressable onPress={() => removeEmotion(em.id)}>
|
|
||||||
<Trash2 size={14} color={danger} />
|
|
||||||
</Pressable>
|
|
||||||
<ChevronRight size={14} color={subtle} />
|
|
||||||
</XStack>
|
|
||||||
}
|
|
||||||
paddingVertical="$2"
|
|
||||||
paddingHorizontal="$3"
|
|
||||||
/>
|
|
||||||
</YGroup.Item>
|
|
||||||
))}
|
|
||||||
</YGroup>
|
|
||||||
</YStack>
|
</YStack>
|
||||||
</MobileSheet>
|
</MobileSheet>
|
||||||
|
|
||||||
@@ -1619,7 +1553,7 @@ export default function MobileEventTasksPage() {
|
|||||||
}
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowFabMenu(false);
|
setShowFabMenu(false);
|
||||||
setShowEmotionSheet(true);
|
setActiveTab('emotions');
|
||||||
}}
|
}}
|
||||||
paddingVertical="$2"
|
paddingVertical="$2"
|
||||||
paddingHorizontal="$3"
|
paddingHorizontal="$3"
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ vi.mock('tamagui', () => ({
|
|||||||
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
||||||
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
Tab: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
Tab: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
||||||
|
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -273,12 +274,14 @@ vi.mock('../theme', () => ({
|
|||||||
import MobileEventTasksPage from '../EventTasksPage';
|
import MobileEventTasksPage from '../EventTasksPage';
|
||||||
|
|
||||||
describe('MobileEventTasksPage', () => {
|
describe('MobileEventTasksPage', () => {
|
||||||
it('renders the quick jump chips and photo task header', async () => {
|
it('renders the tabs and photo task header', async () => {
|
||||||
render(<MobileEventTasksPage />);
|
render(<MobileEventTasksPage />);
|
||||||
|
|
||||||
expect(await screen.findByText('Photo task mode')).toBeInTheDocument();
|
expect(await screen.findByText('Photo task mode')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Quick jump')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Tasks' })).toBeInTheDocument();
|
||||||
expect(screen.getByText('assigned')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Task Library' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Emotions' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Collections' })).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText('Search photo tasks')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Search photo tasks')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Emotion')).toBeInTheDocument();
|
expect(screen.getByText('Emotion')).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user