import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Trash2, Copy, RefreshCcw } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { MobileField, MobileInput, MobileSelect } from './components/FormControls'; import { EventMember, getEventMembers, inviteEventMember, removeEventMember, trackOnboarding } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import toast from 'react-hot-toast'; import { MobileSheet } from './components/Sheet'; import { adminPath } from '../constants'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; export default function MobileEventMembersPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const slug = slugParam ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); const { textStrong, text, muted, border, primary, danger } = useAdminTheme(); const [members, setMembers] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [invite, setInvite] = React.useState({ name: '', email: '', role: 'member' as EventMember['role'] }); const [saving, setSaving] = React.useState(false); const emailInputRef = React.useRef(null); const [search, setSearch] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState<'all' | 'pending' | 'active' | 'invited'>('all'); const [roleFilter, setRoleFilter] = React.useState<'all' | 'member' | 'tenant_admin'>('all'); const [confirmRemove, setConfirmRemove] = React.useState(null); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); const load = React.useCallback(async () => { if (!slug) return; setLoading(true); setError(null); try { const result = await getEventMembers(slug, 1); setMembers(result.data); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Mitglieder konnten nicht geladen werden.'))); } } finally { setLoading(false); } }, [slug, t]); React.useEffect(() => { void load(); }, [load]); const normalizedEmail = invite.email.trim(); const isEmailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail); const canInvite = Boolean(normalizedEmail) && isEmailValid && !saving; const filteredMembers = members.filter((member) => { if (statusFilter !== 'all') { const currentStatus = member.status ?? 'pending'; if (currentStatus !== statusFilter) return false; } if (roleFilter !== 'all' && member.role !== roleFilter) return false; if (!search.trim()) return true; const hay = `${member.name ?? ''} ${member.email ?? ''}`.toLowerCase(); return hay.includes(search.toLowerCase()); }); const statusOptions = [ { key: 'all', label: t('events.members.filters.statusAll', 'All statuses') }, { key: 'pending', label: t('events.members.filters.statusPending', 'Pending') }, { key: 'active', label: t('events.members.filters.statusActive', 'Active') }, { key: 'invited', label: t('events.members.filters.statusInvited', 'Invited') }, ] as const; const roleOptions = [ { key: 'all', label: t('events.members.filters.roleAll', 'All roles') }, { key: 'tenant_admin', label: t('events.members.filters.roleAdmin', 'Admins') }, { key: 'member', label: t('events.members.filters.roleMember', 'Members') }, ] as const; const resolveStatus = (status?: string) => { switch (status ?? 'pending') { case 'active': return { label: t('events.members.statuses.active', 'Active'), tone: 'success' as const }; case 'invited': return { label: t('events.members.statuses.invited', 'Invited'), tone: 'warning' as const }; case 'pending': return { label: t('events.members.statuses.pending', 'Pending'), tone: 'warning' as const }; default: return { label: t('events.members.statuses.unknown', 'Unknown'), tone: 'muted' as const }; } }; async function handleInvite() { if (!slug || !normalizedEmail) return; if (!isEmailValid) { toast.error(t('events.members.emailInvalid', 'Please enter a valid email address.')); return; } setSaving(true); setError(null); try { const member = await inviteEventMember(slug, { email: normalizedEmail, name: invite.name.trim() || undefined, role: invite.role, }); setMembers((prev) => [member, ...prev]); setInvite({ name: '', email: '', role: 'member' }); void trackOnboarding('invite_created', { event_slug: slug }); toast.success(t('events.members.inviteSuccess', 'Einladung gesendet')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Einladung fehlgeschlagen.'))); toast.error(t('events.members.inviteFailed', 'Einladung fehlgeschlagen.')); } } finally { setSaving(false); } } async function handleRemove(member: EventMember) { if (!slug) return; try { await removeEventMember(slug, member.id); setMembers((prev) => prev.filter((m) => m.id !== member.id)); toast.success(t('events.members.removeSuccess', 'Mitglied entfernt')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Mitglied konnte nicht entfernt werden.'))); toast.error(t('events.members.removeFailed', 'Mitglied konnte nicht entfernt werden.')); } } } return ( load()} ariaLabel={t('common.refresh', 'Refresh')}> } > {error ? ( {error} ) : null} {t('events.members.inviteTitle', 'Invite Member')} setInvite((prev) => ({ ...prev, name: e.target.value }))} placeholder={t('events.members.namePlaceholder', 'Alex Example')} /> setInvite((prev) => ({ ...prev, email: e.target.value }))} placeholder={t('events.members.emailPlaceholder', 'alex@example.com')} ref={emailInputRef} /> setInvite((prev) => ({ ...prev, role: e.target.value as EventMember['role'] }))} > handleInvite()} disabled={!canInvite} /> {saving ? ( {t('common.processing', 'Processing...')} ) : null} {!isEmailValid && invite.email.trim().length > 0 ? ( {t('events.members.emailInvalid', 'Please enter a valid email address.')} ) : null} {members.length > 0 || search.trim().length > 0 ? ( setSearch(e.target.value)} placeholder={t('events.members.search', 'Search members')} compact /> ) : null} {members.length > 0 ? ( {t('events.members.filters.statusLabel', 'Status')} {statusOptions.map((option) => { const isActive = statusFilter === option.key; return ( setStatusFilter(option.key)}> {option.label} ); })} {t('events.members.filters.roleLabel', 'Role')} {roleOptions.map((option) => { const isActive = roleFilter === option.key; return ( setRoleFilter(option.key)}> {option.label} ); })} ) : null} {t('events.members.listTitle', 'Team & Guests')} {loading ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : members.length === 0 ? ( {t('events.members.emptyTitle', 'Invite your team')} {t('events.members.emptyBody', 'Send the first invite so helpers can access the event.')} ) : ( {filteredMembers.length === 0 ? ( {t('events.members.emptyFilteredTitle', 'No matching members')} {t('events.members.emptyFilteredBody', 'Adjust your search or filters to see members.')} { setSearch(''); setStatusFilter('all'); setRoleFilter('all'); }} > {t('events.members.clearFilters', 'Clear filters')} ) : ( filteredMembers.map((member) => { const statusInfo = resolveStatus(member.status); return ( {member.name || member.email || t('events.members.fallbackName', 'Guest')} {member.email ?? ''} {statusInfo.label} {member.role === 'tenant_admin' ? t('events.members.admin', 'Admin') : t('events.members.member', 'Member')} { if (!member.email) { toast.error(t('events.members.copyEmailFailed', 'Kopieren nicht möglich')); return; } try { await navigator.clipboard.writeText(member.email); toast.success(t('events.members.copyEmail', 'E-Mail kopiert')); } catch { toast.error(t('events.members.copyEmailFailed', 'Kopieren nicht möglich')); } }} > setConfirmRemove(member)}> ); }) )} )} setConfirmRemove(null)} title={t('events.members.confirmRemove', 'Mitglied entfernen?')} footer={ { if (confirmRemove) { void handleRemove(confirmRemove); } setConfirmRemove(null); }} /> } bottomOffsetPx={120} > {t('events.members.removeHint', 'Dieses Mitglied verliert den Zugang zum Event.')} {confirmRemove?.name || confirmRemove?.email} ); }