Files
fiberops-mobile/app/(app)/clients/index.tsx

116 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { View, Text, FlatList, TextInput, TouchableOpacity, ActivityIndicator, RefreshControl } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useQuery } from '@tanstack/react-query';
import { router } from 'expo-router';
import { api } from '../../../services/api';
const STATUS_CONFIG: Record<string, { label: string; color: string; bg: string; accent: string }> = {
ACTIVE: { label: 'Active', color: '#166534', bg: '#DCFCE7', accent: '#16A34A' },
SUSPENDED: { label: 'Suspended', color: '#92400E', bg: '#FEF3C7', accent: '#D97706' },
CANCELLED: { label: 'Cancelled', color: '#991B1B', bg: '#FEE2E2', accent: '#DC2626' },
PENDING: { label: 'Pending', color: '#475569', bg: '#F1F5F9', accent: '#94A3B8' },
NO_SUB: { label: 'No Sub', color: '#6B7280', bg: '#F1F5F9', accent: '#CBD5E1' },
};
// Client.status is null in API — derive from subscription status instead
function getClientStatus(client: any) {
const sub = client?.subscriptions?.[0];
if (!sub) return 'NO_SUB';
return sub.status ?? 'PENDING';
}
export default function ClientsScreen() {
const [search, setSearch] = useState('');
const { data, isLoading, refetch, isRefetching } = useQuery({
queryKey: ['clients'],
queryFn: () => api.get('/api/v1/clients?limit=100').then(r => r.data?.data ?? r.data),
});
const clients = (data ?? []).filter((c: any) =>
`${c.firstName} ${c.lastName} ${c.accountNumber}`.toLowerCase().includes(search.toLowerCase())
);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
<View style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
{/* Header */}
<View style={{ backgroundColor: '#0891B2', paddingHorizontal: 20, paddingTop: 16, paddingBottom: 20, flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between' }}>
<View>
<Text style={{ color: '#FFF', fontSize: 28, fontWeight: '800' }}>Clients</Text>
<Text style={{ color: '#A5F3FC', fontSize: 15, marginTop: 2 }}>{data?.length ?? 0} subscribers</Text>
</View>
<TouchableOpacity
onPress={() => router.push('/(app)/clients/new')}
style={{ backgroundColor: '#fff', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 8, flexDirection: 'row', alignItems: 'center', gap: 6 }}
>
<Text style={{ fontSize: 18, color: '#0891B2', fontWeight: '800', lineHeight: 20 }}>+</Text>
<Text style={{ fontSize: 14, fontWeight: '700', color: '#0891B2' }}>New Client</Text>
</TouchableOpacity>
</View>
{/* Search */}
<View style={{ backgroundColor: '#FFF', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F1F5F9' }}>
<View style={{ backgroundColor: '#F8FAFC', borderWidth: 1.5, borderColor: '#E2E8F0', borderRadius: 12, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14 }}>
<TextInput
style={{ flex: 1, paddingVertical: 13, fontSize: 16, color: '#0F172A' }}
placeholder="Search by name or account #"
placeholderTextColor="#94A3B8"
value={search}
onChangeText={setSearch}
/>
{search.length > 0 && (
<TouchableOpacity onPress={() => setSearch('')} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<View style={{ width: 20, height: 20, borderRadius: 10, backgroundColor: '#CBD5E1', alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: '#FFF', fontSize: 12, fontWeight: '800', lineHeight: 14 }}>×</Text>
</View>
</TouchableOpacity>
)}
</View>
</View>
{isLoading ? (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator color="#0891B2" size="large" />
</View>
) : (
<FlatList
data={clients}
keyExtractor={(item) => item.id}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} tintColor="#0891B2" />}
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
renderItem={({ item }) => {
const statusKey = getClientStatus(item);
const st = STATUS_CONFIG[statusKey] ?? { label: statusKey, color: '#475569', bg: '#F1F5F9', accent: '#94A3B8' };
const plan = item.subscriptions?.[0]?.plan?.name;
return (
<TouchableOpacity
style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 18, marginBottom: 12, borderWidth: 1, borderColor: '#F1F5F9', borderLeftWidth: 4, borderLeftColor: st.accent }}
onPress={() => router.push(`/(app)/clients/${item.id}`)}
activeOpacity={0.7}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1, marginRight: 12 }}>
<Text style={{ fontSize: 17, fontWeight: '700', color: '#0F172A' }}>{item.firstName} {item.lastName}</Text>
<Text style={{ fontSize: 14, color: '#64748B', marginTop: 3 }}>{item.accountNumber}{plan ? ` · ${plan}` : ''}</Text>
{item.phone && <Text style={{ fontSize: 14, color: '#94A3B8', marginTop: 2 }}>{item.phone}</Text>}
</View>
<View style={{ borderRadius: 20, paddingHorizontal: 12, paddingVertical: 6, backgroundColor: st.bg }}>
<Text style={{ fontSize: 13, fontWeight: '800', color: st.color }}>{st.label}</Text>
</View>
</View>
</TouchableOpacity>
);
}}
ListEmptyComponent={
<View style={{ alignItems: 'center', paddingVertical: 60 }}>
<Text style={{ fontSize: 17, color: '#94A3B8' }}>No clients found</Text>
</View>
}
/>
)}
</View>
</SafeAreaView>
);
}