parent
69f1ee64fb
commit
7e735e3e7d
|
|
@ -1,7 +1,7 @@
|
||||||
// Environment Configuration
|
// Environment Configuration
|
||||||
const ENV = {
|
const ENV = {
|
||||||
dev: {
|
dev: {
|
||||||
API_URL: 'http://192.168.65.3:3000', // Local Dev IP
|
API_URL: 'http://192.168.50.27:3004', // Local Dev IP
|
||||||
},
|
},
|
||||||
prod: {
|
prod: {
|
||||||
API_URL: 'https://crmapi.ignosimoney.in', // Change this to your public IP/Domain
|
API_URL: 'https://crmapi.ignosimoney.in', // Change this to your public IP/Domain
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useCallback, useContext } from 'react';
|
import React, { useState, useCallback, useContext } from 'react';
|
||||||
import {
|
import {
|
||||||
View, Text, StyleSheet, SectionList, TouchableOpacity,
|
View, Text, StyleSheet, FlatList, TouchableOpacity,
|
||||||
RefreshControl, StatusBar, Alert, Linking
|
RefreshControl, StatusBar, Alert, Linking, TextInput, ScrollView
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { AuthContext } from '../context/AuthContext';
|
import { AuthContext } from '../context/AuthContext';
|
||||||
|
|
@ -19,7 +19,7 @@ const TYPE_ICONS = {
|
||||||
'VISIT_SCHEDULED': '📍',
|
'VISIT_SCHEDULED': '📍',
|
||||||
'VISIT_COMPLETED': '🏁',
|
'VISIT_COMPLETED': '🏁',
|
||||||
'NEGOTIATION': '🤝',
|
'NEGOTIATION': '🤝',
|
||||||
'FOLLOWUP': '📅',
|
'FOLLOWUP': '📌',
|
||||||
'DEMO': '📽️',
|
'DEMO': '📽️',
|
||||||
'QUOTE': '📝',
|
'QUOTE': '📝',
|
||||||
};
|
};
|
||||||
|
|
@ -39,41 +39,68 @@ const TYPE_COLORS = {
|
||||||
'QUOTE': '#a855f7',
|
'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 TasksScreen = ({ navigation }) => {
|
||||||
const { userInfo } = useContext(AuthContext);
|
const { userInfo } = useContext(AuthContext);
|
||||||
const insets = useSafeAreaInsets();
|
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 [refreshing, setRefreshing] = useState(false);
|
||||||
const [activeFilter, setActiveFilter] = useState('PENDING'); // ALL, PENDING, DONE
|
const [activeCategory, setActiveCategory] = useState('today');
|
||||||
|
const [inlineRemarks, setInlineRemarks] = useState({});
|
||||||
const groupByDay = (followups) => {
|
const [submitting, setSubmitting] = useState(null); // holds activity id being submitted
|
||||||
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 () => {
|
const fetchTasks = async () => {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ userId: userInfo.id });
|
const params = new URLSearchParams({ userId: userInfo.id, status: 'PENDING' });
|
||||||
if (activeFilter !== 'ALL') params.append('status', activeFilter);
|
|
||||||
const res = await api.get(`/followups?${params.toString()}`);
|
const res = await api.get(`/followups?${params.toString()}`);
|
||||||
setSections(groupByDay(res.data));
|
setAllActivities(res.data);
|
||||||
|
setCategorized(categorizeActivities(res.data));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('TasksScreen fetch error', e);
|
console.error('TasksScreen fetch error', e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -81,74 +108,111 @@ const TasksScreen = ({ navigation }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
|
useFocusEffect(useCallback(() => { fetchTasks(); }, []));
|
||||||
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCall = (phone) => {
|
const handleCall = (phone) => {
|
||||||
if (!phone) return;
|
if (!phone) return;
|
||||||
Linking.openURL(`tel:${phone}`);
|
Linking.openURL(`tel:${phone}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTask = ({ item }) => {
|
const handleInlineMarkDone = async (item) => {
|
||||||
const isPending = item.status === 'PENDING';
|
if (MANDATORY_TYPES.includes(item.type)) {
|
||||||
const isOverdue = isPending && new Date(item.date) < new Date();
|
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 type = item.type || 'FOLLOWUP';
|
||||||
|
const isMandatory = MANDATORY_TYPES.includes(type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.card, isOverdue && styles.cardOverdue, !isPending && styles.cardDone]}>
|
<View style={[styles.card, { borderLeftColor: currentCat.color }]}>
|
||||||
<View style={[styles.typeIconBadge, { backgroundColor: TYPE_COLORS[type] + '20' }]}>
|
{/* Card Header */}
|
||||||
<Text style={styles.typeIconText}>{TYPE_ICONS[type]}</Text>
|
<View style={styles.cardHeader}>
|
||||||
</View>
|
<View style={[styles.typeIconBadge, { backgroundColor: (TYPE_COLORS[type] || '#64748b') + '20' }]}>
|
||||||
<View style={{ flex: 1 }}>
|
<Text style={styles.typeIconText}>{TYPE_ICONS[type] || '📌'}</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
</View>
|
||||||
<Text style={styles.clientName}>{item.client?.companyName || item.client?.name || 'Unknown Client'}</Text>
|
<View style={{ flex: 1 }}>
|
||||||
{item.client?.phone && (
|
<Text style={styles.clientName} numberOfLines={1}>
|
||||||
<TouchableOpacity onPress={() => handleCall(item.client.phone)} style={styles.callCircle}>
|
{item.client?.companyName || item.client?.name || 'Unknown Client'}
|
||||||
<Text style={styles.callIcon}>📞</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
<View style={styles.typeBadgeRow}>
|
||||||
|
<View style={[styles.typeBadge, { backgroundColor: (TYPE_COLORS[type] || '#64748b') + '15' }]}>
|
||||||
|
<Text style={[styles.typeText, { color: TYPE_COLORS[type] || '#64748b' }]}>
|
||||||
|
{type.replace(/_/g, ' ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.timeText}>
|
||||||
|
{new Date(item.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}
|
||||||
|
{' '}
|
||||||
|
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{item.opportunity && (
|
||||||
|
<Text style={styles.oppText} numberOfLines={1}>💼 {item.opportunity.title}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.typeBadge}>
|
{item.client?.phone && (
|
||||||
<Text style={[styles.typeText, { color: TYPE_COLORS[type] || '#64748b' }]}>{type.replace('_', ' ')}</Text>
|
<TouchableOpacity onPress={() => handleCall(item.client.phone)} style={styles.callCircle}>
|
||||||
</View>
|
<Text style={styles.callIcon}>📞</Text>
|
||||||
<Text style={styles.notes} numberOfLines={2}>{item.notes}</Text>
|
</TouchableOpacity>
|
||||||
<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>
|
</View>
|
||||||
{isPending && (
|
|
||||||
<TouchableOpacity style={[styles.doneBtn, { backgroundColor: TYPE_COLORS[type] }]} onPress={() => handleMarkDone(item.id)}>
|
{/* Notes */}
|
||||||
<Text style={styles.doneBtnText}>Done</Text>
|
{!!item.notes && (
|
||||||
|
<Text style={styles.notes} numberOfLines={2}>"{item.notes}"</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Assigned user */}
|
||||||
|
<Text style={styles.assignedText}>
|
||||||
|
👤 {item.user?.name || 'Unassigned'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Inline Remarks + Mark Done */}
|
||||||
|
<View style={styles.inlineRow}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.remarksInput, isMandatory && styles.remarksInputDisabled]}
|
||||||
|
placeholder={isMandatory ? 'Needs feedback modal →' : 'Add remarks...'}
|
||||||
|
placeholderTextColor={isMandatory ? '#cbd5e1' : '#94a3b8'}
|
||||||
|
value={inlineRemarks[item.id] || ''}
|
||||||
|
onChangeText={v => setInlineRemarks(prev => ({ ...prev, [item.id]: v }))}
|
||||||
|
editable={!isMandatory}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.doneBtn, { backgroundColor: currentCat.color }, submitting === item.id && { opacity: 0.6 }]}
|
||||||
|
onPress={() => handleInlineMarkDone(item)}
|
||||||
|
disabled={submitting === item.id}
|
||||||
|
>
|
||||||
|
<Text style={styles.doneBtnText}>
|
||||||
|
{isMandatory ? '📋 Open' : submitting === item.id ? '...' : '✓ Done'}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
</View>
|
||||||
{!isPending && (
|
|
||||||
<View style={styles.completedBadge}>
|
|
||||||
<Text style={styles.completedText}>✓</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -156,49 +220,88 @@ const TasksScreen = ({ navigation }) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
<View style={styles.headerTop}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.headerTitle}>Activities</Text>
|
<Text style={styles.headerTitle}>Activities</Text>
|
||||||
<Text style={styles.headerSub}>Manage your schedule</Text>
|
<Text style={styles.headerSub}>Daily follow-up engine</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.addBtn}
|
style={styles.addBtn}
|
||||||
onPress={() => navigation.navigate('LogActivity', { tab: 'followup' })}
|
onPress={() => navigation.navigate('LogActivity', { tab: 'followup' })}
|
||||||
>
|
>
|
||||||
<Text style={styles.addBtnText}>+ New</Text>
|
<Text style={styles.addBtnText}>+ New</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.filterRow}>
|
|
||||||
{['PENDING', 'DONE', 'ALL'].map(f => (
|
{/* Category Picker */}
|
||||||
<TouchableOpacity
|
<ScrollView
|
||||||
key={f}
|
horizontal
|
||||||
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
|
showsHorizontalScrollIndicator={false}
|
||||||
onPress={() => setActiveFilter(f)}
|
style={styles.categoryScroll}
|
||||||
>
|
contentContainerStyle={styles.categoryScrollContent}
|
||||||
<Text style={[styles.filterText, activeFilter === f && styles.filterTextActive]}>{f}</Text>
|
>
|
||||||
</TouchableOpacity>
|
{CATEGORIES.map(cat => {
|
||||||
))}
|
const count = categorized[cat.key]?.length || 0;
|
||||||
</View>
|
const isActive = activeCategory === cat.key;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={cat.key}
|
||||||
|
style={[
|
||||||
|
styles.catBtn,
|
||||||
|
isActive && { backgroundColor: 'white' },
|
||||||
|
]}
|
||||||
|
onPress={() => setActiveCategory(cat.key)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.catBtnText, isActive && { color: cat.color }]}>
|
||||||
|
{cat.label}
|
||||||
|
</Text>
|
||||||
|
<View style={[styles.catCount, { backgroundColor: isActive ? cat.color : 'rgba(255,255,255,0.25)' }]}>
|
||||||
|
<Text style={styles.catCountText}>{count}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<SectionList
|
{/* Section Header */}
|
||||||
sections={sections}
|
<View style={[styles.sectionBanner, { backgroundColor: currentCat.bg, borderLeftColor: currentCat.color }]}>
|
||||||
|
<Text style={[styles.sectionBannerTitle, { color: currentCat.color }]}>
|
||||||
|
{currentCat.label}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.sectionBannerCount, { color: currentCat.color }]}>
|
||||||
|
{currentItems.length} {currentItems.length === 1 ? 'activity' : 'activities'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={currentItems}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderTask}
|
renderItem={renderItem}
|
||||||
renderSectionHeader={({ section }) => (
|
refreshControl={
|
||||||
<View style={styles.sectionHeader}>
|
<RefreshControl
|
||||||
<Text style={styles.sectionTitle}>{section.title}</Text>
|
refreshing={refreshing}
|
||||||
<Text style={styles.sectionCount}>{section.data.length} item{section.data.length !== 1 ? 's' : ''}</Text>
|
onRefresh={() => { setRefreshing(true); fetchTasks(); }}
|
||||||
</View>
|
colors={[Colors.primary]}
|
||||||
)}
|
/>
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
|
}
|
||||||
contentContainerStyle={{ paddingBottom: 40 }}
|
contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 12, paddingBottom: 40 }}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.empty}>
|
<View style={styles.empty}>
|
||||||
<Text style={styles.emptyIcon}>✨</Text>
|
<Text style={styles.emptyIcon}>
|
||||||
<Text style={styles.emptyTitle}>All Caught Up!</Text>
|
{activeCategory === 'expired' ? '🎉' : '✨'}
|
||||||
<Text style={styles.emptySub}>No activities scheduled here.</Text>
|
</Text>
|
||||||
|
<Text style={styles.emptyTitle}>
|
||||||
|
{activeCategory === 'expired' ? 'No expired follow-ups!' : 'All clear here!'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.emptySub}>
|
||||||
|
{activeCategory === 'expired'
|
||||||
|
? 'You\'re on top of everything.'
|
||||||
|
: 'No activities in this category.'}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -208,39 +311,82 @@ const TasksScreen = ({ navigation }) => {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#f8fafc' },
|
container: { flex: 1, backgroundColor: '#f8fafc' },
|
||||||
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 },
|
|
||||||
headerTitle: { color: 'white', fontSize: 28, fontWeight: '900' },
|
// Header
|
||||||
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 13, marginTop: 2, marginBottom: 16 },
|
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 12 },
|
||||||
filterRow: { flexDirection: 'row', gap: 8 },
|
headerTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
|
||||||
filterBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' },
|
headerTitle: { color: 'white', fontSize: 26, fontWeight: '900' },
|
||||||
filterBtnActive: { backgroundColor: 'white' },
|
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2 },
|
||||||
filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '800' },
|
addBtn: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 },
|
||||||
filterTextActive: { color: Colors.primary },
|
|
||||||
addBtn: { backgroundColor: 'rgba(255,255,255,0.25)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 },
|
|
||||||
addBtnText: { color: 'white', fontWeight: '900', fontSize: 13 },
|
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 },
|
// Category picker
|
||||||
sectionCount: { fontSize: 11, color: '#94a3b8', fontWeight: '700' },
|
categoryScroll: { marginBottom: 4 },
|
||||||
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 },
|
categoryScrollContent: { gap: 8, paddingRight: 4 },
|
||||||
cardOverdue: { borderLeftWidth: 5, borderLeftColor: '#ef4444' },
|
catBtn: {
|
||||||
cardDone: { opacity: 0.7 },
|
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||||
typeIconBadge: { width: 44, height: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', marginRight: 14 },
|
paddingHorizontal: 12, paddingVertical: 7,
|
||||||
typeIconText: { fontSize: 20 },
|
borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.18)',
|
||||||
clientName: { fontSize: 15, fontWeight: '800', color: '#1e293b', marginBottom: 2, flex: 1 },
|
},
|
||||||
callCircle: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
|
catBtnText: { fontSize: 11, fontWeight: '800', color: 'rgba(255,255,255,0.85)' },
|
||||||
callIcon: { fontSize: 14 },
|
catCount: { minWidth: 18, height: 18, borderRadius: 9, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 4 },
|
||||||
typeBadge: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, backgroundColor: '#f8fafc', marginBottom: 6 },
|
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' },
|
typeText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
|
||||||
notes: { fontSize: 13, color: '#475569', lineHeight: 18, marginBottom: 8 },
|
timeText: { fontSize: 10, color: '#94a3b8', fontWeight: '600' },
|
||||||
time: { fontSize: 11, color: '#94a3b8', fontWeight: '600' },
|
oppText: { fontSize: 11, color: '#64748b', marginTop: 4, fontStyle: 'italic' },
|
||||||
doneBtn: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 12, marginLeft: 12 },
|
callCircle: { width: 34, height: 34, borderRadius: 17, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center', marginLeft: 4 },
|
||||||
doneBtnText: { color: 'white', fontSize: 12, fontWeight: '900' },
|
callIcon: { fontSize: 15 },
|
||||||
completedBadge: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 12 },
|
|
||||||
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 16 },
|
notes: { fontSize: 12, color: '#475569', fontStyle: 'italic', marginBottom: 6, lineHeight: 17 },
|
||||||
empty: { alignItems: 'center', paddingTop: 100 },
|
assignedText: { fontSize: 11, color: '#94a3b8', fontWeight: '600', marginBottom: 10 },
|
||||||
emptyIcon: { fontSize: 56, marginBottom: 16 },
|
|
||||||
emptyTitle: { fontSize: 20, fontWeight: '900', color: '#1e293b' },
|
// Inline action row
|
||||||
emptySub: { fontSize: 14, color: '#64748b', marginTop: 8 },
|
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;
|
export default TasksScreen;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue