feat: Sprint 3 — Territory management (list, add, edit, delete, detail), GPS tagging, territory tab

This commit is contained in:
root
2026-02-18 16:41:47 +08:00
parent 419865e8cf
commit 9bd02b1a17
9 changed files with 425 additions and 8 deletions

View File

@@ -13,6 +13,8 @@ export default function ContactsScreen() {
const { contacts, setContacts } = useContactStore();
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('All');
const [territoryFilter, setTerritoryFilter] = useState<string>('All');
const [territories, setTerritories] = useState<string[]>([]);
const [showAdd, setShowAdd] = useState(false);
const statusOptions = ['All', 'Active', 'Return Visit', 'Bible Study', 'Not Interested', 'Do Not Call'];
@@ -23,6 +25,10 @@ export default function ContactsScreen() {
'SELECT * FROM contacts WHERE deleted_at IS NULL ORDER BY full_name ASC'
);
setContacts(rows.map(dbToContact));
// Load territory codes for filter
const tRows = await db.getAllAsync<any>('SELECT territory_code FROM territories ORDER BY territory_code ASC');
setTerritories(tRows.map((r: any) => r.territory_code));
}
useFocusEffect(useCallback(() => { loadContacts(); }, []));
@@ -31,7 +37,8 @@ export default function ContactsScreen() {
const matchSearch = c.fullName.toLowerCase().includes(search.toLowerCase()) ||
(c.address?.toLowerCase().includes(search.toLowerCase()) ?? false);
const matchStatus = statusFilter === 'All' || c.status === statusFilter;
return matchSearch && matchStatus;
const matchTerritory = territoryFilter === 'All' || c.territoryCode === territoryFilter;
return matchSearch && matchStatus && matchTerritory;
});
return (
@@ -59,7 +66,7 @@ export default function ContactsScreen() {
</View>
{/* Status Filter */}
<View className="py-2">
<View className="pt-2">
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
@@ -77,6 +84,27 @@ export default function ContactsScreen() {
/>
</View>
{/* Territory Filter */}
{territories.length > 0 && (
<View className="pb-2">
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 12, gap: 8, paddingTop: 6 }}
data={['All', ...territories]}
keyExtractor={(item) => `t-${item}`}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => setTerritoryFilter(item)}
className={`px-3 py-1 rounded-full border ${territoryFilter === item ? 'bg-accent border-accent' : 'bg-white border-gray-200'}`}
>
<Text className={`text-xs font-medium ${territoryFilter === item ? 'text-white' : 'text-gray-500'}`}>{item === 'All' ? 'All Territories' : item}</Text>
</TouchableOpacity>
)}
/>
</View>
)}
{/* List */}
<FlatList
data={filtered}