diff --git a/package-lock.json b/package-lock.json
index 06c04c7..404dccb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 2f05667..cabeacf 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/navigation/AppNav.js b/src/navigation/AppNav.js
index 32f9592..7769427 100644
--- a/src/navigation/AppNav.js
+++ b/src/navigation/AppNav.js
@@ -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 = () => (
-
+
);
@@ -77,8 +76,7 @@ const AppNav = () => {
-
-
+
diff --git a/src/screens/AddClientScreen.js b/src/screens/AddClientScreen.js
index 1a8b8dd..dd4c26e 100644
--- a/src/screens/AddClientScreen.js
+++ b/src/screens/AddClientScreen.js
@@ -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 (
- Name *
-
+ Company Name
+
+
+ Contact Name *
+
Phone *
-
+
+
+ Assigned To
+ setUserModal(true)}>
+ {assignedUser?.name || 'Myself'}
+ ›
+
Email
-
+
Address
-
+
Landmark
-
+
-
-
- {location && (
-
- Lat: {location.latitude.toFixed(4)}, Lng: {location.longitude.toFixed(4)}
-
+
+ {locating ? : {location ? "✓ Location Captured" : "📍 Capture Current Location"}}
+
+
+
+
+ Files / Attachments
+
+ + ADD FILE
+
+
+
+ {selectedFiles.map((file, index) => (
+
+
+ {file.name}
+ {(file.size / 1024).toFixed(1)} KB
+
+ removeFile(index)}>
+ ✕
+
+
+ ))}
+ {selectedFiles.length === 0 && (
+ No files attached yet
)}
-
+
+ {loading ? : Add Client}
+
-
+
+
+
+
+ Assign To
+ setUserModal(false)}>✕
+
+ u.id !== userInfo?.id)]}
+ keyExtractor={item => item.id}
+ renderItem={({ item }) => (
+ { setAssignedUser(item); setUserModal(false); }}>
+
+ {item.name?.charAt(0)}
+
+
+ {item.name}
+ {item.role}
+
+
+ )}
+ />
+
+
+
);
};
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;
diff --git a/src/screens/AddOpportunityScreen.js b/src/screens/AddOpportunityScreen.js
new file mode 100644
index 0000000..1b2f169
--- /dev/null
+++ b/src/screens/AddOpportunityScreen.js
@@ -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 ;
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+ navigation.goBack()} style={styles.backBtn}>
+ ‹
+
+ New Deal
+
+
+
+
+
+ {/* Client */}
+ Client *
+ !preselectedClientId && setClientModal(true)}
+ activeOpacity={preselectedClientId ? 1 : 0.7}
+ >
+
+ {selectedClient ? (selectedClient.companyName || selectedClient.name) : 'Select a client...'}
+
+ {!preselectedClientId && ›}
+
+
+ {/* Deal Title */}
+ Deal Title / Product *
+ {
+ 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 && (
+
+ {products.filter(p => p.name.toLowerCase().includes(form.title.toLowerCase())).map(p => (
+ {
+ setForm({...form, title: p.name, value: String(p.price)});
+ setShowProductOptions(false);
+ }}>
+ {p.name}
+ ₹{p.price}
+
+ ))}
+ {!products.some(p => p.name.toLowerCase() === form.title.toLowerCase()) && (
+ {
+ setNewProduct({ name: form.title, price: form.value || '', description: '' });
+ setIsProductModalOpen(true);
+ setShowProductOptions(false);
+ }}>
+ + Save "{form.title}" as new product
+
+ )}
+
+ )}
+
+ {/* Value */}
+ Deal Value (₹) *
+ setForm({ ...form, value: v })}
+ />
+
+ {/* Stage */}
+ Stage
+
+
+ {STAGES.map(s => (
+ setForm({ ...form, stage: s.id })}
+ >
+ {s.label}
+
+ ))}
+
+
+
+ {/* Expected Closing Date */}
+ Expected Closing Date
+ setShowDatePicker(true)}
+ >
+
+ {form.expectedClosingDate || 'Select date...'}
+
+ 📅
+
+
+ {showDatePicker && (
+ {
+ setShowDatePicker(false);
+ if (selectedDate) {
+ setForm({ ...form, expectedClosingDate: selectedDate.toISOString().split('T')[0] });
+ }
+ }}
+ />
+ )}
+
+ {/* Notes */}
+ Notes
+ setForm({ ...form, notes: v })}
+ />
+
+ {/* Submit */}
+
+ {loading
+ ?
+ : 💼 Create Deal
+ }
+
+
+
+ {/* Client Modal */}
+ setClientModal(false)}>
+
+
+ Select Client
+ setClientModal(false)}>
+ ✕
+
+
+
+ item.id}
+ renderItem={({ item }) => (
+ { setSelectedClient(item); setClientModal(false); setClientSearch(''); }}
+ >
+
+ {(item.companyName || item.name)?.charAt(0)}
+
+
+ {item.companyName || item.name}
+ {item.phone}
+
+
+ )}
+ ListEmptyComponent={No clients found}
+ />
+
+
+
+ {/* New Product Modal */}
+ setIsProductModalOpen(false)}>
+
+
+
+ Save New Product
+ setIsProductModalOpen(false)}>
+ ✕
+
+
+
+
+ Product Name *
+ setNewProduct({ ...newProduct, name: v })}
+ />
+
+ Default Price (₹) *
+ setNewProduct({ ...newProduct, price: v })}
+ />
+
+ Description
+ setNewProduct({ ...newProduct, description: v })}
+ />
+
+
+ {loading ? : Save Product to Catalog}
+
+
+
+
+
+
+ );
+};
+
+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;
diff --git a/src/screens/ClientDetailsScreen.js b/src/screens/ClientDetailsScreen.js
index 3f0cdf8..8e96a7e 100644
--- a/src/screens/ClientDetailsScreen.js
+++ b/src/screens/ClientDetailsScreen.js
@@ -57,8 +57,16 @@ const ClientDetailsScreen = ({ route, navigation }) => {
- {client.name}
- {client.status}
+ {client.companyName || client.name}
+ {client.companyName && {client.contactName}}
+
+ {client.status}
+ {client.isDemoDone && (
+
+ DEMO DONE
+
+ )}
+
navigation.navigate('EditClient', { client })} style={styles.editButton}>
Edit
@@ -79,6 +87,19 @@ const ClientDetailsScreen = ({ route, navigation }) => {
Landmark:
{client.landmark || 'N/A'}
+
+
+
+
+ Probability
+ {client.closingProbability || 0}%
+
+
+ Timeframe
+ {client.expectedClosingTimeframe || 'Not Set'}
+
+
+
{client.lat && client.lng ? (
@@ -89,6 +110,56 @@ const ClientDetailsScreen = ({ route, navigation }) => {
) : (
No location data available
)}
+
+
+
+
+ navigation.navigate('LogActivity', { tab: 'call', client })}
+ >
+ ✅ Log Activity
+
+ navigation.navigate('LogActivity', { tab: 'followup', client })}
+ >
+ 📅 Schedule
+
+
+
+
+
+ 📁 ATTACHED DOCUMENTS
+ {client.files && client.files.length > 0 ? (
+ client.files.map((file, idx) => (
+ {
+ 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);
+ }}
+ >
+
+ 📄
+
+
+ {file.name}
+ {(file.size / 1024).toFixed(1)} KB
+
+ Open ›
+
+ ))
+ ) : (
+ No documents attached to this client.
+ )}
);
@@ -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
}
});
diff --git a/src/screens/ClientListScreen.js b/src/screens/ClientListScreen.js
index b610bc9..57a24de 100644
--- a/src/screens/ClientListScreen.js
+++ b/src/screens/ClientListScreen.js
@@ -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 }) => (
navigation.navigate('ClientDetails', { client: item })} activeOpacity={0.8}>
- {getInitials(item.name)}
+ {getInitials(item)}
- {item.name}
+ {item.companyName || item.name}
+ {item.companyName && {item.contactName}}
{item.phone}
{item.email ? {item.email} : null}
@@ -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: {
diff --git a/src/screens/EditClientScreen.js b/src/screens/EditClientScreen.js
index ccf8453..c3d4a37 100644
--- a/src/screens/EditClientScreen.js
+++ b/src/screens/EditClientScreen.js
@@ -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 (
- Basic Information
+ Company Name
+
-
- Full Name *
-
-
+ Contact Name *
+
-
- Phone Number *
-
-
+ Phone *
+
-
- Email Address
-
-
-
- Address Details
-
-
- Address
-
-
-
-
- Landmark
-
-
-
- Location Tagging
-
-
-
-
- {location ? "Location Captured" : "No Location Set"}
-
- {location && (
-
- {location.latitude.toFixed(6)}, {location.longitude.toFixed(6)}
-
- )}
-
-
- {locating ? : 📍 Update}
-
-
-
-
-
-
- {loading ? (
-
- ) : (
- Save Changes
- )}
+ Assigned To / Transfer
+ setUserModal(true)}>
+ {assignedUser?.name || 'Select User'}
+ ›
+
+ Email
+
+
+ Address
+
+
+ Landmark
+
+
+
+ {locating ? : {location ? "✓ Location Updated" : "📍 Update Current Location"}}
+
+
+
+
+ Files / Attachments
+
+ + ADD FILE
+
+
+
+ {selectedFiles.map((file, index) => (
+
+
+ {file.name}
+ {(file.size / 1024).toFixed(1)} KB
+
+ removeFile(index)}>
+ ✕
+
+
+ ))}
+ {selectedFiles.length === 0 && (
+ No files attached yet
+ )}
+
+
+
+ {loading ? : Update Client}
+
+
+
+
+
+
+ Transfer To
+ setUserModal(false)}>✕
+
+ u.id !== userInfo?.id)]}
+ keyExtractor={item => item.id}
+ renderItem={({ item }) => (
+ { setAssignedUser(item); setUserModal(false); }}>
+
+ {item.name?.charAt(0)}
+
+
+ {item.name}
+ {item.role}
+
+
+ )}
+ />
+
+
+
);
};
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;
diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js
index 752e331..665e53d 100644
--- a/src/screens/HomeScreen.js
+++ b/src/screens/HomeScreen.js
@@ -212,10 +212,10 @@ const HomeScreen = ({ navigation }) => {
onPress={() => navigation.navigate('Attendance')}
/>
navigation.navigate('EnquiryList')}
+ onPress={() => navigation.navigate('AddOpportunity')}
/>
{
@@ -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 }) => {
setClientModal(true)}>
- {selected ? `${selected.name} • ${selected.phone}` : 'Tap to select client...'}
+ {selected ? `${selected.companyName || selected.name} • ${selected.phone}` : 'Tap to select client...'}
›
@@ -147,10 +181,11 @@ const LogActivityScreen = ({ navigation, route }) => {
renderItem={({ item }) => (
{ onSelect(item); setClientModal(false); setClientSearch(''); }}>
- {item.name?.charAt(0)}
+ {(item.companyName || item.name)?.charAt(0)}
- {item.name}
+ {item.companyName || item.name}
+ {item.companyName && {item.contactName}}
{item.phone}
@@ -162,6 +197,91 @@ const LogActivityScreen = ({ navigation, route }) => {
>
);
+ const OpportunityPicker = () => (
+ <>
+ setOppModal(true)}>
+
+ {fuOpp ? `${fuOpp.title} (${fuOpp.client?.name})` : 'Choose Opportunity...'}
+
+ ›
+
+ setOppModal(false)}>
+
+
+ Select Opportunity
+ setOppModal(false)}>✕
+
+
+ item.id}
+ renderItem={({ item }) => (
+ {
+ setFuOpp(item);
+ setFuClient(item.client);
+ setOppModal(false);
+ setOppSearch('');
+ }}
+ >
+
+ {item.title?.charAt(0)}
+
+
+ {item.title}
+ {item.client?.companyName || item.client?.name}
+ ₹{item.value.toLocaleString()}
+
+
+ )}
+ ListEmptyComponent={No opportunities found}
+ />
+
+
+ >
+ );
+
+ const UserPicker = () => (
+ <>
+ setUserModal(true)}>
+
+ {assignedUser?.name || 'Myself'}
+
+ ›
+
+ setUserModal(false)}>
+
+
+ Assign To
+ setUserModal(false)}>✕
+
+ u.id !== userInfo?.id)]}
+ keyExtractor={item => item.id}
+ renderItem={({ item }) => (
+ { setAssignedUser(item); setUserModal(false); }}>
+
+ {item.name?.charAt(0)}
+
+
+ {item.name}
+ {item.role || 'Staff'}
+
+
+ )}
+ />
+
+
+ >
+ );
+
return (
@@ -171,7 +291,7 @@ const LogActivityScreen = ({ navigation, route }) => {
navigation.goBack()} style={styles.backBtn}>
‹
- Quick Actions
+ Activities
@@ -191,12 +311,12 @@ const LogActivityScreen = ({ navigation, route }) => {
- {/* ── CALL / ACTIVITY TAB ── */}
+ {/* ── DONE NOW TAB (Strategic) ── */}
{activeTab === 'call' && (
<>
- Activity Type
+ Strategic Activity Done
- {ACTIVITY_TYPES.map(a => (
+ {STRATEGIC_TYPES.map(a => (
{
Update Client Status
- Optional: Automatically change status after this call.
{
))}
+
+ setIsDemoDone(!isDemoDone)}
+ >
+
+ Mark Demo as Done
+
)}
- Description *
+ Notes / Description *
- Quantity (Leads Generated)
+ Quantity (e.g. Leads Generated)
- {loading ? : 📤 Log Activity}
+ {loading ? : ✅ Submit Activity}
>
)}
- {/* ── FOLLOWUP TAB ── */}
+ {/* ── SCHEDULE TAB ── */}
{activeTab === 'followup' && (
<>
- Client *
-
+ Activity Type
+
+ {SCHEDULE_TYPES.map(s => (
+ setFuType(s.id)}
+ >
+ {s.icon}
+ {s.label}
+
+ ))}
+
- Notes / Task Description *
+ {fuType === 'QUOTE' ? (
+ <>
+ Link to Opportunity *
+
+ >
+ ) : (
+ <>
+ Client *
+
+ >
+ )}
+
+ {['ADMIN', 'GENERAL_MANAGER', 'MANAGER', 'TEAM_LEADER'].includes(userInfo?.role) && (
+ <>
+ Assign To
+
+ >
+ )}
+
+ Task / Notes *
- Follow-up Date *
-
+
+
+ Date *
+ setShowDatePicker(true)}
+ >
+
+ {fuDate || 'Select date...'}
+
+ 📅
+
- Follow-up Time *
-
+ {showDatePicker && (
+ {
+ setShowDatePicker(false);
+ if (selectedDate) {
+ setFuDate(selectedDate.toISOString().split('T')[0]);
+ }
+ }}
+ />
+ )}
+
+
+ Time *
+ setShowTimePicker(true)}
+ >
+
+ {fuTime || 'Select time...'}
+
+ 🕒
+
-
- 📲 You'll receive a mobile alert at the scheduled time to complete this follow-up.
+ {showTimePicker && (
+ {
+ 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}`);
+ }
+ }}
+ />
+ )}
+
-
- {loading ? : 📅 Schedule Follow-up}
+
+ {loading ? : 📅 Schedule Activity}
>
)}
@@ -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;
diff --git a/src/screens/PipelineScreen.js b/src/screens/PipelineScreen.js
index f032ab2..a4c7170 100644
--- a/src/screens/PipelineScreen.js
+++ b/src/screens/PipelineScreen.js
@@ -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,213 +133,265 @@ const PipelineScreen = () => {
}
};
- const renderItem = ({ item }) => (
- handleOpenModal(item)}>
-
- {item.title}
- ₹{item.value.toLocaleString()}
-
-
-
-
- {item.client?.name?.charAt(0)}
-
- {item.client?.name}
-
-
-
- {item.priority || 'Normal'}
+ const UserPicker = () => {
+ const currentAssignee = users.find(u => u.id === updateData.assignedTo);
+ return (
+ <>
+ Primary Owner (Assigned To)
+ setUserModal(true)}>
+
+ {currentAssignee ? `${currentAssignee.name} (${currentAssignee.role})` : 'Select Teammate'}
-
-
-
- {item.stage === 'WON' && (
- {
- 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");
- }
- }}
- >
- Start Work Order
+ ›
- )}
-
- );
+ >
+ );
+ };
- const filteredItems = opportunities.filter(item => item.stage === selectedStage);
+ const renderItem = ({ item }) => {
+ const isLead = !!item.status; // Clients have status, opportunities have stages
+
+ return (
+ isLead ? handleOpenClientModal(item) : handleOpenModal(item)}
+ >
+
+ {item.title || item.companyName || item.name}
+ {!isLead && ₹{item.value.toLocaleString()}}
+
+
+
+
+ {(item.client?.companyName || item.client?.name || item.name || item.companyName)?.charAt(0)}
+
+
+ {item.client?.companyName || item.client?.name || item.companyName || item.name}
+ {(item.client?.contactName || item.contactName) && {item.client?.contactName || item.contactName}}
+
+
+
+
+ {isLead ? item.status : (item.priority || 'Normal')}
+
+
+
+
+ Owner:
+ {item.user?.name || 'Unassigned'}
+
+
+ );
+ };
+
+ 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 (
- {/* Stage Selector */}
+ {/* Pipeline Type Switcher */}
+
+ { setPipelineType('DEALS'); setSelectedStage('LEAD'); }}
+ >
+ DEALS
+
+ { setPipelineType('LEADS'); setSelectedStage('LEAD'); }}
+ >
+ LEADS
+
+
+
- {stages.map((stage) => (
+ {currentStages.map(stage => (
setSelectedStage(stage.id)}
- style={[
- styles.stageItem,
- selectedStage === stage.id && styles.activeStageItem
- ]}
>
-
- {stage.label}
-
-
+ {stage.label}
+
))}
{loading ? (
-
-
-
+
) : (
item.id}
renderItem={renderItem}
- keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContainer}
- ListEmptyComponent={
-
- No opportunities in this stage
-
- }
- onRefresh={fetchOpportunities}
- refreshing={loading}
+ ListEmptyComponent={No items in this stage}
+ onRefresh={fetchData}
+ refreshing={false}
/>
)}
- {/* UPDATE MODAL */}
- setIsModalOpen(false)}
+ {/* FAB - New Deal */}
+ navigation.navigate('AddOpportunity')}
+ activeOpacity={0.85}
>
-
-
-
- Update Stage
- setIsModalOpen(false)}>
- Cancel
-
+ +
+
+
+
+
+
+
+ Update Opportunity
+ setIsModalOpen(false)}>✕
+
+
+ Expected Value (₹)
+ setUpdateData({ ...updateData, value: v })} keyboardType="numeric" />
+
+
+
+ Pipeline Stage
+
+ {dealStages.map(s => (
+ setUpdateData({ ...updateData, stage: s.id })}
+ >
+ {s.label}
+
+ ))}
-
- Current Stage
-
- {stages.map(s => (
- setUpdateData({...updateData, stage: s.id})}
- >
- {s.label}
-
- ))}
-
+ Expected Close Date
+ setShowDatePicker(true)}
+ >
+
+ {updateData.expectedCloseDate || 'Select date...'}
+
+ 📅
+
- Expected Revenue (₹)
- setUpdateData({...updateData, value: t})}
- keyboardType="numeric"
+ {showDatePicker && (
+ {
+ setShowDatePicker(false);
+ if (selectedDate) {
+ setUpdateData({ ...updateData, expectedCloseDate: selectedDate.toISOString().split('T')[0] });
+ }
+ }}
/>
+ )}
- {(updateData.stage === 'DEMO' || updateData.stage === 'WON') && (
-
- DEMO DETAILS (MANDATORY)
-
- Person Name
- setUpdateData({...updateData, demoPersonName: t})}
- placeholder="Name of person met"
- />
+ Next Action / Remarks
+ setUpdateData({ ...updateData, negotiationRemarks: v })} multiline placeholder="Describe next steps..." />
- Contact Details
- setUpdateData({...updateData, demoContactDetails: t})}
- placeholder="Phone or Email"
- />
+
+ Save Updates
+
- Expected Closing Date (YYYY-MM-DD)
- setUpdateData({...updateData, expectedCloseDate: t})}
- placeholder="2024-12-31"
- />
+ {
+ setIsModalOpen(false);
+ navigation.navigate('LogActivity', { client: selectedOpp?.client, tab: 'followup' });
+ }}
+ >
+ 📅 Schedule Next Activity
+
+
+
- Competitor Mention
- setUpdateData({...updateData, competitorMention: t})}
- placeholder="None or Competitor Name"
- />
-
- Queries / Objections
- setUpdateData({...updateData, keyQueries: t})}
- />
-
- )}
-
- {updateData.stage === 'WON' && (
-
- CLOSING DETAILS (MANDATORY)
-
- Payment Mode
- setUpdateData({...updateData, paymentMode: t})}
- placeholder="Cash / Bank Transfer / UPI"
- />
-
- Special Rate (Optional)
- setUpdateData({...updateData, specialRate: t})}
- placeholder="Final agreed rate"
- keyboardType="numeric"
- />
-
- Negotiation Remarks
- setUpdateData({...updateData, negotiationRemarks: t})}
- />
-
- )}
-
-
- SAVE UPDATE
-
-
-
+ {/* Nested User Modal */}
+
+
+
+
+ Transfer To
+ setUserModal(false)}>✕
+
+ u.id !== userInfo?.id)]}
+ keyExtractor={item => item.id}
+ renderItem={({ item }) => (
+ {
+ setUpdateData({ ...updateData, assignedTo: item.id });
+ setUserModal(false);
+ }}
+ >
+ {item.name?.charAt(0)}
+
+ {item.name}
+ {item.role}
+
+
+ )}
+ />
+
+
+
+
+ {/* Client Update Modal */}
+
+
+
+ Manage Lead
+ setIsClientModalOpen(false)}>✕
+
+
+ Lead / Client Name
+ {selectedClient?.companyName || selectedClient?.name}
+
+ Update Status (Stage)
+
+ {leadStages.map(s => (
+ setUpdateData({ ...updateData, status: s.id })}
+ >
+ {s.label}
+
+ ))}
+
+
+
+ Save Status
+
+
+
+
+ Pipeline Movement
+
+ 🚀 Convert to Deal (Deals Pipeline)
+
+
+ {
+ setIsClientModalOpen(false);
+ navigation.navigate('ClientDetails', { client: selectedClient });
+ }}
+ >
+ 👁️ View Full Details
+
+
@@ -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;
diff --git a/src/screens/TasksScreen.js b/src/screens/TasksScreen.js
index 54cebcf..bd9abd9 100644
--- a/src/screens/TasksScreen.js
+++ b/src/screens/TasksScreen.js
@@ -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 (
-
+
+ {TYPE_ICONS[type]}
+
- {item.client?.name || 'Unknown Client'}
+ {item.client?.companyName || item.client?.name || 'Unknown Client'}
{item.client?.phone && (
- handleCall(item.client.phone)}>
+ handleCall(item.client.phone)} style={styles.callCircle}>
📞
)}
+
+ {type}
+
{item.notes}
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
@@ -96,7 +117,7 @@ const TasksScreen = ({ navigation }) => {
{isPending && (
- handleMarkDone(item.id)}>
+ handleMarkDone(item.id)}>
Done
)}
@@ -113,10 +134,20 @@ const TasksScreen = ({ navigation }) => {
- My Tasks
- Sorted by date
+
+
+ Activities
+ Manage your schedule
+
+ navigation.navigate('LogActivity', { tab: 'followup' })}
+ >
+ + New
+
+
- {['ALL', 'PENDING', 'DONE'].map(f => (
+ {['PENDING', 'DONE', 'ALL'].map(f => (
{
renderSectionHeader={({ section }) => (
{section.title}
- {section.data.length} task{section.data.length !== 1 ? 's' : ''}
+ {section.data.length} item{section.data.length !== 1 ? 's' : ''}
)}
refreshControl={ { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
contentContainerStyle={{ paddingBottom: 40 }}
ListEmptyComponent={
- 🎉
- All Clear!
- No tasks match this filter.
+ ✨
+ All Caught Up!
+ No activities scheduled here.
}
/>
@@ -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;