Add tasks toggle card

This commit is contained in:
Codex Agent
2026-01-16 14:58:24 +01:00
parent 1517eb8631
commit 9e4ea3dafb
4 changed files with 137 additions and 1 deletions

View File

@@ -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}