import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Alert, KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, StatusBar as RNStatusBar, Text, View } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as ImagePicker from 'expo-image-picker'; import { StatusBar } from 'expo-status-bar'; import BottomTab from './components/BottomTab'; import TripPicker from './components/TripPicker'; import DatePickerModal from './components/DatePickerModal'; import ItemModal from './modals/ItemModal'; import CheckupFlowModal from './modals/CheckupFlowModal'; import TripsTab from './tabs/TripsTab'; import ItemsTab from './tabs/ItemsTab'; import CheckupTab from './tabs/CheckupTab'; import HistoryTab from './tabs/HistoryTab'; import { emptyData, STORAGE_KEY } from './constants'; import { findBestTripId, makeId, parseYMD, todayYMD } from './utils/date'; import { styles } from './styles'; const emptyTripForm = () => ({ name: '', location: '', startDate: todayYMD(), endDate: todayYMD(), imageUri: '', copyDefaultTemplate: true, setAsDefaultTemplate: false, }); const emptyItemForm = () => ({ id: null, name: '', description: '', category: '', status: 'unpacked', placement: 'suitcase', lentTo: '', imageUri: '', }); const emptyCheckupNoForm = () => ({ status: 'unpacked', placement: 'suitcase', lentTo: '', updateMasterList: false, }); function buildCheckupSession(items) { return items.map((item) => ({ itemId: item.id, name: item.name, category: item.category, current: { status: item.status, placement: item.placement, lentTo: item.lentTo || '', }, confirmed: false, result: 'pending', })); } export default function AppRoot() { const scrollRef = useRef(null); const [loaded, setLoaded] = useState(false); const [tab, setTab] = useState('trips'); const [data, setData] = useState(emptyData); const [selectedTripId, setSelectedTripId] = useState(null); const [tripForm, setTripForm] = useState(emptyTripForm()); const [datePicker, setDatePicker] = useState({ visible: false, field: 'startDate' }); const [itemModalVisible, setItemModalVisible] = useState(false); const [itemForm, setItemForm] = useState(emptyItemForm()); const [checkupSession, setCheckupSession] = useState([]); const [checkupFlowVisible, setCheckupFlowVisible] = useState(false); const [checkupFlowIndex, setCheckupFlowIndex] = useState(0); const [checkupFlowMode, setCheckupFlowMode] = useState('question'); const [checkupNoForm, setCheckupNoForm] = useState(emptyCheckupNoForm()); const [selectedCheckupId, setSelectedCheckupId] = useState(null); const topInset = Platform.OS === 'android' ? (RNStatusBar.currentHeight || 0) + 10 : 0; const selectedTrip = useMemo(() => data.trips.find((trip) => trip.id === selectedTripId) || null, [data.trips, selectedTripId]); const selectedTripItems = useMemo(() => { if (!selectedTripId) return []; return data.itemsByTrip[selectedTripId] || []; }, [data.itemsByTrip, selectedTripId]); const selectedTripCheckups = useMemo(() => { if (!selectedTripId) return []; return (data.checkupsByTrip[selectedTripId] || []).slice().sort((a, b) => b.createdAt - a.createdAt); }, [data.checkupsByTrip, selectedTripId]); const templateTrip = useMemo( () => data.trips.find((trip) => trip.id === data.defaultTemplateTripId) || null, [data.trips, data.defaultTemplateTripId] ); const checkupStats = useMemo(() => { const total = checkupSession.length; const correct = checkupSession.filter((entry) => entry.result === 'correct').length; const bad = checkupSession.filter((entry) => entry.result === 'bad').length; const pending = total - correct - bad; return { total, correct, bad, pending }; }, [checkupSession]); const checkupCurrentEntry = useMemo(() => { if (!checkupFlowVisible) return null; if (checkupFlowIndex >= checkupSession.length) return null; return checkupSession[checkupFlowIndex] || null; }, [checkupFlowVisible, checkupFlowIndex, checkupSession]); useEffect(() => { (async () => { try { const raw = await AsyncStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); setData({ ...emptyData, ...parsed }); } } catch { Alert.alert('Error', 'Could not load local data.'); } finally { setLoaded(true); } })(); }, []); useEffect(() => { if (!loaded) return; AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(data)).catch(() => { Alert.alert('Error', 'Could not save local data.'); }); }, [data, loaded]); useEffect(() => { if (!loaded) return; if (!data.trips.length) { setSelectedTripId(null); return; } if (selectedTripId && data.trips.some((trip) => trip.id === selectedTripId)) { return; } const bestTripId = findBestTripId(data.trips); setSelectedTripId(bestTripId || data.trips[0].id); }, [data.trips, selectedTripId, loaded]); useEffect(() => { if (tab !== 'checkup') return; createFreshCheckupSession(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedTripId, selectedTripItems.length]); function updateTripForm(field, value) { setTripForm((prev) => ({ ...prev, [field]: value })); } function updateItemForm(field, value) { setItemForm((prev) => ({ ...prev, [field]: value })); } function openDatePicker(field) { setDatePicker({ visible: true, field }); } function onSelectDate(ymd) { updateTripForm(datePicker.field, ymd); setDatePicker((prev) => ({ ...prev, visible: false })); } async function pickImage(onPicked) { const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!perm.granted) { Alert.alert('Permission needed', 'Allow gallery access to select images.'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: false, quality: 0.85, }); if (!result.canceled && result.assets?.[0]?.uri) { onPicked(result.assets[0].uri); } } async function takeImage(onPicked) { const perm = await ImagePicker.requestCameraPermissionsAsync(); if (!perm.granted) { Alert.alert('Permission needed', 'Allow camera access to take photos.'); return; } const result = await ImagePicker.launchCameraAsync({ allowsEditing: false, quality: 0.85, }); if (!result.canceled && result.assets?.[0]?.uri) { onPicked(result.assets[0].uri); } } function createTrip() { if (!tripForm.name.trim()) { Alert.alert('Missing name', 'Trip name is required.'); return false; } const start = parseYMD(tripForm.startDate); const end = parseYMD(tripForm.endDate); if (!start || !end) { Alert.alert('Invalid dates', 'Please select valid trip dates.'); return false; } if (start > end) { Alert.alert('Invalid dates', 'Start date cannot be after end date.'); return false; } const now = Date.now(); const tripId = makeId('trip'); setData((prev) => { const next = { ...prev, trips: [ ...prev.trips, { id: tripId, name: tripForm.name.trim(), location: tripForm.location.trim(), startDate: tripForm.startDate, endDate: tripForm.endDate, imageUri: tripForm.imageUri, createdAt: now, updatedAt: now, }, ], itemsByTrip: { ...prev.itemsByTrip, [tripId]: [] }, checkupsByTrip: { ...prev.checkupsByTrip, [tripId]: [] }, }; if (tripForm.copyDefaultTemplate && prev.defaultTemplateTripId) { const templateItems = prev.itemsByTrip[prev.defaultTemplateTripId] || []; next.itemsByTrip[tripId] = templateItems.map((item) => ({ ...item, id: makeId('item'), createdAt: now, updatedAt: now, })); } if (tripForm.setAsDefaultTemplate) { next.defaultTemplateTripId = tripId; } return next; }); setSelectedTripId(tripId); setTripForm(emptyTripForm()); return true; } function setTripAsTemplate(tripId) { setData((prev) => ({ ...prev, defaultTemplateTripId: tripId })); } function deleteTrip(tripId) { Alert.alert('Delete trip?', 'Trip items and check-up history will also be deleted.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: () => { setData((prev) => { const trips = prev.trips.filter((trip) => trip.id !== tripId); const itemsByTrip = { ...prev.itemsByTrip }; const checkupsByTrip = { ...prev.checkupsByTrip }; delete itemsByTrip[tripId]; delete checkupsByTrip[tripId]; return { ...prev, trips, itemsByTrip, checkupsByTrip, defaultTemplateTripId: prev.defaultTemplateTripId === tripId ? null : prev.defaultTemplateTripId, }; }); }, }, ]); } function openAddItemModal() { setItemForm(emptyItemForm()); setItemModalVisible(true); } function openEditItemModal(item) { setItemForm({ id: item.id, name: item.name || '', description: item.description || '', category: item.category || '', status: item.status || 'unpacked', placement: item.placement || 'suitcase', lentTo: item.lentTo || '', imageUri: item.imageUri || '', }); setItemModalVisible(true); } function saveItemFromModal() { if (!selectedTripId) { Alert.alert('No trip selected', 'Please select or create a trip first.'); return; } if (!itemForm.name.trim()) { Alert.alert('Missing name', 'Item name is required.'); return; } const now = Date.now(); setData((prev) => { const items = prev.itemsByTrip[selectedTripId] || []; const existingCreatedAt = items.find((item) => item.id === itemForm.id)?.createdAt || now; const nextItem = { id: itemForm.id || makeId('item'), name: itemForm.name.trim(), description: itemForm.description.trim(), category: itemForm.category.trim(), status: itemForm.status, placement: itemForm.placement, lentTo: itemForm.status === 'lent-to' ? itemForm.lentTo.trim() : '', imageUri: itemForm.imageUri, createdAt: existingCreatedAt, updatedAt: now, }; const nextItems = itemForm.id ? items.map((item) => (item.id === itemForm.id ? nextItem : item)) : [...items, nextItem]; return { ...prev, itemsByTrip: { ...prev.itemsByTrip, [selectedTripId]: nextItems, }, }; }); setItemModalVisible(false); setItemForm(emptyItemForm()); } function deleteItem(itemId) { setData((prev) => { const items = prev.itemsByTrip[selectedTripId] || []; return { ...prev, itemsByTrip: { ...prev.itemsByTrip, [selectedTripId]: items.filter((item) => item.id !== itemId), }, }; }); } function quickSetItemStatus(itemId, status) { if (!selectedTripId) return; setData((prev) => { const items = prev.itemsByTrip[selectedTripId] || []; return { ...prev, itemsByTrip: { ...prev.itemsByTrip, [selectedTripId]: items.map((item) => item.id === itemId ? { ...item, status, lentTo: status === 'lent-to' ? item.lentTo : '', updatedAt: Date.now(), } : item ), }, }; }); } function deleteCheckup(checkupId) { if (!selectedTripId) return; setData((prev) => { const existing = prev.checkupsByTrip[selectedTripId] || []; return { ...prev, checkupsByTrip: { ...prev.checkupsByTrip, [selectedTripId]: existing.filter((checkup) => checkup.id !== checkupId), }, }; }); setSelectedCheckupId((prev) => (prev === checkupId ? null : prev)); } function createFreshCheckupSession() { if (!selectedTripItems.length) { setCheckupSession([]); return; } setCheckupSession(buildCheckupSession(selectedTripItems)); } function startCheckupFlow() { if (!selectedTripId) { Alert.alert('No trip selected', 'Please select a trip first.'); return; } if (!selectedTripItems.length) { Alert.alert('No items', 'Add items before starting a check-up.'); return; } const fresh = buildCheckupSession(selectedTripItems); setCheckupSession(fresh); setCheckupFlowIndex(0); setCheckupFlowMode('question'); setCheckupNoForm(emptyCheckupNoForm()); setCheckupFlowVisible(true); } function closeCheckupFlow() { setCheckupFlowVisible(false); setCheckupFlowMode('question'); setCheckupNoForm(emptyCheckupNoForm()); } function goNextInCheckup() { setCheckupFlowIndex((prev) => prev + 1); setCheckupFlowMode('question'); setCheckupNoForm(emptyCheckupNoForm()); } function answerCurrentCheckupYes() { const entry = checkupCurrentEntry; if (!entry) return; setCheckupSession((prev) => prev.map((x) => (x.itemId === entry.itemId ? { ...x, confirmed: true, result: 'correct' } : x)) ); goNextInCheckup(); } function openCurrentCheckupNo() { const entry = checkupCurrentEntry; if (!entry) return; setCheckupNoForm({ status: entry.current.status || 'unpacked', placement: entry.current.placement || 'suitcase', lentTo: entry.current.lentTo || '', updateMasterList: false, }); setCheckupFlowMode('edit'); } function saveCurrentCheckupNo() { const entry = checkupCurrentEntry; if (!entry) return; const patch = { status: checkupNoForm.status, placement: checkupNoForm.placement, lentTo: checkupNoForm.status === 'lent-to' ? checkupNoForm.lentTo.trim() : '', }; setCheckupSession((prev) => prev.map((x) => x.itemId === entry.itemId ? { ...x, current: patch, confirmed: true, result: 'bad', } : x ) ); if (checkupNoForm.updateMasterList && selectedTripId) { setData((prev) => { const items = prev.itemsByTrip[selectedTripId] || []; return { ...prev, itemsByTrip: { ...prev.itemsByTrip, [selectedTripId]: items.map((item) => item.id === entry.itemId ? { ...item, status: patch.status, placement: patch.placement, lentTo: patch.lentTo, updatedAt: Date.now(), } : item ), }, }; }); } goNextInCheckup(); } function saveCheckupSnapshot(sessionToSave) { if (!selectedTripId) { Alert.alert('No trip selected', 'Please select a trip first.'); return false; } if (!sessionToSave.length) { Alert.alert('No items', 'Add items before creating a check-up.'); return false; } const pending = sessionToSave.filter((entry) => !entry.confirmed).length; if (pending > 0) { Alert.alert('Incomplete', `Please confirm all items first (${pending} remaining).`); return false; } const snapshot = sessionToSave.map((entry) => ({ itemId: entry.itemId, name: entry.name, category: entry.category, status: entry.current.status, placement: entry.current.placement, lentTo: entry.current.status === 'lent-to' ? entry.current.lentTo : '', result: entry.result || 'pending', })); setData((prev) => { const existing = prev.checkupsByTrip[selectedTripId] || []; return { ...prev, checkupsByTrip: { ...prev.checkupsByTrip, [selectedTripId]: [ ...existing, { id: makeId('checkup'), createdAt: Date.now(), snapshot, stats: { correct: snapshot.filter((entry) => entry.result === 'correct').length, bad: snapshot.filter((entry) => entry.result === 'bad').length, }, }, ], }, }; }); return true; } function finishCheckupFlow() { const ok = saveCheckupSnapshot(checkupSession); if (!ok) return; Alert.alert('Saved', 'Check-up snapshot saved.'); closeCheckupFlow(); createFreshCheckupSession(); } function onInputFocus(event) { const target = event?.target; if (!target) return; setTimeout(() => { const scrollFn = scrollRef.current?.scrollResponderScrollNativeHandleToKeyboard; if (typeof scrollFn === 'function') { scrollFn(target, 90, true); } }, 80); } if (!loaded) { return ( Loading local data... ); } return ( {tab === 'trips' && ( pickImage((uri) => updateTripForm('imageUri', uri))} takeTripImage={() => takeImage((uri) => updateTripForm('imageUri', uri))} templateTrip={templateTrip} createTrip={createTrip} trips={data.trips} selectedTripId={selectedTripId} chooseTrip={setSelectedTripId} setTripAsTemplate={setTripAsTemplate} deleteTrip={deleteTrip} onInputFocus={onInputFocus} defaultTemplateTripId={data.defaultTemplateTripId} openDatePicker={openDatePicker} activeTripItemCount={selectedTripItems.length} activeTripCheckupCount={selectedTripCheckups.length} /> )} {tab === 'items' && ( )} {tab === 'checkup' && ( )} {tab === 'history' && ( )} setDatePicker((prev) => ({ ...prev, visible: false }))} onSelect={onSelectDate} /> pickImage((uri) => updateItemForm('imageUri', uri))} takeItemImage={() => takeImage((uri) => updateItemForm('imageUri', uri))} saveItemFromModal={saveItemFromModal} /> ); }