Add tasks toggle card
This commit is contained in:
@@ -2070,6 +2070,18 @@
|
||||
"tasks": {
|
||||
"disabledTitle": "Task-Modus ist für dieses Event aus",
|
||||
"disabledBody": "Gäste sehen nur den Fotofeed. Aktiviere Tasks in den Event-Einstellungen, um sie wieder anzuzeigen.",
|
||||
"toggle": {
|
||||
"title": "1. Aufgaben aktivieren",
|
||||
"description": "Aktiviere Aufgaben, damit Gäste Challenges und Hinweise in der App sehen.",
|
||||
"active": "AKTIV",
|
||||
"inactive": "INAKTIV",
|
||||
"onLabel": "Gäste sehen Aufgaben-Prompts",
|
||||
"offLabel": "Gäste sehen nur Fotos",
|
||||
"switchLabel": "Aufgaben aktiv",
|
||||
"enabled": "Aufgaben aktiviert",
|
||||
"disabled": "Aufgaben deaktiviert",
|
||||
"permissionHint": "Du hast keine Berechtigung, Aufgaben zu ändern."
|
||||
},
|
||||
"title": "Tasks & Checklisten",
|
||||
"quickNav": "Schnellzugriff",
|
||||
"sections": {
|
||||
|
||||
@@ -2074,6 +2074,18 @@
|
||||
"tasks": {
|
||||
"disabledTitle": "Task mode is off for this event",
|
||||
"disabledBody": "Guests only see the photo feed. Enable tasks in the event settings to show them again.",
|
||||
"toggle": {
|
||||
"title": "1. Activate tasks",
|
||||
"description": "Enable tasks so guests see challenges and prompts in the app.",
|
||||
"active": "ACTIVE",
|
||||
"inactive": "INACTIVE",
|
||||
"onLabel": "Guests see task prompts",
|
||||
"offLabel": "Guest app shows photos only",
|
||||
"switchLabel": "Tasks enabled",
|
||||
"enabled": "Tasks activated",
|
||||
"disabled": "Tasks disabled",
|
||||
"permissionHint": "You do not have permission to change tasks."
|
||||
},
|
||||
"title": "Tasks & checklists",
|
||||
"quickNav": "Quick jump",
|
||||
"sections": {
|
||||
|
||||
@@ -12,13 +12,15 @@ import { Button } from '@tamagui/button';
|
||||
import { AlertDialog } from '@tamagui/alert-dialog';
|
||||
import { ScrollView } from '@tamagui/scroll-view';
|
||||
import { ToggleGroup } from '@tamagui/toggle-group';
|
||||
import { Switch } from '@tamagui/switch';
|
||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||
import { MobileCard, CTAButton, SkeletonCard, FloatingActionButton } from './components/Primitives';
|
||||
import { MobileCard, CTAButton, SkeletonCard, FloatingActionButton, PillBadge } from './components/Primitives';
|
||||
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
|
||||
import {
|
||||
getEvent,
|
||||
getEvents,
|
||||
getEventTasks,
|
||||
updateEvent,
|
||||
updateTask,
|
||||
TenantTask,
|
||||
TenantEvent,
|
||||
@@ -48,6 +50,19 @@ import { buildTaskSummary } from './lib/taskSummary';
|
||||
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
|
||||
import { withAlpha } from './components/colors';
|
||||
import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme';
|
||||
import { resolveEngagementMode } from '../lib/events';
|
||||
import { useAuth } from '../auth/context';
|
||||
|
||||
function allowPermission(permissions: string[], permission: string): boolean {
|
||||
if (permissions.includes('*') || permissions.includes(permission)) {
|
||||
return true;
|
||||
}
|
||||
if (permission.includes(':')) {
|
||||
const [prefix] = permission.split(':');
|
||||
return permissions.includes(`${prefix}:*`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function TaskSummaryCard({ summary }: { summary: ReturnType<typeof buildTaskSummary> }) {
|
||||
const { t } = useTranslation('management');
|
||||
@@ -238,7 +253,9 @@ export default function MobileEventTasksPage() {
|
||||
const slug = slugParam ?? activeEvent?.slug ?? null;
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('management');
|
||||
const { user } = useAuth();
|
||||
const { textStrong, muted, subtle, border, primary, danger, surface, surfaceMuted, dangerBg, dangerText, overlay } = useAdminTheme();
|
||||
const isMember = user?.role === 'member';
|
||||
const [assignedTasks, setAssignedTasks] = React.useState<TenantTask[]>([]);
|
||||
const [library, setLibrary] = React.useState<TenantTask[]>([]);
|
||||
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
|
||||
@@ -271,6 +288,8 @@ export default function MobileEventTasksPage() {
|
||||
const [savingEmotion, setSavingEmotion] = React.useState(false);
|
||||
const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false);
|
||||
const [quickNavSelection, setQuickNavSelection] = React.useState<TaskSectionKey | ''>('');
|
||||
const [eventRecord, setEventRecord] = React.useState<TenantEvent | null>(null);
|
||||
const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false);
|
||||
const text = textStrong;
|
||||
const assignedRef = React.useRef<HTMLDivElement>(null);
|
||||
const libraryRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -281,6 +300,13 @@ export default function MobileEventTasksPage() {
|
||||
collections: collections.length,
|
||||
emotions: emotions.length,
|
||||
});
|
||||
const permissionSource = eventRecord ?? activeEvent;
|
||||
const memberPermissions = Array.isArray(permissionSource?.member_permissions) ? permissionSource?.member_permissions ?? [] : [];
|
||||
const canManageTasks = React.useMemo(
|
||||
() => (isMember ? allowPermission(memberPermissions, 'tasks:manage') : true),
|
||||
[isMember, memberPermissions]
|
||||
);
|
||||
const tasksEnabled = resolveEngagementMode(permissionSource ?? null) !== 'photo_only';
|
||||
const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]);
|
||||
React.useEffect(() => {
|
||||
if (slugParam && activeEvent?.slug !== slugParam) {
|
||||
@@ -339,6 +365,7 @@ export default function MobileEventTasksPage() {
|
||||
try {
|
||||
const event = await getEvent(slug);
|
||||
setEventId(event.id);
|
||||
setEventRecord(event);
|
||||
const [result, libraryTasks] = await Promise.all([
|
||||
getEventTasks(event.id, 1),
|
||||
getTasks({ per_page: 200 }),
|
||||
@@ -574,6 +601,30 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTasksToggle(nextEnabled: boolean) {
|
||||
if (!slug || tasksToggleBusy || !canManageTasks) return;
|
||||
setTasksToggleBusy(true);
|
||||
try {
|
||||
const updated = await updateEvent(slug, {
|
||||
settings: {
|
||||
engagement_mode: nextEnabled ? 'tasks' : 'photo_only',
|
||||
},
|
||||
});
|
||||
setEventRecord(updated);
|
||||
toast.success(
|
||||
nextEnabled
|
||||
? t('events.tasks.toggle.enabled', 'Tasks activated')
|
||||
: t('events.tasks.toggle.disabled', 'Tasks disabled')
|
||||
);
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
toast.error(t('events.errors.toggleFailed', 'Status could not be updated.'));
|
||||
}
|
||||
} finally {
|
||||
setTasksToggleBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="tasks"
|
||||
@@ -601,6 +652,53 @@ export default function MobileEventTasksPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
{!loading ? (
|
||||
<MobileCard space="$3">
|
||||
<YStack space="$1">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('events.tasks.toggle.title', '1. Activate tasks')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t(
|
||||
'events.tasks.toggle.description',
|
||||
'Enable tasks so guests see challenges and prompts in the app.'
|
||||
)}
|
||||
</Text>
|
||||
</YStack>
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$3" flexWrap="wrap">
|
||||
<PillBadge tone={tasksEnabled ? 'success' : 'warning'}>
|
||||
{tasksEnabled
|
||||
? t('events.tasks.toggle.active', 'ACTIVE')
|
||||
: t('events.tasks.toggle.inactive', 'INACTIVE')}
|
||||
</PillBadge>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{tasksEnabled
|
||||
? t('events.tasks.toggle.onLabel', 'Guests see task prompts')
|
||||
: t('events.tasks.toggle.offLabel', 'Guest app shows photos only')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack alignItems="center" justifyContent="space-between" marginTop="$2">
|
||||
<Text fontSize="$xs" color={text} fontWeight="600">
|
||||
{t('events.tasks.toggle.switchLabel', 'Tasks enabled')}
|
||||
</Text>
|
||||
<Switch
|
||||
size="$4"
|
||||
checked={tasksEnabled}
|
||||
onCheckedChange={handleTasksToggle}
|
||||
aria-label={t('events.tasks.toggle.switchLabel', 'Tasks enabled')}
|
||||
disabled={!canManageTasks || tasksToggleBusy}
|
||||
>
|
||||
<Switch.Thumb />
|
||||
</Switch>
|
||||
</XStack>
|
||||
{isMember && !canManageTasks ? (
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.tasks.toggle.permissionHint', 'You do not have permission to change tasks.')}
|
||||
</Text>
|
||||
) : null}
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
{!loading ? (
|
||||
<TaskSummaryCard summary={summary} />
|
||||
) : null}
|
||||
|
||||
@@ -57,6 +57,10 @@ vi.mock('../../context/EventContext', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../auth/context', () => ({
|
||||
useAuth: () => ({ user: { role: 'tenant_admin' } }),
|
||||
}));
|
||||
|
||||
vi.mock('../../api', () => ({
|
||||
getEvent: vi.fn().mockResolvedValue(fixtures.event),
|
||||
getEvents: vi.fn().mockResolvedValue([fixtures.event]),
|
||||
@@ -65,6 +69,7 @@ vi.mock('../../api', () => ({
|
||||
getTaskCollections: vi.fn().mockResolvedValue({ data: fixtures.collections }),
|
||||
getEmotions: vi.fn().mockResolvedValue(fixtures.emotions),
|
||||
assignTasksToEvent: vi.fn(),
|
||||
updateEvent: vi.fn().mockResolvedValue(fixtures.event),
|
||||
updateTask: vi.fn(),
|
||||
importTaskCollection: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
@@ -110,6 +115,13 @@ vi.mock('@tamagui/text', () => ({
|
||||
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/switch', () => ({
|
||||
Switch: Object.assign(
|
||||
({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
{ Thumb: () => <div /> },
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/list-item', () => ({
|
||||
ListItem: ({ title, subTitle, iconAfter }: { title?: React.ReactNode; subTitle?: React.ReactNode; iconAfter?: React.ReactNode }) => (
|
||||
<div>
|
||||
@@ -162,6 +174,7 @@ vi.mock('../components/MobileShell', () => ({
|
||||
|
||||
vi.mock('../components/Primitives', () => ({
|
||||
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress}>
|
||||
{label}
|
||||
@@ -215,6 +228,7 @@ describe('MobileEventTasksPage', () => {
|
||||
it('renders the task overview summary and quick jump chips', async () => {
|
||||
render(<MobileEventTasksPage />);
|
||||
|
||||
expect(await screen.findByText('1. Activate tasks')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Task overview')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tasks total')).toBeInTheDocument();
|
||||
expect(screen.getByText('Quick jump')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user