EventBuzz_Checkin_App/src/screens/AssignBlankScreen.tsx

243 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { AttendanceCheckInResult } from '../api/types';
import { CheckInResultModal } from '../components/CheckInResultModal';
import type { RootStackParamList } from '../navigation/types';
import { performAppAssignBlank, performAppUndoCheckIn } from '../offline/checkInService';
import { useOffline } from '../offline/OfflineContext';
import { colors, spacing, typography } from '../theme';
import { AppIcon, PrimaryButton, TextField } from '../components/ui';
type Props = NativeStackScreenProps<RootStackParamList, 'AssignBlank'>;
export function AssignBlankScreen({ navigation, route }: Props) {
const {
eventId,
eventName,
attendanceId,
attendanceName,
participantId,
firstName: initialFirst,
lastName: initialLast,
email: initialEmail,
phone: initialPhone,
} = route.params;
const insets = useSafeAreaInsets();
const { refreshStatuses } = useOffline();
const [firstName, setFirstName] = useState(initialFirst || '');
const [lastName, setLastName] = useState(initialLast || '');
const [email, setEmail] = useState(initialEmail || '');
const [phone, setPhone] = useState(initialPhone || '');
const [saving, setSaving] = useState(false);
const [result, setResult] = useState<AttendanceCheckInResult | null>(null);
const [resultError, setResultError] = useState<string | null>(null);
const [pendingSync, setPendingSync] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [undoLoading, setUndoLoading] = useState(false);
const save = async () => {
if (!firstName.trim()) {
setResult(null);
setResultError('First name is required');
setPendingSync(false);
setModalVisible(true);
return;
}
setSaving(true);
const outcome = await performAppAssignBlank({
eventId,
attendanceId,
attendanceName,
participantId,
profile: { firstName, lastName, email, phone },
});
setSaving(false);
if (!outcome.ok) {
setResult(null);
setResultError(outcome.error);
setPendingSync(false);
} else {
setResult(outcome.result);
setResultError(null);
setPendingSync(outcome.pendingSync);
refreshStatuses().catch(() => undefined);
}
setModalVisible(true);
};
const closeModal = () => {
if (undoLoading) return;
setModalVisible(false);
if (result && !resultError) {
navigation.goBack();
return;
}
setResult(null);
setResultError(null);
setPendingSync(false);
};
const handleUndo = async () => {
if (!result) return;
setUndoLoading(true);
const outcome = await performAppUndoCheckIn({
eventId,
attendanceId,
attendanceName,
participantId: result.participantId,
});
setUndoLoading(false);
if (!outcome.ok) {
Alert.alert('Undo failed', outcome.error);
return;
}
setModalVisible(false);
setResult(null);
setResultError(null);
setPendingSync(false);
refreshStatuses().catch(() => undefined);
navigation.goBack();
};
return (
<View style={styles.wrap}>
<View style={[styles.header, { paddingTop: insets.top + spacing.md }]}>
<View style={styles.headerBg} />
<Pressable onPress={() => navigation.goBack()} hitSlop={10} style={styles.backBtn}>
<AppIcon name="arrow-back" size={18} color="rgba(255,255,255,0.85)" />
<Text style={styles.backLabel} numberOfLines={1}>
{eventName}
</Text>
</Pressable>
<Text style={styles.headerTitle}>Assign a Blank</Text>
<Text style={styles.headerSubtitle}>{attendanceName}</Text>
</View>
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={[
styles.form,
{ paddingBottom: Math.max(insets.bottom, 16) + 24 },
]}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.lead}>
Update this spare badge’s details, then save to check the person in.
</Text>
<TextField
label="First name"
value={firstName}
onChangeText={setFirstName}
autoCapitalize="words"
autoCorrect={false}
/>
<TextField
label="Last name"
value={lastName}
onChangeText={setLastName}
autoCapitalize="words"
autoCorrect={false}
/>
<TextField
label="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextField
label="Phone"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
autoCorrect={false}
/>
<PrimaryButton
title="Save and check in"
loading={saving}
onPress={() => {
void save();
}}
/>
</ScrollView>
</KeyboardAvoidingView>
<CheckInResultModal
visible={modalVisible}
result={result}
error={resultError}
pendingSync={pendingSync}
onClose={closeModal}
onUndo={result ? handleUndo : undefined}
undoLoading={undoLoading}
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: { flex: 1, backgroundColor: colors.background },
flex: { flex: 1 },
header: {
paddingHorizontal: spacing.xl,
paddingBottom: spacing.lg,
overflow: 'hidden',
},
headerBg: {
...StyleSheet.absoluteFill,
backgroundColor: colors.primary,
zIndex: 0,
},
backBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: spacing.md,
alignSelf: 'flex-start',
zIndex: 1,
},
backLabel: {
color: 'rgba(255,255,255,0.85)',
fontSize: 14,
fontWeight: '500',
},
headerTitle: {
color: '#fff',
fontSize: 20,
fontWeight: '700',
marginBottom: 4,
zIndex: 1,
},
headerSubtitle: {
color: 'rgba(255,255,255,0.90)',
fontSize: 14,
zIndex: 1,
},
form: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
gap: 16,
},
lead: {
...typography.small,
color: colors.muted,
marginBottom: 4,
},
});