From 7e735e3e7d7dad6c767893dca23da7218da4e152 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 12 Jun 2026 09:56:27 +0530 Subject: [PATCH] changes till 12/06/2026 changes till 12/06/2026 --- src/config/env.js | 2 +- src/screens/TasksScreen.js | 436 +++++++++++++++++++++++++------------ 2 files changed, 292 insertions(+), 146 deletions(-) diff --git a/src/config/env.js b/src/config/env.js index 8ba1ad8..8d625f2 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -1,7 +1,7 @@ // Environment Configuration const ENV = { dev: { - API_URL: 'http://192.168.65.3:3000', // Local Dev IP + API_URL: 'http://192.168.50.27:3004', // Local Dev IP }, prod: { API_URL: 'https://crmapi.ignosimoney.in', // Change this to your public IP/Domain diff --git a/src/screens/TasksScreen.js b/src/screens/TasksScreen.js index 450b2f2..aefdf0a 100644 --- a/src/screens/TasksScreen.js +++ b/src/screens/TasksScreen.js @@ -1,7 +1,7 @@ import React, { useState, useCallback, useContext } from 'react'; import { - View, Text, StyleSheet, SectionList, TouchableOpacity, - RefreshControl, StatusBar, Alert, Linking + View, Text, StyleSheet, FlatList, TouchableOpacity, + RefreshControl, StatusBar, Alert, Linking, TextInput, ScrollView } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; import { AuthContext } from '../context/AuthContext'; @@ -19,7 +19,7 @@ const TYPE_ICONS = { 'VISIT_SCHEDULED': '📍', 'VISIT_COMPLETED': '🏁', 'NEGOTIATION': '🤝', - 'FOLLOWUP': '📅', + 'FOLLOWUP': '📌', 'DEMO': '📽️', 'QUOTE': '📝', }; @@ -39,41 +39,68 @@ const TYPE_COLORS = { 'QUOTE': '#a855f7', }; +const MANDATORY_TYPES = ['DEMO_COMPLETED', 'VISIT_COMPLETED', 'DEMO']; + +const CATEGORIES = [ + { key: 'today', label: "Today's Focus", color: '#16a34a', bg: '#dcfce7' }, + { key: 'tomorrow', label: 'Tomorrow', color: '#d97706', bg: '#fef3c7' }, + { key: 'thisWeek', label: 'Later This Week', color: '#2563eb', bg: '#dbeafe' }, + { key: 'upcoming', label: 'Upcoming', color: '#475569', bg: '#f1f5f9' }, + { key: 'expired', label: 'Expired Follow-ups', color: '#dc2626', bg: '#fee2e2' }, +]; + +const startOfDay = (d) => { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; }; +const endOfDay = (d) => { const x = new Date(d); x.setHours(23, 59, 59, 999); return x; }; + +const categorizeActivities = (followups) => { + const now = new Date(); + const todayStart = startOfDay(now); + const todayEnd = endOfDay(now); + + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStart = startOfDay(tomorrow); + const tomorrowEnd = endOfDay(tomorrow); + + // End of current ISO week (Sunday) + const weekEnd = new Date(todayStart); + const day = weekEnd.getDay(); // 0=Sun..6=Sat + const daysToSunday = day === 0 ? 0 : 7 - day; + weekEnd.setDate(weekEnd.getDate() + daysToSunday); + weekEnd.setHours(23, 59, 59, 999); + + const result = { today: [], tomorrow: [], thisWeek: [], upcoming: [], expired: [] }; + + followups.forEach(f => { + if (f.status === 'DONE' || f.status === 'EXPIRED') return; + const d = new Date(f.date); + if (d < todayStart) result.expired.push(f); + else if (d <= todayEnd) result.today.push(f); + else if (d <= tomorrowEnd) result.tomorrow.push(f); + else if (d <= weekEnd) result.thisWeek.push(f); + else result.upcoming.push(f); + }); + + return result; +}; + const TasksScreen = ({ navigation }) => { const { userInfo } = useContext(AuthContext); const insets = useSafeAreaInsets(); - const [sections, setSections] = useState([]); + + const [allActivities, setAllActivities] = useState([]); + const [categorized, setCategorized] = useState({ today: [], tomorrow: [], thisWeek: [], upcoming: [], expired: [] }); const [refreshing, setRefreshing] = useState(false); - const [activeFilter, setActiveFilter] = useState('PENDING'); // 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 [activeCategory, setActiveCategory] = useState('today'); + const [inlineRemarks, setInlineRemarks] = useState({}); + const [submitting, setSubmitting] = useState(null); // holds activity id being submitted const fetchTasks = async () => { try { - const params = new URLSearchParams({ userId: userInfo.id }); - if (activeFilter !== 'ALL') params.append('status', activeFilter); + const params = new URLSearchParams({ userId: userInfo.id, status: 'PENDING' }); const res = await api.get(`/followups?${params.toString()}`); - setSections(groupByDay(res.data)); + setAllActivities(res.data); + setCategorized(categorizeActivities(res.data)); } catch (e) { console.error('TasksScreen fetch error', e); } finally { @@ -81,74 +108,111 @@ const TasksScreen = ({ navigation }) => { } }; - useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter])); - - const handleMarkDone = async (item) => { - const isMandatory = ['DEMO_COMPLETED', 'VISIT_COMPLETED', 'DEMO'].includes(item.type); - - if (isMandatory) { - navigation.navigate('Feedback', { activity: item }); - return; - } - - Alert.alert('Complete Activity?', 'This will mark the activity as done and remove it from pending.', [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Complete ✓', onPress: async () => { - try { - await api.patch(`/followups/${item.id}`, { status: 'DONE' }); - fetchTasks(); - } catch (e) { - Alert.alert('Error', 'Could not update activity.'); - } - } - } - ]); - }; + useFocusEffect(useCallback(() => { fetchTasks(); }, [])); 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(); + const handleInlineMarkDone = async (item) => { + if (MANDATORY_TYPES.includes(item.type)) { + navigation.navigate('Feedback', { activity: item }); + return; + } + + setSubmitting(item.id); + try { + const payload = { status: 'DONE' }; + const remarks = inlineRemarks[item.id]; + if (remarks && remarks.trim()) payload.notes = remarks.trim(); + + await api.patch(`/followups/${item.id}`, payload); + + setInlineRemarks(prev => { + const updated = { ...prev }; + delete updated[item.id]; + return updated; + }); + fetchTasks(); + } catch (e) { + Alert.alert('Error', 'Could not update activity.'); + } finally { + setSubmitting(null); + } + }; + + const currentCat = CATEGORIES.find(c => c.key === activeCategory); + const currentItems = categorized[activeCategory] || []; + + const renderItem = ({ item }) => { const type = item.type || 'FOLLOWUP'; - + const isMandatory = MANDATORY_TYPES.includes(type); + return ( - - - {TYPE_ICONS[type]} - - - - {item.client?.companyName || item.client?.name || 'Unknown Client'} - {item.client?.phone && ( - handleCall(item.client.phone)} style={styles.callCircle}> - 📞 - + + {/* Card Header */} + + + {TYPE_ICONS[type] || '📌'} + + + + {item.client?.companyName || item.client?.name || 'Unknown Client'} + + + + + {type.replace(/_/g, ' ')} + + + + {new Date(item.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })} + {' '} + {new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + {item.opportunity && ( + 💼 {item.opportunity.title} )} - - {type.replace('_', ' ')} - - {item.notes} - - {new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - {item.user?.name ? ` • Assigned by ${item.user.name}` : ''} - + {item.client?.phone && ( + handleCall(item.client.phone)} style={styles.callCircle}> + 📞 + + )} - {isPending && ( - handleMarkDone(item.id)}> - Done + + {/* Notes */} + {!!item.notes && ( + "{item.notes}" + )} + + {/* Assigned user */} + + 👤 {item.user?.name || 'Unassigned'} + + + {/* Inline Remarks + Mark Done */} + + setInlineRemarks(prev => ({ ...prev, [item.id]: v }))} + editable={!isMandatory} + /> + handleInlineMarkDone(item)} + disabled={submitting === item.id} + > + + {isMandatory ? '📋 Open' : submitting === item.id ? '...' : '✓ Done'} + - )} - {!isPending && ( - - - - )} + ); }; @@ -156,49 +220,88 @@ const TasksScreen = ({ navigation }) => { return ( + + {/* Header */} - + Activities - Manage your schedule + Daily follow-up engine - navigation.navigate('LogActivity', { tab: 'followup' })} > + New - - {['PENDING', 'DONE', 'ALL'].map(f => ( - setActiveFilter(f)} - > - {f} - - ))} - + + {/* Category Picker */} + + {CATEGORIES.map(cat => { + const count = categorized[cat.key]?.length || 0; + const isActive = activeCategory === cat.key; + return ( + setActiveCategory(cat.key)} + > + + {cat.label} + + + {count} + + + ); + })} + - + + {currentCat.label} + + + {currentItems.length} {currentItems.length === 1 ? 'activity' : 'activities'} + + + + item.id} - renderItem={renderTask} - renderSectionHeader={({ section }) => ( - - {section.title} - {section.data.length} item{section.data.length !== 1 ? 's' : ''} - - )} - refreshControl={ { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />} - contentContainerStyle={{ paddingBottom: 40 }} + renderItem={renderItem} + refreshControl={ + { setRefreshing(true); fetchTasks(); }} + colors={[Colors.primary]} + /> + } + contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 12, paddingBottom: 40 }} ListEmptyComponent={ - - All Caught Up! - No activities scheduled here. + + {activeCategory === 'expired' ? '🎉' : '✨'} + + + {activeCategory === 'expired' ? 'No expired follow-ups!' : 'All clear here!'} + + + {activeCategory === 'expired' + ? 'You\'re on top of everything.' + : 'No activities in this category.'} + } /> @@ -208,39 +311,82 @@ const TasksScreen = ({ navigation }) => { const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f8fafc' }, - header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 }, - headerTitle: { color: 'white', fontSize: 28, fontWeight: '900' }, - headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 13, marginTop: 2, marginBottom: 16 }, - filterRow: { flexDirection: 'row', gap: 8 }, - filterBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' }, - filterBtnActive: { backgroundColor: 'white' }, - filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '800' }, - filterTextActive: { color: Colors.primary }, - addBtn: { backgroundColor: 'rgba(255,255,255,0.25)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 }, + + // Header + header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 12 }, + headerTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }, + headerTitle: { color: 'white', fontSize: 26, fontWeight: '900' }, + headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2 }, + addBtn: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 }, addBtnText: { color: 'white', fontWeight: '900', fontSize: 13 }, - sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 24, paddingBottom: 10 }, - sectionTitle: { fontSize: 12, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, - sectionCount: { fontSize: 11, color: '#94a3b8', fontWeight: '700' }, - card: { backgroundColor: 'white', marginHorizontal: 16, marginBottom: 10, borderRadius: 18, padding: 16, flexDirection: 'row', alignItems: 'center', elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 }, - cardOverdue: { borderLeftWidth: 5, borderLeftColor: '#ef4444' }, - cardDone: { opacity: 0.7 }, - typeIconBadge: { width: 44, height: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', marginRight: 14 }, - typeIconText: { fontSize: 20 }, - clientName: { fontSize: 15, fontWeight: '800', color: '#1e293b', marginBottom: 2, flex: 1 }, - callCircle: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' }, - callIcon: { fontSize: 14 }, - typeBadge: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, backgroundColor: '#f8fafc', marginBottom: 6 }, + + // Category picker + categoryScroll: { marginBottom: 4 }, + categoryScrollContent: { gap: 8, paddingRight: 4 }, + catBtn: { + flexDirection: 'row', alignItems: 'center', gap: 6, + paddingHorizontal: 12, paddingVertical: 7, + borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.18)', + }, + catBtnText: { fontSize: 11, fontWeight: '800', color: 'rgba(255,255,255,0.85)' }, + catCount: { minWidth: 18, height: 18, borderRadius: 9, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 4 }, + catCountText: { color: 'white', fontSize: 10, fontWeight: '900' }, + + // Section Banner + sectionBanner: { + flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + marginHorizontal: 16, marginTop: 12, marginBottom: 4, + paddingHorizontal: 14, paddingVertical: 10, + borderRadius: 10, borderLeftWidth: 4, + }, + sectionBannerTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionBannerCount: { fontSize: 11, fontWeight: '700' }, + + // Cards + card: { + backgroundColor: 'white', + borderRadius: 14, marginBottom: 12, + padding: 14, + borderLeftWidth: 4, + elevation: 2, + shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.06, shadowRadius: 6, + }, + cardHeader: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, marginBottom: 8 }, + typeIconBadge: { width: 40, height: 40, borderRadius: 12, alignItems: 'center', justifyContent: 'center', marginTop: 2 }, + typeIconText: { fontSize: 18 }, + clientName: { fontSize: 14, fontWeight: '800', color: '#1e293b', marginBottom: 4 }, + typeBadgeRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + typeBadge: { paddingHorizontal: 7, paddingVertical: 2, borderRadius: 6 }, typeText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' }, - notes: { fontSize: 13, color: '#475569', lineHeight: 18, marginBottom: 8 }, - time: { fontSize: 11, color: '#94a3b8', fontWeight: '600' }, - doneBtn: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 12, marginLeft: 12 }, - doneBtnText: { color: 'white', fontSize: 12, fontWeight: '900' }, - completedBadge: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 12 }, - completedText: { color: '#16a34a', fontWeight: '900', fontSize: 16 }, - empty: { alignItems: 'center', paddingTop: 100 }, - emptyIcon: { fontSize: 56, marginBottom: 16 }, - emptyTitle: { fontSize: 20, fontWeight: '900', color: '#1e293b' }, - emptySub: { fontSize: 14, color: '#64748b', marginTop: 8 }, + timeText: { fontSize: 10, color: '#94a3b8', fontWeight: '600' }, + oppText: { fontSize: 11, color: '#64748b', marginTop: 4, fontStyle: 'italic' }, + callCircle: { width: 34, height: 34, borderRadius: 17, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center', marginLeft: 4 }, + callIcon: { fontSize: 15 }, + + notes: { fontSize: 12, color: '#475569', fontStyle: 'italic', marginBottom: 6, lineHeight: 17 }, + assignedText: { fontSize: 11, color: '#94a3b8', fontWeight: '600', marginBottom: 10 }, + + // Inline action row + inlineRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + remarksInput: { + flex: 1, height: 36, + borderWidth: 1, borderColor: '#e2e8f0', + borderRadius: 8, paddingHorizontal: 10, + fontSize: 12, color: '#1e293b', + backgroundColor: '#f8fafc', + }, + remarksInputDisabled: { backgroundColor: '#f1f5f9', color: '#94a3b8' }, + doneBtn: { + paddingHorizontal: 12, paddingVertical: 8, + borderRadius: 8, + }, + doneBtnText: { color: 'white', fontSize: 11, fontWeight: '900' }, + + // Empty + empty: { alignItems: 'center', paddingTop: 80 }, + emptyIcon: { fontSize: 52, marginBottom: 14 }, + emptyTitle: { fontSize: 18, fontWeight: '900', color: '#1e293b' }, + emptySub: { fontSize: 13, color: '#64748b', marginTop: 6, textAlign: 'center' }, }); export default TasksScreen;