187 lines
9.2 KiB
JavaScript
187 lines
9.2 KiB
JavaScript
import React, { useState, useCallback, useContext } from 'react';
|
|
import {
|
|
View, Text, StyleSheet, SectionList, TouchableOpacity,
|
|
RefreshControl, StatusBar, Alert, Linking
|
|
} from 'react-native';
|
|
import { useFocusEffect } from '@react-navigation/native';
|
|
import { AuthContext } from '../context/AuthContext';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import api from '../services/api';
|
|
import Colors from '../constants/Colors';
|
|
|
|
const TasksScreen = ({ navigation }) => {
|
|
const { userInfo } = useContext(AuthContext);
|
|
const insets = useSafeAreaInsets();
|
|
const [sections, setSections] = useState([]);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [activeFilter, setActiveFilter] = useState('ALL'); // ALL, PENDING, DONE
|
|
|
|
const groupByDay = (followups) => {
|
|
const map = {};
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const tomorrow = new Date(today.getTime() + 86400000);
|
|
|
|
followups.forEach(f => {
|
|
const d = new Date(f.date);
|
|
d.setHours(0, 0, 0, 0);
|
|
let label;
|
|
if (d.getTime() === today.getTime()) label = 'Today';
|
|
else if (d.getTime() < today.getTime()) label = `Overdue — ${d.toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}`;
|
|
else if (d.getTime() === tomorrow.getTime()) label = 'Tomorrow';
|
|
else label = d.toLocaleDateString('en-IN', { weekday: 'long', day: 'numeric', month: 'short' });
|
|
|
|
if (!map[label]) map[label] = { title: label, data: [], ts: d.getTime() };
|
|
map[label].data.push(f);
|
|
});
|
|
|
|
return Object.values(map).sort((a, b) => a.ts - b.ts);
|
|
};
|
|
|
|
const fetchTasks = async () => {
|
|
try {
|
|
const params = new URLSearchParams({ userId: userInfo.id });
|
|
if (activeFilter !== 'ALL') params.append('status', activeFilter);
|
|
const res = await api.get(`/followups?${params.toString()}`);
|
|
setSections(groupByDay(res.data));
|
|
} catch (e) {
|
|
console.error('TasksScreen fetch error', e);
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
|
|
|
|
const handleMarkDone = async (id) => {
|
|
Alert.alert('Mark as Done?', 'This will complete the task and dismiss the notification.', [
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
{
|
|
text: 'Done ✓', onPress: async () => {
|
|
try {
|
|
await api.patch(`/followups/${id}`, { status: 'DONE' });
|
|
fetchTasks();
|
|
} catch (e) {
|
|
Alert.alert('Error', 'Could not update task.');
|
|
}
|
|
}
|
|
}
|
|
]);
|
|
};
|
|
|
|
const handleCall = (phone) => {
|
|
if (!phone) return;
|
|
Linking.openURL(`tel:${phone}`);
|
|
};
|
|
|
|
const renderTask = ({ item }) => {
|
|
const isPending = item.status === 'PENDING';
|
|
const isOverdue = isPending && new Date(item.date) < new Date();
|
|
return (
|
|
<View style={[styles.card, isOverdue && styles.cardOverdue, !isPending && styles.cardDone]}>
|
|
<View style={[styles.dot, { backgroundColor: isOverdue ? '#ef4444' : isPending ? Colors.primary : '#10b981' }]} />
|
|
<View style={{ flex: 1 }}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<Text style={styles.clientName}>{item.client?.name || 'Unknown Client'}</Text>
|
|
{item.client?.phone && (
|
|
<TouchableOpacity onPress={() => handleCall(item.client.phone)}>
|
|
<Text style={styles.callIcon}>📞</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
<Text style={styles.notes} numberOfLines={2}>{item.notes}</Text>
|
|
<Text style={styles.time}>
|
|
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
{item.user?.name ? ` • Assigned by ${item.user.name}` : ''}
|
|
</Text>
|
|
</View>
|
|
{isPending && (
|
|
<TouchableOpacity style={styles.doneBtn} onPress={() => handleMarkDone(item.id)}>
|
|
<Text style={styles.doneBtnText}>Done</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
{!isPending && (
|
|
<View style={styles.completedBadge}>
|
|
<Text style={styles.completedText}>✓</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
|
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
|
<Text style={styles.headerTitle}>My Tasks</Text>
|
|
<Text style={styles.headerSub}>Sorted by date</Text>
|
|
<View style={styles.filterRow}>
|
|
{['ALL', 'PENDING', 'DONE'].map(f => (
|
|
<TouchableOpacity
|
|
key={f}
|
|
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
|
|
onPress={() => setActiveFilter(f)}
|
|
>
|
|
<Text style={[styles.filterText, activeFilter === f && styles.filterTextActive]}>{f}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<SectionList
|
|
sections={sections}
|
|
keyExtractor={item => item.id}
|
|
renderItem={renderTask}
|
|
renderSectionHeader={({ section }) => (
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>{section.title}</Text>
|
|
<Text style={styles.sectionCount}>{section.data.length} task{section.data.length !== 1 ? 's' : ''}</Text>
|
|
</View>
|
|
)}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
|
|
contentContainerStyle={{ paddingBottom: 40 }}
|
|
ListEmptyComponent={
|
|
<View style={styles.empty}>
|
|
<Text style={styles.emptyIcon}>🎉</Text>
|
|
<Text style={styles.emptyTitle}>All Clear!</Text>
|
|
<Text style={styles.emptySub}>No tasks match this filter.</Text>
|
|
</View>
|
|
}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#f1f5f9' },
|
|
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 },
|
|
headerTitle: { color: 'white', fontSize: 26, fontWeight: '900' },
|
|
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2, marginBottom: 14 },
|
|
filterRow: { flexDirection: 'row', gap: 8 },
|
|
filterBtn: { paddingHorizontal: 16, paddingVertical: 6, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' },
|
|
filterBtnActive: { backgroundColor: 'white' },
|
|
filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '700' },
|
|
filterTextActive: { color: Colors.primary },
|
|
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 20, paddingBottom: 8 },
|
|
sectionTitle: { fontSize: 13, fontWeight: '900', color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5 },
|
|
sectionCount: { fontSize: 11, color: '#94a3b8', fontWeight: '700' },
|
|
card: { backgroundColor: 'white', marginHorizontal: 16, marginBottom: 8, borderRadius: 14, padding: 14, flexDirection: 'row', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 4 },
|
|
cardOverdue: { borderLeftWidth: 4, borderLeftColor: '#ef4444' },
|
|
cardDone: { opacity: 0.65 },
|
|
dot: { width: 10, height: 10, borderRadius: 5, marginRight: 12 },
|
|
clientName: { fontSize: 14, fontWeight: '800', color: '#1e293b', marginBottom: 3, flex: 1 },
|
|
callIcon: { fontSize: 18, paddingHorizontal: 10 },
|
|
notes: { fontSize: 12, color: '#64748b', lineHeight: 17, marginBottom: 5 },
|
|
time: { fontSize: 10, color: '#94a3b8', fontWeight: '600' },
|
|
doneBtn: { backgroundColor: Colors.primary, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10, marginLeft: 10 },
|
|
doneBtnText: { color: 'white', fontSize: 11, fontWeight: '900' },
|
|
completedBadge: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 10 },
|
|
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 14 },
|
|
empty: { alignItems: 'center', paddingTop: 80 },
|
|
emptyIcon: { fontSize: 48, marginBottom: 12 },
|
|
emptyTitle: { fontSize: 18, fontWeight: '800', color: '#1e293b' },
|
|
emptySub: { fontSize: 13, color: '#94a3b8', marginTop: 6 },
|
|
});
|
|
|
|
export default TasksScreen;
|