igcrmmobile/src/screens/TasksScreen.js

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;