feat: add trip edit/archive flow and item bulk filters
All checks were successful
Luggage List Build / build-web (push) Successful in 28s
Luggage List Build / build-android (push) Successful in 6m32s
Luggage List Build / release (push) Successful in 12s

This commit is contained in:
2026-04-18 14:53:02 +02:00
parent 3080c3affd
commit 61b0a3d1fa
5 changed files with 361 additions and 30 deletions

View File

@@ -83,7 +83,9 @@ export default function AppRoot() {
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 visibleTrips = useMemo(() => data.trips.filter((trip) => !trip.archived), [data.trips]);
const selectedTrip = useMemo(() => visibleTrips.find((trip) => trip.id === selectedTripId) || null, [visibleTrips, selectedTripId]);
const selectedTripItems = useMemo(() => {
if (!selectedTripId) return [];
@@ -139,18 +141,18 @@ export default function AppRoot() {
useEffect(() => {
if (!loaded) return;
if (!data.trips.length) {
if (!visibleTrips.length) {
setSelectedTripId(null);
return;
}
if (selectedTripId && data.trips.some((trip) => trip.id === selectedTripId)) {
if (selectedTripId && visibleTrips.some((trip) => trip.id === selectedTripId)) {
return;
}
const bestTripId = findBestTripId(data.trips);
setSelectedTripId(bestTripId || data.trips[0].id);
}, [data.trips, selectedTripId, loaded]);
const bestTripId = findBestTripId(visibleTrips);
setSelectedTripId(bestTripId || visibleTrips[0].id);
}, [visibleTrips, selectedTripId, loaded]);
useEffect(() => {
if (tab !== 'checkup') return;
@@ -244,6 +246,7 @@ export default function AppRoot() {
startDate: tripForm.startDate,
endDate: tripForm.endDate,
imageUri: tripForm.imageUri,
archived: false,
createdAt: now,
updatedAt: now,
},
@@ -278,6 +281,62 @@ export default function AppRoot() {
setData((prev) => ({ ...prev, defaultTemplateTripId: tripId }));
}
function saveTripEdits(tripId, patch) {
if (!patch.name.trim()) {
Alert.alert('Missing name', 'Trip name is required.');
return false;
}
const start = parseYMD(patch.startDate);
const end = parseYMD(patch.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;
}
setData((prev) => ({
...prev,
trips: prev.trips.map((trip) =>
trip.id === tripId
? {
...trip,
name: patch.name.trim(),
location: patch.location.trim(),
startDate: patch.startDate,
endDate: patch.endDate,
imageUri: patch.imageUri,
updatedAt: Date.now(),
}
: trip
),
}));
return true;
}
function setTripArchived(tripId, archived) {
setData((prev) => ({
...prev,
trips: prev.trips.map((trip) =>
trip.id === tripId
? {
...trip,
archived,
archivedAt: archived ? Date.now() : null,
updatedAt: Date.now(),
}
: trip
),
defaultTemplateTripId: archived && prev.defaultTemplateTripId === tripId ? null : prev.defaultTemplateTripId,
}));
}
function deleteTrip(tripId) {
Alert.alert('Delete trip?', 'Trip items and check-up history will also be deleted.', [
{ text: 'Cancel', style: 'cancel' },
@@ -406,6 +465,30 @@ export default function AppRoot() {
});
}
function bulkSetItemStatus(itemIds, status) {
if (!selectedTripId || !itemIds.length) return;
const idSet = new Set(itemIds);
setData((prev) => {
const items = prev.itemsByTrip[selectedTripId] || [];
return {
...prev,
itemsByTrip: {
...prev.itemsByTrip,
[selectedTripId]: items.map((item) =>
idSet.has(item.id)
? {
...item,
status,
lentTo: status === 'lent-to' ? item.lentTo : '',
updatedAt: Date.now(),
}
: item
),
},
};
});
}
function deleteCheckup(checkupId) {
if (!selectedTripId) return;
setData((prev) => {
@@ -624,20 +707,32 @@ export default function AppRoot() {
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<TripPicker trips={data.trips} selectedTripId={selectedTripId} onChooseTrip={setSelectedTripId} />
<TripPicker trips={visibleTrips} selectedTripId={selectedTripId} onChooseTrip={setSelectedTripId} />
{tab === 'trips' && (
<TripsTab
tripForm={tripForm}
updateTripForm={updateTripForm}
pickTripImage={() => pickImage((uri) => updateTripForm('imageUri', uri))}
takeTripImage={() => takeImage((uri) => updateTripForm('imageUri', uri))}
pickTripImage={(onPicked) =>
pickImage((uri) => {
if (typeof onPicked === 'function') onPicked(uri);
else updateTripForm('imageUri', uri);
})
}
takeTripImage={(onPicked) =>
takeImage((uri) => {
if (typeof onPicked === 'function') onPicked(uri);
else updateTripForm('imageUri', uri);
})
}
templateTrip={templateTrip}
createTrip={createTrip}
trips={data.trips}
selectedTripId={selectedTripId}
chooseTrip={setSelectedTripId}
setTripAsTemplate={setTripAsTemplate}
saveTripEdits={saveTripEdits}
setTripArchived={setTripArchived}
deleteTrip={deleteTrip}
onInputFocus={onInputFocus}
defaultTemplateTripId={data.defaultTemplateTripId}
@@ -655,6 +750,7 @@ export default function AppRoot() {
openEditItemModal={openEditItemModal}
deleteItem={deleteItem}
quickSetItemStatus={quickSetItemStatus}
bulkSetItemStatus={bulkSetItemStatus}
/>
)}