changes till 09/05/2026

New clients creation from opportunities, client conversion%, time taken for conversion, close the modal when touched outside it
   Client and company name separate, Demo becomes a separate activity, all changes done in mobile app as well
2) transfer of clients, demos followups negotiation etc scheduling, quote opportunity in place of enquiry, In opportunity new product add,           	existing dropdown, added option for adding documents on client creation and showing it
main
Manu Krishna 2026-05-09 15:22:21 +05:30
parent 22f0d20020
commit de97592ded
12 changed files with 1676 additions and 787 deletions

38
package-lock.json generated
View File

@ -9,6 +9,8 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/datetimepicker": "^9.1.0",
"@react-native-documents/picker": "^12.0.1",
"@react-native/new-app-screen": "0.83.1", "@react-native/new-app-screen": "0.83.1",
"@react-navigation/bottom-tabs": "^7.15.9", "@react-navigation/bottom-tabs": "^7.15.9",
"@react-navigation/native": "^7.1.26", "@react-navigation/native": "^7.1.26",
@ -2976,6 +2978,42 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@react-native-community/datetimepicker": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-9.1.0.tgz",
"integrity": "sha512-eadbnk+I2vxvW30iTAsm/qlCnMMAadkifIMYNEB2lzhxN/SvlKc7S2V4k5DyrwjdCbqdcMk3t9K6fnUMcAV34w==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
},
"peerDependencies": {
"expo": ">=52.0.0",
"react": "*",
"react-native": "*",
"react-native-windows": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"react-native-windows": {
"optional": true
}
}
},
"node_modules/@react-native-documents/picker": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@react-native-documents/picker/-/picker-12.0.1.tgz",
"integrity": "sha512-vpJKb4t/5bnxe9+gQl+plJfKrrIsmYwANGhNH2B9E1dS1+6FDBzg4Dwmcq4ueaGfkRKEPJ606mJttVEH1ZKZaA==",
"license": "MIT",
"funding": {
"url": "https://github.com/react-native-documents/document-picker?sponsor=1"
},
"peerDependencies": {
"react": "*",
"react-native": ">=0.79.0"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.83.1", "version": "0.83.1",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.1.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.1.tgz",

View File

@ -11,6 +11,8 @@
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/datetimepicker": "^9.1.0",
"@react-native-documents/picker": "^12.0.1",
"@react-native/new-app-screen": "0.83.1", "@react-native/new-app-screen": "0.83.1",
"@react-navigation/bottom-tabs": "^7.15.9", "@react-navigation/bottom-tabs": "^7.15.9",
"@react-navigation/native": "^7.1.26", "@react-navigation/native": "^7.1.26",

View File

@ -16,8 +16,7 @@ import ClientDetailsScreen from '../screens/ClientDetailsScreen';
import EditClientScreen from '../screens/EditClientScreen'; import EditClientScreen from '../screens/EditClientScreen';
import PipelineScreen from '../screens/PipelineScreen'; import PipelineScreen from '../screens/PipelineScreen';
import EnquiryScreen from '../screens/EnquiryScreen'; import AddOpportunityScreen from '../screens/AddOpportunityScreen';
import EnquiryListScreen from '../screens/EnquiryListScreen';
import ExpenseScreen from '../screens/ExpenseScreen'; import ExpenseScreen from '../screens/ExpenseScreen';
import IncentiveScreen from '../screens/IncentiveScreen'; import IncentiveScreen from '../screens/IncentiveScreen';
import LogActivityScreen from '../screens/LogActivityScreen'; import LogActivityScreen from '../screens/LogActivityScreen';
@ -52,7 +51,7 @@ const TabNavigator = () => (
<Tab.Screen name="Dashboard" component={HomeScreen} /> <Tab.Screen name="Dashboard" component={HomeScreen} />
<Tab.Screen name="Pipeline" component={PipelineScreen} /> <Tab.Screen name="Pipeline" component={PipelineScreen} />
<Tab.Screen name="Clients" component={ClientListScreen} /> <Tab.Screen name="Clients" component={ClientListScreen} />
<Tab.Screen name="Tasks" component={TasksScreen} /> <Tab.Screen name="Activities" component={TasksScreen} />
</Tab.Navigator> </Tab.Navigator>
); );
@ -77,8 +76,7 @@ const AppNav = () => {
<Stack.Screen name="AddClient" component={AddClientScreen} /> <Stack.Screen name="AddClient" component={AddClientScreen} />
<Stack.Screen name="ClientDetails" component={ClientDetailsScreen} options={{ title: 'Client Details' }} /> <Stack.Screen name="ClientDetails" component={ClientDetailsScreen} options={{ title: 'Client Details' }} />
<Stack.Screen name="EditClient" component={EditClientScreen} options={{ title: 'Edit Client' }} /> <Stack.Screen name="EditClient" component={EditClientScreen} options={{ title: 'Edit Client' }} />
<Stack.Screen name="EnquiryList" component={EnquiryListScreen} options={{ title: 'Enquiries' }} /> <Stack.Screen name="AddOpportunity" component={AddOpportunityScreen} options={{ headerShown: false }} />
<Stack.Screen name="Enquiry" component={EnquiryScreen} options={{ title: 'Add Enquiry' }} />
<Stack.Screen name="Expense" component={ExpenseScreen} /> <Stack.Screen name="Expense" component={ExpenseScreen} />
<Stack.Screen name="Incentive" component={IncentiveScreen} /> <Stack.Screen name="Incentive" component={IncentiveScreen} />
<Stack.Screen name="LogActivity" component={LogActivityScreen} options={{ title: 'Log Activity' }} /> <Stack.Screen name="LogActivity" component={LogActivityScreen} options={{ title: 'Log Activity' }} />

View File

@ -1,18 +1,39 @@
import React, { useState } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { View, Text, TextInput, Button, StyleSheet, Alert, ScrollView, Platform, PermissionsAndroid, ActivityIndicator } from 'react-native'; import {
View, Text, TextInput, Button, StyleSheet, Alert, ScrollView,
Platform, PermissionsAndroid, ActivityIndicator, TouchableOpacity, Modal, FlatList
} from 'react-native';
import Geolocation from 'react-native-geolocation-service'; import Geolocation from 'react-native-geolocation-service';
import { pick } from '@react-native-documents/picker';
import api from '../services/api'; import api from '../services/api';
import Colors from '../constants/Colors'; import Colors from '../constants/Colors';
import { AuthContext } from '../context/AuthContext';
const AddClientScreen = ({ navigation }) => { const AddClientScreen = ({ navigation }) => {
const [name, setName] = useState(''); const { userInfo } = useContext(AuthContext);
const [companyName, setCompanyName] = useState('');
const [contactName, setContactName] = useState('');
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [address, setAddress] = useState(''); const [address, setAddress] = useState('');
const [landmark, setLandmark] = useState(''); const [landmark, setLandmark] = useState('');
const [closingProbability, setClosingProbability] = useState('');
const [expectedClosingTimeframe, setExpectedClosingTimeframe] = useState('');
const [isDemoDone, setIsDemoDone] = useState(false);
const [location, setLocation] = useState(null); const [location, setLocation] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [locating, setLocating] = useState(false); const [locating, setLocating] = useState(false);
const [selectedFiles, setSelectedFiles] = useState([]);
// Assignment state
const [users, setUsers] = useState([]);
const [assignedUser, setAssignedUser] = useState(null);
const [userModal, setUserModal] = useState(false);
useEffect(() => {
setAssignedUser({ id: userInfo?.id, name: 'Myself' });
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
}, [userInfo]);
const requestLocationPermission = async () => { const requestLocationPermission = async () => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
@ -43,13 +64,11 @@ const AddClientScreen = ({ navigation }) => {
setLocating(true); setLocating(true);
Geolocation.getCurrentPosition( Geolocation.getCurrentPosition(
(position) => { (position) => {
console.log('Location success:', position);
setLocation(position.coords); setLocation(position.coords);
setLocating(false); setLocating(false);
Alert.alert("Success", "Location Captured!"); Alert.alert("Success", "Location Captured!");
}, },
(error) => { (error) => {
console.log('Location error:', error);
setLocating(false); setLocating(false);
Alert.alert("Location Error", error.message); Alert.alert("Location Error", error.message);
}, },
@ -57,30 +76,71 @@ const AddClientScreen = ({ navigation }) => {
); );
}; };
const pickFiles = async () => {
try {
const results = await pick({
multiple: true,
});
const uploadedFiles = [];
for (const res of results) {
const formData = new FormData();
formData.append('file', {
uri: Platform.OS === 'ios' ? res.uri.replace('file://', '') : res.uri,
type: res.type || 'application/octet-stream',
name: res.name || 'file',
});
try {
const uploadRes = await api.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
uploadedFiles.push({
name: res.name,
type: res.type,
size: res.size,
url: uploadRes.data.url
});
} catch (err) {
console.error('Upload failed', err);
Alert.alert('Upload Failed', `Could not upload ${res.name}`);
}
}
setSelectedFiles([...selectedFiles, ...uploadedFiles]);
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
console.error(err);
}
}
};
const removeFile = (index) => {
setSelectedFiles(selectedFiles.filter((_, i) => i !== index));
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!name || !phone) { if (!contactName || !phone) {
Alert.alert("Error", "Name and Phone are required"); Alert.alert("Error", "Contact Name and Phone are required");
return; return;
} }
console.log('Current Location Check Before Submit:', location);
if (!location) {
Alert.alert("Debug", "Location state is null! Did you click capture?");
}
const payload = { const payload = {
name, name: companyName || contactName,
companyName,
contactName,
phone, phone,
status: 'LEAD', status: 'LEAD',
assignedTo: assignedUser?.id || userInfo?.id,
closingProbability: closingProbability ? parseInt(closingProbability) : 0,
expectedClosingTimeframe,
isDemoDone,
files: selectedFiles,
...(email ? { email } : {}), ...(email ? { email } : {}),
...(address ? { address } : {}), ...(address ? { address } : {}),
...(landmark ? { landmark } : {}), ...(landmark ? { landmark } : {}),
...(location ? { lat: location.latitude, lng: location.longitude } : {}) ...(location ? { lat: location.latitude, lng: location.longitude } : {})
}; };
console.log('Submitting Payload:', JSON.stringify(payload, null, 2));
setLoading(true); setLoading(true);
try { try {
await api.post('/clients', payload); await api.post('/clients', payload);
@ -97,76 +157,122 @@ const AddClientScreen = ({ navigation }) => {
return ( return (
<ScrollView contentContainerStyle={styles.container}> <ScrollView contentContainerStyle={styles.container}>
<Text style={styles.label}>Name *</Text> <Text style={styles.label}>Company Name</Text>
<TextInput style={styles.input} value={name} onChangeText={setName} /> <TextInput style={styles.input} value={companyName} onChangeText={setCompanyName} placeholder="Enter company name" />
<Text style={styles.label}>Contact Name *</Text>
<TextInput style={styles.input} value={contactName} onChangeText={setContactName} placeholder="Enter contact person name" />
<Text style={styles.label}>Phone *</Text> <Text style={styles.label}>Phone *</Text>
<TextInput style={styles.input} value={phone} onChangeText={setPhone} keyboardType="phone-pad" /> <TextInput style={styles.input} value={phone} onChangeText={setPhone} placeholder="Enter phone number" keyboardType="phone-pad" />
<Text style={styles.label}>Assigned To</Text>
<TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
<Text style={styles.pickerBtnText}>{assignedUser?.name || 'Myself'}</Text>
<Text style={styles.pickerArrow}></Text>
</TouchableOpacity>
<Text style={styles.label}>Email</Text> <Text style={styles.label}>Email</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} keyboardType="email-address" /> <TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="Enter email address" keyboardType="email-address" />
<Text style={styles.label}>Address</Text> <Text style={styles.label}>Address</Text>
<TextInput style={styles.input} value={address} onChangeText={setAddress} multiline /> <TextInput style={[styles.input, { height: 80 }]} value={address} onChangeText={setAddress} placeholder="Enter address" multiline />
<Text style={styles.label}>Landmark</Text> <Text style={styles.label}>Landmark</Text>
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} /> <TextInput style={styles.input} value={landmark} onChangeText={setLandmark} placeholder="Enter nearby landmark" />
<View style={styles.locationContainer}> <TouchableOpacity style={[styles.locationBtn, { backgroundColor: location ? '#16a34a' : Colors.secondary }]} onPress={getCurrentLocation} disabled={locating}>
<Button {locating ? <ActivityIndicator color="white" /> : <Text style={styles.locationBtnText}>{location ? "✓ Location Captured" : "📍 Capture Current Location"}</Text>}
title={locating ? "Locating..." : (location ? "Update Location" : "Capture Location")} </TouchableOpacity>
onPress={getCurrentLocation}
disabled={locating} <View style={styles.fileSection}>
color={Colors.secondary} <View style={styles.fileHeader}>
/> <Text style={styles.label}>Files / Attachments</Text>
{location && ( <TouchableOpacity onPress={pickFiles} style={styles.addFileBtn}>
<Text style={styles.locationText}> <Text style={styles.addFileBtnText}>+ ADD FILE</Text>
Lat: {location.latitude.toFixed(4)}, Lng: {location.longitude.toFixed(4)} </TouchableOpacity>
</Text> </View>
{selectedFiles.map((file, index) => (
<View key={index} style={styles.fileRow}>
<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
<Text style={styles.fileSize}>{(file.size / 1024).toFixed(1)} KB</Text>
</View>
<TouchableOpacity onPress={() => removeFile(index)}>
<Text style={styles.removeFile}></Text>
</TouchableOpacity>
</View>
))}
{selectedFiles.length === 0 && (
<Text style={styles.noFiles}>No files attached yet</Text>
)} )}
</View> </View>
<View style={styles.spacer} /> <TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>Add Client</Text>}
</TouchableOpacity>
<Button title={loading ? "Saving..." : "Save Client"} onPress={handleSubmit} disabled={loading} color={Colors.primary} /> <Modal visible={userModal} animationType="slide" transparent={true}>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Assign To</Text>
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<FlatList
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity style={styles.userRow} onPress={() => { setAssignedUser(item); setUserModal(false); }}>
<View style={styles.userAvatar}>
<Text style={styles.userAvatarText}>{item.name?.charAt(0)}</Text>
</View>
<View>
<Text style={styles.userName}>{item.name}</Text>
<Text style={styles.userRole}>{item.role}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
</View>
</Modal>
</ScrollView> </ScrollView>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: { padding: 20, backgroundColor: '#fff' },
padding: 20, label: { fontSize: 13, fontWeight: 'bold', color: '#64748b', marginBottom: 6, marginTop: 15, textTransform: 'uppercase' },
backgroundColor: Colors.background input: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, fontSize: 15, backgroundColor: '#f8fafc' },
}, pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
label: { pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '600' },
fontSize: 16, pickerArrow: { fontSize: 20, color: '#94a3b8' },
marginBottom: 5, locationBtn: { padding: 15, borderRadius: 12, marginTop: 20, alignItems: 'center' },
fontWeight: 'bold', locationBtnText: { color: 'white', fontWeight: 'bold' },
color: Colors.text submitBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 12, marginTop: 30, alignItems: 'center', marginBottom: 50 },
}, submitBtnText: { color: 'white', fontSize: 16, fontWeight: 'bold' },
input: { modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
borderWidth: 1, modalContent: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '70%', paddingBottom: 20 },
borderColor: Colors.border, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
borderRadius: 5, modalTitle: { fontSize: 18, fontWeight: 'bold', color: '#1e293b' },
padding: 10, modalClose: { fontSize: 20, color: '#94a3b8', padding: 5 },
marginBottom: 15, userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
backgroundColor: 'white', userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
color: Colors.text userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
}, userName: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
locationContainer: { userRole: { fontSize: 12, color: '#64748b' },
marginBottom: 20, fileSection: { marginTop: 20, padding: 15, backgroundColor: '#f8fafc', borderRadius: 12, borderStyle: 'dashed', borderWidth: 1.5, borderColor: '#e2e8f0' },
padding: 10, fileHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
backgroundColor: Colors.backgroundSecondary, addFileBtn: { backgroundColor: '#10b981', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 },
borderRadius: 5 addFileBtnText: { color: 'white', fontSize: 11, fontWeight: 'bold' },
}, fileRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: 'white', padding: 10, borderRadius: 8, marginBottom: 8, borderWidth: 1, borderColor: '#f1f5f9' },
locationText: { fileInfo: { flex: 1, marginRight: 10 },
marginTop: 5, fileName: { fontSize: 13, fontWeight: '600', color: '#1e293b' },
textAlign: 'center', fileSize: { fontSize: 10, color: '#94a3b8', marginTop: 2 },
color: Colors.textMuted removeFile: { color: '#ef4444', fontSize: 16, fontWeight: 'bold', padding: 5 },
}, noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 11, paddingVertical: 10, fontStyle: 'italic' }
spacer: {
height: 20
}
}); });
export default AddClientScreen; export default AddClientScreen;

View File

@ -0,0 +1,392 @@
import React, { useState, useEffect, useContext } from 'react';
import {
View, Text, TextInput, StyleSheet, Alert, ScrollView,
TouchableOpacity, Modal, FlatList, ActivityIndicator, StatusBar
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import DateTimePicker from '@react-native-community/datetimepicker';
import api from '../services/api';
import { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors';
const STAGES = [
{ id: 'NEW', label: 'New', color: '#6366f1' },
{ id: 'QUALIFIED', label: 'Qualified', color: '#3b82f6' },
{ id: 'PROPOSITION', label: 'Proposition', color: '#f59e0b' },
{ id: 'WON', label: 'Won', color: '#10b981' },
{ id: 'LOST', label: 'Lost', color: '#ef4444' },
];
const AddOpportunityScreen = ({ navigation, route }) => {
const insets = useSafeAreaInsets();
const { userInfo } = useContext(AuthContext);
const preselectedClientId = route?.params?.clientId || null;
const [clients, setClients] = useState([]);
const [clientSearch, setClientSearch] = useState('');
const [clientModal, setClientModal] = useState(false);
const [selectedClient, setSelectedClient] = useState(null);
const [products, setProducts] = useState([]);
const [showProductOptions, setShowProductOptions] = useState(false);
const [isProductModalOpen, setIsProductModalOpen] = useState(false);
const [newProduct, setNewProduct] = useState({ name: '', price: '', description: '' });
const [loading, setLoading] = useState(false);
const [initialLoad, setInitialLoad] = useState(true);
const [showDatePicker, setShowDatePicker] = useState(false);
const [form, setForm] = useState({
title: '',
value: '',
stage: 'NEW',
expectedClosingDate: '',
notes: '',
});
useEffect(() => {
Promise.all([
api.get('/clients'),
api.get('/products')
]).then(([clientRes, productRes]) => {
setClients(clientRes.data);
setProducts(productRes.data);
if (preselectedClientId) {
const c = clientRes.data.find(cl => cl.id === preselectedClientId);
if (c) setSelectedClient(c);
}
setInitialLoad(false);
}).catch(() => setInitialLoad(false));
}, []);
const handleSaveProduct = async () => {
if (!newProduct.name.trim() || !newProduct.price) {
Alert.alert('Error', 'Product Name and Price are required.');
return;
}
setLoading(true);
try {
const res = await api.post('/products', {
name: newProduct.name,
price: Number(newProduct.price),
description: newProduct.description
});
setProducts([...products, res.data]);
setForm({ ...form, title: res.data.name, value: String(res.data.price) });
setIsProductModalOpen(false);
Alert.alert('Success', 'Product saved to catalog!');
} catch (e) {
Alert.alert('Error', 'Failed to save product.');
} finally {
setLoading(false);
}
};
const filteredClients = clients.filter(c =>
(c.companyName || c.name || '').toLowerCase().includes(clientSearch.toLowerCase()) ||
(c.phone || '').includes(clientSearch)
);
const handleSubmit = async () => {
if (!selectedClient) { Alert.alert('Error', 'Please select a client'); return; }
if (!form.title.trim()) { Alert.alert('Error', 'Deal title is required'); return; }
if (!form.value || isNaN(Number(form.value))) { Alert.alert('Error', 'Please enter a valid deal value'); return; }
setLoading(true);
try {
await api.post('/opportunities', {
title: form.title,
value: Number(form.value),
stage: form.stage,
clientId: selectedClient.id,
assignedTo: userInfo?.id,
expectedClosingDate: form.expectedClosingDate || null,
notes: form.notes || null,
});
Alert.alert('✅ Deal Created!', `"${form.title}" has been added to your pipeline.`, [
{ text: 'Add Another', onPress: () => { setForm({ title: '', value: '', stage: 'NEW', expectedClosingDate: '', notes: '' }); setSelectedClient(null); } },
{ text: 'View Pipeline', onPress: () => navigation.navigate('Pipeline') },
]);
} catch (e) {
Alert.alert('Error', 'Failed to create opportunity. Please try again.');
} finally {
setLoading(false);
}
};
if (initialLoad) {
return <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}><ActivityIndicator color={Colors.primary} size="large" /></View>;
}
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backBtnText}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>New Deal</Text>
<View style={{ width: 36 }} />
</View>
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
{/* Client */}
<Text style={styles.label}>Client *</Text>
<TouchableOpacity
style={styles.picker}
onPress={() => !preselectedClientId && setClientModal(true)}
activeOpacity={preselectedClientId ? 1 : 0.7}
>
<Text style={selectedClient ? styles.pickerSelected : styles.pickerPlaceholder}>
{selectedClient ? (selectedClient.companyName || selectedClient.name) : 'Select a client...'}
</Text>
{!preselectedClientId && <Text style={styles.pickerArrow}></Text>}
</TouchableOpacity>
{/* Deal Title */}
<Text style={styles.label}>Deal Title / Product *</Text>
<TextInput
style={styles.input}
placeholder="Type a product name..."
value={form.title}
onChangeText={v => {
const matched = products.find(p => p.name.toLowerCase() === v.toLowerCase());
setForm({ ...form, title: v, value: matched ? String(matched.price) : form.value });
setShowProductOptions(true);
}}
onFocus={() => setShowProductOptions(true)}
onBlur={() => setTimeout(() => setShowProductOptions(false), 200)}
/>
{showProductOptions && form.title.length > 0 && (
<View style={styles.autocompleteContainer}>
{products.filter(p => p.name.toLowerCase().includes(form.title.toLowerCase())).map(p => (
<TouchableOpacity key={p.id} style={styles.autocompleteRow} onPress={() => {
setForm({...form, title: p.name, value: String(p.price)});
setShowProductOptions(false);
}}>
<Text style={styles.autocompleteText}>{p.name}</Text>
<Text style={styles.autocompletePrice}>{p.price}</Text>
</TouchableOpacity>
))}
{!products.some(p => p.name.toLowerCase() === form.title.toLowerCase()) && (
<TouchableOpacity style={styles.saveProductRow} onPress={() => {
setNewProduct({ name: form.title, price: form.value || '', description: '' });
setIsProductModalOpen(true);
setShowProductOptions(false);
}}>
<Text style={styles.saveProductText}>+ Save "{form.title}" as new product</Text>
</TouchableOpacity>
)}
</View>
)}
{/* Value */}
<Text style={styles.label}>Deal Value () *</Text>
<TextInput
style={styles.input}
placeholder="e.g. 500000"
keyboardType="numeric"
value={form.value}
onChangeText={v => setForm({ ...form, value: v })}
/>
{/* Stage */}
<Text style={styles.label}>Stage</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 16 }}>
<View style={{ flexDirection: 'row', gap: 8 }}>
{STAGES.map(s => (
<TouchableOpacity
key={s.id}
style={[styles.stagePill, { borderColor: s.color }, form.stage === s.id && { backgroundColor: s.color }]}
onPress={() => setForm({ ...form, stage: s.id })}
>
<Text style={[styles.stagePillText, { color: form.stage === s.id ? 'white' : s.color }]}>{s.label}</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
{/* Expected Closing Date */}
<Text style={styles.label}>Expected Closing Date</Text>
<TouchableOpacity
style={styles.picker}
onPress={() => setShowDatePicker(true)}
>
<Text style={form.expectedClosingDate ? styles.pickerSelected : styles.pickerPlaceholder}>
{form.expectedClosingDate || 'Select date...'}
</Text>
<Text style={styles.pickerArrow}>📅</Text>
</TouchableOpacity>
{showDatePicker && (
<DateTimePicker
value={form.expectedClosingDate ? new Date(form.expectedClosingDate) : new Date()}
mode="date"
display="default"
onChange={(event, selectedDate) => {
setShowDatePicker(false);
if (selectedDate) {
setForm({ ...form, expectedClosingDate: selectedDate.toISOString().split('T')[0] });
}
}}
/>
)}
{/* Notes */}
<Text style={styles.label}>Notes</Text>
<TextInput
style={[styles.input, styles.textArea]}
placeholder="Any additional details..."
multiline
numberOfLines={4}
value={form.notes}
onChangeText={v => setForm({ ...form, notes: v })}
/>
{/* Submit */}
<TouchableOpacity
style={[styles.submitBtn, loading && { opacity: 0.6 }]}
onPress={handleSubmit}
disabled={loading}
>
{loading
? <ActivityIndicator color="white" />
: <Text style={styles.submitBtnText}>💼 Create Deal</Text>
}
</TouchableOpacity>
</ScrollView>
{/* Client Modal */}
<Modal visible={clientModal} animationType="slide" onRequestClose={() => setClientModal(false)}>
<View style={styles.modal}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Select Client</Text>
<TouchableOpacity onPress={() => setClientModal(false)}>
<Text style={styles.modalClose}></Text>
</TouchableOpacity>
</View>
<TextInput
style={styles.searchInput}
placeholder="Search by name or phone..."
value={clientSearch}
onChangeText={setClientSearch}
autoFocus
/>
<FlatList
data={filteredClients}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.clientRow}
onPress={() => { setSelectedClient(item); setClientModal(false); setClientSearch(''); }}
>
<View style={styles.avatar}>
<Text style={styles.avatarText}>{(item.companyName || item.name)?.charAt(0)}</Text>
</View>
<View>
<Text style={styles.clientName}>{item.companyName || item.name}</Text>
<Text style={styles.clientPhone}>{item.phone}</Text>
</View>
</TouchableOpacity>
)}
ListEmptyComponent={<Text style={styles.emptyText}>No clients found</Text>}
/>
</View>
</Modal>
{/* New Product Modal */}
<Modal visible={isProductModalOpen} animationType="fade" transparent onRequestClose={() => setIsProductModalOpen(false)}>
<View style={styles.modalOverlay}>
<View style={styles.productModalBox}>
<View style={styles.productModalHeader}>
<Text style={styles.productModalTitle}>Save New Product</Text>
<TouchableOpacity onPress={() => setIsProductModalOpen(false)}>
<Text style={styles.productModalClose}></Text>
</TouchableOpacity>
</View>
<View style={styles.productModalBody}>
<Text style={styles.label}>Product Name *</Text>
<TextInput
style={styles.input}
value={newProduct.name}
onChangeText={v => setNewProduct({ ...newProduct, name: v })}
/>
<Text style={styles.label}>Default Price () *</Text>
<TextInput
style={styles.input}
keyboardType="numeric"
value={newProduct.price}
onChangeText={v => setNewProduct({ ...newProduct, price: v })}
/>
<Text style={styles.label}>Description</Text>
<TextInput
style={[styles.input, styles.textArea]}
multiline numberOfLines={3}
value={newProduct.description}
onChangeText={v => setNewProduct({ ...newProduct, description: v })}
/>
<TouchableOpacity style={styles.saveBtn} onPress={handleSaveProduct} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.saveBtnText}>Save Product to Catalog</Text>}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f8f9fa' },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: Colors.primary, paddingHorizontal: 16, paddingBottom: 14, paddingTop: 10 },
backBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' },
backBtnText: { color: 'white', fontSize: 24, fontWeight: '300', lineHeight: 28 },
headerTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
body: { padding: 20, paddingBottom: 48 },
label: { fontSize: 11, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8, marginTop: 16 },
input: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15, color: '#1e293b' },
textArea: { minHeight: 100, textAlignVertical: 'top' },
picker: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
pickerPlaceholder: { color: '#94a3b8', fontSize: 15 },
pickerSelected: { color: '#1e293b', fontSize: 15, fontWeight: '700' },
pickerArrow: { color: '#94a3b8', fontSize: 20 },
stagePill: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, borderWidth: 1.5 },
stagePillText: { fontSize: 13, fontWeight: '700' },
submitBtn: { backgroundColor: Colors.primary, borderRadius: 14, padding: 18, alignItems: 'center', marginTop: 28, elevation: 4, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
modal: { flex: 1, backgroundColor: '#f8f9fa' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary },
modalTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
modalClose: { color: 'white', fontSize: 22 },
searchInput: { margin: 12, padding: 14, backgroundColor: 'white', borderRadius: 12, borderWidth: 1, borderColor: '#e2e8f0', fontSize: 15 },
clientRow: { flexDirection: 'row', alignItems: 'center', padding: 16, backgroundColor: 'white', marginHorizontal: 12, marginBottom: 4, borderRadius: 10 },
avatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: Colors.primary + '20', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
avatarText: { color: Colors.primary, fontWeight: '900', fontSize: 16 },
clientName: { fontSize: 15, fontWeight: '700', color: '#1e293b' },
clientPhone: { fontSize: 12, color: '#94a3b8', marginTop: 2 },
emptyText: { textAlign: 'center', color: '#9ca3af', padding: 20, marginTop: 20 },
// Autocomplete & Modals
autocompleteContainer: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', marginTop: 6, maxHeight: 180, overflow: 'hidden', elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } },
autocompleteRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
autocompleteText: { fontSize: 14, color: '#1e293b', fontWeight: '700' },
autocompletePrice: { fontSize: 14, color: '#64748b', fontWeight: '500' },
saveProductRow: { padding: 14, backgroundColor: Colors.primary + '15' },
saveProductText: { fontSize: 14, color: Colors.primary, fontWeight: '800' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 20 },
productModalBox: { backgroundColor: 'white', borderRadius: 20, overflow: 'hidden', elevation: 10 },
productModalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary },
productModalTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
productModalClose: { color: 'white', fontSize: 20, paddingHorizontal: 4 },
productModalBody: { padding: 20 },
saveBtn: { backgroundColor: '#10b981', borderRadius: 14, padding: 16, alignItems: 'center', marginTop: 24 },
saveBtnText: { color: 'white', fontSize: 16, fontWeight: '900' }
});
export default AddOpportunityScreen;

View File

@ -57,8 +57,16 @@ const ClientDetailsScreen = ({ route, navigation }) => {
<View style={styles.card}> <View style={styles.card}>
<View style={styles.headerRow}> <View style={styles.headerRow}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.name}>{client.name}</Text> <Text style={styles.name}>{client.companyName || client.name}</Text>
{client.companyName && <Text style={styles.contactSub}>{client.contactName}</Text>}
<View style={styles.statusRow}>
<Text style={styles.status}>{client.status}</Text> <Text style={styles.status}>{client.status}</Text>
{client.isDemoDone && (
<View style={styles.demoBadge}>
<Text style={styles.demoBadgeText}>DEMO DONE</Text>
</View>
)}
</View>
</View> </View>
<TouchableOpacity onPress={() => navigation.navigate('EditClient', { client })} style={styles.editButton}> <TouchableOpacity onPress={() => navigation.navigate('EditClient', { client })} style={styles.editButton}>
<Text style={styles.editButtonText}>Edit</Text> <Text style={styles.editButtonText}>Edit</Text>
@ -79,6 +87,19 @@ const ClientDetailsScreen = ({ route, navigation }) => {
<Text style={styles.label}>Landmark:</Text> <Text style={styles.label}>Landmark:</Text>
<Text style={styles.value}>{client.landmark || 'N/A'}</Text> <Text style={styles.value}>{client.landmark || 'N/A'}</Text>
<View style={styles.divider} />
<View style={styles.statsRow}>
<View style={styles.statBox}>
<Text style={styles.label}>Probability</Text>
<Text style={[styles.value, {marginBottom: 0, fontWeight: 'bold'}]}>{client.closingProbability || 0}%</Text>
</View>
<View style={styles.statBox}>
<Text style={styles.label}>Timeframe</Text>
<Text style={[styles.value, {marginBottom: 0, fontSize: 13}]}>{client.expectedClosingTimeframe || 'Not Set'}</Text>
</View>
</View>
{client.lat && client.lng ? ( {client.lat && client.lng ? (
<View style={styles.mapContainer}> <View style={styles.mapContainer}>
<Button title="Get Directions" onPress={openMap} color={Colors.secondary} /> <Button title="Get Directions" onPress={openMap} color={Colors.secondary} />
@ -89,6 +110,56 @@ const ClientDetailsScreen = ({ route, navigation }) => {
) : ( ) : (
<Text style={styles.noLocation}>No location data available</Text> <Text style={styles.noLocation}>No location data available</Text>
)} )}
<View style={styles.divider} />
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.actionBtn, {backgroundColor: Colors.primary}]}
onPress={() => navigation.navigate('LogActivity', { tab: 'call', client })}
>
<Text style={styles.actionBtnText}> Log Activity</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionBtn, {backgroundColor: '#6366f1'}]}
onPress={() => navigation.navigate('LogActivity', { tab: 'followup', client })}
>
<Text style={styles.actionBtnText}>📅 Schedule</Text>
</TouchableOpacity>
</View>
<View style={styles.divider} />
<Text style={[styles.label, { color: Colors.primary, fontSize: 12, marginBottom: 15 }]}>📁 ATTACHED DOCUMENTS</Text>
{client.files && client.files.length > 0 ? (
client.files.map((file, idx) => (
<TouchableOpacity
key={idx}
style={styles.fileItem}
onPress={() => {
console.log('Opening URL:', file.url);
if (!file.url || file.url.includes('fake-storage.com')) {
Alert.alert('Old File', 'This file was created before the new storage system and is no longer available. Please re-upload it.');
return;
}
const url = file.url.startsWith('http') ? file.url : `${api.defaults.baseURL}${file.url}`;
console.log('Resolved mobile URL:', url);
Linking.openURL(url);
}}
>
<View style={styles.fileIcon}>
<Text style={{ fontSize: 20 }}>📄</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
<Text style={styles.fileSize}>{(file.size / 1024).toFixed(1)} KB</Text>
</View>
<Text style={styles.openText}>Open </Text>
</TouchableOpacity>
))
) : (
<Text style={styles.noFiles}>No documents attached to this client.</Text>
)}
</View> </View>
</ScrollView> </ScrollView>
); );
@ -114,6 +185,12 @@ const styles = StyleSheet.create({
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 4 shadowRadius: 4
}, },
fileItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 14, marginBottom: 10, borderWidth: 1, borderColor: '#f1f5f9' },
fileIcon: { width: 40, height: 40, backgroundColor: 'white', borderRadius: 10, alignItems: 'center', justifyContent: 'center', marginRight: 12, borderWidth: 1, borderColor: '#e2e8f0' },
fileName: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
fileSize: { fontSize: 11, color: '#94a3b8', marginTop: 2 },
openText: { fontSize: 12, color: Colors.primary, fontWeight: '800' },
noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 13, paddingVertical: 20, fontStyle: 'italic' },
headerRow: { headerRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -141,7 +218,39 @@ const styles = StyleSheet.create({
color: Colors.secondary, color: Colors.secondary,
fontWeight: 'bold', fontWeight: 'bold',
textTransform: 'uppercase', textTransform: 'uppercase',
marginBottom: 10 },
statusRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
gap: 10
},
demoBadge: {
backgroundColor: '#dbeafe',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 4,
borderWidth: 1,
borderColor: '#bfdbfe'
},
demoBadgeText: {
color: '#1e40af',
fontSize: 10,
fontWeight: 'bold'
},
contactSub: {
fontSize: 16,
color: Colors.textMuted,
marginBottom: 5,
fontStyle: 'italic'
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 5
},
statBox: {
flex: 1
}, },
divider: { divider: {
height: 1, height: 1,
@ -172,6 +281,28 @@ const styles = StyleSheet.create({
fontStyle: 'italic', fontStyle: 'italic',
color: Colors.textLight, color: Colors.textLight,
textAlign: 'center' textAlign: 'center'
},
actionRow: {
flexDirection: 'row',
gap: 12,
marginTop: 5
},
actionBtn: {
flex: 1,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4
},
actionBtnText: {
color: 'white',
fontWeight: 'bold',
fontSize: 14
} }
}); });

View File

@ -34,7 +34,9 @@ const ClientListScreen = ({ navigation }) => {
if (query) { if (query) {
const lowerCaseQuery = query.toLowerCase(); const lowerCaseQuery = query.toLowerCase();
const filtered = clients.filter(client => const filtered = clients.filter(client =>
client.name.toLowerCase().includes(lowerCaseQuery) || (client.name && client.name.toLowerCase().includes(lowerCaseQuery)) ||
(client.companyName && client.companyName.toLowerCase().includes(lowerCaseQuery)) ||
(client.contactName && client.contactName.toLowerCase().includes(lowerCaseQuery)) ||
(client.email && client.email.toLowerCase().includes(lowerCaseQuery)) || (client.email && client.email.toLowerCase().includes(lowerCaseQuery)) ||
(client.phone && client.phone.includes(lowerCaseQuery)) (client.phone && client.phone.includes(lowerCaseQuery))
); );
@ -44,8 +46,8 @@ const ClientListScreen = ({ navigation }) => {
} }
}; };
const getInitials = (name) => { const getInitials = (client) => {
if (!name) return 'C'; const name = client.companyName || client.name || 'C';
const parts = name.split(' '); const parts = name.split(' ');
if (parts.length > 1) { if (parts.length > 1) {
return (parts[0][0] + parts[1][0]).toUpperCase(); return (parts[0][0] + parts[1][0]).toUpperCase();
@ -56,10 +58,11 @@ const ClientListScreen = ({ navigation }) => {
const renderItem = ({ item }) => ( const renderItem = ({ item }) => (
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('ClientDetails', { client: item })} activeOpacity={0.8}> <TouchableOpacity style={styles.card} onPress={() => navigation.navigate('ClientDetails', { client: item })} activeOpacity={0.8}>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
<Text style={styles.avatarText}>{getInitials(item.name)}</Text> <Text style={styles.avatarText}>{getInitials(item)}</Text>
</View> </View>
<View style={styles.cardContent}> <View style={styles.cardContent}>
<Text style={styles.name}>{item.name}</Text> <Text style={styles.name}>{item.companyName || item.name}</Text>
{item.companyName && <Text style={styles.contactName}>{item.contactName}</Text>}
<Text style={styles.details}>{item.phone}</Text> <Text style={styles.details}>{item.phone}</Text>
{item.email ? <Text style={styles.subDetails}>{item.email}</Text> : null} {item.email ? <Text style={styles.subDetails}>{item.email}</Text> : null}
<View style={styles.statusBadge}> <View style={styles.statusBadge}>
@ -186,6 +189,12 @@ const styles = StyleSheet.create({
fontSize: 16, fontSize: 16,
fontWeight: 'bold', fontWeight: 'bold',
color: Colors.text, color: Colors.text,
marginBottom: 1
},
contactName: {
fontSize: 13,
color: Colors.textMuted,
fontStyle: 'italic',
marginBottom: 2 marginBottom: 2
}, },
details: { details: {

View File

@ -1,22 +1,49 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { View, Text, TextInput, StyleSheet, Alert, ScrollView, Platform, PermissionsAndroid, TouchableOpacity, ActivityIndicator } from 'react-native'; import {
View, Text, TextInput, StyleSheet, Alert, ScrollView, Platform,
PermissionsAndroid, TouchableOpacity, ActivityIndicator, Modal, FlatList
} from 'react-native';
import Geolocation from 'react-native-geolocation-service'; import Geolocation from 'react-native-geolocation-service';
import { pick } from '@react-native-documents/picker';
import api from '../services/api'; import api from '../services/api';
import Colors from '../constants/Colors'; import Colors from '../constants/Colors';
import { AuthContext } from '../context/AuthContext';
const EditClientScreen = ({ navigation, route }) => { const EditClientScreen = ({ navigation, route }) => {
const { userInfo } = useContext(AuthContext);
const { client } = route.params; const { client } = route.params;
const [name, setName] = useState(client.name); const [companyName, setCompanyName] = useState(client.companyName || '');
const [contactName, setContactName] = useState(client.contactName || '');
const [phone, setPhone] = useState(client.phone); const [phone, setPhone] = useState(client.phone);
const [email, setEmail] = useState(client.email || ''); const [email, setEmail] = useState(client.email || '');
const [address, setAddress] = useState(client.address || ''); const [address, setAddress] = useState(client.address || '');
const [landmark, setLandmark] = useState(client.landmark || ''); const [landmark, setLandmark] = useState(client.landmark || '');
const [closingProbability, setClosingProbability] = useState(client.closingProbability ? String(client.closingProbability) : '0');
const [expectedClosingTimeframe, setExpectedClosingTimeframe] = useState(client.expectedClosingTimeframe || '');
const [isDemoDone, setIsDemoDone] = useState(!!client.isDemoDone);
const [location, setLocation] = useState(client.lat && client.lng ? { latitude: client.lat, longitude: client.lng } : null); const [location, setLocation] = useState(client.lat && client.lng ? { latitude: client.lat, longitude: client.lng } : null);
const [selectedFiles, setSelectedFiles] = useState(client.files || []);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [locating, setLocating] = useState(false); const [locating, setLocating] = useState(false);
// Assignment state
const [users, setUsers] = useState([]);
const [assignedUser, setAssignedUser] = useState(null);
const [userModal, setUserModal] = useState(false);
useEffect(() => {
api.get('/users').then(r => {
setUsers(r.data);
const currentAssignee = r.data.find(u => u.id === client.assignedTo);
if (currentAssignee) {
setAssignedUser(currentAssignee);
} else if (client.assignedTo === userInfo?.id) {
setAssignedUser({ id: userInfo?.id, name: 'Myself' });
}
}).catch(() => {});
}, [client.assignedTo, userInfo]);
const requestLocationPermission = async () => { const requestLocationPermission = async () => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
try { try {
@ -58,15 +85,64 @@ const EditClientScreen = ({ navigation, route }) => {
); );
}; };
const pickFiles = async () => {
try {
const results = await pick({
multiple: true,
});
const uploadedFiles = [];
for (const res of results) {
const formData = new FormData();
formData.append('file', {
uri: Platform.OS === 'ios' ? res.uri.replace('file://', '') : res.uri,
type: res.type || 'application/octet-stream',
name: res.name || 'file',
});
try {
const uploadRes = await api.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
uploadedFiles.push({
name: res.name,
type: res.type,
size: res.size,
url: uploadRes.data.url
});
} catch (err) {
console.error('Upload failed', err);
Alert.alert('Upload Failed', `Could not upload ${res.name}`);
}
}
setSelectedFiles([...selectedFiles, ...uploadedFiles]);
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
console.error(err);
}
}
};
const removeFile = (index) => {
setSelectedFiles(selectedFiles.filter((_, i) => i !== index));
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!name || !phone) { if (!contactName || !phone) {
Alert.alert("Error", "Name and Phone are required"); Alert.alert("Error", "Contact Name and Phone are required");
return; return;
} }
const payload = { const payload = {
name, name: companyName || contactName,
companyName,
contactName,
phone, phone,
assignedTo: assignedUser?.id,
closingProbability: closingProbability ? parseInt(closingProbability) : 0,
expectedClosingTimeframe,
isDemoDone,
files: selectedFiles,
...(email ? { email } : {}), ...(email ? { email } : {}),
...(address ? { address } : {}), ...(address ? { address } : {}),
...(landmark ? { landmark } : {}), ...(landmark ? { landmark } : {}),
@ -89,169 +165,122 @@ const EditClientScreen = ({ navigation, route }) => {
return ( return (
<ScrollView contentContainerStyle={styles.container}> <ScrollView contentContainerStyle={styles.container}>
<Text style={styles.sectionHeader}>Basic Information</Text> <Text style={styles.label}>Company Name</Text>
<TextInput style={styles.input} value={companyName} onChangeText={setCompanyName} placeholder="Enter company name" />
<View style={styles.formGroup}> <Text style={styles.label}>Contact Name *</Text>
<Text style={styles.label}>Full Name *</Text> <TextInput style={styles.input} value={contactName} onChangeText={setContactName} placeholder="Enter contact person name" />
<TextInput style={styles.input} value={name} onChangeText={setName} placeholder="Enter client name" />
</View>
<View style={styles.formGroup}> <Text style={styles.label}>Phone *</Text>
<Text style={styles.label}>Phone Number *</Text> <TextInput style={styles.input} value={phone} onChangeText={setPhone} placeholder="Enter phone number" keyboardType="phone-pad" />
<TextInput style={styles.input} value={phone} onChangeText={setPhone} keyboardType="phone-pad" placeholder="Enter phone number" />
</View>
<View style={styles.formGroup}> <Text style={styles.label}>Assigned To / Transfer</Text>
<Text style={styles.label}>Email Address</Text> <TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
<TextInput style={styles.input} value={email} onChangeText={setEmail} keyboardType="email-address" placeholder="Enter email" /> <Text style={styles.pickerBtnText}>{assignedUser?.name || 'Select User'}</Text>
</View> <Text style={styles.pickerArrow}></Text>
</TouchableOpacity>
<Text style={styles.sectionHeader}>Address Details</Text> <Text style={styles.label}>Email</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="Enter email address" keyboardType="email-address" />
<View style={styles.formGroup}>
<Text style={styles.label}>Address</Text> <Text style={styles.label}>Address</Text>
<TextInput style={[styles.input, styles.textArea]} value={address} onChangeText={setAddress} multiline numberOfLines={3} placeholder="Enter full address" /> <TextInput style={[styles.input, { height: 80 }]} value={address} onChangeText={setAddress} placeholder="Enter address" multiline />
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Landmark</Text> <Text style={styles.label}>Landmark</Text>
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} placeholder="Enter nearby landmark" /> <TextInput style={styles.input} value={landmark} onChangeText={setLandmark} placeholder="Enter nearby landmark" />
</View>
<Text style={styles.sectionHeader}>Location Tagging</Text> <TouchableOpacity style={[styles.locationBtn, { backgroundColor: location ? '#16a34a' : Colors.secondary }]} onPress={getCurrentLocation} disabled={locating}>
{locating ? <ActivityIndicator color="white" /> : <Text style={styles.locationBtnText}>{location ? "✓ Location Updated" : "📍 Update Current Location"}</Text>}
</TouchableOpacity>
<View style={styles.locationCard}> <View style={styles.fileSection}>
<View style={styles.locationInfo}> <View style={styles.fileHeader}>
<Text style={styles.locationLabel}> <Text style={styles.label}>Files / Attachments</Text>
{location ? "Location Captured" : "No Location Set"} <TouchableOpacity onPress={pickFiles} style={styles.addFileBtn}>
</Text> <Text style={styles.addFileBtnText}>+ ADD FILE</Text>
{location && (
<Text style={styles.coords}>
{location.latitude.toFixed(6)}, {location.longitude.toFixed(6)}
</Text>
)}
</View>
<TouchableOpacity
style={[styles.locationButton, locating && styles.disabledButton]}
onPress={getCurrentLocation}
disabled={locating}
>
{locating ? <ActivityIndicator color="white" size="small" /> : <Text style={styles.locationButtonText}>📍 Update</Text>}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.spacer} /> {selectedFiles.map((file, index) => (
<View key={index} style={styles.fileRow}>
<TouchableOpacity <View style={styles.fileInfo}>
style={[styles.submitButton, loading && styles.disabledButton]} <Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
onPress={handleSubmit} <Text style={styles.fileSize}>{(file.size / 1024).toFixed(1)} KB</Text>
disabled={loading} </View>
> <TouchableOpacity onPress={() => removeFile(index)}>
{loading ? ( <Text style={styles.removeFile}></Text>
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitButtonText}>Save Changes</Text>
)}
</TouchableOpacity> </TouchableOpacity>
</View>
))}
{selectedFiles.length === 0 && (
<Text style={styles.noFiles}>No files attached yet</Text>
)}
</View>
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>Update Client</Text>}
</TouchableOpacity>
<Modal visible={userModal} animationType="slide" transparent={true}>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Transfer To</Text>
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<FlatList
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity style={styles.userRow} onPress={() => { setAssignedUser(item); setUserModal(false); }}>
<View style={styles.userAvatar}>
<Text style={styles.userAvatarText}>{item.name?.charAt(0)}</Text>
</View>
<View>
<Text style={styles.userName}>{item.name}</Text>
<Text style={styles.userRole}>{item.role}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
</View>
</Modal>
</ScrollView> </ScrollView>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: { padding: 20, backgroundColor: '#fff' },
padding: 20, label: { fontSize: 13, fontWeight: 'bold', color: '#64748b', marginBottom: 6, marginTop: 15, textTransform: 'uppercase' },
backgroundColor: Colors.background, input: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, fontSize: 15, backgroundColor: '#f8fafc' },
flexGrow: 1 pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
}, pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '600' },
sectionHeader: { pickerArrow: { fontSize: 20, color: '#94a3b8' },
fontSize: 18, locationBtn: { padding: 15, borderRadius: 12, marginTop: 20, alignItems: 'center' },
fontWeight: 'bold', locationBtnText: { color: 'white', fontWeight: 'bold' },
color: '#334155', submitBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 12, marginTop: 30, alignItems: 'center', marginBottom: 50 },
marginBottom: 15, submitBtnText: { color: 'white', fontSize: 16, fontWeight: 'bold' },
marginTop: 10 modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
}, modalContent: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '70%', paddingBottom: 20 },
formGroup: { modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
marginBottom: 15 modalTitle: { fontSize: 18, fontWeight: 'bold', color: '#1e293b' },
}, modalClose: { fontSize: 20, color: '#94a3b8', padding: 5 },
label: { userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
fontSize: 14, userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
fontWeight: '600', userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
color: Colors.textMuted, userName: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
marginBottom: 8 userRole: { fontSize: 12, color: '#64748b' },
}, fileSection: { marginTop: 20, padding: 15, backgroundColor: '#f8fafc', borderRadius: 12, borderStyle: 'dashed', borderWidth: 1.5, borderColor: '#e2e8f0' },
input: { fileHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
backgroundColor: 'white', addFileBtn: { backgroundColor: '#10b981', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 },
borderWidth: 1, addFileBtnText: { color: 'white', fontSize: 11, fontWeight: 'bold' },
borderColor: Colors.border, fileRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: 'white', padding: 10, borderRadius: 8, marginBottom: 8, borderWidth: 1, borderColor: '#f1f5f9' },
borderRadius: 12, fileInfo: { flex: 1, marginRight: 10 },
paddingHorizontal: 15, fileName: { fontSize: 13, fontWeight: '600', color: '#1e293b' },
paddingVertical: 12, fileSize: { fontSize: 10, color: '#94a3b8', marginTop: 2 },
fontSize: 16, removeFile: { color: '#ef4444', fontSize: 16, fontWeight: 'bold', padding: 5 },
color: Colors.text noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 11, paddingVertical: 10, fontStyle: 'italic' }
},
textArea: {
height: 100,
textAlignVertical: 'top'
},
locationCard: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: 'white',
padding: 15,
borderRadius: 12,
borderWidth: 1,
borderColor: Colors.border
},
locationInfo: {
flex: 1
},
locationLabel: {
fontSize: 16,
fontWeight: 'bold',
color: Colors.text
},
coords: {
fontSize: 12,
color: Colors.textMuted,
marginTop: 2,
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace'
},
locationButton: {
backgroundColor: Colors.secondary,
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 8,
marginLeft: 10
},
locationButtonText: {
color: 'white',
fontWeight: 'bold',
fontSize: 14
},
spacer: {
height: 30
},
submitButton: {
backgroundColor: Colors.primary,
borderRadius: 12,
paddingVertical: 16,
alignItems: 'center',
elevation: 4,
shadowColor: Colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 5,
marginBottom: 30
},
submitButtonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold'
},
disabledButton: {
opacity: 0.7
}
}); });
export default EditClientScreen; export default EditClientScreen;

View File

@ -212,10 +212,10 @@ const HomeScreen = ({ navigation }) => {
onPress={() => navigation.navigate('Attendance')} onPress={() => navigation.navigate('Attendance')}
/> />
<MenuCard <MenuCard
title="Enquiries" title="New Deal"
icon="📝" icon="💼"
color={Colors.primary} color={Colors.primary}
onPress={() => navigation.navigate('EnquiryList')} onPress={() => navigation.navigate('AddOpportunity')}
/> />
<MenuCard <MenuCard
title="Expenses" title="Expenses"

View File

@ -4,21 +4,29 @@ import {
TextInput, Alert, ActivityIndicator, StatusBar, Modal, FlatList, Linking, Switch TextInput, Alert, ActivityIndicator, StatusBar, Modal, FlatList, Linking, Switch
} from 'react-native'; } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import DateTimePicker from '@react-native-community/datetimepicker';
import { AuthContext } from '../context/AuthContext'; import { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors'; import Colors from '../constants/Colors';
import api from '../services/api'; import api from '../services/api';
const ACTIVITY_TYPES = [ const STRATEGIC_TYPES = [
{ id: 'COLD_CALLING', label: 'Cold Calling', icon: '📞', funnelKey: 'calls' }, { id: 'COLD_CALLING', label: 'Cold Calling', icon: '📞' },
{ id: 'WHATSAPP_CAMPAIGN', label: 'WhatsApp Campaign', icon: '📱', funnelKey: 'calls' }, { id: 'WHATSAPP_CAMPAIGN', label: 'WhatsApp Campaign', icon: '📱' },
{ id: 'POSTER_PASTING', label: 'Poster Pasting', icon: '🖼️', funnelKey: null }, { id: 'POSTER_PASTING', label: 'Poster Pasting', icon: '🖼️' },
{ id: 'EXHIBITION', label: 'Exhibition/Event', icon: '🎪', funnelKey: null }, { id: 'EXHIBITION', label: 'Exhibition/Event', icon: '🎪' },
{ id: 'DATA_COLLECTION', label: 'Data Collection', icon: '📊', funnelKey: null }, { id: 'DATA_COLLECTION', label: 'Data Collection', icon: '📊' },
];
const SCHEDULE_TYPES = [
{ id: 'FOLLOWUP', label: 'Follow-up', icon: '📅', color: '#6366f1' },
{ id: 'DEMO', label: 'Demo', icon: '📽️', color: '#3b82f6' },
{ id: 'QUOTE', label: 'Quote', icon: '📝', color: '#a855f7' },
{ id: 'NEGOTIATION', label: 'Negotiate', icon: '🤝', color: '#f59e0b' },
]; ];
const TABS = [ const TABS = [
{ id: 'call', label: 'Log Call / Activity', icon: '📞' }, { id: 'call', label: 'Done Now', icon: '✅' },
{ id: 'followup', label: 'Schedule Follow-up', icon: '📅' }, { id: 'followup', label: 'Schedule', icon: '📅' },
]; ];
const LogActivityScreen = ({ navigation, route }) => { const LogActivityScreen = ({ navigation, route }) => {
@ -29,8 +37,13 @@ const LogActivityScreen = ({ navigation, route }) => {
const [activeTab, setActiveTab] = useState(defaultTab); const [activeTab, setActiveTab] = useState(defaultTab);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [clients, setClients] = useState([]); const [clients, setClients] = useState([]);
const [users, setUsers] = useState([]);
const [opportunities, setOpportunities] = useState([]);
const [clientSearch, setClientSearch] = useState(''); const [clientSearch, setClientSearch] = useState('');
const [oppSearch, setOppSearch] = useState('');
const [clientModal, setClientModal] = useState(false); const [clientModal, setClientModal] = useState(false);
const [userModal, setUserModal] = useState(false);
const [oppModal, setOppModal] = useState(false);
// Call / Activity state // Call / Activity state
const [actType, setActType] = useState(null); const [actType, setActType] = useState(null);
@ -38,39 +51,57 @@ const LogActivityScreen = ({ navigation, route }) => {
const [quantity, setQuantity] = useState('1'); const [quantity, setQuantity] = useState('1');
const [callClient, setCallClient] = useState(null); const [callClient, setCallClient] = useState(null);
const [updateClientStatus, setUpdateClientStatus] = useState(null); const [updateClientStatus, setUpdateClientStatus] = useState(null);
const [isDemoDone, setIsDemoDone] = useState(false);
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ id: 'LEAD', label: 'Lead', color: '#6366f1', bg: '#eef2ff' },
{ id: 'QUALITY', label: 'Quality', color: '#16a34a', bg: '#dcfce7' }, { id: 'QUALITY', label: 'Quality', color: '#16a34a', bg: '#dcfce7' },
{ id: 'POTENTIAL', label: 'Potential', color: '#eab308', bg: '#fef9c3' }, { id: 'POTENTIAL', label: 'Potential', color: '#eab308', bg: '#fef9c3' },
{ id: 'DEMO', label: 'Demo', color: '#a855f7', bg: '#f3e8ff' },
{ id: 'SALES', label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' }, { id: 'SALES', label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
{ id: 'CLOSED', label: 'Closed', color: '#ef4444', bg: '#fee2e2' } { id: 'CLOSED', label: 'Closed', color: '#ef4444', bg: '#fee2e2' }
]; ];
// Followup state // Followup state
const [fuType, setFuType] = useState('FOLLOWUP');
const [fuClient, setFuClient] = useState(null); const [fuClient, setFuClient] = useState(null);
const [fuOpp, setFuOpp] = useState(null);
const [assignedUser, setAssignedUser] = useState(null);
const [fuNotes, setFuNotes] = useState(''); const [fuNotes, setFuNotes] = useState('');
const [fuDate, setFuDate] = useState(''); const [fuDate, setFuDate] = useState(new Date().toISOString().split('T')[0]);
const [fuTime, setFuTime] = useState(''); const [fuTime, setFuTime] = useState('10:00');
const [showDatePicker, setShowDatePicker] = useState(false);
const [showTimePicker, setShowTimePicker] = useState(false);
useEffect(() => {
setAssignedUser({ id: userInfo?.id, name: 'Myself' });
}, [userInfo]);
useEffect(() => {
api.get('/clients').then(r => setClients(r.data)).catch(() => {});
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
api.get('/opportunities').then(r => setOpportunities(r.data)).catch(() => {});
}, []);
const filteredClients = clients.filter(c =>
(c.companyName || c.name || '').toLowerCase().includes(clientSearch.toLowerCase()) ||
(c.phone || '').includes(clientSearch)
);
const filteredOpps = opportunities.filter(o =>
(o.title || '').toLowerCase().includes(oppSearch.toLowerCase()) ||
(o.client?.companyName || o.client?.name || '').toLowerCase().includes(oppSearch.toLowerCase())
);
const handleCall = (phone) => { const handleCall = (phone) => {
if (!phone) return; if (!phone) return;
Linking.openURL(`tel:${phone}`); Linking.openURL(`tel:${phone}`);
}; };
useEffect(() => { // ── Submit Strategic Activity ────────────────────────────────────────────
api.get('/clients').then(r => setClients(r.data)).catch(() => {});
}, []);
const filteredClients = clients.filter(c =>
c.name?.toLowerCase().includes(clientSearch.toLowerCase()) ||
c.phone?.includes(clientSearch)
);
// ── Submit Call/Activity ──────────────────────────────────────
const handleSubmitCall = async () => { const handleSubmitCall = async () => {
if (!actType) { Alert.alert('Error', 'Please select an activity type'); return; } if (!actType) { Alert.alert('Error', 'Please select activity type'); return; }
if (!description.trim()) { Alert.alert('Error', 'Please enter a description'); return; } if (!description.trim()) { Alert.alert('Error', 'Please add notes'); return; }
setLoading(true); setLoading(true);
try { try {
await api.post('/strategic-activities', { await api.post('/strategic-activities', {
@ -78,19 +109,20 @@ const LogActivityScreen = ({ navigation, route }) => {
description, description,
leadsGenerated: parseInt(quantity) || 0, leadsGenerated: parseInt(quantity) || 0,
updateClientStatus, updateClientStatus,
metadata: { clientId: callClient?.id, clientName: callClient?.name } isDemoDone,
metadata: { clientId: callClient?.id, clientName: callClient?.companyName || callClient?.name }
}); });
Alert.alert('Done! ✅', `${actType.replace('_', ' ')} logged successfully.`, [ Alert.alert('Success ✅', `${actType.replace('_', ' ')} logged.`, [
{ text: 'Log Another', onPress: () => { setActType(null); setDescription(''); setQuantity('1'); setCallClient(null); setUpdateClientStatus(null); } }, { text: 'Log Another', onPress: () => { setActType(null); setDescription(''); setQuantity('1'); setCallClient(null); setUpdateClientStatus(null); setIsDemoDone(false); } },
{ text: 'Go to Tasks', onPress: () => navigation.navigate('Main', { screen: 'Tasks' }) }, { text: 'Go to Activities', onPress: () => navigation.navigate('Main', { screen: 'Activities' }) },
]); ]);
} catch (e) { } catch (e) {
Alert.alert('Error', 'Failed to log activity.'); Alert.alert('Error', 'Failed to log activity.');
} finally { setLoading(false); } } finally { setLoading(false); }
}; };
// ── Submit Followup ──────────────────────────────────────────── // ── Submit Scheduled Activity ────────────────────────────────────────────
const handleSubmitFollowup = async () => { const handleSubmitSchedule = async () => {
if (!fuClient) { Alert.alert('Error', 'Please select a client'); return; } if (!fuClient) { Alert.alert('Error', 'Please select a client'); return; }
if (!fuNotes.trim()) { Alert.alert('Error', 'Please add a note'); return; } if (!fuNotes.trim()) { Alert.alert('Error', 'Please add a note'); return; }
if (!fuDate || !fuTime) { Alert.alert('Error', 'Please set date and time'); return; } if (!fuDate || !fuTime) { Alert.alert('Error', 'Please set date and time'); return; }
@ -99,17 +131,19 @@ const LogActivityScreen = ({ navigation, route }) => {
try { try {
await api.post('/followups', { await api.post('/followups', {
clientId: fuClient.id, clientId: fuClient.id,
userId: userInfo?.id, opportunityId: fuOpp?.id,
userId: assignedUser?.id || userInfo?.id,
type: fuType,
notes: fuNotes, notes: fuNotes,
date: new Date(dateStr).toISOString(), date: new Date(dateStr).toISOString(),
status: 'PENDING', status: 'PENDING',
}); });
Alert.alert('Scheduled! 📅', `Follow-up with ${fuClient.name} scheduled.`, [ Alert.alert('Scheduled! 📅', `${fuType} for ${fuClient.companyName || fuClient.name} scheduled.`, [
{ text: 'Schedule Another', onPress: () => { setFuClient(null); setFuNotes(''); setFuDate(''); setFuTime(''); } }, { text: 'Schedule Another', onPress: () => { setFuClient(null); setFuOpp(null); setFuNotes(''); setFuDate(''); setFuTime(''); } },
{ text: 'View Tasks', onPress: () => navigation.navigate('Main', { screen: 'Tasks' }) }, { text: 'View Activities', onPress: () => navigation.navigate('Main', { screen: 'Activities' }) },
]); ]);
} catch (e) { } catch (e) {
Alert.alert('Error', 'Failed to schedule follow-up.'); Alert.alert('Error', 'Failed to schedule activity.');
} finally { setLoading(false); } } finally { setLoading(false); }
}; };
@ -118,7 +152,7 @@ const LogActivityScreen = ({ navigation, route }) => {
<View style={styles.clientPickerContainer}> <View style={styles.clientPickerContainer}>
<TouchableOpacity style={styles.clientPicker} onPress={() => setClientModal(true)}> <TouchableOpacity style={styles.clientPicker} onPress={() => setClientModal(true)}>
<Text style={selected ? styles.clientPickerSelected : styles.clientPickerPlaceholder} numberOfLines={1}> <Text style={selected ? styles.clientPickerSelected : styles.clientPickerPlaceholder} numberOfLines={1}>
{selected ? `${selected.name}${selected.phone}` : 'Tap to select client...'} {selected ? `${selected.companyName || selected.name}${selected.phone}` : 'Tap to select client...'}
</Text> </Text>
<Text style={styles.clientPickerArrow}></Text> <Text style={styles.clientPickerArrow}></Text>
</TouchableOpacity> </TouchableOpacity>
@ -147,10 +181,11 @@ const LogActivityScreen = ({ navigation, route }) => {
renderItem={({ item }) => ( renderItem={({ item }) => (
<TouchableOpacity style={styles.clientRow} onPress={() => { onSelect(item); setClientModal(false); setClientSearch(''); }}> <TouchableOpacity style={styles.clientRow} onPress={() => { onSelect(item); setClientModal(false); setClientSearch(''); }}>
<View style={styles.clientAvatar}> <View style={styles.clientAvatar}>
<Text style={styles.clientAvatarText}>{item.name?.charAt(0)}</Text> <Text style={styles.clientAvatarText}>{(item.companyName || item.name)?.charAt(0)}</Text>
</View> </View>
<View> <View>
<Text style={styles.clientRowName}>{item.name}</Text> <Text style={styles.clientRowName}>{item.companyName || item.name}</Text>
{item.companyName && <Text style={styles.clientRowSub}>{item.contactName}</Text>}
<Text style={styles.clientRowPhone}>{item.phone}</Text> <Text style={styles.clientRowPhone}>{item.phone}</Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -162,6 +197,91 @@ const LogActivityScreen = ({ navigation, route }) => {
</> </>
); );
const OpportunityPicker = () => (
<>
<TouchableOpacity style={styles.clientPicker} onPress={() => setOppModal(true)}>
<Text style={fuOpp ? styles.clientPickerSelected : styles.clientPickerPlaceholder} numberOfLines={1}>
{fuOpp ? `${fuOpp.title} (${fuOpp.client?.name})` : 'Choose Opportunity...'}
</Text>
<Text style={styles.clientPickerArrow}></Text>
</TouchableOpacity>
<Modal visible={oppModal} animationType="slide" onRequestClose={() => setOppModal(false)}>
<View style={styles.modalContainer}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Select Opportunity</Text>
<TouchableOpacity onPress={() => setOppModal(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<TextInput
style={styles.searchInput}
placeholder="Search deals..."
value={oppSearch}
onChangeText={setOppSearch}
autoFocus
/>
<FlatList
data={filteredOpps}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.clientRow}
onPress={() => {
setFuOpp(item);
setFuClient(item.client);
setOppModal(false);
setOppSearch('');
}}
>
<View style={[styles.clientAvatar, { backgroundColor: '#eef2ff' }]}>
<Text style={[styles.clientAvatarText, { color: '#6366f1' }]}>{item.title?.charAt(0)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.clientRowName}>{item.title}</Text>
<Text style={styles.clientRowSub}>{item.client?.companyName || item.client?.name}</Text>
<Text style={[styles.clientRowPhone, { color: Colors.primary }]}>{item.value.toLocaleString()}</Text>
</View>
</TouchableOpacity>
)}
ListEmptyComponent={<Text style={styles.emptyText}>No opportunities found</Text>}
/>
</View>
</Modal>
</>
);
const UserPicker = () => (
<>
<TouchableOpacity style={styles.clientPicker} onPress={() => setUserModal(true)}>
<Text style={styles.clientPickerSelected} numberOfLines={1}>
{assignedUser?.name || 'Myself'}
</Text>
<Text style={styles.clientPickerArrow}></Text>
</TouchableOpacity>
<Modal visible={userModal} animationType="slide" onRequestClose={() => setUserModal(false)}>
<View style={styles.modalContainer}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Assign To</Text>
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<FlatList
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity style={styles.clientRow} onPress={() => { setAssignedUser(item); setUserModal(false); }}>
<View style={[styles.clientAvatar, { backgroundColor: '#f0f4ff' }]}>
<Text style={styles.clientAvatarText}>{item.name?.charAt(0)}</Text>
</View>
<View>
<Text style={styles.clientRowName}>{item.name}</Text>
<Text style={styles.clientRowPhone}>{item.role || 'Staff'}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
</Modal>
</>
);
return ( return (
<View style={[styles.container, { paddingTop: insets.top }]}> <View style={[styles.container, { paddingTop: insets.top }]}>
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" /> <StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
@ -171,7 +291,7 @@ const LogActivityScreen = ({ navigation, route }) => {
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}> <TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backBtnText}></Text> <Text style={styles.backBtnText}></Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.headerTitle}>Quick Actions</Text> <Text style={styles.headerTitle}>Activities</Text>
<View style={{ width: 36 }} /> <View style={{ width: 36 }} />
</View> </View>
@ -191,12 +311,12 @@ const LogActivityScreen = ({ navigation, route }) => {
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled"> <ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
{/* ── CALL / ACTIVITY TAB ── */} {/* ── DONE NOW TAB (Strategic) ── */}
{activeTab === 'call' && ( {activeTab === 'call' && (
<> <>
<Text style={styles.section}>Activity Type</Text> <Text style={styles.section}>Strategic Activity Done</Text>
<View style={styles.typeGrid}> <View style={styles.typeGrid}>
{ACTIVITY_TYPES.map(a => ( {STRATEGIC_TYPES.map(a => (
<TouchableOpacity <TouchableOpacity
key={a.id} key={a.id}
style={[styles.typeCard, actType === a.id && styles.typeCardActive]} style={[styles.typeCard, actType === a.id && styles.typeCardActive]}
@ -215,7 +335,6 @@ const LogActivityScreen = ({ navigation, route }) => {
<View style={styles.statusSection}> <View style={styles.statusSection}>
<View style={{ marginBottom: 10 }}> <View style={{ marginBottom: 10 }}>
<Text style={styles.switchLabel}>Update Client Status</Text> <Text style={styles.switchLabel}>Update Client Status</Text>
<Text style={styles.switchSub}>Optional: Automatically change status after this call.</Text>
</View> </View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, paddingBottom: 5 }}> <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, paddingBottom: 5 }}>
<TouchableOpacity <TouchableOpacity
@ -234,74 +353,155 @@ const LogActivityScreen = ({ navigation, route }) => {
</TouchableOpacity> </TouchableOpacity>
))} ))}
</ScrollView> </ScrollView>
<TouchableOpacity
style={styles.demoToggle}
onPress={() => setIsDemoDone(!isDemoDone)}
>
<View style={[styles.checkbox, isDemoDone && styles.checkboxChecked]} />
<Text style={styles.checkboxLabel}>Mark Demo as Done</Text>
</TouchableOpacity>
</View> </View>
)} )}
<Text style={styles.section}>Description *</Text> <Text style={styles.section}>Notes / Description *</Text>
<TextInput <TextInput
style={styles.textArea} style={styles.textArea}
placeholder="What did you do? e.g. Called 20 leads from the database list..." placeholder="What did you do? Details here..."
multiline multiline
numberOfLines={4} numberOfLines={4}
value={description} value={description}
onChangeText={setDescription} onChangeText={setDescription}
/> />
<Text style={styles.section}>Quantity (Leads Generated)</Text> <Text style={styles.section}>Quantity (e.g. Leads Generated)</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="How many leads did this generate?" placeholder="Enter number..."
keyboardType="numeric" keyboardType="numeric"
value={quantity} value={quantity}
onChangeText={setQuantity} onChangeText={setQuantity}
/> />
<TouchableOpacity style={[styles.submitBtn, loading && { opacity: 0.6 }]} onPress={handleSubmitCall} disabled={loading}> <TouchableOpacity style={[styles.submitBtn, loading && { opacity: 0.6 }]} onPress={handleSubmitCall} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📤 Log Activity</Text>} {loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>Submit Activity</Text>}
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}
{/* ── FOLLOWUP TAB ── */} {/* ── SCHEDULE TAB ── */}
{activeTab === 'followup' && ( {activeTab === 'followup' && (
<>
<Text style={styles.section}>Activity Type</Text>
<View style={styles.typeGrid}>
{SCHEDULE_TYPES.map(s => (
<TouchableOpacity
key={s.id}
style={[styles.typeCard, fuType === s.id && { borderColor: s.color, backgroundColor: s.color + '10' }]}
onPress={() => setFuType(s.id)}
>
<Text style={styles.typeIcon}>{s.icon}</Text>
<Text style={[styles.typeLabel, fuType === s.id && { color: s.color }]}>{s.label}</Text>
</TouchableOpacity>
))}
</View>
{fuType === 'QUOTE' ? (
<>
<Text style={styles.section}>Link to Opportunity *</Text>
<OpportunityPicker />
</>
) : (
<> <>
<Text style={styles.section}>Client *</Text> <Text style={styles.section}>Client *</Text>
<ClientPicker selected={fuClient} onSelect={setFuClient} /> <ClientPicker selected={fuClient} onSelect={setFuClient} />
</>
)}
<Text style={styles.section}>Notes / Task Description *</Text> {['ADMIN', 'GENERAL_MANAGER', 'MANAGER', 'TEAM_LEADER'].includes(userInfo?.role) && (
<>
<Text style={styles.section}>Assign To</Text>
<UserPicker />
</>
)}
<Text style={styles.section}>Task / Notes *</Text>
<TextInput <TextInput
style={styles.textArea} style={styles.textArea}
placeholder="What needs to be done? e.g. Call back regarding demo pricing..." placeholder="Details of what needs to be done..."
multiline multiline
numberOfLines={4} numberOfLines={4}
value={fuNotes} value={fuNotes}
onChangeText={setFuNotes} onChangeText={setFuNotes}
/> />
<Text style={styles.section}>Follow-up Date *</Text> <View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput <View style={{ flex: 1 }}>
style={styles.input} <Text style={styles.section}>Date *</Text>
placeholder="YYYY-MM-DD" <TouchableOpacity
value={fuDate} style={styles.picker}
onChangeText={setFuDate} onPress={() => setShowDatePicker(true)}
keyboardType="numeric" >
/> <Text style={fuDate ? styles.pickerSelected : styles.pickerPlaceholder}>
{fuDate || 'Select date...'}
</Text>
<Text style={styles.pickerArrow}>📅</Text>
</TouchableOpacity>
<Text style={styles.section}>Follow-up Time *</Text> {showDatePicker && (
<TextInput <DateTimePicker
style={styles.input} value={fuDate ? new Date(fuDate) : new Date()}
placeholder="HH:MM (24h format, e.g. 14:30)" mode="date"
value={fuTime} display="default"
onChangeText={setFuTime} onChange={(event, selectedDate) => {
keyboardType="numeric" setShowDatePicker(false);
if (selectedDate) {
setFuDate(selectedDate.toISOString().split('T')[0]);
}
}}
/> />
)}
</View>
<View style={{ flex: 1 }}>
<Text style={styles.section}>Time *</Text>
<TouchableOpacity
style={styles.picker}
onPress={() => setShowTimePicker(true)}
>
<Text style={fuTime ? styles.pickerSelected : styles.pickerPlaceholder}>
{fuTime || 'Select time...'}
</Text>
<Text style={styles.pickerArrow}>🕒</Text>
</TouchableOpacity>
<View style={styles.reminderBox}> {showTimePicker && (
<Text style={styles.reminderText}>📲 You'll receive a mobile alert at the scheduled time to complete this follow-up.</Text> <DateTimePicker
value={(() => {
const d = new Date();
if (fuTime) {
const [h, m] = fuTime.split(':');
d.setHours(parseInt(h), parseInt(m));
}
return d;
})()}
mode="time"
is24Hour={true}
display="default"
onChange={(event, selectedTime) => {
setShowTimePicker(false);
if (selectedTime) {
const h = selectedTime.getHours().toString().padStart(2, '0');
const m = selectedTime.getMinutes().toString().padStart(2, '0');
setFuTime(`${h}:${m}`);
}
}}
/>
)}
</View>
</View> </View>
<TouchableOpacity style={[styles.submitBtn, { backgroundColor: '#6366f1' }, loading && { opacity: 0.6 }]} onPress={handleSubmitFollowup} disabled={loading}> <TouchableOpacity style={[styles.submitBtn, { backgroundColor: '#6366f1' }, loading && { opacity: 0.6 }]} onPress={handleSubmitSchedule} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📅 Schedule Follow-up</Text>} {loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📅 Schedule Activity</Text>}
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}
@ -329,7 +529,6 @@ const styles = StyleSheet.create({
typeCardActive: { borderColor: Colors.primary, backgroundColor: '#f0f4ff' }, typeCardActive: { borderColor: Colors.primary, backgroundColor: '#f0f4ff' },
typeIcon: { fontSize: 26, marginBottom: 6 }, typeIcon: { fontSize: 26, marginBottom: 6 },
typeLabel: { fontSize: 11, fontWeight: '700', color: '#64748b', textAlign: 'center' }, typeLabel: { fontSize: 11, fontWeight: '700', color: '#64748b', textAlign: 'center' },
typeLabelActive: { color: Colors.primary },
clientPickerContainer: { flexDirection: 'row', gap: 10, alignItems: 'center' }, clientPickerContainer: { flexDirection: 'row', gap: 10, alignItems: 'center' },
clientPicker: { flex: 1, backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, clientPicker: { flex: 1, backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
inlineCallBtn: { backgroundColor: '#eef2ff', width: 48, height: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(0,0,0,0.05)' }, inlineCallBtn: { backgroundColor: '#eef2ff', width: 48, height: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
@ -338,12 +537,13 @@ const styles = StyleSheet.create({
clientPickerSelected: { color: '#1e293b', fontSize: 14, fontWeight: '700' }, clientPickerSelected: { color: '#1e293b', fontSize: 14, fontWeight: '700' },
clientPickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' }, clientPickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' },
input: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15 }, input: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15 },
picker: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
pickerPlaceholder: { color: '#94a3b8', fontSize: 14 },
pickerSelected: { color: '#1e293b', fontSize: 14, fontWeight: '700' },
pickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' },
textArea: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15, minHeight: 100, textAlignVertical: 'top' }, textArea: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15, minHeight: 100, textAlignVertical: 'top' },
reminderBox: { backgroundColor: '#f0f4ff', borderRadius: 12, padding: 14, marginTop: 16, borderLeftWidth: 4, borderLeftColor: '#6366f1' },
reminderText: { fontSize: 12, color: '#6366f1', fontWeight: '600', lineHeight: 18 },
submitBtn: { backgroundColor: Colors.primary, borderRadius: 14, padding: 18, alignItems: 'center', marginTop: 24, elevation: 4, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 }, submitBtn: { backgroundColor: Colors.primary, borderRadius: 14, padding: 18, alignItems: 'center', marginTop: 24, elevation: 4, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' }, submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
// Modal
modalContainer: { flex: 1, backgroundColor: '#f8f9fa' }, modalContainer: { flex: 1, backgroundColor: '#f8f9fa' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary },
modalTitle: { color: 'white', fontSize: 18, fontWeight: '900' }, modalTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
@ -360,7 +560,11 @@ const styles = StyleSheet.create({
statusPillActive: { backgroundColor: '#64748b', borderColor: '#64748b' }, statusPillActive: { backgroundColor: '#64748b', borderColor: '#64748b' },
statusPillText: { fontSize: 12, fontWeight: '700', color: '#64748b' }, statusPillText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
switchLabel: { fontSize: 14, fontWeight: '700', color: '#374151' }, switchLabel: { fontSize: 14, fontWeight: '700', color: '#374151' },
switchSub: { fontSize: 11, color: '#6b7280', marginTop: 2 } clientRowSub: { fontSize: 11, color: Colors.textMuted, fontStyle: 'italic' },
demoToggle: { flexDirection: 'row', alignItems: 'center', marginTop: 15, paddingTop: 15, borderTopWidth: 1, borderTopColor: '#f1f5f9' },
checkbox: { width: 18, height: 18, borderWidth: 2, borderColor: Colors.primary, borderRadius: 4, marginRight: 10 },
checkboxChecked: { backgroundColor: Colors.primary },
checkboxLabel: { fontSize: 13, fontWeight: '700', color: '#374151' }
}); });
export default LogActivityScreen; export default LogActivityScreen;

View File

@ -1,49 +1,77 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useContext } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView, Alert, Modal, TextInput, ScrollView } from 'react-native'; import {
View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator,
SafeAreaView, Alert, Modal, TextInput, ScrollView
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import DateTimePicker from '@react-native-community/datetimepicker';
import api from '../services/api'; import api from '../services/api';
import { useAuth } from '../context/AuthContext'; import { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors'; import Colors from '../constants/Colors';
const PipelineScreen = () => { const PipelineScreen = ({ navigation }) => {
const { userInfo } = useContext(AuthContext);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [pipelineType, setPipelineType] = useState('DEALS'); // 'DEALS' or 'LEADS'
const [opportunities, setOpportunities] = useState([]); const [opportunities, setOpportunities] = useState([]);
const [clients, setClients] = useState([]);
const [selectedStage, setSelectedStage] = useState('LEAD'); const [selectedStage, setSelectedStage] = useState('LEAD');
const stages = [ const dealStages = [
{ id: 'LEAD', label: 'Lead' }, { id: 'LEAD', label: 'Lead' },
{ id: 'QUALIFIED', label: 'Qual' }, { id: 'QUALIFIED', label: 'Qual' },
{ id: 'POTENTIAL', label: 'Poten' }, { id: 'POTENTIAL', label: 'Poten' },
{ id: 'DEMO', label: 'Demo' },
{ id: 'WON', label: 'Won' }, { id: 'WON', label: 'Won' },
]; ];
const [isModalOpen, setIsModalOpen] = useState(false); const leadStages = [
const [selectedOpp, setSelectedOpp] = useState(null); { id: 'LEAD', label: 'New Lead' },
const [updateData, setUpdateData] = useState({}); { id: 'PROSPECT', label: 'Prospect' },
{ id: 'CLIENT', label: 'Client' },
];
const fetchOpportunities = useCallback(async () => { const currentStages = pipelineType === 'DEALS' ? dealStages : leadStages;
const [isModalOpen, setIsModalOpen] = useState(false);
const [isClientModalOpen, setIsClientModalOpen] = useState(false);
const [selectedOpp, setSelectedOpp] = useState(null);
const [selectedClient, setSelectedClient] = useState(null);
const [updateData, setUpdateData] = useState({});
const [userModal, setUserModal] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
const fetchData = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
const { data } = await api.get('/opportunities'); const [oppRes, clientRes] = await Promise.all([
setOpportunities(data); api.get('/opportunities'),
api.get('/clients')
]);
setOpportunities(oppRes.data);
setClients(clientRes.data);
} catch (error) { } catch (error) {
console.error('Failed to fetch opportunities', error); console.error('Failed to fetch pipeline data', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, []);
const [users, setUsers] = useState([]);
useEffect(() => {
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
}, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
fetchOpportunities(); fetchData();
}, [fetchOpportunities]) }, [fetchData])
); );
const handleOpenModal = (item) => { const handleOpenModal = (item) => {
setSelectedOpp(item); setSelectedOpp(item);
setUpdateData({ setUpdateData({
stage: item.stage, stage: item.stage,
assignedTo: item.assignedTo,
demoPersonName: item.demoPersonName || '', demoPersonName: item.demoPersonName || '',
demoContactDetails: item.demoContactDetails || '', demoContactDetails: item.demoContactDetails || '',
expectedCloseDate: item.expectedCloseDate ? item.expectedCloseDate.split('T')[0] : '', expectedCloseDate: item.expectedCloseDate ? item.expectedCloseDate.split('T')[0] : '',
@ -53,11 +81,40 @@ const PipelineScreen = () => {
specialRate: item.specialRate ? String(item.specialRate) : '', specialRate: item.specialRate ? String(item.specialRate) : '',
freeOffers: item.freeOffers || '', freeOffers: item.freeOffers || '',
negotiationRemarks: item.negotiationRemarks || '', negotiationRemarks: item.negotiationRemarks || '',
value: String(item.value) value: String(item.value),
isDemoDone: !!item.isDemoDone
}); });
setIsModalOpen(true); setIsModalOpen(true);
}; };
const handleOpenClientModal = (item) => {
setSelectedClient(item);
setUpdateData({
status: item.status,
assignedTo: item.assignedTo
});
setIsClientModalOpen(true);
};
const handleUpdateClientStatus = async () => {
try {
await api.patch(`/clients/${selectedClient.id}`, { status: updateData.status });
setIsClientModalOpen(false);
fetchData();
Alert.alert("Success", "Client status updated");
} catch (error) {
Alert.alert("Error", "Failed to update status");
}
};
const handleConvertToDeal = () => {
setIsClientModalOpen(false);
navigation.navigate('AddOpportunity', {
client: selectedClient,
prefill: { title: `Opportunity for ${selectedClient.companyName || selectedClient.name}` }
});
};
const handleUpdate = async () => { const handleUpdate = async () => {
try { try {
const payload = { const payload = {
@ -68,7 +125,7 @@ const PipelineScreen = () => {
await api.patch(`/opportunities/${selectedOpp.id}`, payload); await api.patch(`/opportunities/${selectedOpp.id}`, payload);
setIsModalOpen(false); setIsModalOpen(false);
fetchOpportunities(); fetchData();
Alert.alert("Success", "Opportunity updated"); Alert.alert("Success", "Opportunity updated");
} catch (error) { } catch (error) {
const msg = error.response?.data?.message || error.message; const msg = error.response?.data?.message || error.message;
@ -76,213 +133,265 @@ const PipelineScreen = () => {
} }
}; };
const renderItem = ({ item }) => ( const UserPicker = () => {
<TouchableOpacity style={styles.card} activeOpacity={0.7} onPress={() => handleOpenModal(item)}> const currentAssignee = users.find(u => u.id === updateData.assignedTo);
return (
<>
<Text style={styles.modalLabel}>Primary Owner (Assigned To)</Text>
<TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
<Text style={styles.pickerBtnText}>
{currentAssignee ? `${currentAssignee.name} (${currentAssignee.role})` : 'Select Teammate'}
</Text>
<Text style={styles.pickerArrow}></Text>
</TouchableOpacity>
</>
);
};
const renderItem = ({ item }) => {
const isLead = !!item.status; // Clients have status, opportunities have stages
return (
<TouchableOpacity
style={styles.card}
activeOpacity={0.7}
onPress={() => isLead ? handleOpenClientModal(item) : handleOpenModal(item)}
>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{item.title}</Text> <Text style={styles.cardTitle}>{item.title || item.companyName || item.name}</Text>
<Text style={styles.cardValue}>{item.value.toLocaleString()}</Text> {!isLead && <Text style={styles.cardValue}>{item.value.toLocaleString()}</Text>}
</View> </View>
<View style={styles.cardFooter}> <View style={styles.cardFooter}>
<View style={styles.clientContainer}> <View style={styles.clientContainer}>
<View style={styles.avatar}> <View style={styles.avatar}>
<Text style={styles.avatarText}>{item.client?.name?.charAt(0)}</Text> <Text style={styles.avatarText}>{(item.client?.companyName || item.client?.name || item.name || item.companyName)?.charAt(0)}</Text>
</View> </View>
<Text style={styles.clientName}>{item.client?.name}</Text> <View>
<Text style={styles.clientName}>{item.client?.companyName || item.client?.name || item.companyName || item.name}</Text>
{(item.client?.contactName || item.contactName) && <Text style={styles.contactSubText}>{item.client?.contactName || item.contactName}</Text>}
</View> </View>
<View style={[styles.priorityBadge, { backgroundColor: item.priority === 'High' ? '#fee2e2' : '#fef3c7' }]}> </View>
<Text style={[styles.priorityText, { color: item.priority === 'High' ? '#ef4444' : '#f59e0b' }]}> <View style={[styles.priorityBadge, { backgroundColor: (item.priority === 'High' || isLead) ? '#fee2e2' : '#fef3c7' }]}>
{item.priority || 'Normal'} <Text style={[styles.priorityText, { color: (item.priority === 'High' || isLead) ? '#ef4444' : '#f59e0b' }]}>
{isLead ? item.status : (item.priority || 'Normal')}
</Text> </Text>
</View> </View>
</View> </View>
<View style={styles.assignedContainer}>
{item.stage === 'WON' && ( <Text style={styles.assignedLabel}>Owner: </Text>
<TouchableOpacity <Text style={styles.assignedName}>{item.user?.name || 'Unassigned'}</Text>
style={styles.workOrderButton} </View>
onPress={async () => {
try {
await api.post('/work-orders/from-opportunity', { opportunityId: item.id });
Alert.alert("Success", "Work order created!");
} catch (e) {
Alert.alert("Error", "Already converted or failed");
}
}}
>
<Text style={styles.workOrderButtonText}>Start Work Order</Text>
</TouchableOpacity>
)}
</TouchableOpacity> </TouchableOpacity>
); );
};
const filteredItems = opportunities.filter(item => item.stage === selectedStage); const filteredData = (pipelineType === 'DEALS' ? opportunities : clients).filter(item => {
const matchesStage = (pipelineType === 'DEALS' ? item.stage : item.status) === selectedStage;
const matchesUser = item.assignedTo === userInfo?.id;
return matchesStage && matchesUser;
});
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
{/* Stage Selector */} {/* Pipeline Type Switcher */}
<View style={styles.switcherContainer}>
<TouchableOpacity
style={[styles.switcherBtn, pipelineType === 'DEALS' && styles.activeSwitcherBtn]}
onPress={() => { setPipelineType('DEALS'); setSelectedStage('LEAD'); }}
>
<Text style={[styles.switcherText, pipelineType === 'DEALS' && styles.activeSwitcherText]}>DEALS</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.switcherBtn, pipelineType === 'LEADS' && styles.activeSwitcherBtn]}
onPress={() => { setPipelineType('LEADS'); setSelectedStage('LEAD'); }}
>
<Text style={[styles.switcherText, pipelineType === 'LEADS' && styles.activeSwitcherText]}>LEADS</Text>
</TouchableOpacity>
</View>
<View style={styles.stageBar}> <View style={styles.stageBar}>
{stages.map((stage) => ( {currentStages.map(stage => (
<TouchableOpacity <TouchableOpacity
key={stage.id} key={stage.id}
style={[styles.stageItem, selectedStage === stage.id && styles.activeStageItem]}
onPress={() => setSelectedStage(stage.id)} onPress={() => setSelectedStage(stage.id)}
style={[
styles.stageItem,
selectedStage === stage.id && styles.activeStageItem
]}
> >
<Text style={[ <Text style={[styles.stageLabel, selectedStage === stage.id && styles.activeStageLabel]}>{stage.label}</Text>
styles.stageLabel, <View style={[styles.stageIndicator, selectedStage === stage.id && { backgroundColor: Colors.primary }]} />
selectedStage === stage.id && styles.activeStageLabel
]}>
{stage.label}
</Text>
<View style={[
styles.stageIndicator,
selectedStage === stage.id && { backgroundColor: selectedStage === 'WON' ? Colors.secondary : Colors.primary }
]} />
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
{loading ? ( {loading ? (
<View style={styles.center}> <View style={styles.center}><ActivityIndicator size="large" color={Colors.primary} /></View>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
) : ( ) : (
<FlatList <FlatList
data={filteredItems} data={filteredData}
keyExtractor={item => item.id}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContainer} contentContainerStyle={styles.listContainer}
ListEmptyComponent={ ListEmptyComponent={<View style={styles.empty}><Text style={styles.emptyText}>No items in this stage</Text></View>}
<View style={styles.emptyContainer}> onRefresh={fetchData}
<Text style={styles.emptyText}>No opportunities in this stage</Text> refreshing={false}
</View>
}
onRefresh={fetchOpportunities}
refreshing={loading}
/> />
)} )}
{/* UPDATE MODAL */} {/* FAB - New Deal */}
<Modal <TouchableOpacity
visible={isModalOpen} style={styles.fab}
animationType="slide" onPress={() => navigation.navigate('AddOpportunity')}
transparent={true} activeOpacity={0.85}
onRequestClose={() => setIsModalOpen(false)}
> >
<View style={styles.modalOverlay}> <Text style={styles.fabText}>+</Text>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Update Stage</Text>
<TouchableOpacity onPress={() => setIsModalOpen(false)}>
<Text style={styles.closeButton}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
</View>
<ScrollView style={styles.modalForm}>
<Text style={styles.label}>Current Stage</Text> <Modal visible={isModalOpen} animationType="slide">
<View style={styles.stagePicker}> <View style={styles.modalContainer}>
{stages.map(s => ( <View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Update Opportunity</Text>
<TouchableOpacity onPress={() => setIsModalOpen(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<ScrollView style={styles.modalBody} contentContainerStyle={{ paddingBottom: 50 }}>
<Text style={styles.modalLabel}>Expected Value ()</Text>
<TextInput style={styles.modalInput} value={updateData.value} onChangeText={v => setUpdateData({ ...updateData, value: v })} keyboardType="numeric" />
<UserPicker />
<Text style={styles.modalLabel}>Pipeline Stage</Text>
<View style={styles.stagePickerContainer}>
{dealStages.map(s => (
<TouchableOpacity <TouchableOpacity
key={s.id} key={s.id}
style={[styles.stageChip, updateData.stage === s.id && styles.activeStageChip]} style={[styles.stageChip, updateData.stage === s.id && styles.activeStageChip]}
onPress={() => setUpdateData({...updateData, stage: s.id})} onPress={() => setUpdateData({ ...updateData, stage: s.id })}
> >
<Text style={[styles.stageChipText, updateData.stage === s.id && styles.activeStageChipText]}>{s.label}</Text> <Text style={[styles.stageChipText, updateData.stage === s.id && styles.activeStageChipText]}>{s.label}</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
<Text style={styles.label}>Expected Revenue ()</Text> <Text style={styles.modalLabel}>Expected Close Date</Text>
<TextInput <TouchableOpacity
style={styles.input} style={styles.pickerBtn}
value={updateData.value} onPress={() => setShowDatePicker(true)}
onChangeText={t => setUpdateData({...updateData, value: t})} >
keyboardType="numeric" <Text style={updateData.expectedCloseDate ? styles.pickerBtnText : styles.pickerPlaceholder}>
/> {updateData.expectedCloseDate || 'Select date...'}
</Text>
{(updateData.stage === 'DEMO' || updateData.stage === 'WON') && ( <Text style={styles.pickerArrow}>📅</Text>
<View style={styles.mandatorySection}> </TouchableOpacity>
<Text style={styles.sectionHeader}>DEMO DETAILS (MANDATORY)</Text>
{showDatePicker && (
<Text style={styles.label}>Person Name</Text> <DateTimePicker
<TextInput value={updateData.expectedCloseDate ? new Date(updateData.expectedCloseDate) : new Date()}
style={styles.input} mode="date"
value={updateData.demoPersonName} display="default"
onChangeText={t => setUpdateData({...updateData, demoPersonName: t})} onChange={(event, selectedDate) => {
placeholder="Name of person met" setShowDatePicker(false);
/> if (selectedDate) {
setUpdateData({ ...updateData, expectedCloseDate: selectedDate.toISOString().split('T')[0] });
<Text style={styles.label}>Contact Details</Text> }
<TextInput }}
style={styles.input} />
value={updateData.demoContactDetails} )}
onChangeText={t => setUpdateData({...updateData, demoContactDetails: t})}
placeholder="Phone or Email" <Text style={styles.modalLabel}>Next Action / Remarks</Text>
/> <TextInput style={[styles.modalInput, { height: 80 }]} value={updateData.negotiationRemarks} onChangeText={v => setUpdateData({ ...updateData, negotiationRemarks: v })} multiline placeholder="Describe next steps..." />
<Text style={styles.label}>Expected Closing Date (YYYY-MM-DD)</Text> <TouchableOpacity style={styles.updateBtn} onPress={handleUpdate}>
<TextInput <Text style={styles.updateBtnText}>Save Updates</Text>
style={styles.input} </TouchableOpacity>
value={updateData.expectedCloseDate}
onChangeText={t => setUpdateData({...updateData, expectedCloseDate: t})} <TouchableOpacity
placeholder="2024-12-31" style={styles.activityBtn}
/> onPress={() => {
setIsModalOpen(false);
<Text style={styles.label}>Competitor Mention</Text> navigation.navigate('LogActivity', { client: selectedOpp?.client, tab: 'followup' });
<TextInput }}
style={styles.input} >
value={updateData.competitorMention} <Text style={styles.activityBtnText}>📅 Schedule Next Activity</Text>
onChangeText={t => setUpdateData({...updateData, competitorMention: t})}
placeholder="None or Competitor Name"
/>
<Text style={styles.label}>Queries / Objections</Text>
<TextInput
style={[styles.input, {height: 60}]}
multiline
value={updateData.keyQueries}
onChangeText={t => setUpdateData({...updateData, keyQueries: t})}
/>
</View>
)}
{updateData.stage === 'WON' && (
<View style={[styles.mandatorySection, {backgroundColor: '#f0fdf4', borderColor: '#bbf7d0'}]}>
<Text style={[styles.sectionHeader, {color: '#166534'}]}>CLOSING DETAILS (MANDATORY)</Text>
<Text style={styles.label}>Payment Mode</Text>
<TextInput
style={styles.input}
value={updateData.paymentMode}
onChangeText={t => setUpdateData({...updateData, paymentMode: t})}
placeholder="Cash / Bank Transfer / UPI"
/>
<Text style={styles.label}>Special Rate (Optional)</Text>
<TextInput
style={styles.input}
value={updateData.specialRate}
onChangeText={t => setUpdateData({...updateData, specialRate: t})}
placeholder="Final agreed rate"
keyboardType="numeric"
/>
<Text style={styles.label}>Negotiation Remarks</Text>
<TextInput
style={styles.input}
value={updateData.negotiationRemarks}
onChangeText={t => setUpdateData({...updateData, negotiationRemarks: t})}
/>
</View>
)}
<TouchableOpacity style={styles.saveButton} onPress={handleUpdate}>
<Text style={styles.saveButtonText}>SAVE UPDATE</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={{height: 40}} />
</ScrollView> </ScrollView>
</View> </View>
{/* Nested User Modal */}
<Modal visible={userModal} animationType="fade" transparent={true}>
<View style={styles.modalOverlay}>
<View style={styles.userListContainer}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Transfer To</Text>
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<FlatList
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.userRow}
onPress={() => {
setUpdateData({ ...updateData, assignedTo: item.id });
setUserModal(false);
}}
>
<View style={styles.userAvatar}><Text style={styles.userAvatarText}>{item.name?.charAt(0)}</Text></View>
<View>
<Text style={styles.userName}>{item.name}</Text>
<Text style={styles.userRole}>{item.role}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
</View>
</Modal>
</Modal>
{/* Client Update Modal */}
<Modal visible={isClientModalOpen} animationType="slide">
<View style={styles.modalContainer}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Manage Lead</Text>
<TouchableOpacity onPress={() => setIsClientModalOpen(false)}><Text style={styles.modalClose}></Text></TouchableOpacity>
</View>
<ScrollView style={styles.modalBody}>
<Text style={styles.modalLabel}>Lead / Client Name</Text>
<Text style={styles.clientNameBig}>{selectedClient?.companyName || selectedClient?.name}</Text>
<Text style={styles.modalLabel}>Update Status (Stage)</Text>
<View style={styles.stagePickerContainer}>
{leadStages.map(s => (
<TouchableOpacity
key={s.id}
style={[styles.stageChip, updateData.status === s.id && styles.activeStageChip]}
onPress={() => setUpdateData({ ...updateData, status: s.id })}
>
<Text style={[styles.stageChipText, updateData.status === s.id && styles.activeStageChipText]}>{s.label}</Text>
</TouchableOpacity>
))}
</View>
<TouchableOpacity style={styles.updateBtn} onPress={handleUpdateClientStatus}>
<Text style={styles.updateBtnText}>Save Status</Text>
</TouchableOpacity>
<View style={styles.divider} />
<Text style={styles.modalLabel}>Pipeline Movement</Text>
<TouchableOpacity style={styles.convertBtn} onPress={handleConvertToDeal}>
<Text style={styles.convertBtnText}>🚀 Convert to Deal (Deals Pipeline)</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.activityBtn}
onPress={() => {
setIsClientModalOpen(false);
navigation.navigate('ClientDetails', { client: selectedClient });
}}
>
<Text style={styles.activityBtnText}>👁 View Full Details</Text>
</TouchableOpacity>
</ScrollView>
</View> </View>
</Modal> </Modal>
</SafeAreaView> </SafeAreaView>
@ -290,242 +399,76 @@ const PipelineScreen = () => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: { flex: 1, backgroundColor: '#f8fafc' },
flex: 1, switcherContainer: { flexDirection: 'row', backgroundColor: 'white', padding: 8, marginHorizontal: 16, marginTop: 10, borderRadius: 12, borderWidth: 1, borderColor: '#edf2f7' },
backgroundColor: Colors.background, switcherBtn: { flex: 1, paddingVertical: 8, alignItems: 'center', borderRadius: 8 },
}, activeSwitcherBtn: { backgroundColor: Colors.primary },
stageBar: { switcherText: { fontSize: 12, fontWeight: '800', color: '#94a3b8' },
flexDirection: 'row', activeSwitcherText: { color: 'white' },
backgroundColor: 'white', filterBar: { paddingVertical: 12 },
borderBottomWidth: 1, filterScroll: { paddingHorizontal: 16, gap: 10 },
borderBottomColor: '#edf2f7', filterChip: { paddingHorizontal: 16, paddingVertical: 8, backgroundColor: 'white', borderRadius: 20, borderWidth: 1, borderColor: '#e2e8f0' },
paddingTop: 10, activeFilterChip: { backgroundColor: Colors.primary, borderColor: Colors.primary },
}, filterChipText: { fontSize: 13, fontWeight: '600', color: '#64748b' },
stageItem: { activeFilterChipText: { color: 'white' },
flex: 1, stagePickerContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 10 },
alignItems: 'center', stageChip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, borderWidth: 1, borderColor: '#e2e8f0', backgroundColor: '#f8fafc' },
paddingVertical: 12, activeStageChip: { backgroundColor: Colors.primary, borderColor: Colors.primary },
}, stageChipText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
activeStageItem: { activeStageChipText: { color: 'white' },
// backgroundColor: '#fdf2f8', clientNameBig: { fontSize: 20, fontWeight: '800', color: Colors.primary, marginBottom: 10 },
}, divider: { height: 1, backgroundColor: '#f1f5f9', marginVertical: 25 },
stageLabel: { convertBtn: { backgroundColor: '#f0f9ff', padding: 18, borderRadius: 14, alignItems: 'center', borderWidth: 1.5, borderColor: '#0ea5e9' },
fontSize: 12, convertBtnText: { color: '#0369a1', fontSize: 15, fontWeight: '800' },
fontWeight: '600', stageBar: { flexDirection: 'row', backgroundColor: 'white', borderBottomWidth: 1, borderBottomColor: '#edf2f7' },
color: Colors.textMuted, stageItem: { flex: 1, alignItems: 'center', paddingVertical: 14 },
marginBottom: 8, activeStageItem: { borderBottomWidth: 0 },
}, stageLabel: { fontSize: 11, fontWeight: '700', color: '#94a3b8', textTransform: 'uppercase' },
activeStageLabel: { activeStageLabel: { color: Colors.primary },
color: Colors.text, stageIndicator: { height: 3, width: '40%', marginTop: 8, borderRadius: 2 },
fontWeight: 'bold', listContainer: { padding: 16 },
}, card: { backgroundColor: 'white', borderRadius: 16, padding: 16, marginBottom: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
stageIndicator: { cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
height: 3, cardTitle: { fontSize: 15, fontWeight: '800', color: '#1e293b', flex: 1 },
width: '60%', cardValue: { fontSize: 15, fontWeight: '900', color: Colors.primary },
borderRadius: 3, cardFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' },
backgroundColor: 'transparent', clientContainer: { flexDirection: 'row', alignItems: 'center', gap: 10 },
}, avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
listContainer: { avatarText: { fontSize: 12, fontWeight: 'bold', color: '#64748b' },
padding: 16, clientName: { fontSize: 13, fontWeight: '700', color: '#334155' },
}, contactSubText: { fontSize: 11, color: '#94a3b8', fontStyle: 'italic' },
center: { priorityBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
flex: 1, priorityText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' },
justifyContent: 'center', assignedContainer: { flexDirection: 'row', marginTop: 12, paddingTop: 12, borderTopWidth: 1, borderTopColor: '#f1f5f9' },
alignItems: 'center', assignedLabel: { fontSize: 11, color: '#94a3b8' },
}, assignedName: { fontSize: 11, fontWeight: 'bold', color: '#64748b' },
card: { center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
backgroundColor: 'white', empty: { alignItems: 'center', marginTop: 100 },
borderRadius: 12, emptyText: { color: '#94a3b8', fontWeight: '600' },
padding: 16, modalContainer: { flex: 1, backgroundColor: 'white' },
marginBottom: 12, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', backgroundColor: '#f8fafc' },
shadowColor: '#000', modalTitle: { fontSize: 18, fontWeight: '900', color: '#1e293b' },
shadowOffset: { width: 0, height: 2 }, modalClose: { fontSize: 24, color: '#94a3b8' },
shadowOpacity: 0.05, modalBody: { padding: 20 },
shadowRadius: 4, modalLabel: { fontSize: 11, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', marginBottom: 8, marginTop: 20 },
elevation: 3, modalInput: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 14, fontSize: 15, backgroundColor: '#f8fafc' },
borderLeftWidth: 4, pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
borderLeftColor: Colors.primary, pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '700' },
}, pickerPlaceholder: { color: '#94a3b8', fontSize: 15 },
cardHeader: { pickerArrow: { fontSize: 20, color: '#94a3b8' },
flexDirection: 'row', updateBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 14, marginTop: 40, alignItems: 'center' },
justifyContent: 'space-between', updateBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
alignItems: 'flex-start', activityBtn: { padding: 18, borderRadius: 14, marginTop: 15, alignItems: 'center', borderWidth: 1.5, borderColor: Colors.primary },
marginBottom: 12, activityBtnText: { color: Colors.primary, fontSize: 14, fontWeight: '800' },
}, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'flex-end' },
cardTitle: { userListContainer: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '60%' },
fontSize: 15, userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
fontWeight: 'bold', userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
color: Colors.text, userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
flex: 1, userName: { fontSize: 15, fontWeight: '700', color: '#1e293b' },
marginRight: 8, userRole: { fontSize: 12, color: '#64748b' },
}, fab: { position: 'absolute', right: 20, bottom: 30, width: 56, height: 56, borderRadius: 28, backgroundColor: Colors.primary, alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 8 },
cardValue: { fabText: { color: 'white', fontSize: 32, fontWeight: '300', lineHeight: 38 }
fontSize: 15,
fontWeight: '800',
color: Colors.primary,
},
cardFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
clientContainer: {
flexDirection: 'row',
alignItems: 'center',
},
avatar: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: Colors.border,
justifyContent: 'center',
alignItems: 'center',
marginRight: 8,
},
avatarText: {
fontSize: 10,
fontWeight: 'bold',
color: Colors.textMuted,
},
clientName: {
fontSize: 13,
color: Colors.textMuted,
},
priorityBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
priorityText: {
fontSize: 11,
fontWeight: 'bold',
},
emptyContainer: {
padding: 40,
alignItems: 'center',
},
emptyText: {
color: '#a0aec0',
fontSize: 14,
},
workOrderButton: {
marginTop: 15,
backgroundColor: Colors.secondary,
padding: 10,
borderRadius: 8,
alignItems: 'center',
},
workOrderButtonText: {
color: 'white',
fontWeight: 'bold',
fontSize: 13,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: 'white',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
height: '85%',
paddingTop: 20,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
},
modalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: Colors.text,
},
closeButton: {
color: Colors.textMuted,
fontWeight: '600',
},
modalForm: {
padding: 20,
},
label: {
fontSize: 12,
fontWeight: 'bold',
color: Colors.textMuted,
marginBottom: 8,
marginTop: 15,
textTransform: 'uppercase',
},
input: {
backgroundColor: '#f8fafc',
borderRadius: 8,
padding: 12,
fontSize: 14,
color: Colors.text,
borderWidth: 1,
borderColor: '#e2e8f0',
},
stagePicker: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
stageChip: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
backgroundColor: '#f1f5f9',
borderWidth: 1,
borderColor: '#e2e8f0',
},
activeStageChip: {
backgroundColor: Colors.primary,
borderColor: Colors.primary,
},
stageChipText: {
fontSize: 11,
fontWeight: 'bold',
color: Colors.textMuted,
},
activeStageChipText: {
color: 'white',
},
mandatorySection: {
marginTop: 20,
padding: 15,
backgroundColor: '#f0f7ff',
borderRadius: 12,
borderWidth: 1,
borderColor: '#bae6fd',
},
sectionHeader: {
fontSize: 11,
fontWeight: 'black',
color: '#0369a1',
marginBottom: 10,
},
saveButton: {
backgroundColor: Colors.primary,
borderRadius: 12,
paddingVertical: 16,
alignItems: 'center',
marginTop: 30,
shadowColor: Colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
},
saveButtonText: {
color: 'white',
fontWeight: 'bold',
fontSize: 16,
}
}); });
export default PipelineScreen; export default PipelineScreen;

View File

@ -9,12 +9,26 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import api from '../services/api'; import api from '../services/api';
import Colors from '../constants/Colors'; import Colors from '../constants/Colors';
const TYPE_ICONS = {
'FOLLOWUP': '📅',
'DEMO': '📽️',
'QUOTE': '📝',
'NEGOTIATION': '🤝',
};
const TYPE_COLORS = {
'FOLLOWUP': '#6366f1',
'DEMO': '#3b82f6',
'QUOTE': '#a855f7',
'NEGOTIATION': '#f59e0b',
};
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 [sections, setSections] = useState([]);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [activeFilter, setActiveFilter] = useState('ALL'); // ALL, PENDING, DONE const [activeFilter, setActiveFilter] = useState('PENDING'); // ALL, PENDING, DONE
const groupByDay = (followups) => { const groupByDay = (followups) => {
const map = {}; const map = {};
@ -54,15 +68,15 @@ const TasksScreen = ({ navigation }) => {
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter])); useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
const handleMarkDone = async (id) => { const handleMarkDone = async (id) => {
Alert.alert('Mark as Done?', 'This will complete the task and dismiss the notification.', [ Alert.alert('Complete Activity?', 'This will mark the activity as done and remove it from pending.', [
{ text: 'Cancel', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
{ {
text: 'Done ✓', onPress: async () => { text: 'Complete ✓', onPress: async () => {
try { try {
await api.patch(`/followups/${id}`, { status: 'DONE' }); await api.patch(`/followups/${id}`, { status: 'DONE' });
fetchTasks(); fetchTasks();
} catch (e) { } catch (e) {
Alert.alert('Error', 'Could not update task.'); Alert.alert('Error', 'Could not update activity.');
} }
} }
} }
@ -77,18 +91,25 @@ const TasksScreen = ({ navigation }) => {
const renderTask = ({ item }) => { const renderTask = ({ item }) => {
const isPending = item.status === 'PENDING'; const isPending = item.status === 'PENDING';
const isOverdue = isPending && new Date(item.date) < new Date(); const isOverdue = isPending && new Date(item.date) < new Date();
const type = item.type || 'FOLLOWUP';
return ( return (
<View style={[styles.card, isOverdue && styles.cardOverdue, !isPending && styles.cardDone]}> <View style={[styles.card, isOverdue && styles.cardOverdue, !isPending && styles.cardDone]}>
<View style={[styles.dot, { backgroundColor: isOverdue ? '#ef4444' : isPending ? Colors.primary : '#10b981' }]} /> <View style={[styles.typeIconBadge, { backgroundColor: TYPE_COLORS[type] + '20' }]}>
<Text style={styles.typeIconText}>{TYPE_ICONS[type]}</Text>
</View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={styles.clientName}>{item.client?.name || 'Unknown Client'}</Text> <Text style={styles.clientName}>{item.client?.companyName || item.client?.name || 'Unknown Client'}</Text>
{item.client?.phone && ( {item.client?.phone && (
<TouchableOpacity onPress={() => handleCall(item.client.phone)}> <TouchableOpacity onPress={() => handleCall(item.client.phone)} style={styles.callCircle}>
<Text style={styles.callIcon}>📞</Text> <Text style={styles.callIcon}>📞</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<View style={styles.typeBadge}>
<Text style={[styles.typeText, { color: TYPE_COLORS[type] }]}>{type}</Text>
</View>
<Text style={styles.notes} numberOfLines={2}>{item.notes}</Text> <Text style={styles.notes} numberOfLines={2}>{item.notes}</Text>
<Text style={styles.time}> <Text style={styles.time}>
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
@ -96,7 +117,7 @@ const TasksScreen = ({ navigation }) => {
</Text> </Text>
</View> </View>
{isPending && ( {isPending && (
<TouchableOpacity style={styles.doneBtn} onPress={() => handleMarkDone(item.id)}> <TouchableOpacity style={[styles.doneBtn, { backgroundColor: TYPE_COLORS[type] }]} onPress={() => handleMarkDone(item.id)}>
<Text style={styles.doneBtnText}>Done</Text> <Text style={styles.doneBtnText}>Done</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
@ -113,10 +134,20 @@ const TasksScreen = ({ navigation }) => {
<View style={styles.container}> <View style={styles.container}>
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" /> <StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
<View style={[styles.header, { paddingTop: insets.top + 16 }]}> <View style={[styles.header, { paddingTop: insets.top + 16 }]}>
<Text style={styles.headerTitle}>My Tasks</Text> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={styles.headerSub}>Sorted by date</Text> <View>
<Text style={styles.headerTitle}>Activities</Text>
<Text style={styles.headerSub}>Manage your schedule</Text>
</View>
<TouchableOpacity
style={styles.addBtn}
onPress={() => navigation.navigate('LogActivity', { tab: 'followup' })}
>
<Text style={styles.addBtnText}>+ New</Text>
</TouchableOpacity>
</View>
<View style={styles.filterRow}> <View style={styles.filterRow}>
{['ALL', 'PENDING', 'DONE'].map(f => ( {['PENDING', 'DONE', 'ALL'].map(f => (
<TouchableOpacity <TouchableOpacity
key={f} key={f}
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]} style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
@ -135,16 +166,16 @@ const TasksScreen = ({ navigation }) => {
renderSectionHeader={({ section }) => ( renderSectionHeader={({ section }) => (
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text> <Text style={styles.sectionTitle}>{section.title}</Text>
<Text style={styles.sectionCount}>{section.data.length} task{section.data.length !== 1 ? 's' : ''}</Text> <Text style={styles.sectionCount}>{section.data.length} item{section.data.length !== 1 ? 's' : ''}</Text>
</View> </View>
)} )}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
contentContainerStyle={{ paddingBottom: 40 }} contentContainerStyle={{ paddingBottom: 40 }}
ListEmptyComponent={ ListEmptyComponent={
<View style={styles.empty}> <View style={styles.empty}>
<Text style={styles.emptyIcon}>🎉</Text> <Text style={styles.emptyIcon}></Text>
<Text style={styles.emptyTitle}>All Clear!</Text> <Text style={styles.emptyTitle}>All Caught Up!</Text>
<Text style={styles.emptySub}>No tasks match this filter.</Text> <Text style={styles.emptySub}>No activities scheduled here.</Text>
</View> </View>
} }
/> />
@ -153,34 +184,40 @@ const TasksScreen = ({ navigation }) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f1f5f9' }, container: { flex: 1, backgroundColor: '#f8fafc' },
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 }, header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 },
headerTitle: { color: 'white', fontSize: 26, fontWeight: '900' }, headerTitle: { color: 'white', fontSize: 28, fontWeight: '900' },
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2, marginBottom: 14 }, headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 13, marginTop: 2, marginBottom: 16 },
filterRow: { flexDirection: 'row', gap: 8 }, filterRow: { flexDirection: 'row', gap: 8 },
filterBtn: { paddingHorizontal: 16, paddingVertical: 6, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' }, filterBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' },
filterBtnActive: { backgroundColor: 'white' }, filterBtnActive: { backgroundColor: 'white' },
filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '700' }, filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '800' },
filterTextActive: { color: Colors.primary }, filterTextActive: { color: Colors.primary },
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 20, paddingBottom: 8 }, addBtn: { backgroundColor: 'rgba(255,255,255,0.25)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 },
sectionTitle: { fontSize: 13, fontWeight: '900', color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5 }, 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' }, 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 }, 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: 4, borderLeftColor: '#ef4444' }, cardOverdue: { borderLeftWidth: 5, borderLeftColor: '#ef4444' },
cardDone: { opacity: 0.65 }, cardDone: { opacity: 0.7 },
dot: { width: 10, height: 10, borderRadius: 5, marginRight: 12 }, typeIconBadge: { width: 44, height: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', marginRight: 14 },
clientName: { fontSize: 14, fontWeight: '800', color: '#1e293b', marginBottom: 3, flex: 1 }, typeIconText: { fontSize: 20 },
callIcon: { fontSize: 18, paddingHorizontal: 10 }, clientName: { fontSize: 15, fontWeight: '800', color: '#1e293b', marginBottom: 2, flex: 1 },
notes: { fontSize: 12, color: '#64748b', lineHeight: 17, marginBottom: 5 }, callCircle: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
time: { fontSize: 10, color: '#94a3b8', fontWeight: '600' }, callIcon: { fontSize: 14 },
doneBtn: { backgroundColor: Colors.primary, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10, marginLeft: 10 }, typeBadge: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, backgroundColor: '#f8fafc', marginBottom: 6 },
doneBtnText: { color: 'white', fontSize: 11, fontWeight: '900' }, typeText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
completedBadge: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 10 }, notes: { fontSize: 13, color: '#475569', lineHeight: 18, marginBottom: 8 },
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 14 }, time: { fontSize: 11, color: '#94a3b8', fontWeight: '600' },
empty: { alignItems: 'center', paddingTop: 80 }, doneBtn: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 12, marginLeft: 12 },
emptyIcon: { fontSize: 48, marginBottom: 12 }, doneBtnText: { color: 'white', fontSize: 12, fontWeight: '900' },
emptyTitle: { fontSize: 18, fontWeight: '800', color: '#1e293b' }, completedBadge: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 12 },
emptySub: { fontSize: 13, color: '#94a3b8', marginTop: 6 }, 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 },
}); });
export default TasksScreen; export default TasksScreen;