feat: leads module - list, detail, add, convert to client, delete; leads section on dashboard
This commit is contained in:
@@ -10,6 +10,14 @@ import { api } from '../../services/api';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { SlideToConfirm } from '../../components/SlideToConfirm';
|
||||
|
||||
const LEAD_STATUS: Record<string, { label: string; color: string; bg: string }> = {
|
||||
NEW: { label: 'New', color: '#0891B2', bg: '#ECFEFF' },
|
||||
CONTACTED: { label: 'Contacted', color: '#D97706', bg: '#FEF3C7' },
|
||||
INTERESTED: { label: 'Interested', color: '#7C3AED', bg: '#F5F3FF' },
|
||||
CONVERTED: { label: 'Converted', color: '#166534', bg: '#DCFCE7' },
|
||||
LOST: { label: 'Lost', color: '#6B7280', bg: '#F1F5F9' },
|
||||
};
|
||||
|
||||
const PRIORITY_COLOR: Record<string, string> = { HIGH: '#DC2626', NORMAL: '#0891B2' };
|
||||
const PRIORITY_BG: Record<string, string> = { HIGH: '#FEE2E2', NORMAL: '#ECFEFF' };
|
||||
const TYPE_COLOR: Record<string, string> = { INSTALLATION: '#0891B2', SUPPORT: '#7C3AED', BILLING: '#D97706' };
|
||||
@@ -157,7 +165,7 @@ export default function DashboardScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const [summaryQ, ticketsQ, invoicesQ] = useQueries({
|
||||
const [summaryQ, ticketsQ, invoicesQ, leadsQ] = useQueries({
|
||||
queries: [
|
||||
{ queryKey: ['dashboard'], queryFn: () => api.get('/api/v1/dashboard/summary').then(r => r.data) },
|
||||
{
|
||||
@@ -182,6 +190,14 @@ export default function DashboardScreen() {
|
||||
return unpaid.slice(0, 10);
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: ['dashboard-leads'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/api/v1/leads?limit=20');
|
||||
const all: any[] = res.data?.data ?? res.data ?? [];
|
||||
return all.filter((l: any) => l.status !== 'CONVERTED' && l.status !== 'LOST').slice(0, 5);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -190,6 +206,7 @@ export default function DashboardScreen() {
|
||||
const summary = summaryQ.data ?? {};
|
||||
const allActiveTickets = ticketsQ.data ?? [];
|
||||
const unpaidInvoices = invoicesQ.data ?? [];
|
||||
const activeLeads = leadsQ.data ?? [];
|
||||
|
||||
const unassigned = (allActiveTickets as any[]).filter((t: any) => !t.assignedToId);
|
||||
const assignedToMe = (allActiveTickets as any[]).filter((t: any) => t.assignedToId === user?.id);
|
||||
@@ -198,7 +215,7 @@ export default function DashboardScreen() {
|
||||
if (seen.has(t.id)) return false; seen.add(t.id); return true;
|
||||
}).slice(0, 10).sort((a: any, b: any) => (a.priority === 'HIGH' ? -1 : 1) - (b.priority === 'HIGH' ? -1 : 1));
|
||||
|
||||
const refetchAll = () => { summaryQ.refetch(); ticketsQ.refetch(); invoicesQ.refetch(); };
|
||||
const refetchAll = () => { summaryQ.refetch(); ticketsQ.refetch(); invoicesQ.refetch(); leadsQ.refetch(); };
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0891B2' }} edges={['top']}>
|
||||
@@ -282,6 +299,64 @@ export default function DashboardScreen() {
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{/* ── Leads ── */}
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 17, fontWeight: '800', color: '#0F172A' }}>Leads</Text>
|
||||
{(activeLeads as any[]).length > 0 && (
|
||||
<View style={{ backgroundColor: '#F5F3FF', borderRadius: 20, paddingHorizontal: 8, paddingVertical: 2, marginLeft: 8 }}>
|
||||
<Text style={{ fontSize: 12, fontWeight: '700', color: '#7C3AED' }}>{(activeLeads as any[]).length}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/(app)/leads')}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Text style={{ fontSize: 14, fontWeight: '600', color: '#0891B2' }}>View all</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{(activeLeads as any[]).length === 0 ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/(app)/leads')}
|
||||
style={{ backgroundColor: '#fff', borderRadius: 14, padding: 20, alignItems: 'center', marginBottom: 20, borderWidth: 1, borderColor: '#F1F5F9', borderStyle: 'dashed' }}
|
||||
>
|
||||
<Text style={{ fontSize: 15, color: '#94A3B8' }}>No active leads — <Text style={{ color: '#0891B2', fontWeight: '700' }}>+ Add one</Text></Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={{ marginBottom: 20 }}>
|
||||
{(activeLeads as any[]).map((l: any) => {
|
||||
const cfg = LEAD_STATUS[l.status] ?? LEAD_STATUS.NEW;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={l.id}
|
||||
onPress={() => router.push(`/(app)/leads/${l.id}`)}
|
||||
activeOpacity={0.7}
|
||||
style={{ backgroundColor: '#fff', borderRadius: 14, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#F1F5F9', flexDirection: 'row', alignItems: 'center' }}
|
||||
>
|
||||
<View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: '#F5F3FF', alignItems: 'center', justifyContent: 'center', marginRight: 12 }}>
|
||||
<Text style={{ fontSize: 16, fontWeight: '800', color: '#7C3AED' }}>{l.firstName?.[0]?.toUpperCase()}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ fontSize: 15, fontWeight: '700', color: '#0F172A' }}>{l.firstName} {l.lastName !== '—' ? l.lastName : ''}</Text>
|
||||
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 1 }}>{l.phone}</Text>
|
||||
</View>
|
||||
<View style={{ backgroundColor: cfg.bg, borderRadius: 20, paddingHorizontal: 10, paddingVertical: 4 }}>
|
||||
<Text style={{ fontSize: 12, fontWeight: '700', color: cfg.color }}>{cfg.label}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/(app)/leads')}
|
||||
style={{ alignItems: 'center', paddingVertical: 10 }}
|
||||
>
|
||||
<Text style={{ fontSize: 14, fontWeight: '600', color: '#0891B2' }}>+ Add New Lead</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={{ height: 24 }} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user