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",
"dependencies": {
"@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-navigation/bottom-tabs": "^7.15.9",
"@react-navigation/native": "^7.1.26",
@ -2976,6 +2978,42 @@
"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": {
"version": "0.83.1",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.1.tgz",

View File

@ -11,6 +11,8 @@
},
"dependencies": {
"@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-navigation/bottom-tabs": "^7.15.9",
"@react-navigation/native": "^7.1.26",

View File

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

View File

@ -1,18 +1,39 @@
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet, Alert, ScrollView, Platform, PermissionsAndroid, ActivityIndicator } from 'react-native';
import React, { useState, useEffect, useContext } from 'react';
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 { pick } from '@react-native-documents/picker';
import api from '../services/api';
import Colors from '../constants/Colors';
import { AuthContext } from '../context/AuthContext';
const AddClientScreen = ({ navigation }) => {
const [name, setName] = useState('');
const { userInfo } = useContext(AuthContext);
const [companyName, setCompanyName] = useState('');
const [contactName, setContactName] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [address, setAddress] = useState('');
const [landmark, setLandmark] = useState('');
const [closingProbability, setClosingProbability] = useState('');
const [expectedClosingTimeframe, setExpectedClosingTimeframe] = useState('');
const [isDemoDone, setIsDemoDone] = useState(false);
const [location, setLocation] = useState(null);
const [loading, setLoading] = 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 () => {
if (Platform.OS === 'android') {
@ -43,13 +64,11 @@ const AddClientScreen = ({ navigation }) => {
setLocating(true);
Geolocation.getCurrentPosition(
(position) => {
console.log('Location success:', position);
setLocation(position.coords);
setLocating(false);
Alert.alert("Success", "Location Captured!");
},
(error) => {
console.log('Location error:', error);
setLocating(false);
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 () => {
if (!name || !phone) {
Alert.alert("Error", "Name and Phone are required");
if (!contactName || !phone) {
Alert.alert("Error", "Contact Name and Phone are required");
return;
}
console.log('Current Location Check Before Submit:', location);
if (!location) {
Alert.alert("Debug", "Location state is null! Did you click capture?");
}
const payload = {
name,
name: companyName || contactName,
companyName,
contactName,
phone,
status: 'LEAD',
assignedTo: assignedUser?.id || userInfo?.id,
closingProbability: closingProbability ? parseInt(closingProbability) : 0,
expectedClosingTimeframe,
isDemoDone,
files: selectedFiles,
...(email ? { email } : {}),
...(address ? { address } : {}),
...(landmark ? { landmark } : {}),
...(location ? { lat: location.latitude, lng: location.longitude } : {})
};
console.log('Submitting Payload:', JSON.stringify(payload, null, 2));
setLoading(true);
try {
await api.post('/clients', payload);
@ -97,76 +157,122 @@ const AddClientScreen = ({ navigation }) => {
return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.label}>Name *</Text>
<TextInput style={styles.input} value={name} onChangeText={setName} />
<Text style={styles.label}>Company Name</Text>
<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>
<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>
<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>
<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>
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} />
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} placeholder="Enter nearby landmark" />
<View style={styles.locationContainer}>
<Button
title={locating ? "Locating..." : (location ? "Update Location" : "Capture Location")}
onPress={getCurrentLocation}
disabled={locating}
color={Colors.secondary}
/>
{location && (
<Text style={styles.locationText}>
Lat: {location.latitude.toFixed(4)}, Lng: {location.longitude.toFixed(4)}
</Text>
<TouchableOpacity style={[styles.locationBtn, { backgroundColor: location ? '#16a34a' : Colors.secondary }]} onPress={getCurrentLocation} disabled={locating}>
{locating ? <ActivityIndicator color="white" /> : <Text style={styles.locationBtnText}>{location ? "✓ Location Captured" : "📍 Capture Current Location"}</Text>}
</TouchableOpacity>
<View style={styles.fileSection}>
<View style={styles.fileHeader}>
<Text style={styles.label}>Files / Attachments</Text>
<TouchableOpacity onPress={pickFiles} style={styles.addFileBtn}>
<Text style={styles.addFileBtnText}>+ ADD FILE</Text>
</TouchableOpacity>
</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 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>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: Colors.background
},
label: {
fontSize: 16,
marginBottom: 5,
fontWeight: 'bold',
color: Colors.text
},
input: {
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 5,
padding: 10,
marginBottom: 15,
backgroundColor: 'white',
color: Colors.text
},
locationContainer: {
marginBottom: 20,
padding: 10,
backgroundColor: Colors.backgroundSecondary,
borderRadius: 5
},
locationText: {
marginTop: 5,
textAlign: 'center',
color: Colors.textMuted
},
spacer: {
height: 20
}
container: { padding: 20, backgroundColor: '#fff' },
label: { fontSize: 13, fontWeight: 'bold', color: '#64748b', marginBottom: 6, marginTop: 15, textTransform: 'uppercase' },
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' },
pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '600' },
pickerArrow: { fontSize: 20, color: '#94a3b8' },
locationBtn: { padding: 15, borderRadius: 12, marginTop: 20, alignItems: 'center' },
locationBtnText: { color: 'white', fontWeight: 'bold' },
submitBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 12, marginTop: 30, alignItems: 'center', marginBottom: 50 },
submitBtnText: { color: 'white', fontSize: 16, fontWeight: 'bold' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
modalContent: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '70%', paddingBottom: 20 },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
modalTitle: { fontSize: 18, fontWeight: 'bold', color: '#1e293b' },
modalClose: { fontSize: 20, color: '#94a3b8', padding: 5 },
userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
userName: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
userRole: { fontSize: 12, color: '#64748b' },
fileSection: { marginTop: 20, padding: 15, backgroundColor: '#f8fafc', borderRadius: 12, borderStyle: 'dashed', borderWidth: 1.5, borderColor: '#e2e8f0' },
fileHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
addFileBtn: { backgroundColor: '#10b981', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 },
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' },
fileInfo: { flex: 1, marginRight: 10 },
fileName: { fontSize: 13, fontWeight: '600', color: '#1e293b' },
fileSize: { fontSize: 10, color: '#94a3b8', marginTop: 2 },
removeFile: { color: '#ef4444', fontSize: 16, fontWeight: 'bold', padding: 5 },
noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 11, paddingVertical: 10, fontStyle: 'italic' }
});
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.headerRow}>
<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>
{client.isDemoDone && (
<View style={styles.demoBadge}>
<Text style={styles.demoBadgeText}>DEMO DONE</Text>
</View>
)}
</View>
</View>
<TouchableOpacity onPress={() => navigation.navigate('EditClient', { client })} style={styles.editButton}>
<Text style={styles.editButtonText}>Edit</Text>
@ -79,6 +87,19 @@ const ClientDetailsScreen = ({ route, navigation }) => {
<Text style={styles.label}>Landmark:</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 ? (
<View style={styles.mapContainer}>
<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>
)}
<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>
</ScrollView>
);
@ -114,6 +185,12 @@ const styles = StyleSheet.create({
shadowOpacity: 0.1,
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: {
flexDirection: 'row',
justifyContent: 'space-between',
@ -141,7 +218,39 @@ const styles = StyleSheet.create({
color: Colors.secondary,
fontWeight: 'bold',
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: {
height: 1,
@ -172,6 +281,28 @@ const styles = StyleSheet.create({
fontStyle: 'italic',
color: Colors.textLight,
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) {
const lowerCaseQuery = query.toLowerCase();
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.phone && client.phone.includes(lowerCaseQuery))
);
@ -44,8 +46,8 @@ const ClientListScreen = ({ navigation }) => {
}
};
const getInitials = (name) => {
if (!name) return 'C';
const getInitials = (client) => {
const name = client.companyName || client.name || 'C';
const parts = name.split(' ');
if (parts.length > 1) {
return (parts[0][0] + parts[1][0]).toUpperCase();
@ -56,10 +58,11 @@ const ClientListScreen = ({ navigation }) => {
const renderItem = ({ item }) => (
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('ClientDetails', { client: item })} activeOpacity={0.8}>
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>{getInitials(item.name)}</Text>
<Text style={styles.avatarText}>{getInitials(item)}</Text>
</View>
<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>
{item.email ? <Text style={styles.subDetails}>{item.email}</Text> : null}
<View style={styles.statusBadge}>
@ -186,6 +189,12 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: 'bold',
color: Colors.text,
marginBottom: 1
},
contactName: {
fontSize: 13,
color: Colors.textMuted,
fontStyle: 'italic',
marginBottom: 2
},
details: {

View File

@ -1,22 +1,49 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, Alert, ScrollView, Platform, PermissionsAndroid, TouchableOpacity, ActivityIndicator } from 'react-native';
import React, { useState, useEffect, useContext } from 'react';
import {
View, Text, TextInput, StyleSheet, Alert, ScrollView, Platform,
PermissionsAndroid, TouchableOpacity, ActivityIndicator, Modal, FlatList
} from 'react-native';
import Geolocation from 'react-native-geolocation-service';
import { pick } from '@react-native-documents/picker';
import api from '../services/api';
import Colors from '../constants/Colors';
import { AuthContext } from '../context/AuthContext';
const EditClientScreen = ({ navigation, route }) => {
const { userInfo } = useContext(AuthContext);
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 [email, setEmail] = useState(client.email || '');
const [address, setAddress] = useState(client.address || '');
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 [selectedFiles, setSelectedFiles] = useState(client.files || []);
const [loading, setLoading] = 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 () => {
if (Platform.OS === 'android') {
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 () => {
if (!name || !phone) {
Alert.alert("Error", "Name and Phone are required");
if (!contactName || !phone) {
Alert.alert("Error", "Contact Name and Phone are required");
return;
}
const payload = {
name,
name: companyName || contactName,
companyName,
contactName,
phone,
assignedTo: assignedUser?.id,
closingProbability: closingProbability ? parseInt(closingProbability) : 0,
expectedClosingTimeframe,
isDemoDone,
files: selectedFiles,
...(email ? { email } : {}),
...(address ? { address } : {}),
...(landmark ? { landmark } : {}),
@ -89,169 +165,122 @@ const EditClientScreen = ({ navigation, route }) => {
return (
<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}>Full Name *</Text>
<TextInput style={styles.input} value={name} onChangeText={setName} placeholder="Enter client name" />
</View>
<Text style={styles.label}>Contact Name *</Text>
<TextInput style={styles.input} value={contactName} onChangeText={setContactName} placeholder="Enter contact person name" />
<View style={styles.formGroup}>
<Text style={styles.label}>Phone Number *</Text>
<TextInput style={styles.input} value={phone} onChangeText={setPhone} keyboardType="phone-pad" placeholder="Enter phone number" />
</View>
<Text style={styles.label}>Phone *</Text>
<TextInput style={styles.input} value={phone} onChangeText={setPhone} placeholder="Enter phone number" keyboardType="phone-pad" />
<View style={styles.formGroup}>
<Text style={styles.label}>Email Address</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} keyboardType="email-address" placeholder="Enter email" />
</View>
<Text style={styles.label}>Assigned To / Transfer</Text>
<TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
<Text style={styles.pickerBtnText}>{assignedUser?.name || 'Select User'}</Text>
<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>
<TextInput style={[styles.input, styles.textArea]} value={address} onChangeText={setAddress} multiline numberOfLines={3} placeholder="Enter full address" />
</View>
<TextInput style={[styles.input, { height: 80 }]} value={address} onChangeText={setAddress} placeholder="Enter address" multiline />
<View style={styles.formGroup}>
<Text style={styles.label}>Landmark</Text>
<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.locationInfo}>
<Text style={styles.locationLabel}>
{location ? "Location Captured" : "No Location Set"}
</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>}
<View style={styles.fileSection}>
<View style={styles.fileHeader}>
<Text style={styles.label}>Files / Attachments</Text>
<TouchableOpacity onPress={pickFiles} style={styles.addFileBtn}>
<Text style={styles.addFileBtnText}>+ ADD FILE</Text>
</TouchableOpacity>
</View>
<View style={styles.spacer} />
<TouchableOpacity
style={[styles.submitButton, loading && styles.disabledButton]}
onPress={handleSubmit}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitButtonText}>Save Changes</Text>
)}
{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>
<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>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: Colors.background,
flexGrow: 1
},
sectionHeader: {
fontSize: 18,
fontWeight: 'bold',
color: '#334155',
marginBottom: 15,
marginTop: 10
},
formGroup: {
marginBottom: 15
},
label: {
fontSize: 14,
fontWeight: '600',
color: Colors.textMuted,
marginBottom: 8
},
input: {
backgroundColor: 'white',
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 12,
paddingHorizontal: 15,
paddingVertical: 12,
fontSize: 16,
color: Colors.text
},
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
}
container: { padding: 20, backgroundColor: '#fff' },
label: { fontSize: 13, fontWeight: 'bold', color: '#64748b', marginBottom: 6, marginTop: 15, textTransform: 'uppercase' },
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' },
pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '600' },
pickerArrow: { fontSize: 20, color: '#94a3b8' },
locationBtn: { padding: 15, borderRadius: 12, marginTop: 20, alignItems: 'center' },
locationBtnText: { color: 'white', fontWeight: 'bold' },
submitBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 12, marginTop: 30, alignItems: 'center', marginBottom: 50 },
submitBtnText: { color: 'white', fontSize: 16, fontWeight: 'bold' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
modalContent: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '70%', paddingBottom: 20 },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
modalTitle: { fontSize: 18, fontWeight: 'bold', color: '#1e293b' },
modalClose: { fontSize: 20, color: '#94a3b8', padding: 5 },
userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
userName: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
userRole: { fontSize: 12, color: '#64748b' },
fileSection: { marginTop: 20, padding: 15, backgroundColor: '#f8fafc', borderRadius: 12, borderStyle: 'dashed', borderWidth: 1.5, borderColor: '#e2e8f0' },
fileHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
addFileBtn: { backgroundColor: '#10b981', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 },
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' },
fileInfo: { flex: 1, marginRight: 10 },
fileName: { fontSize: 13, fontWeight: '600', color: '#1e293b' },
fileSize: { fontSize: 10, color: '#94a3b8', marginTop: 2 },
removeFile: { color: '#ef4444', fontSize: 16, fontWeight: 'bold', padding: 5 },
noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 11, paddingVertical: 10, fontStyle: 'italic' }
});
export default EditClientScreen;

View File

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

View File

@ -4,21 +4,29 @@ import {
TextInput, Alert, ActivityIndicator, StatusBar, Modal, FlatList, Linking, Switch
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import DateTimePicker from '@react-native-community/datetimepicker';
import { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors';
import api from '../services/api';
const ACTIVITY_TYPES = [
{ id: 'COLD_CALLING', label: 'Cold Calling', icon: '📞', funnelKey: 'calls' },
{ id: 'WHATSAPP_CAMPAIGN', label: 'WhatsApp Campaign', icon: '📱', funnelKey: 'calls' },
{ id: 'POSTER_PASTING', label: 'Poster Pasting', icon: '🖼️', funnelKey: null },
{ id: 'EXHIBITION', label: 'Exhibition/Event', icon: '🎪', funnelKey: null },
{ id: 'DATA_COLLECTION', label: 'Data Collection', icon: '📊', funnelKey: null },
const STRATEGIC_TYPES = [
{ id: 'COLD_CALLING', label: 'Cold Calling', icon: '📞' },
{ id: 'WHATSAPP_CAMPAIGN', label: 'WhatsApp Campaign', icon: '📱' },
{ id: 'POSTER_PASTING', label: 'Poster Pasting', icon: '🖼️' },
{ id: 'EXHIBITION', label: 'Exhibition/Event', icon: '🎪' },
{ 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 = [
{ id: 'call', label: 'Log Call / Activity', icon: '📞' },
{ id: 'followup', label: 'Schedule Follow-up', icon: '📅' },
{ id: 'call', label: 'Done Now', icon: '✅' },
{ id: 'followup', label: 'Schedule', icon: '📅' },
];
const LogActivityScreen = ({ navigation, route }) => {
@ -29,8 +37,13 @@ const LogActivityScreen = ({ navigation, route }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
const [loading, setLoading] = useState(false);
const [clients, setClients] = useState([]);
const [users, setUsers] = useState([]);
const [opportunities, setOpportunities] = useState([]);
const [clientSearch, setClientSearch] = useState('');
const [oppSearch, setOppSearch] = useState('');
const [clientModal, setClientModal] = useState(false);
const [userModal, setUserModal] = useState(false);
const [oppModal, setOppModal] = useState(false);
// Call / Activity state
const [actType, setActType] = useState(null);
@ -38,39 +51,57 @@ const LogActivityScreen = ({ navigation, route }) => {
const [quantity, setQuantity] = useState('1');
const [callClient, setCallClient] = useState(null);
const [updateClientStatus, setUpdateClientStatus] = useState(null);
const [isDemoDone, setIsDemoDone] = useState(false);
const STATUS_OPTIONS = [
{ id: 'LEAD', label: 'Lead', color: '#6366f1', bg: '#eef2ff' },
{ id: 'QUALITY', label: 'Quality', color: '#16a34a', bg: '#dcfce7' },
{ 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: 'CLOSED', label: 'Closed', color: '#ef4444', bg: '#fee2e2' }
];
// Followup state
const [fuType, setFuType] = useState('FOLLOWUP');
const [fuClient, setFuClient] = useState(null);
const [fuOpp, setFuOpp] = useState(null);
const [assignedUser, setAssignedUser] = useState(null);
const [fuNotes, setFuNotes] = useState('');
const [fuDate, setFuDate] = useState('');
const [fuTime, setFuTime] = useState('');
const [fuDate, setFuDate] = useState(new Date().toISOString().split('T')[0]);
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) => {
if (!phone) return;
Linking.openURL(`tel:${phone}`);
};
useEffect(() => {
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 ──────────────────────────────────────
// ── Submit Strategic Activity ────────────────────────────────────────────
const handleSubmitCall = async () => {
if (!actType) { Alert.alert('Error', 'Please select an activity type'); return; }
if (!description.trim()) { Alert.alert('Error', 'Please enter a description'); return; }
if (!actType) { Alert.alert('Error', 'Please select activity type'); return; }
if (!description.trim()) { Alert.alert('Error', 'Please add notes'); return; }
setLoading(true);
try {
await api.post('/strategic-activities', {
@ -78,19 +109,20 @@ const LogActivityScreen = ({ navigation, route }) => {
description,
leadsGenerated: parseInt(quantity) || 0,
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.`, [
{ text: 'Log Another', onPress: () => { setActType(null); setDescription(''); setQuantity('1'); setCallClient(null); setUpdateClientStatus(null); } },
{ text: 'Go to Tasks', onPress: () => navigation.navigate('Main', { screen: 'Tasks' }) },
Alert.alert('Success ✅', `${actType.replace('_', ' ')} logged.`, [
{ text: 'Log Another', onPress: () => { setActType(null); setDescription(''); setQuantity('1'); setCallClient(null); setUpdateClientStatus(null); setIsDemoDone(false); } },
{ text: 'Go to Activities', onPress: () => navigation.navigate('Main', { screen: 'Activities' }) },
]);
} catch (e) {
Alert.alert('Error', 'Failed to log activity.');
} finally { setLoading(false); }
};
// ── Submit Followup ────────────────────────────────────────────
const handleSubmitFollowup = async () => {
// ── Submit Scheduled Activity ────────────────────────────────────────────
const handleSubmitSchedule = async () => {
if (!fuClient) { Alert.alert('Error', 'Please select a client'); return; }
if (!fuNotes.trim()) { Alert.alert('Error', 'Please add a note'); return; }
if (!fuDate || !fuTime) { Alert.alert('Error', 'Please set date and time'); return; }
@ -99,17 +131,19 @@ const LogActivityScreen = ({ navigation, route }) => {
try {
await api.post('/followups', {
clientId: fuClient.id,
userId: userInfo?.id,
opportunityId: fuOpp?.id,
userId: assignedUser?.id || userInfo?.id,
type: fuType,
notes: fuNotes,
date: new Date(dateStr).toISOString(),
status: 'PENDING',
});
Alert.alert('Scheduled! 📅', `Follow-up with ${fuClient.name} scheduled.`, [
{ text: 'Schedule Another', onPress: () => { setFuClient(null); setFuNotes(''); setFuDate(''); setFuTime(''); } },
{ text: 'View Tasks', onPress: () => navigation.navigate('Main', { screen: 'Tasks' }) },
Alert.alert('Scheduled! 📅', `${fuType} for ${fuClient.companyName || fuClient.name} scheduled.`, [
{ text: 'Schedule Another', onPress: () => { setFuClient(null); setFuOpp(null); setFuNotes(''); setFuDate(''); setFuTime(''); } },
{ text: 'View Activities', onPress: () => navigation.navigate('Main', { screen: 'Activities' }) },
]);
} catch (e) {
Alert.alert('Error', 'Failed to schedule follow-up.');
Alert.alert('Error', 'Failed to schedule activity.');
} finally { setLoading(false); }
};
@ -118,7 +152,7 @@ const LogActivityScreen = ({ navigation, route }) => {
<View style={styles.clientPickerContainer}>
<TouchableOpacity style={styles.clientPicker} onPress={() => setClientModal(true)}>
<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 style={styles.clientPickerArrow}></Text>
</TouchableOpacity>
@ -147,10 +181,11 @@ const LogActivityScreen = ({ navigation, route }) => {
renderItem={({ item }) => (
<TouchableOpacity style={styles.clientRow} onPress={() => { onSelect(item); setClientModal(false); setClientSearch(''); }}>
<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>
<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>
</View>
</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 (
<View style={[styles.container, { paddingTop: insets.top }]}>
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
@ -171,7 +291,7 @@ const LogActivityScreen = ({ navigation, route }) => {
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backBtnText}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Quick Actions</Text>
<Text style={styles.headerTitle}>Activities</Text>
<View style={{ width: 36 }} />
</View>
@ -191,12 +311,12 @@ const LogActivityScreen = ({ navigation, route }) => {
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
{/* ── CALL / ACTIVITY TAB ── */}
{/* ── DONE NOW TAB (Strategic) ── */}
{activeTab === 'call' && (
<>
<Text style={styles.section}>Activity Type</Text>
<Text style={styles.section}>Strategic Activity Done</Text>
<View style={styles.typeGrid}>
{ACTIVITY_TYPES.map(a => (
{STRATEGIC_TYPES.map(a => (
<TouchableOpacity
key={a.id}
style={[styles.typeCard, actType === a.id && styles.typeCardActive]}
@ -215,7 +335,6 @@ const LogActivityScreen = ({ navigation, route }) => {
<View style={styles.statusSection}>
<View style={{ marginBottom: 10 }}>
<Text style={styles.switchLabel}>Update Client Status</Text>
<Text style={styles.switchSub}>Optional: Automatically change status after this call.</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, paddingBottom: 5 }}>
<TouchableOpacity
@ -234,74 +353,155 @@ const LogActivityScreen = ({ navigation, route }) => {
</TouchableOpacity>
))}
</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>
)}
<Text style={styles.section}>Description *</Text>
<Text style={styles.section}>Notes / Description *</Text>
<TextInput
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
numberOfLines={4}
value={description}
onChangeText={setDescription}
/>
<Text style={styles.section}>Quantity (Leads Generated)</Text>
<Text style={styles.section}>Quantity (e.g. Leads Generated)</Text>
<TextInput
style={styles.input}
placeholder="How many leads did this generate?"
placeholder="Enter number..."
keyboardType="numeric"
value={quantity}
onChangeText={setQuantity}
/>
<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>
</>
)}
{/* ── FOLLOWUP TAB ── */}
{/* ── SCHEDULE TAB ── */}
{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>
<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
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
numberOfLines={4}
value={fuNotes}
onChangeText={setFuNotes}
/>
<Text style={styles.section}>Follow-up Date *</Text>
<TextInput
style={styles.input}
placeholder="YYYY-MM-DD"
value={fuDate}
onChangeText={setFuDate}
keyboardType="numeric"
/>
<View style={{ flexDirection: 'row', gap: 10 }}>
<View style={{ flex: 1 }}>
<Text style={styles.section}>Date *</Text>
<TouchableOpacity
style={styles.picker}
onPress={() => setShowDatePicker(true)}
>
<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>
<TextInput
style={styles.input}
placeholder="HH:MM (24h format, e.g. 14:30)"
value={fuTime}
onChangeText={setFuTime}
keyboardType="numeric"
{showDatePicker && (
<DateTimePicker
value={fuDate ? new Date(fuDate) : new Date()}
mode="date"
display="default"
onChange={(event, selectedDate) => {
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}>
<Text style={styles.reminderText}>📲 You'll receive a mobile alert at the scheduled time to complete this follow-up.</Text>
{showTimePicker && (
<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>
<TouchableOpacity style={[styles.submitBtn, { backgroundColor: '#6366f1' }, loading && { opacity: 0.6 }]} onPress={handleSubmitFollowup} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📅 Schedule Follow-up</Text>}
<TouchableOpacity style={[styles.submitBtn, { backgroundColor: '#6366f1' }, loading && { opacity: 0.6 }]} onPress={handleSubmitSchedule} disabled={loading}>
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📅 Schedule Activity</Text>}
</TouchableOpacity>
</>
)}
@ -329,7 +529,6 @@ const styles = StyleSheet.create({
typeCardActive: { borderColor: Colors.primary, backgroundColor: '#f0f4ff' },
typeIcon: { fontSize: 26, marginBottom: 6 },
typeLabel: { fontSize: 11, fontWeight: '700', color: '#64748b', textAlign: 'center' },
typeLabelActive: { color: Colors.primary },
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' },
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' },
clientPickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' },
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' },
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 },
submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
// Modal
modalContainer: { flex: 1, backgroundColor: '#f8f9fa' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary },
modalTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
@ -360,7 +560,11 @@ const styles = StyleSheet.create({
statusPillActive: { backgroundColor: '#64748b', borderColor: '#64748b' },
statusPillText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
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;

View File

@ -1,49 +1,77 @@
import React, { useState, useEffect, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView, Alert, Modal, TextInput, ScrollView } from 'react-native';
import React, { useState, useEffect, useCallback, useContext } from 'react';
import {
View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator,
SafeAreaView, Alert, Modal, TextInput, ScrollView
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import DateTimePicker from '@react-native-community/datetimepicker';
import api from '../services/api';
import { useAuth } from '../context/AuthContext';
import { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors';
const PipelineScreen = () => {
const PipelineScreen = ({ navigation }) => {
const { userInfo } = useContext(AuthContext);
const [loading, setLoading] = useState(true);
const [pipelineType, setPipelineType] = useState('DEALS'); // 'DEALS' or 'LEADS'
const [opportunities, setOpportunities] = useState([]);
const [clients, setClients] = useState([]);
const [selectedStage, setSelectedStage] = useState('LEAD');
const stages = [
const dealStages = [
{ id: 'LEAD', label: 'Lead' },
{ id: 'QUALIFIED', label: 'Qual' },
{ id: 'POTENTIAL', label: 'Poten' },
{ id: 'DEMO', label: 'Demo' },
{ id: 'WON', label: 'Won' },
];
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedOpp, setSelectedOpp] = useState(null);
const [updateData, setUpdateData] = useState({});
const leadStages = [
{ id: 'LEAD', label: 'New Lead' },
{ 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 {
setLoading(true);
const { data } = await api.get('/opportunities');
setOpportunities(data);
const [oppRes, clientRes] = await Promise.all([
api.get('/opportunities'),
api.get('/clients')
]);
setOpportunities(oppRes.data);
setClients(clientRes.data);
} catch (error) {
console.error('Failed to fetch opportunities', error);
console.error('Failed to fetch pipeline data', error);
} finally {
setLoading(false);
}
}, []);
const [users, setUsers] = useState([]);
useEffect(() => {
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
}, []);
useFocusEffect(
useCallback(() => {
fetchOpportunities();
}, [fetchOpportunities])
fetchData();
}, [fetchData])
);
const handleOpenModal = (item) => {
setSelectedOpp(item);
setUpdateData({
stage: item.stage,
assignedTo: item.assignedTo,
demoPersonName: item.demoPersonName || '',
demoContactDetails: item.demoContactDetails || '',
expectedCloseDate: item.expectedCloseDate ? item.expectedCloseDate.split('T')[0] : '',
@ -53,11 +81,40 @@ const PipelineScreen = () => {
specialRate: item.specialRate ? String(item.specialRate) : '',
freeOffers: item.freeOffers || '',
negotiationRemarks: item.negotiationRemarks || '',
value: String(item.value)
value: String(item.value),
isDemoDone: !!item.isDemoDone
});
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 () => {
try {
const payload = {
@ -68,7 +125,7 @@ const PipelineScreen = () => {
await api.patch(`/opportunities/${selectedOpp.id}`, payload);
setIsModalOpen(false);
fetchOpportunities();
fetchData();
Alert.alert("Success", "Opportunity updated");
} catch (error) {
const msg = error.response?.data?.message || error.message;
@ -76,113 +133,134 @@ const PipelineScreen = () => {
}
};
const renderItem = ({ item }) => (
<TouchableOpacity style={styles.card} activeOpacity={0.7} onPress={() => handleOpenModal(item)}>
const UserPicker = () => {
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}>
<Text style={styles.cardTitle}>{item.title}</Text>
<Text style={styles.cardValue}>{item.value.toLocaleString()}</Text>
<Text style={styles.cardTitle}>{item.title || item.companyName || item.name}</Text>
{!isLead && <Text style={styles.cardValue}>{item.value.toLocaleString()}</Text>}
</View>
<View style={styles.cardFooter}>
<View style={styles.clientContainer}>
<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>
<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 style={[styles.priorityBadge, { backgroundColor: item.priority === 'High' ? '#fee2e2' : '#fef3c7' }]}>
<Text style={[styles.priorityText, { color: item.priority === 'High' ? '#ef4444' : '#f59e0b' }]}>
{item.priority || 'Normal'}
</View>
<View style={[styles.priorityBadge, { backgroundColor: (item.priority === 'High' || isLead) ? '#fee2e2' : '#fef3c7' }]}>
<Text style={[styles.priorityText, { color: (item.priority === 'High' || isLead) ? '#ef4444' : '#f59e0b' }]}>
{isLead ? item.status : (item.priority || 'Normal')}
</Text>
</View>
</View>
{item.stage === 'WON' && (
<TouchableOpacity
style={styles.workOrderButton}
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>
)}
<View style={styles.assignedContainer}>
<Text style={styles.assignedLabel}>Owner: </Text>
<Text style={styles.assignedName}>{item.user?.name || 'Unassigned'}</Text>
</View>
</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 (
<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}>
{stages.map((stage) => (
{currentStages.map(stage => (
<TouchableOpacity
key={stage.id}
style={[styles.stageItem, selectedStage === stage.id && styles.activeStageItem]}
onPress={() => setSelectedStage(stage.id)}
style={[
styles.stageItem,
selectedStage === stage.id && styles.activeStageItem
]}
>
<Text style={[
styles.stageLabel,
selectedStage === stage.id && styles.activeStageLabel
]}>
{stage.label}
</Text>
<View style={[
styles.stageIndicator,
selectedStage === stage.id && { backgroundColor: selectedStage === 'WON' ? Colors.secondary : Colors.primary }
]} />
<Text style={[styles.stageLabel, selectedStage === stage.id && styles.activeStageLabel]}>{stage.label}</Text>
<View style={[styles.stageIndicator, selectedStage === stage.id && { backgroundColor: Colors.primary }]} />
</TouchableOpacity>
))}
</View>
{loading ? (
<View style={styles.center}>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
<View style={styles.center}><ActivityIndicator size="large" color={Colors.primary} /></View>
) : (
<FlatList
data={filteredItems}
data={filteredData}
keyExtractor={item => item.id}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContainer}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No opportunities in this stage</Text>
</View>
}
onRefresh={fetchOpportunities}
refreshing={loading}
ListEmptyComponent={<View style={styles.empty}><Text style={styles.emptyText}>No items in this stage</Text></View>}
onRefresh={fetchData}
refreshing={false}
/>
)}
{/* UPDATE MODAL */}
<Modal
visible={isModalOpen}
animationType="slide"
transparent={true}
onRequestClose={() => setIsModalOpen(false)}
{/* FAB - New Deal */}
<TouchableOpacity
style={styles.fab}
onPress={() => navigation.navigate('AddOpportunity')}
activeOpacity={0.85}
>
<View style={styles.modalOverlay}>
<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>
<Text style={styles.fabText}>+</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.modalForm}>
<Text style={styles.label}>Current Stage</Text>
<View style={styles.stagePicker}>
{stages.map(s => (
<Modal visible={isModalOpen} animationType="slide">
<View style={styles.modalContainer}>
<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
key={s.id}
style={[styles.stageChip, updateData.stage === s.id && styles.activeStageChip]}
@ -193,96 +271,127 @@ const PipelineScreen = () => {
))}
</View>
<Text style={styles.label}>Expected Revenue ()</Text>
<TextInput
style={styles.input}
value={updateData.value}
onChangeText={t => setUpdateData({...updateData, value: t})}
keyboardType="numeric"
/>
{(updateData.stage === 'DEMO' || updateData.stage === 'WON') && (
<View style={styles.mandatorySection}>
<Text style={styles.sectionHeader}>DEMO DETAILS (MANDATORY)</Text>
<Text style={styles.label}>Person Name</Text>
<TextInput
style={styles.input}
value={updateData.demoPersonName}
onChangeText={t => setUpdateData({...updateData, demoPersonName: t})}
placeholder="Name of person met"
/>
<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.label}>Expected Closing Date (YYYY-MM-DD)</Text>
<TextInput
style={styles.input}
value={updateData.expectedCloseDate}
onChangeText={t => setUpdateData({...updateData, expectedCloseDate: t})}
placeholder="2024-12-31"
/>
<Text style={styles.label}>Competitor Mention</Text>
<TextInput
style={styles.input}
value={updateData.competitorMention}
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>
<Text style={styles.modalLabel}>Expected Close Date</Text>
<TouchableOpacity
style={styles.pickerBtn}
onPress={() => setShowDatePicker(true)}
>
<Text style={updateData.expectedCloseDate ? styles.pickerBtnText : styles.pickerPlaceholder}>
{updateData.expectedCloseDate || 'Select date...'}
</Text>
<Text style={styles.pickerArrow}>📅</Text>
</TouchableOpacity>
{showDatePicker && (
<DateTimePicker
value={updateData.expectedCloseDate ? new Date(updateData.expectedCloseDate) : new Date()}
mode="date"
display="default"
onChange={(event, selectedDate) => {
setShowDatePicker(false);
if (selectedDate) {
setUpdateData({ ...updateData, expectedCloseDate: selectedDate.toISOString().split('T')[0] });
}
}}
/>
)}
<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..." />
<TouchableOpacity style={styles.updateBtn} onPress={handleUpdate}>
<Text style={styles.updateBtnText}>Save Updates</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.activityBtn}
onPress={() => {
setIsModalOpen(false);
navigation.navigate('LogActivity', { client: selectedOpp?.client, tab: 'followup' });
}}
>
<Text style={styles.activityBtnText}>📅 Schedule Next Activity</Text>
</TouchableOpacity>
<View style={{height: 40}} />
</ScrollView>
</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>
</Modal>
</SafeAreaView>
@ -290,242 +399,76 @@ const PipelineScreen = () => {
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
},
stageBar: {
flexDirection: 'row',
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#edf2f7',
paddingTop: 10,
},
stageItem: {
flex: 1,
alignItems: 'center',
paddingVertical: 12,
},
activeStageItem: {
// backgroundColor: '#fdf2f8',
},
stageLabel: {
fontSize: 12,
fontWeight: '600',
color: Colors.textMuted,
marginBottom: 8,
},
activeStageLabel: {
color: Colors.text,
fontWeight: 'bold',
},
stageIndicator: {
height: 3,
width: '60%',
borderRadius: 3,
backgroundColor: 'transparent',
},
listContainer: {
padding: 16,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
card: {
backgroundColor: 'white',
borderRadius: 12,
padding: 16,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 3,
borderLeftWidth: 4,
borderLeftColor: Colors.primary,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
cardTitle: {
fontSize: 15,
fontWeight: 'bold',
color: Colors.text,
flex: 1,
marginRight: 8,
},
cardValue: {
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,
}
container: { flex: 1, backgroundColor: '#f8fafc' },
switcherContainer: { flexDirection: 'row', backgroundColor: 'white', padding: 8, marginHorizontal: 16, marginTop: 10, borderRadius: 12, borderWidth: 1, borderColor: '#edf2f7' },
switcherBtn: { flex: 1, paddingVertical: 8, alignItems: 'center', borderRadius: 8 },
activeSwitcherBtn: { backgroundColor: Colors.primary },
switcherText: { fontSize: 12, fontWeight: '800', color: '#94a3b8' },
activeSwitcherText: { color: 'white' },
filterBar: { paddingVertical: 12 },
filterScroll: { paddingHorizontal: 16, gap: 10 },
filterChip: { paddingHorizontal: 16, paddingVertical: 8, backgroundColor: 'white', borderRadius: 20, borderWidth: 1, borderColor: '#e2e8f0' },
activeFilterChip: { backgroundColor: Colors.primary, borderColor: Colors.primary },
filterChipText: { fontSize: 13, fontWeight: '600', color: '#64748b' },
activeFilterChipText: { color: 'white' },
stagePickerContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 10 },
stageChip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, borderWidth: 1, borderColor: '#e2e8f0', backgroundColor: '#f8fafc' },
activeStageChip: { backgroundColor: Colors.primary, borderColor: Colors.primary },
stageChipText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
activeStageChipText: { color: 'white' },
clientNameBig: { fontSize: 20, fontWeight: '800', color: Colors.primary, marginBottom: 10 },
divider: { height: 1, backgroundColor: '#f1f5f9', marginVertical: 25 },
convertBtn: { backgroundColor: '#f0f9ff', padding: 18, borderRadius: 14, alignItems: 'center', borderWidth: 1.5, borderColor: '#0ea5e9' },
convertBtnText: { color: '#0369a1', fontSize: 15, fontWeight: '800' },
stageBar: { flexDirection: 'row', backgroundColor: 'white', borderBottomWidth: 1, borderBottomColor: '#edf2f7' },
stageItem: { flex: 1, alignItems: 'center', paddingVertical: 14 },
activeStageItem: { borderBottomWidth: 0 },
stageLabel: { fontSize: 11, fontWeight: '700', color: '#94a3b8', textTransform: 'uppercase' },
activeStageLabel: { color: Colors.primary },
stageIndicator: { height: 3, width: '40%', marginTop: 8, borderRadius: 2 },
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 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
cardTitle: { fontSize: 15, fontWeight: '800', color: '#1e293b', flex: 1 },
cardValue: { fontSize: 15, fontWeight: '900', color: Colors.primary },
cardFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' },
clientContainer: { flexDirection: 'row', alignItems: 'center', gap: 10 },
avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
avatarText: { fontSize: 12, fontWeight: 'bold', color: '#64748b' },
clientName: { fontSize: 13, fontWeight: '700', color: '#334155' },
contactSubText: { fontSize: 11, color: '#94a3b8', fontStyle: 'italic' },
priorityBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
priorityText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' },
assignedContainer: { flexDirection: 'row', marginTop: 12, paddingTop: 12, borderTopWidth: 1, borderTopColor: '#f1f5f9' },
assignedLabel: { fontSize: 11, color: '#94a3b8' },
assignedName: { fontSize: 11, fontWeight: 'bold', color: '#64748b' },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
empty: { alignItems: 'center', marginTop: 100 },
emptyText: { color: '#94a3b8', fontWeight: '600' },
modalContainer: { flex: 1, backgroundColor: 'white' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', backgroundColor: '#f8fafc' },
modalTitle: { fontSize: 18, fontWeight: '900', color: '#1e293b' },
modalClose: { fontSize: 24, color: '#94a3b8' },
modalBody: { padding: 20 },
modalLabel: { fontSize: 11, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', marginBottom: 8, marginTop: 20 },
modalInput: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 14, fontSize: 15, backgroundColor: '#f8fafc' },
pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '700' },
pickerPlaceholder: { color: '#94a3b8', fontSize: 15 },
pickerArrow: { fontSize: 20, color: '#94a3b8' },
updateBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 14, marginTop: 40, alignItems: 'center' },
updateBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
activityBtn: { padding: 18, borderRadius: 14, marginTop: 15, alignItems: 'center', borderWidth: 1.5, borderColor: Colors.primary },
activityBtnText: { color: Colors.primary, fontSize: 14, fontWeight: '800' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'flex-end' },
userListContainer: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '60%' },
userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
userName: { fontSize: 15, fontWeight: '700', color: '#1e293b' },
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 },
fabText: { color: 'white', fontSize: 32, fontWeight: '300', lineHeight: 38 }
});
export default PipelineScreen;

View File

@ -9,12 +9,26 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import api from '../services/api';
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 { userInfo } = useContext(AuthContext);
const insets = useSafeAreaInsets();
const [sections, setSections] = useState([]);
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 map = {};
@ -54,15 +68,15 @@ const TasksScreen = ({ navigation }) => {
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
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: 'Done ✓', onPress: async () => {
text: 'Complete ✓', onPress: async () => {
try {
await api.patch(`/followups/${id}`, { status: 'DONE' });
fetchTasks();
} 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 isPending = item.status === 'PENDING';
const isOverdue = isPending && new Date(item.date) < new Date();
const type = item.type || 'FOLLOWUP';
return (
<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={{ 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 && (
<TouchableOpacity onPress={() => handleCall(item.client.phone)}>
<TouchableOpacity onPress={() => handleCall(item.client.phone)} style={styles.callCircle}>
<Text style={styles.callIcon}>📞</Text>
</TouchableOpacity>
)}
</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.time}>
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
@ -96,7 +117,7 @@ const TasksScreen = ({ navigation }) => {
</Text>
</View>
{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>
</TouchableOpacity>
)}
@ -113,10 +134,20 @@ const TasksScreen = ({ navigation }) => {
<View style={styles.container}>
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
<Text style={styles.headerTitle}>My Tasks</Text>
<Text style={styles.headerSub}>Sorted by date</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<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}>
{['ALL', 'PENDING', 'DONE'].map(f => (
{['PENDING', 'DONE', 'ALL'].map(f => (
<TouchableOpacity
key={f}
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
@ -135,16 +166,16 @@ const TasksScreen = ({ navigation }) => {
renderSectionHeader={({ section }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<Text style={styles.sectionCount}>{section.data.length} task{section.data.length !== 1 ? 's' : ''}</Text>
<Text style={styles.sectionCount}>{section.data.length} item{section.data.length !== 1 ? 's' : ''}</Text>
</View>
)}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
contentContainerStyle={{ paddingBottom: 40 }}
ListEmptyComponent={
<View style={styles.empty}>
<Text style={styles.emptyIcon}>🎉</Text>
<Text style={styles.emptyTitle}>All Clear!</Text>
<Text style={styles.emptySub}>No tasks match this filter.</Text>
<Text style={styles.emptyIcon}></Text>
<Text style={styles.emptyTitle}>All Caught Up!</Text>
<Text style={styles.emptySub}>No activities scheduled here.</Text>
</View>
}
/>
@ -153,34 +184,40 @@ const TasksScreen = ({ navigation }) => {
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f1f5f9' },
container: { flex: 1, backgroundColor: '#f8fafc' },
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 },
headerTitle: { color: 'white', fontSize: 26, fontWeight: '900' },
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2, marginBottom: 14 },
headerTitle: { color: 'white', fontSize: 28, fontWeight: '900' },
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 13, marginTop: 2, marginBottom: 16 },
filterRow: { flexDirection: 'row', gap: 8 },
filterBtn: { paddingHorizontal: 16, paddingVertical: 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' },
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 },
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 20, paddingBottom: 8 },
sectionTitle: { fontSize: 13, fontWeight: '900', color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5 },
addBtn: { backgroundColor: 'rgba(255,255,255,0.25)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 },
addBtnText: { color: 'white', fontWeight: '900', fontSize: 13 },
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 24, paddingBottom: 10 },
sectionTitle: { fontSize: 12, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 },
sectionCount: { fontSize: 11, color: '#94a3b8', fontWeight: '700' },
card: { backgroundColor: 'white', marginHorizontal: 16, marginBottom: 8, borderRadius: 14, padding: 14, flexDirection: 'row', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 4 },
cardOverdue: { borderLeftWidth: 4, borderLeftColor: '#ef4444' },
cardDone: { opacity: 0.65 },
dot: { width: 10, height: 10, borderRadius: 5, marginRight: 12 },
clientName: { fontSize: 14, fontWeight: '800', color: '#1e293b', marginBottom: 3, flex: 1 },
callIcon: { fontSize: 18, paddingHorizontal: 10 },
notes: { fontSize: 12, color: '#64748b', lineHeight: 17, marginBottom: 5 },
time: { fontSize: 10, color: '#94a3b8', fontWeight: '600' },
doneBtn: { backgroundColor: Colors.primary, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10, marginLeft: 10 },
doneBtnText: { color: 'white', fontSize: 11, fontWeight: '900' },
completedBadge: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 10 },
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 14 },
empty: { alignItems: 'center', paddingTop: 80 },
emptyIcon: { fontSize: 48, marginBottom: 12 },
emptyTitle: { fontSize: 18, fontWeight: '800', color: '#1e293b' },
emptySub: { fontSize: 13, color: '#94a3b8', marginTop: 6 },
card: { backgroundColor: 'white', marginHorizontal: 16, marginBottom: 10, borderRadius: 18, padding: 16, flexDirection: 'row', alignItems: 'center', elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
cardOverdue: { borderLeftWidth: 5, borderLeftColor: '#ef4444' },
cardDone: { opacity: 0.7 },
typeIconBadge: { width: 44, height: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', marginRight: 14 },
typeIconText: { fontSize: 20 },
clientName: { fontSize: 15, fontWeight: '800', color: '#1e293b', marginBottom: 2, flex: 1 },
callCircle: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
callIcon: { fontSize: 14 },
typeBadge: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, backgroundColor: '#f8fafc', marginBottom: 6 },
typeText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
notes: { fontSize: 13, color: '#475569', lineHeight: 18, marginBottom: 8 },
time: { fontSize: 11, color: '#94a3b8', fontWeight: '600' },
doneBtn: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 12, marginLeft: 12 },
doneBtnText: { color: 'white', fontSize: 12, fontWeight: '900' },
completedBadge: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 12 },
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 16 },
empty: { alignItems: 'center', paddingTop: 100 },
emptyIcon: { fontSize: 56, marginBottom: 16 },
emptyTitle: { fontSize: 20, fontWeight: '900', color: '#1e293b' },
emptySub: { fontSize: 14, color: '#64748b', marginTop: 8 },
});
export default TasksScreen;