Compare commits
3 Commits
luggage-li
...
luggage-li
| Author | SHA1 | Date | |
|---|---|---|---|
| e1bfbdbf1e | |||
| 063e5090ed | |||
| 354a13e9a9 |
@@ -14,7 +14,7 @@ import TripsTab from './tabs/TripsTab';
|
|||||||
import ItemsTab from './tabs/ItemsTab';
|
import ItemsTab from './tabs/ItemsTab';
|
||||||
import CheckupTab from './tabs/CheckupTab';
|
import CheckupTab from './tabs/CheckupTab';
|
||||||
import HistoryTab from './tabs/HistoryTab';
|
import HistoryTab from './tabs/HistoryTab';
|
||||||
import { emptyData, STORAGE_KEY } from './constants';
|
import { emptyData, ITEM_PLACEMENTS, STORAGE_KEY } from './constants';
|
||||||
import { findBestTripId, makeId, parseYMD, todayYMD } from './utils/date';
|
import { findBestTripId, makeId, parseYMD, todayYMD } from './utils/date';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ const emptyItemForm = () => ({
|
|||||||
category: '',
|
category: '',
|
||||||
status: 'unpacked',
|
status: 'unpacked',
|
||||||
placement: 'suitcase',
|
placement: 'suitcase',
|
||||||
|
placementCustom: '',
|
||||||
lentTo: '',
|
lentTo: '',
|
||||||
imageUri: '',
|
imageUri: '',
|
||||||
imageQuality: 'balanced',
|
imageQuality: 'balanced',
|
||||||
@@ -43,6 +44,7 @@ const emptyItemForm = () => ({
|
|||||||
const emptyCheckupNoForm = () => ({
|
const emptyCheckupNoForm = () => ({
|
||||||
status: 'unpacked',
|
status: 'unpacked',
|
||||||
placement: 'suitcase',
|
placement: 'suitcase',
|
||||||
|
placementCustom: '',
|
||||||
lentTo: '',
|
lentTo: '',
|
||||||
updateMasterList: false,
|
updateMasterList: false,
|
||||||
});
|
});
|
||||||
@@ -107,6 +109,35 @@ export default function AppRoot() {
|
|||||||
return (data.checkupsByTrip[selectedTripId] || []).slice().sort((a, b) => b.createdAt - a.createdAt);
|
return (data.checkupsByTrip[selectedTripId] || []).slice().sort((a, b) => b.createdAt - a.createdAt);
|
||||||
}, [data.checkupsByTrip, selectedTripId]);
|
}, [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(
|
const templateTrip = useMemo(
|
||||||
() => data.trips.find((trip) => trip.id === data.defaultTemplateTripId) || null,
|
() => data.trips.find((trip) => trip.id === data.defaultTemplateTripId) || null,
|
||||||
[data.trips, data.defaultTemplateTripId]
|
[data.trips, data.defaultTemplateTripId]
|
||||||
@@ -227,7 +258,18 @@ export default function AppRoot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateItemForm(field, value) {
|
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) {
|
function openDatePicker(field) {
|
||||||
@@ -430,13 +472,17 @@ export default function AppRoot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEditItemModal(item) {
|
function openEditItemModal(item) {
|
||||||
|
const existingPlacement = item.placement || 'suitcase';
|
||||||
|
const hasPresetPlacement = ITEM_PLACEMENTS.includes(existingPlacement);
|
||||||
|
|
||||||
setItemForm({
|
setItemForm({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name || '',
|
name: item.name || '',
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
category: item.category || '',
|
category: item.category || '',
|
||||||
status: item.status || 'unpacked',
|
status: item.status || 'unpacked',
|
||||||
placement: item.placement || 'suitcase',
|
placement: hasPresetPlacement ? existingPlacement : 'other',
|
||||||
|
placementCustom: hasPresetPlacement || existingPlacement === 'other' ? '' : existingPlacement,
|
||||||
lentTo: item.lentTo || '',
|
lentTo: item.lentTo || '',
|
||||||
imageUri: item.imageUri || '',
|
imageUri: item.imageUri || '',
|
||||||
imageQuality: item.imageQuality || 'balanced',
|
imageQuality: item.imageQuality || 'balanced',
|
||||||
@@ -456,6 +502,12 @@ export default function AppRoot() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedPlacement = itemForm.placement === 'other' ? itemForm.placementCustom.trim() : itemForm.placement;
|
||||||
|
if (!resolvedPlacement) {
|
||||||
|
showAlert('Missing location', 'Please enter a custom location for "other".');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
@@ -467,7 +519,7 @@ export default function AppRoot() {
|
|||||||
description: itemForm.description.trim(),
|
description: itemForm.description.trim(),
|
||||||
category: itemForm.category.trim(),
|
category: itemForm.category.trim(),
|
||||||
status: itemForm.status,
|
status: itemForm.status,
|
||||||
placement: itemForm.placement,
|
placement: resolvedPlacement,
|
||||||
lentTo: itemForm.status === 'lent-to' ? itemForm.lentTo.trim() : '',
|
lentTo: itemForm.status === 'lent-to' ? itemForm.lentTo.trim() : '',
|
||||||
imageUri: itemForm.imageUri,
|
imageUri: itemForm.imageUri,
|
||||||
imageQuality: itemForm.imageQuality,
|
imageQuality: itemForm.imageQuality,
|
||||||
@@ -650,9 +702,14 @@ export default function AppRoot() {
|
|||||||
function openCurrentCheckupNo() {
|
function openCurrentCheckupNo() {
|
||||||
const entry = checkupCurrentEntry;
|
const entry = checkupCurrentEntry;
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
|
||||||
|
const existingPlacement = entry.current.placement || 'suitcase';
|
||||||
|
const hasPresetPlacement = ITEM_PLACEMENTS.includes(existingPlacement);
|
||||||
|
|
||||||
setCheckupNoForm({
|
setCheckupNoForm({
|
||||||
status: entry.current.status || 'unpacked',
|
status: entry.current.status || 'unpacked',
|
||||||
placement: entry.current.placement || 'suitcase',
|
placement: hasPresetPlacement ? existingPlacement : 'other',
|
||||||
|
placementCustom: hasPresetPlacement || existingPlacement === 'other' ? '' : existingPlacement,
|
||||||
lentTo: entry.current.lentTo || '',
|
lentTo: entry.current.lentTo || '',
|
||||||
updateMasterList: false,
|
updateMasterList: false,
|
||||||
});
|
});
|
||||||
@@ -663,9 +720,15 @@ export default function AppRoot() {
|
|||||||
const entry = checkupCurrentEntry;
|
const entry = checkupCurrentEntry;
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
|
||||||
|
const resolvedPlacement = checkupNoForm.placement === 'other' ? checkupNoForm.placementCustom.trim() : checkupNoForm.placement;
|
||||||
|
if (!resolvedPlacement) {
|
||||||
|
showAlert('Missing location', 'Please enter a custom location for "other".');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const patch = {
|
const patch = {
|
||||||
status: checkupNoForm.status,
|
status: checkupNoForm.status,
|
||||||
placement: checkupNoForm.placement,
|
placement: resolvedPlacement,
|
||||||
lentTo: checkupNoForm.status === 'lent-to' ? checkupNoForm.lentTo.trim() : '',
|
lentTo: checkupNoForm.status === 'lent-to' ? checkupNoForm.lentTo.trim() : '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -936,6 +999,7 @@ export default function AppRoot() {
|
|||||||
<ItemModal
|
<ItemModal
|
||||||
visible={itemModalVisible}
|
visible={itemModalVisible}
|
||||||
itemForm={itemForm}
|
itemForm={itemForm}
|
||||||
|
previousCustomPlacements={previousCustomPlacements}
|
||||||
setItemModalVisible={setItemModalVisible}
|
setItemModalVisible={setItemModalVisible}
|
||||||
updateItemForm={updateItemForm}
|
updateItemForm={updateItemForm}
|
||||||
pickItemImage={(options) => pickImage((uri) => updateItemForm('imageUri', uri), options)}
|
pickItemImage={(options) => pickImage((uri) => updateItemForm('imageUri', uri), options)}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
|||||||
<View style={styles.itemMain}>
|
<View style={styles.itemMain}>
|
||||||
<View style={styles.cardRow}>
|
<View style={styles.cardRow}>
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
<Text style={styles.itemTitle}>{item.name}</Text>
|
<Text style={styles.itemTitle} numberOfLines={1}>{item.name}</Text>
|
||||||
<Text style={styles.itemMeta}>{item.category || 'uncategorized'} · {formatStatusLabel(item.status, item.lentTo)}</Text>
|
<Text style={styles.itemMeta} numberOfLines={1}>{item.category || 'uncategorized'} · {formatStatusLabel(item.status, item.lentTo)}</Text>
|
||||||
<Text style={styles.itemMeta}>Location: {item.placement}</Text>
|
<Text style={styles.itemMeta}>Location: {item.placement}</Text>
|
||||||
{item.status === 'lent-to' && !!item.lentTo ? <Text style={styles.itemMeta}>Borrower: {item.lentTo}</Text> : null}
|
{item.status === 'lent-to' && !!item.lentTo ? <Text style={styles.itemMeta} numberOfLines={1}>Borrower: {item.lentTo}</Text> : null}
|
||||||
{!!item.description ? <Text style={styles.itemMeta}>{item.description}</Text> : null}
|
{!!item.description ? <Text style={styles.itemMeta} numberOfLines={2}>{item.description}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.stackButtons}>
|
<View style={styles.stackButtons}>
|
||||||
<Pressable style={styles.miniBtn} onPress={() => onEdit(item)}>
|
<Pressable style={styles.miniBtn} onPress={() => onEdit(item)}>
|
||||||
@@ -49,10 +49,10 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
|||||||
|
|
||||||
<View style={styles.quickStatusRow}>
|
<View style={styles.quickStatusRow}>
|
||||||
<Pressable style={[styles.quickStatusBtn, isPacked && styles.quickStatusBtnActive]} onPress={onQuickPack}>
|
<Pressable style={[styles.quickStatusBtn, isPacked && styles.quickStatusBtnActive]} onPress={onQuickPack}>
|
||||||
<Text style={[styles.quickStatusBtnText, isPacked && styles.quickStatusBtnTextActive]}>Pack</Text>
|
<Text style={[styles.quickStatusBtnText, isPacked && styles.quickStatusBtnTextActive]}>Packed</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable style={[styles.quickStatusBtn, isUnpacked && styles.quickStatusBtnActive]} onPress={onQuickUnpack}>
|
<Pressable style={[styles.quickStatusBtn, isUnpacked && styles.quickStatusBtnActive]} onPress={onQuickUnpack}>
|
||||||
<Text style={[styles.quickStatusBtnText, isUnpacked && styles.quickStatusBtnTextActive]}>Unpack</Text>
|
<Text style={[styles.quickStatusBtnText, isUnpacked && styles.quickStatusBtnTextActive]}>Unpacked</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -103,6 +103,18 @@ export default function CheckupFlowModal({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
{noForm.placement === 'other' ? (
|
||||||
|
<Field label="Custom location">
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={noForm.placementCustom}
|
||||||
|
onChangeText={(v) => setNoForm((prev) => ({ ...prev, placementCustom: v }))}
|
||||||
|
placeholder="bath-kit"
|
||||||
|
placeholderTextColor="#6b7280"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{noForm.status === 'lent-to' ? (
|
{noForm.status === 'lent-to' ? (
|
||||||
<Field label="Lent to">
|
<Field label="Lent to">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function qualityValue(level) {
|
|||||||
export default function ItemModal({
|
export default function ItemModal({
|
||||||
visible,
|
visible,
|
||||||
itemForm,
|
itemForm,
|
||||||
|
previousCustomPlacements,
|
||||||
setItemModalVisible,
|
setItemModalVisible,
|
||||||
updateItemForm,
|
updateItemForm,
|
||||||
pickItemImage,
|
pickItemImage,
|
||||||
@@ -81,6 +82,27 @@ export default function ItemModal({
|
|||||||
<ChipGroup options={ITEM_PLACEMENTS} value={itemForm.placement} onChange={(v) => updateItemForm('placement', v)} />
|
<ChipGroup options={ITEM_PLACEMENTS} value={itemForm.placement} onChange={(v) => updateItemForm('placement', v)} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
{itemForm.placement === 'other' ? (
|
||||||
|
<Field label="Custom location">
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={itemForm.placementCustom}
|
||||||
|
onChangeText={(v) => updateItemForm('placementCustom', v)}
|
||||||
|
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}
|
||||||
|
|
||||||
{itemForm.status === 'lent-to' ? (
|
{itemForm.status === 'lent-to' ? (
|
||||||
<Field label="Lent to">
|
<Field label="Lent to">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 14,
|
||||||
paddingTop: 12,
|
paddingTop: 10,
|
||||||
paddingBottom: TAB_BAR_HEIGHT + 22,
|
paddingBottom: TAB_BAR_HEIGHT + 20,
|
||||||
gap: 14,
|
gap: 10,
|
||||||
},
|
},
|
||||||
statusSpacer: {
|
statusSpacer: {
|
||||||
height: Platform.OS === 'android' ? 8 : 0,
|
height: Platform.OS === 'android' ? 8 : 0,
|
||||||
@@ -86,7 +86,7 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
section: {
|
section: {
|
||||||
gap: 12,
|
gap: 10,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
color: '#f1f5f9',
|
color: '#f1f5f9',
|
||||||
@@ -105,8 +105,8 @@ export const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#1f2937',
|
borderColor: '#1f2937',
|
||||||
padding: 12,
|
padding: 10,
|
||||||
gap: 8,
|
gap: 6,
|
||||||
},
|
},
|
||||||
cardActive: {
|
cardActive: {
|
||||||
borderColor: '#60a5fa',
|
borderColor: '#60a5fa',
|
||||||
@@ -120,12 +120,12 @@ export const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#1e293b',
|
borderColor: '#1e293b',
|
||||||
padding: 12,
|
padding: 10,
|
||||||
gap: 10,
|
gap: 8,
|
||||||
},
|
},
|
||||||
cardRow: {
|
cardRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 10,
|
gap: 8,
|
||||||
},
|
},
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
color: '#f8fafc',
|
color: '#f8fafc',
|
||||||
@@ -134,8 +134,8 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
cardMeta: {
|
cardMeta: {
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
marginTop: 3,
|
marginTop: 2,
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
tripHistoryLabel: {
|
tripHistoryLabel: {
|
||||||
color: '#93c5fd',
|
color: '#93c5fd',
|
||||||
@@ -148,8 +148,8 @@ export const styles = StyleSheet.create({
|
|||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#334155',
|
borderColor: '#334155',
|
||||||
padding: 12,
|
padding: 10,
|
||||||
gap: 8,
|
gap: 6,
|
||||||
},
|
},
|
||||||
tripHeroImage: {
|
tripHeroImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -160,7 +160,7 @@ export const styles = StyleSheet.create({
|
|||||||
tripHeroTitle: {
|
tripHeroTitle: {
|
||||||
color: '#f8fafc',
|
color: '#f8fafc',
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
fontSize: 22,
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
tripListTitle: {
|
tripListTitle: {
|
||||||
color: '#cbd5e1',
|
color: '#cbd5e1',
|
||||||
@@ -290,19 +290,19 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
stackButtons: {
|
stackButtons: {
|
||||||
gap: 7,
|
gap: 6,
|
||||||
},
|
},
|
||||||
miniBtn: {
|
miniBtn: {
|
||||||
backgroundColor: '#1e293b',
|
backgroundColor: '#1e293b',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 7,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 9,
|
||||||
},
|
},
|
||||||
miniBtnDanger: {
|
miniBtnDanger: {
|
||||||
backgroundColor: '#3b1d22',
|
backgroundColor: '#3b1d22',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 7,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 9,
|
||||||
},
|
},
|
||||||
miniBtnText: {
|
miniBtnText: {
|
||||||
color: '#e2e8f0',
|
color: '#e2e8f0',
|
||||||
@@ -324,18 +324,18 @@ export const styles = StyleSheet.create({
|
|||||||
alignSelf: 'stretch',
|
alignSelf: 'stretch',
|
||||||
},
|
},
|
||||||
itemThumbWrap: {
|
itemThumbWrap: {
|
||||||
paddingTop: 10,
|
paddingTop: 8,
|
||||||
paddingLeft: 10,
|
paddingLeft: 8,
|
||||||
},
|
},
|
||||||
itemThumbSmall: {
|
itemThumbSmall: {
|
||||||
width: 46,
|
width: 42,
|
||||||
height: 46,
|
height: 42,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: '#0b1220',
|
backgroundColor: '#0b1220',
|
||||||
},
|
},
|
||||||
itemThumbPlaceholder: {
|
itemThumbPlaceholder: {
|
||||||
width: 46,
|
width: 42,
|
||||||
height: 46,
|
height: 42,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: '#0b1220',
|
backgroundColor: '#0b1220',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -344,12 +344,12 @@ export const styles = StyleSheet.create({
|
|||||||
borderColor: '#243244',
|
borderColor: '#243244',
|
||||||
},
|
},
|
||||||
itemThumbPlaceholderText: {
|
itemThumbPlaceholderText: {
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
itemMain: {
|
itemMain: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 10,
|
padding: 8,
|
||||||
gap: 8,
|
gap: 6,
|
||||||
},
|
},
|
||||||
itemTitle: {
|
itemTitle: {
|
||||||
color: '#f8fafc',
|
color: '#f8fafc',
|
||||||
@@ -358,17 +358,17 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
itemMeta: {
|
itemMeta: {
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
marginTop: 2,
|
marginTop: 1,
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
quickStatusRow: {
|
quickStatusRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 8,
|
gap: 6,
|
||||||
marginTop: 2,
|
marginTop: 1,
|
||||||
},
|
},
|
||||||
quickStatusBtn: {
|
quickStatusBtn: {
|
||||||
paddingVertical: 6,
|
paddingVertical: 5,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 10,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#334155',
|
borderColor: '#334155',
|
||||||
@@ -381,7 +381,7 @@ export const styles = StyleSheet.create({
|
|||||||
quickStatusBtnText: {
|
quickStatusBtnText: {
|
||||||
color: '#cbd5e1',
|
color: '#cbd5e1',
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
quickStatusBtnTextActive: {
|
quickStatusBtnTextActive: {
|
||||||
color: '#dbeafe',
|
color: '#dbeafe',
|
||||||
@@ -576,8 +576,8 @@ export const styles = StyleSheet.create({
|
|||||||
tabItem: {
|
tabItem: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 4,
|
gap: 3,
|
||||||
minWidth: 58,
|
minWidth: 56,
|
||||||
},
|
},
|
||||||
tabAddBtn: {
|
tabAddBtn: {
|
||||||
width: 54,
|
width: 54,
|
||||||
@@ -595,7 +595,7 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
tabLabel: {
|
tabLabel: {
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
tabLabelActive: {
|
tabLabelActive: {
|
||||||
@@ -673,8 +673,8 @@ export const styles = StyleSheet.create({
|
|||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#1e293b',
|
borderColor: '#1e293b',
|
||||||
padding: 16,
|
padding: 14,
|
||||||
gap: 10,
|
gap: 8,
|
||||||
},
|
},
|
||||||
closeText: {
|
closeText: {
|
||||||
color: '#93c5fd',
|
color: '#93c5fd',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function ItemsTab({
|
|||||||
}) {
|
}) {
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||||
|
const [locationFilter, setLocationFilter] = useState('all');
|
||||||
const [imagePreviewUri, setImagePreviewUri] = useState('');
|
const [imagePreviewUri, setImagePreviewUri] = useState('');
|
||||||
|
|
||||||
const categories = useMemo(
|
const categories = useMemo(
|
||||||
@@ -23,19 +24,27 @@ export default function ItemsTab({
|
|||||||
[selectedTripItems]
|
[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(
|
const filteredItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectedTripItems.filter((item) => {
|
selectedTripItems.filter((item) => {
|
||||||
const matchStatus = statusFilter === 'all' || item.status === statusFilter;
|
const matchStatus = statusFilter === 'all' || item.status === statusFilter;
|
||||||
const itemCategory = item.category?.trim() || '';
|
const itemCategory = item.category?.trim() || '';
|
||||||
|
const itemLocation = item.placement?.trim() || '';
|
||||||
const matchCategory = categoryFilter === 'all' || itemCategory === categoryFilter;
|
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 filterStatusOptions = ['all', ...ITEM_STATUSES];
|
||||||
const filterCategoryOptions = ['all', ...categories];
|
const filterCategoryOptions = ['all', ...categories];
|
||||||
|
const filterLocationOptions = ['all', ...locations];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
@@ -90,6 +99,18 @@ export default function ItemsTab({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</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>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export default function TripsTab({
|
|||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
<Text style={styles.cardTitle}>{trip.name}</Text>
|
<Text style={styles.cardTitle}>{trip.name}</Text>
|
||||||
<Text style={styles.cardMeta}>{trip.location || 'No location'} · {trip.startDate} → {trip.endDate}</Text>
|
<Text style={styles.cardMeta}>{trip.location || 'No location'} · {trip.startDate} → {trip.endDate}</Text>
|
||||||
<Text style={styles.cardMeta}>{defaultTemplateTripId === trip.id ? 'Default template' : ' '}</Text>
|
{defaultTemplateTripId === trip.id ? <Text style={styles.cardMeta}>Default template</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.stackButtons}>
|
<View style={styles.stackButtons}>
|
||||||
<Pressable style={styles.miniBtn} onPress={() => openView(trip.id)}>
|
<Pressable style={styles.miniBtn} onPress={() => openView(trip.id)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user