1 Commits

Author SHA1 Message Date
e1bfbdbf1e feat(#17,#18): add location filter and custom-location autofill
All checks were successful
Luggage List Build / build-web (push) Successful in 52s
Luggage List Build / build-android (push) Successful in 7m2s
Luggage List Build / release (push) Successful in 17s
2026-04-18 21:59:23 +02:00
3 changed files with 75 additions and 3 deletions

View File

@@ -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() {
<ItemModal
visible={itemModalVisible}
itemForm={itemForm}
previousCustomPlacements={previousCustomPlacements}
setItemModalVisible={setItemModalVisible}
updateItemForm={updateItemForm}
pickItemImage={(options) => pickImage((uri) => updateItemForm('imageUri', uri), options)}

View File

@@ -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 ? (
<View style={styles.chipGroup}>
{previousCustomPlacements.slice(0, 6).map((location) => (
<Pressable key={location} style={styles.chip} onPress={() => updateItemForm('placementCustom', location)}>
<Text style={styles.chipText}>{location}</Text>
</Pressable>
))}
</View>
) : null}
</Field>
) : null}

View File

@@ -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 (
<View style={styles.section}>
@@ -90,6 +99,18 @@ export default function ItemsTab({
);
})}
</View>
<Text style={styles.cardMeta}>Location</Text>
<View style={styles.chipGroup}>
{filterLocationOptions.map((location) => {
const active = locationFilter === location;
return (
<Pressable key={location} style={[styles.chip, active && styles.chipActive]} onPress={() => setLocationFilter(location)}>
<Text style={[styles.chipText, active && styles.chipTextActive]}>{formatFilterLabel(location)}</Text>
</Pressable>
);
})}
</View>
</View>
) : null}