From e1bfbdbf1e3190c5c4146e54723522dc0793a6e3 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 18 Apr 2026 21:59:23 +0200 Subject: [PATCH] feat(#17,#18): add location filter and custom-location autofill --- src/AppRoot.js | 43 ++++++++++++++++++++++++++++++++++++++++- src/modals/ItemModal.js | 10 ++++++++++ src/tabs/ItemsTab.js | 25 ++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/AppRoot.js b/src/AppRoot.js index dab1267..59d345d 100644 --- a/src/AppRoot.js +++ b/src/AppRoot.js @@ -109,6 +109,35 @@ export default function AppRoot() { return (data.checkupsByTrip[selectedTripId] || []).slice().sort((a, b) => b.createdAt - a.createdAt); }, [data.checkupsByTrip, selectedTripId]); + const previousCustomPlacements = useMemo(() => { + const seen = new Set(); + + function takeFrom(items = [], bucket = []) { + items + .slice() + .sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0)) + .forEach((item) => { + const location = item.placement?.trim(); + if (!location || ITEM_PLACEMENTS.includes(location) || seen.has(location)) return; + seen.add(location); + bucket.push(location); + }); + return bucket; + } + + const collected = []; + if (selectedTripId) { + takeFrom(data.itemsByTrip[selectedTripId] || [], collected); + } + + Object.entries(data.itemsByTrip).forEach(([tripId, items]) => { + if (tripId === selectedTripId) return; + takeFrom(items || [], collected); + }); + + return collected; + }, [data.itemsByTrip, selectedTripId]); + const templateTrip = useMemo( () => data.trips.find((trip) => trip.id === data.defaultTemplateTripId) || null, [data.trips, data.defaultTemplateTripId] @@ -229,7 +258,18 @@ export default function AppRoot() { } function updateItemForm(field, value) { - setItemForm((prev) => ({ ...prev, [field]: value })); + setItemForm((prev) => { + if (field === 'placement' && value === 'other') { + const fallbackLocation = previousCustomPlacements[0] || ''; + return { + ...prev, + placement: value, + placementCustom: prev.placementCustom?.trim() ? prev.placementCustom : fallbackLocation, + }; + } + + return { ...prev, [field]: value }; + }); } function openDatePicker(field) { @@ -959,6 +999,7 @@ export default function AppRoot() { pickImage((uri) => updateItemForm('imageUri', uri), options)} diff --git a/src/modals/ItemModal.js b/src/modals/ItemModal.js index a86197c..1284457 100644 --- a/src/modals/ItemModal.js +++ b/src/modals/ItemModal.js @@ -14,6 +14,7 @@ function qualityValue(level) { export default function ItemModal({ visible, itemForm, + previousCustomPlacements, setItemModalVisible, updateItemForm, pickItemImage, @@ -90,6 +91,15 @@ export default function ItemModal({ placeholder="bath-kit" placeholderTextColor="#6b7280" /> + {previousCustomPlacements?.length ? ( + + {previousCustomPlacements.slice(0, 6).map((location) => ( + updateItemForm('placementCustom', location)}> + {location} + + ))} + + ) : null} ) : null} diff --git a/src/tabs/ItemsTab.js b/src/tabs/ItemsTab.js index 78027d0..f4ff341 100644 --- a/src/tabs/ItemsTab.js +++ b/src/tabs/ItemsTab.js @@ -16,6 +16,7 @@ export default function ItemsTab({ }) { const [statusFilter, setStatusFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); + const [locationFilter, setLocationFilter] = useState('all'); const [imagePreviewUri, setImagePreviewUri] = useState(''); const categories = useMemo( @@ -23,19 +24,27 @@ export default function ItemsTab({ [selectedTripItems] ); + const locations = useMemo( + () => Array.from(new Set(selectedTripItems.map((item) => item.placement?.trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b)), + [selectedTripItems] + ); + const filteredItems = useMemo( () => selectedTripItems.filter((item) => { const matchStatus = statusFilter === 'all' || item.status === statusFilter; const itemCategory = item.category?.trim() || ''; + const itemLocation = item.placement?.trim() || ''; const matchCategory = categoryFilter === 'all' || itemCategory === categoryFilter; - return matchStatus && matchCategory; + const matchLocation = locationFilter === 'all' || itemLocation === locationFilter; + return matchStatus && matchCategory && matchLocation; }), - [selectedTripItems, statusFilter, categoryFilter] + [selectedTripItems, statusFilter, categoryFilter, locationFilter] ); const filterStatusOptions = ['all', ...ITEM_STATUSES]; const filterCategoryOptions = ['all', ...categories]; + const filterLocationOptions = ['all', ...locations]; return ( @@ -90,6 +99,18 @@ export default function ItemsTab({ ); })} + + Location + + {filterLocationOptions.map((location) => { + const active = locationFilter === location; + return ( + setLocationFilter(location)}> + {formatFilterLabel(location)} + + ); + })} + ) : null}