diff --git a/src/AppRoot.js b/src/AppRoot.js index e32a209..6097049 100644 --- a/src/AppRoot.js +++ b/src/AppRoot.js @@ -7,7 +7,7 @@ import BottomTab from './components/BottomTab'; import TripPicker from './components/TripPicker'; import DatePickerModal from './components/DatePickerModal'; import ItemModal from './modals/ItemModal'; -import CheckupFixModal from './modals/CheckupFixModal'; +import CheckupFlowModal from './modals/CheckupFlowModal'; import TripsTab from './tabs/TripsTab'; import ItemsTab from './tabs/ItemsTab'; import CheckupTab from './tabs/CheckupTab'; @@ -37,6 +37,28 @@ const emptyItemForm = () => ({ 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); @@ -52,14 +74,10 @@ export default function AppRoot() { const [itemForm, setItemForm] = useState(emptyItemForm()); const [checkupSession, setCheckupSession] = useState([]); - const [checkupFixModalVisible, setCheckupFixModalVisible] = useState(false); - const [checkupFixTargetId, setCheckupFixTargetId] = useState(null); - const [checkupFixForm, setCheckupFixForm] = useState({ - status: 'unpacked', - placement: 'suitcase', - lentTo: '', - updateMasterList: false, - }); + 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); @@ -90,6 +108,12 @@ export default function AppRoot() { 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 { @@ -189,7 +213,7 @@ export default function AppRoot() { function createTrip() { if (!tripForm.name.trim()) { Alert.alert('Missing name', 'Trip name is required.'); - return; + return false; } const start = parseYMD(tripForm.startDate); @@ -197,12 +221,12 @@ export default function AppRoot() { if (!start || !end) { Alert.alert('Invalid dates', 'Please select valid trip dates.'); - return; + return false; } if (start > end) { Alert.alert('Invalid dates', 'Start date cannot be after end date.'); - return; + return false; } const now = Date.now(); @@ -247,6 +271,7 @@ export default function AppRoot() { setSelectedTripId(tripId); setTripForm(emptyTripForm()); + return true; } function setTripAsTemplate(tripId) { @@ -401,67 +426,85 @@ export default function AppRoot() { setCheckupSession([]); return; } + setCheckupSession(buildCheckupSession(selectedTripItems)); + } - const fresh = selectedTripItems.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', - })); + 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 answerCheckupYes(itemId) { - setCheckupSession((prev) => - prev.map((entry) => (entry.itemId === itemId ? { ...entry, confirmed: true, result: 'correct' } : entry)) - ); + function closeCheckupFlow() { + setCheckupFlowVisible(false); + setCheckupFlowMode('question'); + setCheckupNoForm(emptyCheckupNoForm()); } - function openFixModal(itemId) { - const entry = checkupSession.find((x) => x.itemId === itemId); + function goNextInCheckup() { + setCheckupFlowIndex((prev) => prev + 1); + setCheckupFlowMode('question'); + setCheckupNoForm(emptyCheckupNoForm()); + } + + function answerCurrentCheckupYes() { + const entry = checkupCurrentEntry; if (!entry) return; - setCheckupFixTargetId(itemId); - setCheckupFixForm({ + 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, }); - setCheckupFixModalVisible(true); + setCheckupFlowMode('edit'); } - function saveFixModal() { - if (!checkupFixTargetId) return; + function saveCurrentCheckupNo() { + const entry = checkupCurrentEntry; + if (!entry) return; - const targetId = checkupFixTargetId; const patch = { - status: checkupFixForm.status, - placement: checkupFixForm.placement, - lentTo: checkupFixForm.status === 'lent-to' ? checkupFixForm.lentTo.trim() : '', + status: checkupNoForm.status, + placement: checkupNoForm.placement, + lentTo: checkupNoForm.status === 'lent-to' ? checkupNoForm.lentTo.trim() : '', }; setCheckupSession((prev) => - prev.map((entry) => - entry.itemId === targetId + prev.map((x) => + x.itemId === entry.itemId ? { - ...entry, + ...x, current: patch, confirmed: true, result: 'bad', } - : entry + : x ) ); - if (checkupFixForm.updateMasterList && selectedTripId) { + if (checkupNoForm.updateMasterList && selectedTripId) { setData((prev) => { const items = prev.itemsByTrip[selectedTripId] || []; return { @@ -469,7 +512,7 @@ export default function AppRoot() { itemsByTrip: { ...prev.itemsByTrip, [selectedTripId]: items.map((item) => - item.id === targetId + item.id === entry.itemId ? { ...item, status: patch.status, @@ -484,28 +527,27 @@ export default function AppRoot() { }); } - setCheckupFixModalVisible(false); - setCheckupFixTargetId(null); + goNextInCheckup(); } - function saveCheckup() { + function saveCheckupSnapshot(sessionToSave) { if (!selectedTripId) { Alert.alert('No trip selected', 'Please select a trip first.'); - return; + return false; } - if (!checkupSession.length) { + if (!sessionToSave.length) { Alert.alert('No items', 'Add items before creating a check-up.'); - return; + return false; } - const pending = checkupSession.filter((entry) => !entry.confirmed).length; + const pending = sessionToSave.filter((entry) => !entry.confirmed).length; if (pending > 0) { Alert.alert('Incomplete', `Please confirm all items first (${pending} remaining).`); - return; + return false; } - const snapshot = checkupSession.map((entry) => ({ + const snapshot = sessionToSave.map((entry) => ({ itemId: entry.itemId, name: entry.name, category: entry.category, @@ -537,7 +579,15 @@ export default function AppRoot() { }; }); + return true; + } + + function finishCheckupFlow() { + const ok = saveCheckupSnapshot(checkupSession); + if (!ok) return; + Alert.alert('Saved', 'Check-up snapshot saved.'); + closeCheckupFlow(); createFreshCheckupSession(); } @@ -592,6 +642,8 @@ export default function AppRoot() { onInputFocus={onInputFocus} defaultTemplateTripId={data.defaultTemplateTripId} openDatePicker={openDatePicker} + activeTripItemCount={selectedTripItems.length} + activeTripCheckupCount={selectedTripCheckups.length} /> )} @@ -608,12 +660,10 @@ export default function AppRoot() { {tab === 'checkup' && ( )} @@ -649,12 +699,19 @@ export default function AppRoot() { saveItemFromModal={saveItemFromModal} /> - ); diff --git a/src/modals/CheckupFlowModal.js b/src/modals/CheckupFlowModal.js new file mode 100644 index 0000000..7254551 --- /dev/null +++ b/src/modals/CheckupFlowModal.js @@ -0,0 +1,121 @@ +import React from 'react'; +import { KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native'; +import { ITEM_PLACEMENTS, ITEM_STATUSES } from '../constants'; +import ChipGroup from '../components/ChipGroup'; +import Field from '../components/Field'; +import { styles } from '../styles'; + +export default function CheckupFlowModal({ + visible, + entry, + stepIndex, + total, + mode, + noForm, + setNoForm, + onClose, + onYes, + onNo, + onSaveNo, + onFinish, +}) { + const finished = !entry; + + return ( + + + + + + Check-Up + + Close + + + + {finished ? ( + + Done. Save this snapshot? + All {total} items were checked. + + Save Check-Up Snapshot + + + ) : ( + + + Item {stepIndex + 1} / {total} + + {entry.name} + {entry.category || 'uncategorized'} + + Current: {entry.current.status} · {entry.current.placement} + {entry.current.status === 'lent-to' && entry.current.lentTo ? ` · ${entry.current.lentTo}` : ''} + + + {mode === 'question' ? ( + + + Yes, correct + + + No, update + + + ) : ( + + + setNoForm((prev) => ({ ...prev, status: v }))} + /> + + + + setNoForm((prev) => ({ ...prev, placement: v }))} + /> + + + {noForm.status === 'lent-to' ? ( + + setNoForm((prev) => ({ ...prev, lentTo: v }))} + placeholder="Person name" + placeholderTextColor="#6b7280" + /> + + ) : null} + + setNoForm((prev) => ({ ...prev, updateMasterList: !prev.updateMasterList }))} + > + + {noForm.updateMasterList ? '☑' : '☐'} Also update item in trip list + + + + + Save update + next + + + )} + + )} + + + + + ); +} diff --git a/src/styles.js b/src/styles.js index cb100b6..32ccb9d 100644 --- a/src/styles.js +++ b/src/styles.js @@ -117,6 +117,32 @@ export const styles = StyleSheet.create({ marginTop: -2, marginBottom: 2, }, + tripHeroCard: { + backgroundColor: '#0f172a', + borderRadius: 18, + borderWidth: 1, + borderColor: '#334155', + padding: 12, + gap: 8, + }, + tripHeroImage: { + width: '100%', + height: 180, + borderRadius: 12, + backgroundColor: '#111827', + }, + tripHeroTitle: { + color: '#f8fafc', + fontWeight: '800', + fontSize: 22, + }, + tripListTitle: { + color: '#cbd5e1', + fontWeight: '700', + fontSize: 13, + letterSpacing: 0.4, + textTransform: 'uppercase', + }, fieldWrap: { gap: 6, @@ -323,6 +349,28 @@ export const styles = StyleSheet.create({ color: '#f8fafc', fontWeight: '700', }, + answerRowWide: { + marginTop: 14, + gap: 10, + }, + answerYesWide: { + backgroundColor: '#163223', + borderWidth: 1, + borderColor: '#1f7a4e', + borderRadius: 10, + paddingHorizontal: 16, + paddingVertical: 11, + alignItems: 'center', + }, + answerNoWide: { + backgroundColor: '#3b1d22', + borderWidth: 1, + borderColor: '#7f1d1d', + borderRadius: 10, + paddingHorizontal: 16, + paddingVertical: 11, + alignItems: 'center', + }, answerStateDot: { width: 10, height: 10, diff --git a/src/tabs/CheckupTab.js b/src/tabs/CheckupTab.js index af6e2c6..0d3cd7e 100644 --- a/src/tabs/CheckupTab.js +++ b/src/tabs/CheckupTab.js @@ -2,71 +2,36 @@ import React from 'react'; import { Pressable, Text, View } from 'react-native'; import { styles } from '../styles'; -export default function CheckupTab({ - checkupSession, - checkupStats, - answerCheckupYes, - openFixModal, - createFreshCheckupSession, - saveCheckup, -}) { +export default function CheckupTab({ selectedTrip, selectedTripItems, checkupStats, startCheckupFlow }) { return ( - - Check-Up - - Check-up - - + Check-Up - {!!checkupSession.length && ( - - - Correct: {checkupStats.correct} - - - Bad: {checkupStats.bad} - - - Pending: {checkupStats.pending} + {!selectedTrip ? Select a trip first. : null} + {selectedTrip && selectedTripItems.length === 0 ? No items for this trip yet. : null} + + {selectedTrip && selectedTripItems.length > 0 ? ( + + Run a check-up for {selectedTrip.name} + {selectedTripItems.length} items will be checked one by one. + + + + Correct: {checkupStats.correct} + + + Bad: {checkupStats.bad} + + + Pending: {checkupStats.pending} + + + + Start Check-Up + - )} - - {checkupSession.length === 0 ? No items for this trip yet. : null} - - {checkupSession.map((entry) => ( - - {entry.name} - {entry.category || 'uncategorized'} - - {entry.current.status} · {entry.current.placement} - {entry.current.status === 'lent-to' && entry.current.lentTo ? ` · ${entry.current.lentTo}` : ''} - - - - answerCheckupYes(entry.itemId)}> - Yes - - openFixModal(entry.itemId)}> - No - - - - - ))} - - {!!checkupSession.length && ( - - Save Check-Up Snapshot - - )} + ) : null} ); } diff --git a/src/tabs/TripsTab.js b/src/tabs/TripsTab.js index 542f6d1..53a9eee 100644 --- a/src/tabs/TripsTab.js +++ b/src/tabs/TripsTab.js @@ -1,5 +1,5 @@ -import React from 'react'; -import { Image, Pressable, Text, TextInput, View } from 'react-native'; +import React, { useMemo, useState } from 'react'; +import { Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native'; import Field from '../components/Field'; import { styles } from '../styles'; @@ -28,65 +28,40 @@ export default function TripsTab({ onInputFocus, defaultTemplateTripId, openDatePicker, + activeTripItemCount, + activeTripCheckupCount, }) { + const [createModalVisible, setCreateModalVisible] = useState(false); + + const activeTrip = useMemo(() => trips.find((trip) => trip.id === selectedTripId) || null, [trips, selectedTripId]); + + function submitCreateTrip() { + const ok = createTrip(); + if (ok) setCreateModalVisible(false); + } + return ( - Trips - - - - updateTripForm('name', v)} - placeholder="Summer Weekend" - placeholderTextColor="#6b7280" - onFocus={onInputFocus} - /> - - - - updateTripForm('location', v)} - placeholder="Berlin" - placeholderTextColor="#6b7280" - onFocus={onInputFocus} - /> - - - openDatePicker('startDate')} /> - openDatePicker('endDate')} /> - - - - Take photo - - - {tripForm.imageUri ? 'From gallery (change)' : 'From gallery'} - - - - {tripForm.imageUri ? : null} - - {templateTrip ? ( - updateTripForm('copyDefaultTemplate', !tripForm.copyDefaultTemplate)}> - - {tripForm.copyDefaultTemplate ? '☑' : '☐'} Copy items from template ({templateTrip.name}) - - - ) : null} - - updateTripForm('setAsDefaultTemplate', !tripForm.setAsDefaultTemplate)}> - {tripForm.setAsDefaultTemplate ? '☑' : '☐'} Set as default template - - - - Create Trip + + Trips + setCreateModalVisible(true)}> + + New Trip + {activeTrip ? ( + + {activeTrip.imageUri ? : null} + {activeTrip.name} + {activeTrip.location || 'No location'} · {activeTrip.startDate} → {activeTrip.endDate} + {activeTripItemCount} items · {activeTripCheckupCount} check-ups + + ) : ( + Create your first trip to get started. + )} + + All Trips + {trips .slice() .sort((a, b) => b.startDate.localeCompare(a.startDate)) @@ -113,6 +88,80 @@ export default function TripsTab({ {trip.imageUri ? : null} ))} + + + + + + + Create Trip + setCreateModalVisible(false)}> + Close + + + + + + updateTripForm('name', v)} + placeholder="Summer Weekend" + placeholderTextColor="#6b7280" + onFocus={onInputFocus} + /> + + + + updateTripForm('location', v)} + placeholder="Berlin" + placeholderTextColor="#6b7280" + onFocus={onInputFocus} + /> + + + openDatePicker('startDate')} /> + openDatePicker('endDate')} /> + + + + Take photo + + + {tripForm.imageUri ? 'From gallery (change)' : 'From gallery'} + + + + {tripForm.imageUri ? : null} + + {templateTrip ? ( + updateTripForm('copyDefaultTemplate', !tripForm.copyDefaultTemplate)}> + + {tripForm.copyDefaultTemplate ? '☑' : '☐'} Copy items from template ({templateTrip.name}) + + + ) : null} + + updateTripForm('setAsDefaultTemplate', !tripForm.setAsDefaultTemplate)}> + {tripForm.setAsDefaultTemplate ? '☑' : '☐'} Set as default template + + + + Create Trip + + + + + + ); }