From db5fea9f2ab74cea9b4ef10a2b626fa90ee4a6b6 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 22 Jan 2026 20:33:02 +0100 Subject: [PATCH] Refactor event tasks tabs --- .../js/admin/i18n/locales/de/management.json | 8 + .../js/admin/i18n/locales/en/management.json | 8 + resources/js/admin/mobile/EventTasksPage.tsx | 520 ++++++++---------- .../mobile/__tests__/EventTasksPage.test.tsx | 9 +- 4 files changed, 249 insertions(+), 296 deletions(-) diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index 22bdca0..58aac7c 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -558,6 +558,12 @@ "disabled": "Fotoaufgaben deaktiviert", "permissionHint": "Du hast keine Berechtigung, Fotoaufgaben zu ändern." }, + "tabs": { + "tasks": "Aufgaben", + "library": "Aufgabenbibliothek", + "emotions": "Emotionen", + "collections": "Sammlungen" + }, "quickNav": "Schnellzugriff", "sections": { "assigned": "Zugewiesen", @@ -606,6 +612,8 @@ "bulkRemoveTitle": "Auswahl löschen", "bulkRemoveBody": "Dies entfernt die ausgewählten Fotoaufgaben aus dem Event.", "select": "Fotoaufgabe auswählen", + "addEmotion": "Emotion hinzufügen", + "emotionsEmpty": "Noch keine Emotionen vorhanden. Füge eine hinzu, um Aufgaben zu kategorisieren.", "manageEmotions": "Emotionen verwalten", "manageEmotionsHint": "Filtere und halte deine Taxonomie gepflegt.", "saveEmotion": "Emotion speichern", diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index af1bbf2..17febff 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -554,6 +554,12 @@ "disabled": "Photo tasks disabled", "permissionHint": "You do not have permission to change photo tasks." }, + "tabs": { + "tasks": "Tasks", + "library": "Task Library", + "emotions": "Emotions", + "collections": "Collections" + }, "quickNav": "Quick jump", "sections": { "assigned": "Assigned", @@ -602,6 +608,8 @@ "bulkRemoveTitle": "Delete selection", "bulkRemoveBody": "This will remove the selected photo tasks from the event.", "select": "Select photo task", + "addEmotion": "Add emotion", + "emotionsEmpty": "No emotions yet. Add one to help categorize tasks.", "manageEmotions": "Manage emotions", "manageEmotionsHint": "Filter and keep your taxonomy tidy.", "saveEmotion": "Save emotion", diff --git a/resources/js/admin/mobile/EventTasksPage.tsx b/resources/js/admin/mobile/EventTasksPage.tsx index 7332a0b..5726d13 100644 --- a/resources/js/admin/mobile/EventTasksPage.tsx +++ b/resources/js/admin/mobile/EventTasksPage.tsx @@ -10,7 +10,6 @@ import { ListItem } from '@tamagui/list-item'; import { Pressable } from '@tamagui/react-native-web-lite'; import { Button } from '@tamagui/button'; import { AlertDialog } from '@tamagui/alert-dialog'; -import { ScrollView } from '@tamagui/scroll-view'; import { Switch } from '@tamagui/switch'; import { Checkbox } from '@tamagui/checkbox'; import { Tabs } from 'tamagui'; @@ -47,8 +46,7 @@ import { Tag } from './components/Tag'; import { useEventContext } from '../context/EventContext'; import { RadioGroup } from '@tamagui/radio-group'; import { useBackNavigation } from './hooks/useBackNavigation'; -import { buildTaskSummary } from './lib/taskSummary'; -import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts'; +import { type TaskSectionKey } from './lib/taskSectionCounts'; import { withAlpha } from './components/colors'; import { useAdminTheme } from './theme'; import { resolveEngagementMode } from '../lib/events'; @@ -97,7 +95,7 @@ export default function MobileEventTasksPage() { const [library, setLibrary] = React.useState([]); const [collections, setCollections] = React.useState([]); const [emotions, setEmotions] = React.useState([]); - const [showCollectionSheet, setShowCollectionSheet] = React.useState(false); + const [activeTab, setActiveTab] = React.useState('assigned'); const [showTaskSheet, setShowTaskSheet] = React.useState(false); const [newTask, setNewTask] = React.useState({ id: null as number | null, @@ -131,19 +129,10 @@ export default function MobileEventTasksPage() { const [savingEmotion, setSavingEmotion] = React.useState(false); const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false); const [showTaskDetails, setShowTaskDetails] = React.useState(false); - const [quickNavSelection, setQuickNavSelection] = React.useState(''); const [eventRecord, setEventRecord] = React.useState(null); const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false); const text = textStrong; - const assignedRef = React.useRef(null); - const libraryRef = React.useRef(null); 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 memberPermissions = Array.isArray(permissionSource?.member_permissions) ? permissionSource?.member_permissions ?? [] : []; const canManageTasks = React.useMemo( @@ -151,7 +140,6 @@ export default function MobileEventTasksPage() { [isMember, memberPermissions] ); const tasksEnabled = resolveEngagementMode(permissionSource ?? null) !== 'photo_only'; - const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]); const maxTasks = React.useMemo(() => { const limit = eventRecord?.limits?.tasks?.limit; return typeof limit === 'number' && Number.isFinite(limit) ? limit : null; @@ -187,34 +175,6 @@ export default function MobileEventTasksPage() { setSearchTerm(''); }, [slug]); - const scrollToSection = (ref: React.RefObject) => { - 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 () => { if (!slug) { try { @@ -654,7 +614,7 @@ export default function MobileEventTasksPage() { setShowCollectionSheet(true)} + onPress={() => setActiveTab('collections')} disabled={!canAddTasks} fullWidth={false} /> @@ -708,7 +668,7 @@ export default function MobileEventTasksPage() { toast.error(limitReachedMessage); return; } - setShowCollectionSheet(true); + setActiveTab('collections'); }} title={ @@ -741,7 +701,6 @@ export default function MobileEventTasksPage() { ) : ( - {t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })} @@ -845,29 +804,41 @@ export default function MobileEventTasksPage() { ))} - - - - {t('events.tasks.library', 'Weitere Fotoaufgaben')} + + ); + + const libraryPanel = ( + + + + {t('events.tasks.tabs.library', 'Task Library')} - setShowCollectionSheet(true)}> + setActiveTab('collections')}> {t('events.tasks.import', 'Import photo task pack')} - setExpandedLibrary((prev) => !prev)}> - - {expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')} + {!canAddTasks ? ( + + {limitReachedMessage} + {limitReachedHint ? ` ${limitReachedHint}` : ''} - + ) : null} + {library.length > 6 ? ( + setExpandedLibrary((prev) => !prev)}> + + {expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')} + + + ) : null} {library.length === 0 ? ( - {t('events.tasks.libraryEmpty', 'Keine weiteren Fotoaufgaben verfügbar.')} + {t('events.tasks.libraryEmpty', 'No more photo tasks available.')} ) : ( - {(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => ( + {(expandedLibrary ? library : library.slice(0, 6)).map((task) => ( ); + const collectionsPanel = ( + + + {t('events.tasks.importHint', 'Use predefined packs for your event type.')} + + {!canAddTasks ? ( + + {limitReachedMessage} + {limitReachedHint ? ` ${limitReachedHint}` : ''} + + ) : null} + {collections.length > 6 ? ( + setExpandedCollections((prev) => !prev)}> + + {expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')} + + + ) : null} + {collections.length === 0 ? ( + + {t('events.tasks.collectionsEmpty', 'No collections available.')} + + ) : ( + + {(expandedCollections ? collections : collections.slice(0, 6)).map((collection) => ( + + + {collection.name} + + } + subTitle={ + collection.description ? ( + + {collection.description} + + ) : null + } + iconAfter={ + + { + if (!canAddTasks) { + toast.error(limitReachedMessage); + return; + } + importCollection(collection.id); + }} + > + + {t('events.tasks.import', 'Import')} + + + + + } + paddingVertical="$2" + paddingHorizontal="$3" + /> + + ))} + + )} + + ); + + const emotionsPanel = ( + + + + {t('events.tasks.tabs.emotions', 'Emotions')} + + { + setEditingEmotion(null); + setEmotionForm({ name: '', color: border }); + setShowEmotionSheet(true); + }} + /> + + {emotions.length === 0 ? ( + + {t('events.tasks.emotionsEmpty', 'No emotions yet. Add one to help categorize tasks.')} + + ) : ( + + {emotions.map((emotion) => ( + + + + + } + iconAfter={ + + { + setEditingEmotion(emotion); + setEmotionForm({ name: emotion.name ?? '', color: emotion.color ?? border }); + setShowEmotionSheet(true); + }} + > + + + removeEmotion(emotion.id)}> + + + + + } + paddingVertical="$2" + paddingHorizontal="$3" + /> + + ))} + + )} + + ); + return ( ) : ( - - - - - setActiveTab(value as TaskSectionKey)}> + + {t('events.tasks.tabs.tasks', 'Tasks')} + {t('events.tasks.tabs.library', 'Task Library')} + {t('events.tasks.tabs.emotions', 'Emotions')} + {t('events.tasks.tabs.collections', 'Collections')} + + + + + + - - {t('events.tasks.quickNav', 'Quick jump')} - - - - - - { - const key = next as TaskSectionKey | ''; - if (!key) { - return; - } - setQuickNavSelection(key); - handleQuickNav(key); - }} - orientation="horizontal" - > - - {sectionCounts.map((section) => { - const isActive = quickNavSelection === section.key; - const activeBorder = withAlpha(primary, 0.45); - const activeBackground = withAlpha(primary, 0.16); - return ( - 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 }} - > - - - {t(`events.tasks.sections.${section.key}`, section.key)} - - - - {section.count} - - - - - ); - })} - - - - - - -
- - - - setSearchTerm(e.target.value)} - placeholder={t('events.tasks.search', 'Search photo tasks')} - compact - /> - - setShowEmotionFilterSheet(true)}> - - - {t('events.tasks.emotionFilterShort', 'Emotion')} - - - {emotionFilter - ? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion') - : t('events.tasks.allEmotions', 'All')} - - - - - - -
- - {taskPanel} -
- )} - - setShowCollectionSheet(false)} - title={t('events.tasks.import', 'Fotoaufgabenpaket importieren')} - footer={null} - > - - {!canAddTasks ? ( - - {limitReachedMessage} - {limitReachedHint ? ` ${limitReachedHint}` : ''} - - ) : null} - {collections.length > 6 ? ( - setExpandedCollections((prev) => !prev)}> - - {expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')} - - - ) : null} - {collections.length === 0 ? ( - - {t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')} - - ) : ( - - {(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => ( - - - {collection.name} - - } - subTitle={ - collection.description ? ( - - {collection.description} + + + setSearchTerm(e.target.value)} + placeholder={t('events.tasks.search', 'Search photo tasks')} + compact + /> + + setShowEmotionFilterSheet(true)}> + + + {t('events.tasks.emotionFilterShort', 'Emotion')} - ) : null - } - iconAfter={ - - { - if (!canAddTasks) { - toast.error(limitReachedMessage); - return; - } - importCollection(collection.id); - }} - > - - {t('events.tasks.import', 'Import')} - - - + + {emotionFilter + ? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion') + : t('events.tasks.allEmotions', 'All')} + + - } - paddingVertical="$2" - paddingHorizontal="$3" - /> - - ))} - - )} - - + + + + + {taskPanel} + + + + + {libraryPanel} + + + + {emotionsPanel} + + + + {collectionsPanel} + + + )} - - {emotions.map((em, idx) => ( - - - - - } - iconAfter={ - - { - setEditingEmotion(em); - setEmotionForm({ name: em.name ?? '', color: em.color ?? border }); - }} - > - - - removeEmotion(em.id)}> - - - - - } - paddingVertical="$2" - paddingHorizontal="$3" - /> - - ))} - @@ -1619,7 +1553,7 @@ export default function MobileEventTasksPage() { } onPress={() => { setShowFabMenu(false); - setShowEmotionSheet(true); + setActiveTab('emotions'); }} paddingVertical="$2" paddingHorizontal="$3" diff --git a/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx b/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx index c550747..549b537 100644 --- a/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx @@ -112,6 +112,7 @@ vi.mock('tamagui', () => ({ Tabs: Object.assign(({ children }: { children: React.ReactNode }) =>
{children}
, { List: ({ children }: { children: React.ReactNode }) =>
{children}
, Tab: ({ children }: { children: React.ReactNode }) => , + Content: ({ children }: { children: React.ReactNode }) =>
{children}
, }), })); @@ -273,12 +274,14 @@ vi.mock('../theme', () => ({ import MobileEventTasksPage from '../EventTasksPage'; describe('MobileEventTasksPage', () => { - it('renders the quick jump chips and photo task header', async () => { + it('renders the tabs and photo task header', async () => { render(); expect(await screen.findByText('Photo task mode')).toBeInTheDocument(); - expect(screen.getByText('Quick jump')).toBeInTheDocument(); - expect(screen.getByText('assigned')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Tasks' })).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.getByText('Emotion')).toBeInTheDocument();