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 itmain
parent
22f0d20020
commit
de97592ded
|
|
@ -9,6 +9,8 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
|
"@react-native-community/datetimepicker": "^9.1.0",
|
||||||
|
"@react-native-documents/picker": "^12.0.1",
|
||||||
"@react-native/new-app-screen": "0.83.1",
|
"@react-native/new-app-screen": "0.83.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.15.9",
|
"@react-navigation/bottom-tabs": "^7.15.9",
|
||||||
"@react-navigation/native": "^7.1.26",
|
"@react-navigation/native": "^7.1.26",
|
||||||
|
|
@ -2976,6 +2978,42 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-community/datetimepicker": {
|
||||||
|
"version": "9.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-9.1.0.tgz",
|
||||||
|
"integrity": "sha512-eadbnk+I2vxvW30iTAsm/qlCnMMAadkifIMYNEB2lzhxN/SvlKc7S2V4k5DyrwjdCbqdcMk3t9K6fnUMcAV34w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": ">=52.0.0",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-windows": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"expo": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native-windows": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-native-documents/picker": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-documents/picker/-/picker-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-vpJKb4t/5bnxe9+gQl+plJfKrrIsmYwANGhNH2B9E1dS1+6FDBzg4Dwmcq4ueaGfkRKEPJ606mJttVEH1ZKZaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/react-native-documents/document-picker?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": ">=0.79.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.83.1",
|
"version": "0.83.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
|
"@react-native-community/datetimepicker": "^9.1.0",
|
||||||
|
"@react-native-documents/picker": "^12.0.1",
|
||||||
"@react-native/new-app-screen": "0.83.1",
|
"@react-native/new-app-screen": "0.83.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.15.9",
|
"@react-navigation/bottom-tabs": "^7.15.9",
|
||||||
"@react-navigation/native": "^7.1.26",
|
"@react-navigation/native": "^7.1.26",
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@ import ClientDetailsScreen from '../screens/ClientDetailsScreen';
|
||||||
import EditClientScreen from '../screens/EditClientScreen';
|
import EditClientScreen from '../screens/EditClientScreen';
|
||||||
import PipelineScreen from '../screens/PipelineScreen';
|
import PipelineScreen from '../screens/PipelineScreen';
|
||||||
|
|
||||||
import EnquiryScreen from '../screens/EnquiryScreen';
|
import AddOpportunityScreen from '../screens/AddOpportunityScreen';
|
||||||
import EnquiryListScreen from '../screens/EnquiryListScreen';
|
|
||||||
import ExpenseScreen from '../screens/ExpenseScreen';
|
import ExpenseScreen from '../screens/ExpenseScreen';
|
||||||
import IncentiveScreen from '../screens/IncentiveScreen';
|
import IncentiveScreen from '../screens/IncentiveScreen';
|
||||||
import LogActivityScreen from '../screens/LogActivityScreen';
|
import LogActivityScreen from '../screens/LogActivityScreen';
|
||||||
|
|
@ -52,7 +51,7 @@ const TabNavigator = () => (
|
||||||
<Tab.Screen name="Dashboard" component={HomeScreen} />
|
<Tab.Screen name="Dashboard" component={HomeScreen} />
|
||||||
<Tab.Screen name="Pipeline" component={PipelineScreen} />
|
<Tab.Screen name="Pipeline" component={PipelineScreen} />
|
||||||
<Tab.Screen name="Clients" component={ClientListScreen} />
|
<Tab.Screen name="Clients" component={ClientListScreen} />
|
||||||
<Tab.Screen name="Tasks" component={TasksScreen} />
|
<Tab.Screen name="Activities" component={TasksScreen} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -77,8 +76,7 @@ const AppNav = () => {
|
||||||
<Stack.Screen name="AddClient" component={AddClientScreen} />
|
<Stack.Screen name="AddClient" component={AddClientScreen} />
|
||||||
<Stack.Screen name="ClientDetails" component={ClientDetailsScreen} options={{ title: 'Client Details' }} />
|
<Stack.Screen name="ClientDetails" component={ClientDetailsScreen} options={{ title: 'Client Details' }} />
|
||||||
<Stack.Screen name="EditClient" component={EditClientScreen} options={{ title: 'Edit Client' }} />
|
<Stack.Screen name="EditClient" component={EditClientScreen} options={{ title: 'Edit Client' }} />
|
||||||
<Stack.Screen name="EnquiryList" component={EnquiryListScreen} options={{ title: 'Enquiries' }} />
|
<Stack.Screen name="AddOpportunity" component={AddOpportunityScreen} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="Enquiry" component={EnquiryScreen} options={{ title: 'Add Enquiry' }} />
|
|
||||||
<Stack.Screen name="Expense" component={ExpenseScreen} />
|
<Stack.Screen name="Expense" component={ExpenseScreen} />
|
||||||
<Stack.Screen name="Incentive" component={IncentiveScreen} />
|
<Stack.Screen name="Incentive" component={IncentiveScreen} />
|
||||||
<Stack.Screen name="LogActivity" component={LogActivityScreen} options={{ title: 'Log Activity' }} />
|
<Stack.Screen name="LogActivity" component={LogActivityScreen} options={{ title: 'Log Activity' }} />
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,39 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { View, Text, TextInput, Button, StyleSheet, Alert, ScrollView, Platform, PermissionsAndroid, ActivityIndicator } from 'react-native';
|
import {
|
||||||
|
View, Text, TextInput, Button, StyleSheet, Alert, ScrollView,
|
||||||
|
Platform, PermissionsAndroid, ActivityIndicator, TouchableOpacity, Modal, FlatList
|
||||||
|
} from 'react-native';
|
||||||
import Geolocation from 'react-native-geolocation-service';
|
import Geolocation from 'react-native-geolocation-service';
|
||||||
|
import { pick } from '@react-native-documents/picker';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
import { AuthContext } from '../context/AuthContext';
|
||||||
|
|
||||||
const AddClientScreen = ({ navigation }) => {
|
const AddClientScreen = ({ navigation }) => {
|
||||||
const [name, setName] = useState('');
|
const { userInfo } = useContext(AuthContext);
|
||||||
|
const [companyName, setCompanyName] = useState('');
|
||||||
|
const [contactName, setContactName] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [address, setAddress] = useState('');
|
const [address, setAddress] = useState('');
|
||||||
const [landmark, setLandmark] = useState('');
|
const [landmark, setLandmark] = useState('');
|
||||||
|
const [closingProbability, setClosingProbability] = useState('');
|
||||||
|
const [expectedClosingTimeframe, setExpectedClosingTimeframe] = useState('');
|
||||||
|
const [isDemoDone, setIsDemoDone] = useState(false);
|
||||||
const [location, setLocation] = useState(null);
|
const [location, setLocation] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [locating, setLocating] = useState(false);
|
const [locating, setLocating] = useState(false);
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||||
|
|
||||||
|
// Assignment state
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [assignedUser, setAssignedUser] = useState(null);
|
||||||
|
const [userModal, setUserModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAssignedUser({ id: userInfo?.id, name: 'Myself' });
|
||||||
|
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
|
||||||
|
}, [userInfo]);
|
||||||
|
|
||||||
const requestLocationPermission = async () => {
|
const requestLocationPermission = async () => {
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
|
|
@ -43,13 +64,11 @@ const AddClientScreen = ({ navigation }) => {
|
||||||
setLocating(true);
|
setLocating(true);
|
||||||
Geolocation.getCurrentPosition(
|
Geolocation.getCurrentPosition(
|
||||||
(position) => {
|
(position) => {
|
||||||
console.log('Location success:', position);
|
|
||||||
setLocation(position.coords);
|
setLocation(position.coords);
|
||||||
setLocating(false);
|
setLocating(false);
|
||||||
Alert.alert("Success", "Location Captured!");
|
Alert.alert("Success", "Location Captured!");
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.log('Location error:', error);
|
|
||||||
setLocating(false);
|
setLocating(false);
|
||||||
Alert.alert("Location Error", error.message);
|
Alert.alert("Location Error", error.message);
|
||||||
},
|
},
|
||||||
|
|
@ -57,30 +76,71 @@ const AddClientScreen = ({ navigation }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pickFiles = async () => {
|
||||||
|
try {
|
||||||
|
const results = await pick({
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadedFiles = [];
|
||||||
|
for (const res of results) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', {
|
||||||
|
uri: Platform.OS === 'ios' ? res.uri.replace('file://', '') : res.uri,
|
||||||
|
type: res.type || 'application/octet-stream',
|
||||||
|
name: res.name || 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadRes = await api.post('/upload', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
uploadedFiles.push({
|
||||||
|
name: res.name,
|
||||||
|
type: res.type,
|
||||||
|
size: res.size,
|
||||||
|
url: uploadRes.data.url
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Upload failed', err);
|
||||||
|
Alert.alert('Upload Failed', `Could not upload ${res.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedFiles([...selectedFiles, ...uploadedFiles]);
|
||||||
|
} catch (err) {
|
||||||
|
if (!DocumentPicker.isCancel(err)) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
setSelectedFiles(selectedFiles.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!name || !phone) {
|
if (!contactName || !phone) {
|
||||||
Alert.alert("Error", "Name and Phone are required");
|
Alert.alert("Error", "Contact Name and Phone are required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Current Location Check Before Submit:', location);
|
|
||||||
if (!location) {
|
|
||||||
Alert.alert("Debug", "Location state is null! Did you click capture?");
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name: companyName || contactName,
|
||||||
|
companyName,
|
||||||
|
contactName,
|
||||||
phone,
|
phone,
|
||||||
status: 'LEAD',
|
status: 'LEAD',
|
||||||
|
assignedTo: assignedUser?.id || userInfo?.id,
|
||||||
|
closingProbability: closingProbability ? parseInt(closingProbability) : 0,
|
||||||
|
expectedClosingTimeframe,
|
||||||
|
isDemoDone,
|
||||||
|
files: selectedFiles,
|
||||||
...(email ? { email } : {}),
|
...(email ? { email } : {}),
|
||||||
...(address ? { address } : {}),
|
...(address ? { address } : {}),
|
||||||
...(landmark ? { landmark } : {}),
|
...(landmark ? { landmark } : {}),
|
||||||
...(location ? { lat: location.latitude, lng: location.longitude } : {})
|
...(location ? { lat: location.latitude, lng: location.longitude } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Submitting Payload:', JSON.stringify(payload, null, 2));
|
|
||||||
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.post('/clients', payload);
|
await api.post('/clients', payload);
|
||||||
|
|
@ -97,76 +157,122 @@ const AddClientScreen = ({ navigation }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView contentContainerStyle={styles.container}>
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<Text style={styles.label}>Name *</Text>
|
<Text style={styles.label}>Company Name</Text>
|
||||||
<TextInput style={styles.input} value={name} onChangeText={setName} />
|
<TextInput style={styles.input} value={companyName} onChangeText={setCompanyName} placeholder="Enter company name" />
|
||||||
|
|
||||||
|
<Text style={styles.label}>Contact Name *</Text>
|
||||||
|
<TextInput style={styles.input} value={contactName} onChangeText={setContactName} placeholder="Enter contact person name" />
|
||||||
|
|
||||||
<Text style={styles.label}>Phone *</Text>
|
<Text style={styles.label}>Phone *</Text>
|
||||||
<TextInput style={styles.input} value={phone} onChangeText={setPhone} keyboardType="phone-pad" />
|
<TextInput style={styles.input} value={phone} onChangeText={setPhone} placeholder="Enter phone number" keyboardType="phone-pad" />
|
||||||
|
|
||||||
|
<Text style={styles.label}>Assigned To</Text>
|
||||||
|
<TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
|
||||||
|
<Text style={styles.pickerBtnText}>{assignedUser?.name || 'Myself'}</Text>
|
||||||
|
<Text style={styles.pickerArrow}>›</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Text style={styles.label}>Email</Text>
|
<Text style={styles.label}>Email</Text>
|
||||||
<TextInput style={styles.input} value={email} onChangeText={setEmail} keyboardType="email-address" />
|
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="Enter email address" keyboardType="email-address" />
|
||||||
|
|
||||||
<Text style={styles.label}>Address</Text>
|
<Text style={styles.label}>Address</Text>
|
||||||
<TextInput style={styles.input} value={address} onChangeText={setAddress} multiline />
|
<TextInput style={[styles.input, { height: 80 }]} value={address} onChangeText={setAddress} placeholder="Enter address" multiline />
|
||||||
|
|
||||||
<Text style={styles.label}>Landmark</Text>
|
<Text style={styles.label}>Landmark</Text>
|
||||||
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} />
|
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} placeholder="Enter nearby landmark" />
|
||||||
|
|
||||||
<View style={styles.locationContainer}>
|
<TouchableOpacity style={[styles.locationBtn, { backgroundColor: location ? '#16a34a' : Colors.secondary }]} onPress={getCurrentLocation} disabled={locating}>
|
||||||
<Button
|
{locating ? <ActivityIndicator color="white" /> : <Text style={styles.locationBtnText}>{location ? "✓ Location Captured" : "📍 Capture Current Location"}</Text>}
|
||||||
title={locating ? "Locating..." : (location ? "Update Location" : "Capture Location")}
|
</TouchableOpacity>
|
||||||
onPress={getCurrentLocation}
|
|
||||||
disabled={locating}
|
<View style={styles.fileSection}>
|
||||||
color={Colors.secondary}
|
<View style={styles.fileHeader}>
|
||||||
/>
|
<Text style={styles.label}>Files / Attachments</Text>
|
||||||
{location && (
|
<TouchableOpacity onPress={pickFiles} style={styles.addFileBtn}>
|
||||||
<Text style={styles.locationText}>
|
<Text style={styles.addFileBtnText}>+ ADD FILE</Text>
|
||||||
Lat: {location.latitude.toFixed(4)}, Lng: {location.longitude.toFixed(4)}
|
</TouchableOpacity>
|
||||||
</Text>
|
</View>
|
||||||
|
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<View key={index} style={styles.fileRow}>
|
||||||
|
<View style={styles.fileInfo}>
|
||||||
|
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
|
||||||
|
<Text style={styles.fileSize}>{(file.size / 1024).toFixed(1)} KB</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity onPress={() => removeFile(index)}>
|
||||||
|
<Text style={styles.removeFile}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{selectedFiles.length === 0 && (
|
||||||
|
<Text style={styles.noFiles}>No files attached yet</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.spacer} />
|
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||||
|
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>Add Client</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Button title={loading ? "Saving..." : "Save Client"} onPress={handleSubmit} disabled={loading} color={Colors.primary} />
|
<Modal visible={userModal} animationType="slide" transparent={true}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Assign To</Text>
|
||||||
|
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}>✕</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity style={styles.userRow} onPress={() => { setAssignedUser(item); setUserModal(false); }}>
|
||||||
|
<View style={styles.userAvatar}>
|
||||||
|
<Text style={styles.userAvatarText}>{item.name?.charAt(0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.userName}>{item.name}</Text>
|
||||||
|
<Text style={styles.userRole}>{item.role}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: { padding: 20, backgroundColor: '#fff' },
|
||||||
padding: 20,
|
label: { fontSize: 13, fontWeight: 'bold', color: '#64748b', marginBottom: 6, marginTop: 15, textTransform: 'uppercase' },
|
||||||
backgroundColor: Colors.background
|
input: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, fontSize: 15, backgroundColor: '#f8fafc' },
|
||||||
},
|
pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
|
||||||
label: {
|
pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '600' },
|
||||||
fontSize: 16,
|
pickerArrow: { fontSize: 20, color: '#94a3b8' },
|
||||||
marginBottom: 5,
|
locationBtn: { padding: 15, borderRadius: 12, marginTop: 20, alignItems: 'center' },
|
||||||
fontWeight: 'bold',
|
locationBtnText: { color: 'white', fontWeight: 'bold' },
|
||||||
color: Colors.text
|
submitBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 12, marginTop: 30, alignItems: 'center', marginBottom: 50 },
|
||||||
},
|
submitBtnText: { color: 'white', fontSize: 16, fontWeight: 'bold' },
|
||||||
input: {
|
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
|
||||||
borderWidth: 1,
|
modalContent: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '70%', paddingBottom: 20 },
|
||||||
borderColor: Colors.border,
|
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
|
||||||
borderRadius: 5,
|
modalTitle: { fontSize: 18, fontWeight: 'bold', color: '#1e293b' },
|
||||||
padding: 10,
|
modalClose: { fontSize: 20, color: '#94a3b8', padding: 5 },
|
||||||
marginBottom: 15,
|
userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
|
||||||
backgroundColor: 'white',
|
userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
|
||||||
color: Colors.text
|
userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
|
||||||
},
|
userName: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
|
||||||
locationContainer: {
|
userRole: { fontSize: 12, color: '#64748b' },
|
||||||
marginBottom: 20,
|
fileSection: { marginTop: 20, padding: 15, backgroundColor: '#f8fafc', borderRadius: 12, borderStyle: 'dashed', borderWidth: 1.5, borderColor: '#e2e8f0' },
|
||||||
padding: 10,
|
fileHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
|
||||||
backgroundColor: Colors.backgroundSecondary,
|
addFileBtn: { backgroundColor: '#10b981', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 },
|
||||||
borderRadius: 5
|
addFileBtnText: { color: 'white', fontSize: 11, fontWeight: 'bold' },
|
||||||
},
|
fileRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: 'white', padding: 10, borderRadius: 8, marginBottom: 8, borderWidth: 1, borderColor: '#f1f5f9' },
|
||||||
locationText: {
|
fileInfo: { flex: 1, marginRight: 10 },
|
||||||
marginTop: 5,
|
fileName: { fontSize: 13, fontWeight: '600', color: '#1e293b' },
|
||||||
textAlign: 'center',
|
fileSize: { fontSize: 10, color: '#94a3b8', marginTop: 2 },
|
||||||
color: Colors.textMuted
|
removeFile: { color: '#ef4444', fontSize: 16, fontWeight: 'bold', padding: 5 },
|
||||||
},
|
noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 11, paddingVertical: 10, fontStyle: 'italic' }
|
||||||
spacer: {
|
|
||||||
height: 20
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default AddClientScreen;
|
export default AddClientScreen;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -57,8 +57,16 @@ const ClientDetailsScreen = ({ route, navigation }) => {
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.headerRow}>
|
<View style={styles.headerRow}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={styles.name}>{client.name}</Text>
|
<Text style={styles.name}>{client.companyName || client.name}</Text>
|
||||||
<Text style={styles.status}>{client.status}</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>
|
</View>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('EditClient', { client })} style={styles.editButton}>
|
<TouchableOpacity onPress={() => navigation.navigate('EditClient', { client })} style={styles.editButton}>
|
||||||
<Text style={styles.editButtonText}>Edit</Text>
|
<Text style={styles.editButtonText}>Edit</Text>
|
||||||
|
|
@ -79,6 +87,19 @@ const ClientDetailsScreen = ({ route, navigation }) => {
|
||||||
<Text style={styles.label}>Landmark:</Text>
|
<Text style={styles.label}>Landmark:</Text>
|
||||||
<Text style={styles.value}>{client.landmark || 'N/A'}</Text>
|
<Text style={styles.value}>{client.landmark || 'N/A'}</Text>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<View style={styles.statsRow}>
|
||||||
|
<View style={styles.statBox}>
|
||||||
|
<Text style={styles.label}>Probability</Text>
|
||||||
|
<Text style={[styles.value, {marginBottom: 0, fontWeight: 'bold'}]}>{client.closingProbability || 0}%</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statBox}>
|
||||||
|
<Text style={styles.label}>Timeframe</Text>
|
||||||
|
<Text style={[styles.value, {marginBottom: 0, fontSize: 13}]}>{client.expectedClosingTimeframe || 'Not Set'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{client.lat && client.lng ? (
|
{client.lat && client.lng ? (
|
||||||
<View style={styles.mapContainer}>
|
<View style={styles.mapContainer}>
|
||||||
<Button title="Get Directions" onPress={openMap} color={Colors.secondary} />
|
<Button title="Get Directions" onPress={openMap} color={Colors.secondary} />
|
||||||
|
|
@ -89,6 +110,56 @@ const ClientDetailsScreen = ({ route, navigation }) => {
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.noLocation}>No location data available</Text>
|
<Text style={styles.noLocation}>No location data available</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<View style={styles.actionRow}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionBtn, {backgroundColor: Colors.primary}]}
|
||||||
|
onPress={() => navigation.navigate('LogActivity', { tab: 'call', client })}
|
||||||
|
>
|
||||||
|
<Text style={styles.actionBtnText}>✅ Log Activity</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionBtn, {backgroundColor: '#6366f1'}]}
|
||||||
|
onPress={() => navigation.navigate('LogActivity', { tab: 'followup', client })}
|
||||||
|
>
|
||||||
|
<Text style={styles.actionBtnText}>📅 Schedule</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<Text style={[styles.label, { color: Colors.primary, fontSize: 12, marginBottom: 15 }]}>📁 ATTACHED DOCUMENTS</Text>
|
||||||
|
{client.files && client.files.length > 0 ? (
|
||||||
|
client.files.map((file, idx) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={idx}
|
||||||
|
style={styles.fileItem}
|
||||||
|
onPress={() => {
|
||||||
|
console.log('Opening URL:', file.url);
|
||||||
|
if (!file.url || file.url.includes('fake-storage.com')) {
|
||||||
|
Alert.alert('Old File', 'This file was created before the new storage system and is no longer available. Please re-upload it.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = file.url.startsWith('http') ? file.url : `${api.defaults.baseURL}${file.url}`;
|
||||||
|
console.log('Resolved mobile URL:', url);
|
||||||
|
Linking.openURL(url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={styles.fileIcon}>
|
||||||
|
<Text style={{ fontSize: 20 }}>📄</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
|
||||||
|
<Text style={styles.fileSize}>{(file.size / 1024).toFixed(1)} KB</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.openText}>Open ›</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text style={styles.noFiles}>No documents attached to this client.</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|
@ -114,6 +185,12 @@ const styles = StyleSheet.create({
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4
|
shadowRadius: 4
|
||||||
},
|
},
|
||||||
|
fileItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 14, marginBottom: 10, borderWidth: 1, borderColor: '#f1f5f9' },
|
||||||
|
fileIcon: { width: 40, height: 40, backgroundColor: 'white', borderRadius: 10, alignItems: 'center', justifyContent: 'center', marginRight: 12, borderWidth: 1, borderColor: '#e2e8f0' },
|
||||||
|
fileName: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
|
||||||
|
fileSize: { fontSize: 11, color: '#94a3b8', marginTop: 2 },
|
||||||
|
openText: { fontSize: 12, color: Colors.primary, fontWeight: '800' },
|
||||||
|
noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 13, paddingVertical: 20, fontStyle: 'italic' },
|
||||||
headerRow: {
|
headerRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|
@ -141,7 +218,39 @@ const styles = StyleSheet.create({
|
||||||
color: Colors.secondary,
|
color: Colors.secondary,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
marginBottom: 10
|
},
|
||||||
|
statusRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 10,
|
||||||
|
gap: 10
|
||||||
|
},
|
||||||
|
demoBadge: {
|
||||||
|
backgroundColor: '#dbeafe',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#bfdbfe'
|
||||||
|
},
|
||||||
|
demoBadgeText: {
|
||||||
|
color: '#1e40af',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
},
|
||||||
|
contactSub: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.textMuted,
|
||||||
|
marginBottom: 5,
|
||||||
|
fontStyle: 'italic'
|
||||||
|
},
|
||||||
|
statsRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: 5
|
||||||
|
},
|
||||||
|
statBox: {
|
||||||
|
flex: 1
|
||||||
},
|
},
|
||||||
divider: {
|
divider: {
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|
@ -172,6 +281,28 @@ const styles = StyleSheet.create({
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
color: Colors.textLight,
|
color: Colors.textLight,
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
actionRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
marginTop: 5
|
||||||
|
},
|
||||||
|
actionBtn: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4
|
||||||
|
},
|
||||||
|
actionBtnText: {
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 14
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ const ClientListScreen = ({ navigation }) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
const filtered = clients.filter(client =>
|
const filtered = clients.filter(client =>
|
||||||
client.name.toLowerCase().includes(lowerCaseQuery) ||
|
(client.name && client.name.toLowerCase().includes(lowerCaseQuery)) ||
|
||||||
|
(client.companyName && client.companyName.toLowerCase().includes(lowerCaseQuery)) ||
|
||||||
|
(client.contactName && client.contactName.toLowerCase().includes(lowerCaseQuery)) ||
|
||||||
(client.email && client.email.toLowerCase().includes(lowerCaseQuery)) ||
|
(client.email && client.email.toLowerCase().includes(lowerCaseQuery)) ||
|
||||||
(client.phone && client.phone.includes(lowerCaseQuery))
|
(client.phone && client.phone.includes(lowerCaseQuery))
|
||||||
);
|
);
|
||||||
|
|
@ -44,8 +46,8 @@ const ClientListScreen = ({ navigation }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitials = (name) => {
|
const getInitials = (client) => {
|
||||||
if (!name) return 'C';
|
const name = client.companyName || client.name || 'C';
|
||||||
const parts = name.split(' ');
|
const parts = name.split(' ');
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||||
|
|
@ -56,10 +58,11 @@ const ClientListScreen = ({ navigation }) => {
|
||||||
const renderItem = ({ item }) => (
|
const renderItem = ({ item }) => (
|
||||||
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('ClientDetails', { client: item })} activeOpacity={0.8}>
|
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('ClientDetails', { client: item })} activeOpacity={0.8}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
<Text style={styles.avatarText}>{getInitials(item.name)}</Text>
|
<Text style={styles.avatarText}>{getInitials(item)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.cardContent}>
|
<View style={styles.cardContent}>
|
||||||
<Text style={styles.name}>{item.name}</Text>
|
<Text style={styles.name}>{item.companyName || item.name}</Text>
|
||||||
|
{item.companyName && <Text style={styles.contactName}>{item.contactName}</Text>}
|
||||||
<Text style={styles.details}>{item.phone}</Text>
|
<Text style={styles.details}>{item.phone}</Text>
|
||||||
{item.email ? <Text style={styles.subDetails}>{item.email}</Text> : null}
|
{item.email ? <Text style={styles.subDetails}>{item.email}</Text> : null}
|
||||||
<View style={styles.statusBadge}>
|
<View style={styles.statusBadge}>
|
||||||
|
|
@ -186,6 +189,12 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: Colors.text,
|
color: Colors.text,
|
||||||
|
marginBottom: 1
|
||||||
|
},
|
||||||
|
contactName: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.textMuted,
|
||||||
|
fontStyle: 'italic',
|
||||||
marginBottom: 2
|
marginBottom: 2
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,49 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { View, Text, TextInput, StyleSheet, Alert, ScrollView, Platform, PermissionsAndroid, TouchableOpacity, ActivityIndicator } from 'react-native';
|
import {
|
||||||
|
View, Text, TextInput, StyleSheet, Alert, ScrollView, Platform,
|
||||||
|
PermissionsAndroid, TouchableOpacity, ActivityIndicator, Modal, FlatList
|
||||||
|
} from 'react-native';
|
||||||
import Geolocation from 'react-native-geolocation-service';
|
import Geolocation from 'react-native-geolocation-service';
|
||||||
|
import { pick } from '@react-native-documents/picker';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
import { AuthContext } from '../context/AuthContext';
|
||||||
|
|
||||||
const EditClientScreen = ({ navigation, route }) => {
|
const EditClientScreen = ({ navigation, route }) => {
|
||||||
|
const { userInfo } = useContext(AuthContext);
|
||||||
const { client } = route.params;
|
const { client } = route.params;
|
||||||
|
|
||||||
const [name, setName] = useState(client.name);
|
const [companyName, setCompanyName] = useState(client.companyName || '');
|
||||||
|
const [contactName, setContactName] = useState(client.contactName || '');
|
||||||
const [phone, setPhone] = useState(client.phone);
|
const [phone, setPhone] = useState(client.phone);
|
||||||
const [email, setEmail] = useState(client.email || '');
|
const [email, setEmail] = useState(client.email || '');
|
||||||
const [address, setAddress] = useState(client.address || '');
|
const [address, setAddress] = useState(client.address || '');
|
||||||
const [landmark, setLandmark] = useState(client.landmark || '');
|
const [landmark, setLandmark] = useState(client.landmark || '');
|
||||||
|
const [closingProbability, setClosingProbability] = useState(client.closingProbability ? String(client.closingProbability) : '0');
|
||||||
|
const [expectedClosingTimeframe, setExpectedClosingTimeframe] = useState(client.expectedClosingTimeframe || '');
|
||||||
|
const [isDemoDone, setIsDemoDone] = useState(!!client.isDemoDone);
|
||||||
const [location, setLocation] = useState(client.lat && client.lng ? { latitude: client.lat, longitude: client.lng } : null);
|
const [location, setLocation] = useState(client.lat && client.lng ? { latitude: client.lat, longitude: client.lng } : null);
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState(client.files || []);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [locating, setLocating] = useState(false);
|
const [locating, setLocating] = useState(false);
|
||||||
|
|
||||||
|
// Assignment state
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [assignedUser, setAssignedUser] = useState(null);
|
||||||
|
const [userModal, setUserModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get('/users').then(r => {
|
||||||
|
setUsers(r.data);
|
||||||
|
const currentAssignee = r.data.find(u => u.id === client.assignedTo);
|
||||||
|
if (currentAssignee) {
|
||||||
|
setAssignedUser(currentAssignee);
|
||||||
|
} else if (client.assignedTo === userInfo?.id) {
|
||||||
|
setAssignedUser({ id: userInfo?.id, name: 'Myself' });
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}, [client.assignedTo, userInfo]);
|
||||||
|
|
||||||
const requestLocationPermission = async () => {
|
const requestLocationPermission = async () => {
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
try {
|
try {
|
||||||
|
|
@ -58,15 +85,64 @@ const EditClientScreen = ({ navigation, route }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pickFiles = async () => {
|
||||||
|
try {
|
||||||
|
const results = await pick({
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadedFiles = [];
|
||||||
|
for (const res of results) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', {
|
||||||
|
uri: Platform.OS === 'ios' ? res.uri.replace('file://', '') : res.uri,
|
||||||
|
type: res.type || 'application/octet-stream',
|
||||||
|
name: res.name || 'file',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadRes = await api.post('/upload', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
uploadedFiles.push({
|
||||||
|
name: res.name,
|
||||||
|
type: res.type,
|
||||||
|
size: res.size,
|
||||||
|
url: uploadRes.data.url
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Upload failed', err);
|
||||||
|
Alert.alert('Upload Failed', `Could not upload ${res.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedFiles([...selectedFiles, ...uploadedFiles]);
|
||||||
|
} catch (err) {
|
||||||
|
if (!DocumentPicker.isCancel(err)) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
setSelectedFiles(selectedFiles.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!name || !phone) {
|
if (!contactName || !phone) {
|
||||||
Alert.alert("Error", "Name and Phone are required");
|
Alert.alert("Error", "Contact Name and Phone are required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name: companyName || contactName,
|
||||||
|
companyName,
|
||||||
|
contactName,
|
||||||
phone,
|
phone,
|
||||||
|
assignedTo: assignedUser?.id,
|
||||||
|
closingProbability: closingProbability ? parseInt(closingProbability) : 0,
|
||||||
|
expectedClosingTimeframe,
|
||||||
|
isDemoDone,
|
||||||
|
files: selectedFiles,
|
||||||
...(email ? { email } : {}),
|
...(email ? { email } : {}),
|
||||||
...(address ? { address } : {}),
|
...(address ? { address } : {}),
|
||||||
...(landmark ? { landmark } : {}),
|
...(landmark ? { landmark } : {}),
|
||||||
|
|
@ -89,169 +165,122 @@ const EditClientScreen = ({ navigation, route }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView contentContainerStyle={styles.container}>
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<Text style={styles.sectionHeader}>Basic Information</Text>
|
<Text style={styles.label}>Company Name</Text>
|
||||||
|
<TextInput style={styles.input} value={companyName} onChangeText={setCompanyName} placeholder="Enter company name" />
|
||||||
|
|
||||||
<View style={styles.formGroup}>
|
<Text style={styles.label}>Contact Name *</Text>
|
||||||
<Text style={styles.label}>Full Name *</Text>
|
<TextInput style={styles.input} value={contactName} onChangeText={setContactName} placeholder="Enter contact person name" />
|
||||||
<TextInput style={styles.input} value={name} onChangeText={setName} placeholder="Enter client name" />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.formGroup}>
|
<Text style={styles.label}>Phone *</Text>
|
||||||
<Text style={styles.label}>Phone Number *</Text>
|
<TextInput style={styles.input} value={phone} onChangeText={setPhone} placeholder="Enter phone number" keyboardType="phone-pad" />
|
||||||
<TextInput style={styles.input} value={phone} onChangeText={setPhone} keyboardType="phone-pad" placeholder="Enter phone number" />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.formGroup}>
|
<Text style={styles.label}>Assigned To / Transfer</Text>
|
||||||
<Text style={styles.label}>Email Address</Text>
|
<TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
|
||||||
<TextInput style={styles.input} value={email} onChangeText={setEmail} keyboardType="email-address" placeholder="Enter email" />
|
<Text style={styles.pickerBtnText}>{assignedUser?.name || 'Select User'}</Text>
|
||||||
</View>
|
<Text style={styles.pickerArrow}>›</Text>
|
||||||
|
|
||||||
<Text style={styles.sectionHeader}>Address Details</Text>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>}
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Email</Text>
|
||||||
|
<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, { height: 80 }]} value={address} onChangeText={setAddress} placeholder="Enter address" multiline />
|
||||||
|
|
||||||
|
<Text style={styles.label}>Landmark</Text>
|
||||||
|
<TextInput style={styles.input} value={landmark} onChangeText={setLandmark} placeholder="Enter nearby landmark" />
|
||||||
|
|
||||||
|
<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.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>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||||
|
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>Update Client</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Modal visible={userModal} animationType="slide" transparent={true}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Transfer To</Text>
|
||||||
|
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}>✕</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity style={styles.userRow} onPress={() => { setAssignedUser(item); setUserModal(false); }}>
|
||||||
|
<View style={styles.userAvatar}>
|
||||||
|
<Text style={styles.userAvatarText}>{item.name?.charAt(0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.userName}>{item.name}</Text>
|
||||||
|
<Text style={styles.userRole}>{item.role}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: { padding: 20, backgroundColor: '#fff' },
|
||||||
padding: 20,
|
label: { fontSize: 13, fontWeight: 'bold', color: '#64748b', marginBottom: 6, marginTop: 15, textTransform: 'uppercase' },
|
||||||
backgroundColor: Colors.background,
|
input: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, fontSize: 15, backgroundColor: '#f8fafc' },
|
||||||
flexGrow: 1
|
pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
|
||||||
},
|
pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '600' },
|
||||||
sectionHeader: {
|
pickerArrow: { fontSize: 20, color: '#94a3b8' },
|
||||||
fontSize: 18,
|
locationBtn: { padding: 15, borderRadius: 12, marginTop: 20, alignItems: 'center' },
|
||||||
fontWeight: 'bold',
|
locationBtnText: { color: 'white', fontWeight: 'bold' },
|
||||||
color: '#334155',
|
submitBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 12, marginTop: 30, alignItems: 'center', marginBottom: 50 },
|
||||||
marginBottom: 15,
|
submitBtnText: { color: 'white', fontSize: 16, fontWeight: 'bold' },
|
||||||
marginTop: 10
|
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
|
||||||
},
|
modalContent: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '70%', paddingBottom: 20 },
|
||||||
formGroup: {
|
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
|
||||||
marginBottom: 15
|
modalTitle: { fontSize: 18, fontWeight: 'bold', color: '#1e293b' },
|
||||||
},
|
modalClose: { fontSize: 20, color: '#94a3b8', padding: 5 },
|
||||||
label: {
|
userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
|
||||||
fontSize: 14,
|
userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
|
||||||
fontWeight: '600',
|
userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
|
||||||
color: Colors.textMuted,
|
userName: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
|
||||||
marginBottom: 8
|
userRole: { fontSize: 12, color: '#64748b' },
|
||||||
},
|
fileSection: { marginTop: 20, padding: 15, backgroundColor: '#f8fafc', borderRadius: 12, borderStyle: 'dashed', borderWidth: 1.5, borderColor: '#e2e8f0' },
|
||||||
input: {
|
fileHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
|
||||||
backgroundColor: 'white',
|
addFileBtn: { backgroundColor: '#10b981', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 },
|
||||||
borderWidth: 1,
|
addFileBtnText: { color: 'white', fontSize: 11, fontWeight: 'bold' },
|
||||||
borderColor: Colors.border,
|
fileRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: 'white', padding: 10, borderRadius: 8, marginBottom: 8, borderWidth: 1, borderColor: '#f1f5f9' },
|
||||||
borderRadius: 12,
|
fileInfo: { flex: 1, marginRight: 10 },
|
||||||
paddingHorizontal: 15,
|
fileName: { fontSize: 13, fontWeight: '600', color: '#1e293b' },
|
||||||
paddingVertical: 12,
|
fileSize: { fontSize: 10, color: '#94a3b8', marginTop: 2 },
|
||||||
fontSize: 16,
|
removeFile: { color: '#ef4444', fontSize: 16, fontWeight: 'bold', padding: 5 },
|
||||||
color: Colors.text
|
noFiles: { textAlign: 'center', color: '#94a3b8', fontSize: 11, paddingVertical: 10, fontStyle: 'italic' }
|
||||||
},
|
|
||||||
textArea: {
|
|
||||||
height: 100,
|
|
||||||
textAlignVertical: 'top'
|
|
||||||
},
|
|
||||||
locationCard: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
padding: 15,
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: Colors.border
|
|
||||||
},
|
|
||||||
locationInfo: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
locationLabel: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: Colors.text
|
|
||||||
},
|
|
||||||
coords: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.textMuted,
|
|
||||||
marginTop: 2,
|
|
||||||
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace'
|
|
||||||
},
|
|
||||||
locationButton: {
|
|
||||||
backgroundColor: Colors.secondary,
|
|
||||||
paddingHorizontal: 15,
|
|
||||||
paddingVertical: 10,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginLeft: 10
|
|
||||||
},
|
|
||||||
locationButtonText: {
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 14
|
|
||||||
},
|
|
||||||
spacer: {
|
|
||||||
height: 30
|
|
||||||
},
|
|
||||||
submitButton: {
|
|
||||||
backgroundColor: Colors.primary,
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingVertical: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
elevation: 4,
|
|
||||||
shadowColor: Colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 5,
|
|
||||||
marginBottom: 30
|
|
||||||
},
|
|
||||||
submitButtonText: {
|
|
||||||
color: 'white',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
},
|
|
||||||
disabledButton: {
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default EditClientScreen;
|
export default EditClientScreen;
|
||||||
|
|
|
||||||
|
|
@ -212,10 +212,10 @@ const HomeScreen = ({ navigation }) => {
|
||||||
onPress={() => navigation.navigate('Attendance')}
|
onPress={() => navigation.navigate('Attendance')}
|
||||||
/>
|
/>
|
||||||
<MenuCard
|
<MenuCard
|
||||||
title="Enquiries"
|
title="New Deal"
|
||||||
icon="📝"
|
icon="💼"
|
||||||
color={Colors.primary}
|
color={Colors.primary}
|
||||||
onPress={() => navigation.navigate('EnquiryList')}
|
onPress={() => navigation.navigate('AddOpportunity')}
|
||||||
/>
|
/>
|
||||||
<MenuCard
|
<MenuCard
|
||||||
title="Expenses"
|
title="Expenses"
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,29 @@ import {
|
||||||
TextInput, Alert, ActivityIndicator, StatusBar, Modal, FlatList, Linking, Switch
|
TextInput, Alert, ActivityIndicator, StatusBar, Modal, FlatList, Linking, Switch
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
import { AuthContext } from '../context/AuthContext';
|
import { AuthContext } from '../context/AuthContext';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
const ACTIVITY_TYPES = [
|
const STRATEGIC_TYPES = [
|
||||||
{ id: 'COLD_CALLING', label: 'Cold Calling', icon: '📞', funnelKey: 'calls' },
|
{ id: 'COLD_CALLING', label: 'Cold Calling', icon: '📞' },
|
||||||
{ id: 'WHATSAPP_CAMPAIGN', label: 'WhatsApp Campaign', icon: '📱', funnelKey: 'calls' },
|
{ id: 'WHATSAPP_CAMPAIGN', label: 'WhatsApp Campaign', icon: '📱' },
|
||||||
{ id: 'POSTER_PASTING', label: 'Poster Pasting', icon: '🖼️', funnelKey: null },
|
{ id: 'POSTER_PASTING', label: 'Poster Pasting', icon: '🖼️' },
|
||||||
{ id: 'EXHIBITION', label: 'Exhibition/Event', icon: '🎪', funnelKey: null },
|
{ id: 'EXHIBITION', label: 'Exhibition/Event', icon: '🎪' },
|
||||||
{ id: 'DATA_COLLECTION', label: 'Data Collection', icon: '📊', funnelKey: null },
|
{ id: 'DATA_COLLECTION', label: 'Data Collection', icon: '📊' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SCHEDULE_TYPES = [
|
||||||
|
{ id: 'FOLLOWUP', label: 'Follow-up', icon: '📅', color: '#6366f1' },
|
||||||
|
{ id: 'DEMO', label: 'Demo', icon: '📽️', color: '#3b82f6' },
|
||||||
|
{ id: 'QUOTE', label: 'Quote', icon: '📝', color: '#a855f7' },
|
||||||
|
{ id: 'NEGOTIATION', label: 'Negotiate', icon: '🤝', color: '#f59e0b' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'call', label: 'Log Call / Activity', icon: '📞' },
|
{ id: 'call', label: 'Done Now', icon: '✅' },
|
||||||
{ id: 'followup', label: 'Schedule Follow-up', icon: '📅' },
|
{ id: 'followup', label: 'Schedule', icon: '📅' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LogActivityScreen = ({ navigation, route }) => {
|
const LogActivityScreen = ({ navigation, route }) => {
|
||||||
|
|
@ -29,8 +37,13 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
const [activeTab, setActiveTab] = useState(defaultTab);
|
const [activeTab, setActiveTab] = useState(defaultTab);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [clients, setClients] = useState([]);
|
const [clients, setClients] = useState([]);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [opportunities, setOpportunities] = useState([]);
|
||||||
const [clientSearch, setClientSearch] = useState('');
|
const [clientSearch, setClientSearch] = useState('');
|
||||||
|
const [oppSearch, setOppSearch] = useState('');
|
||||||
const [clientModal, setClientModal] = useState(false);
|
const [clientModal, setClientModal] = useState(false);
|
||||||
|
const [userModal, setUserModal] = useState(false);
|
||||||
|
const [oppModal, setOppModal] = useState(false);
|
||||||
|
|
||||||
// Call / Activity state
|
// Call / Activity state
|
||||||
const [actType, setActType] = useState(null);
|
const [actType, setActType] = useState(null);
|
||||||
|
|
@ -38,39 +51,57 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
const [quantity, setQuantity] = useState('1');
|
const [quantity, setQuantity] = useState('1');
|
||||||
const [callClient, setCallClient] = useState(null);
|
const [callClient, setCallClient] = useState(null);
|
||||||
const [updateClientStatus, setUpdateClientStatus] = useState(null);
|
const [updateClientStatus, setUpdateClientStatus] = useState(null);
|
||||||
|
const [isDemoDone, setIsDemoDone] = useState(false);
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
|
{ id: 'LEAD', label: 'Lead', color: '#6366f1', bg: '#eef2ff' },
|
||||||
{ id: 'QUALITY', label: 'Quality', color: '#16a34a', bg: '#dcfce7' },
|
{ id: 'QUALITY', label: 'Quality', color: '#16a34a', bg: '#dcfce7' },
|
||||||
{ id: 'POTENTIAL', label: 'Potential', color: '#eab308', bg: '#fef9c3' },
|
{ id: 'POTENTIAL', label: 'Potential', color: '#eab308', bg: '#fef9c3' },
|
||||||
{ id: 'DEMO', label: 'Demo', color: '#a855f7', bg: '#f3e8ff' },
|
|
||||||
{ id: 'SALES', label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
|
{ id: 'SALES', label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
|
||||||
{ id: 'CLOSED', label: 'Closed', color: '#ef4444', bg: '#fee2e2' }
|
{ id: 'CLOSED', label: 'Closed', color: '#ef4444', bg: '#fee2e2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Followup state
|
// Followup state
|
||||||
|
const [fuType, setFuType] = useState('FOLLOWUP');
|
||||||
const [fuClient, setFuClient] = useState(null);
|
const [fuClient, setFuClient] = useState(null);
|
||||||
|
const [fuOpp, setFuOpp] = useState(null);
|
||||||
|
const [assignedUser, setAssignedUser] = useState(null);
|
||||||
const [fuNotes, setFuNotes] = useState('');
|
const [fuNotes, setFuNotes] = useState('');
|
||||||
const [fuDate, setFuDate] = useState('');
|
const [fuDate, setFuDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
const [fuTime, setFuTime] = useState('');
|
const [fuTime, setFuTime] = useState('10:00');
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
|
const [showTimePicker, setShowTimePicker] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAssignedUser({ id: userInfo?.id, name: 'Myself' });
|
||||||
|
}, [userInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get('/clients').then(r => setClients(r.data)).catch(() => {});
|
||||||
|
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
|
||||||
|
api.get('/opportunities').then(r => setOpportunities(r.data)).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredClients = clients.filter(c =>
|
||||||
|
(c.companyName || c.name || '').toLowerCase().includes(clientSearch.toLowerCase()) ||
|
||||||
|
(c.phone || '').includes(clientSearch)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredOpps = opportunities.filter(o =>
|
||||||
|
(o.title || '').toLowerCase().includes(oppSearch.toLowerCase()) ||
|
||||||
|
(o.client?.companyName || o.client?.name || '').toLowerCase().includes(oppSearch.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const handleCall = (phone) => {
|
const handleCall = (phone) => {
|
||||||
if (!phone) return;
|
if (!phone) return;
|
||||||
Linking.openURL(`tel:${phone}`);
|
Linking.openURL(`tel:${phone}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// ── Submit Strategic Activity ────────────────────────────────────────────
|
||||||
api.get('/clients').then(r => setClients(r.data)).catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filteredClients = clients.filter(c =>
|
|
||||||
c.name?.toLowerCase().includes(clientSearch.toLowerCase()) ||
|
|
||||||
c.phone?.includes(clientSearch)
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Submit Call/Activity ──────────────────────────────────────
|
|
||||||
const handleSubmitCall = async () => {
|
const handleSubmitCall = async () => {
|
||||||
if (!actType) { Alert.alert('Error', 'Please select an activity type'); return; }
|
if (!actType) { Alert.alert('Error', 'Please select activity type'); return; }
|
||||||
if (!description.trim()) { Alert.alert('Error', 'Please enter a description'); return; }
|
if (!description.trim()) { Alert.alert('Error', 'Please add notes'); return; }
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.post('/strategic-activities', {
|
await api.post('/strategic-activities', {
|
||||||
|
|
@ -78,19 +109,20 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
description,
|
description,
|
||||||
leadsGenerated: parseInt(quantity) || 0,
|
leadsGenerated: parseInt(quantity) || 0,
|
||||||
updateClientStatus,
|
updateClientStatus,
|
||||||
metadata: { clientId: callClient?.id, clientName: callClient?.name }
|
isDemoDone,
|
||||||
|
metadata: { clientId: callClient?.id, clientName: callClient?.companyName || callClient?.name }
|
||||||
});
|
});
|
||||||
Alert.alert('Done! ✅', `${actType.replace('_', ' ')} logged successfully.`, [
|
Alert.alert('Success ✅', `${actType.replace('_', ' ')} logged.`, [
|
||||||
{ text: 'Log Another', onPress: () => { setActType(null); setDescription(''); setQuantity('1'); setCallClient(null); setUpdateClientStatus(null); } },
|
{ text: 'Log Another', onPress: () => { setActType(null); setDescription(''); setQuantity('1'); setCallClient(null); setUpdateClientStatus(null); setIsDemoDone(false); } },
|
||||||
{ text: 'Go to Tasks', onPress: () => navigation.navigate('Main', { screen: 'Tasks' }) },
|
{ text: 'Go to Activities', onPress: () => navigation.navigate('Main', { screen: 'Activities' }) },
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('Error', 'Failed to log activity.');
|
Alert.alert('Error', 'Failed to log activity.');
|
||||||
} finally { setLoading(false); }
|
} finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Submit Followup ────────────────────────────────────────────
|
// ── Submit Scheduled Activity ────────────────────────────────────────────
|
||||||
const handleSubmitFollowup = async () => {
|
const handleSubmitSchedule = async () => {
|
||||||
if (!fuClient) { Alert.alert('Error', 'Please select a client'); return; }
|
if (!fuClient) { Alert.alert('Error', 'Please select a client'); return; }
|
||||||
if (!fuNotes.trim()) { Alert.alert('Error', 'Please add a note'); return; }
|
if (!fuNotes.trim()) { Alert.alert('Error', 'Please add a note'); return; }
|
||||||
if (!fuDate || !fuTime) { Alert.alert('Error', 'Please set date and time'); return; }
|
if (!fuDate || !fuTime) { Alert.alert('Error', 'Please set date and time'); return; }
|
||||||
|
|
@ -99,17 +131,19 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
try {
|
try {
|
||||||
await api.post('/followups', {
|
await api.post('/followups', {
|
||||||
clientId: fuClient.id,
|
clientId: fuClient.id,
|
||||||
userId: userInfo?.id,
|
opportunityId: fuOpp?.id,
|
||||||
|
userId: assignedUser?.id || userInfo?.id,
|
||||||
|
type: fuType,
|
||||||
notes: fuNotes,
|
notes: fuNotes,
|
||||||
date: new Date(dateStr).toISOString(),
|
date: new Date(dateStr).toISOString(),
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
});
|
});
|
||||||
Alert.alert('Scheduled! 📅', `Follow-up with ${fuClient.name} scheduled.`, [
|
Alert.alert('Scheduled! 📅', `${fuType} for ${fuClient.companyName || fuClient.name} scheduled.`, [
|
||||||
{ text: 'Schedule Another', onPress: () => { setFuClient(null); setFuNotes(''); setFuDate(''); setFuTime(''); } },
|
{ text: 'Schedule Another', onPress: () => { setFuClient(null); setFuOpp(null); setFuNotes(''); setFuDate(''); setFuTime(''); } },
|
||||||
{ text: 'View Tasks', onPress: () => navigation.navigate('Main', { screen: 'Tasks' }) },
|
{ text: 'View Activities', onPress: () => navigation.navigate('Main', { screen: 'Activities' }) },
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('Error', 'Failed to schedule follow-up.');
|
Alert.alert('Error', 'Failed to schedule activity.');
|
||||||
} finally { setLoading(false); }
|
} finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -118,7 +152,7 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
<View style={styles.clientPickerContainer}>
|
<View style={styles.clientPickerContainer}>
|
||||||
<TouchableOpacity style={styles.clientPicker} onPress={() => setClientModal(true)}>
|
<TouchableOpacity style={styles.clientPicker} onPress={() => setClientModal(true)}>
|
||||||
<Text style={selected ? styles.clientPickerSelected : styles.clientPickerPlaceholder} numberOfLines={1}>
|
<Text style={selected ? styles.clientPickerSelected : styles.clientPickerPlaceholder} numberOfLines={1}>
|
||||||
{selected ? `${selected.name} • ${selected.phone}` : 'Tap to select client...'}
|
{selected ? `${selected.companyName || selected.name} • ${selected.phone}` : 'Tap to select client...'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.clientPickerArrow}>›</Text>
|
<Text style={styles.clientPickerArrow}>›</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -147,10 +181,11 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity style={styles.clientRow} onPress={() => { onSelect(item); setClientModal(false); setClientSearch(''); }}>
|
<TouchableOpacity style={styles.clientRow} onPress={() => { onSelect(item); setClientModal(false); setClientSearch(''); }}>
|
||||||
<View style={styles.clientAvatar}>
|
<View style={styles.clientAvatar}>
|
||||||
<Text style={styles.clientAvatarText}>{item.name?.charAt(0)}</Text>
|
<Text style={styles.clientAvatarText}>{(item.companyName || item.name)?.charAt(0)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.clientRowName}>{item.name}</Text>
|
<Text style={styles.clientRowName}>{item.companyName || item.name}</Text>
|
||||||
|
{item.companyName && <Text style={styles.clientRowSub}>{item.contactName}</Text>}
|
||||||
<Text style={styles.clientRowPhone}>{item.phone}</Text>
|
<Text style={styles.clientRowPhone}>{item.phone}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -162,6 +197,91 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const OpportunityPicker = () => (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity style={styles.clientPicker} onPress={() => setOppModal(true)}>
|
||||||
|
<Text style={fuOpp ? styles.clientPickerSelected : styles.clientPickerPlaceholder} numberOfLines={1}>
|
||||||
|
{fuOpp ? `${fuOpp.title} (${fuOpp.client?.name})` : 'Choose Opportunity...'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.clientPickerArrow}>›</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Modal visible={oppModal} animationType="slide" onRequestClose={() => setOppModal(false)}>
|
||||||
|
<View style={styles.modalContainer}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Select Opportunity</Text>
|
||||||
|
<TouchableOpacity onPress={() => setOppModal(false)}><Text style={styles.modalClose}>✕</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={styles.searchInput}
|
||||||
|
placeholder="Search deals..."
|
||||||
|
value={oppSearch}
|
||||||
|
onChangeText={setOppSearch}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<FlatList
|
||||||
|
data={filteredOpps}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.clientRow}
|
||||||
|
onPress={() => {
|
||||||
|
setFuOpp(item);
|
||||||
|
setFuClient(item.client);
|
||||||
|
setOppModal(false);
|
||||||
|
setOppSearch('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={[styles.clientAvatar, { backgroundColor: '#eef2ff' }]}>
|
||||||
|
<Text style={[styles.clientAvatarText, { color: '#6366f1' }]}>{item.title?.charAt(0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.clientRowName}>{item.title}</Text>
|
||||||
|
<Text style={styles.clientRowSub}>{item.client?.companyName || item.client?.name}</Text>
|
||||||
|
<Text style={[styles.clientRowPhone, { color: Colors.primary }]}>₹{item.value.toLocaleString()}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
ListEmptyComponent={<Text style={styles.emptyText}>No opportunities found</Text>}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserPicker = () => (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity style={styles.clientPicker} onPress={() => setUserModal(true)}>
|
||||||
|
<Text style={styles.clientPickerSelected} numberOfLines={1}>
|
||||||
|
{assignedUser?.name || 'Myself'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.clientPickerArrow}>›</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Modal visible={userModal} animationType="slide" onRequestClose={() => setUserModal(false)}>
|
||||||
|
<View style={styles.modalContainer}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Assign To</Text>
|
||||||
|
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}>✕</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity style={styles.clientRow} onPress={() => { setAssignedUser(item); setUserModal(false); }}>
|
||||||
|
<View style={[styles.clientAvatar, { backgroundColor: '#f0f4ff' }]}>
|
||||||
|
<Text style={styles.clientAvatarText}>{item.name?.charAt(0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.clientRowName}>{item.name}</Text>
|
||||||
|
<Text style={styles.clientRowPhone}>{item.role || 'Staff'}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||||
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
||||||
|
|
@ -171,7 +291,7 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
|
||||||
<Text style={styles.backBtnText}>‹</Text>
|
<Text style={styles.backBtnText}>‹</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Quick Actions</Text>
|
<Text style={styles.headerTitle}>Activities</Text>
|
||||||
<View style={{ width: 36 }} />
|
<View style={{ width: 36 }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -191,12 +311,12 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
|
|
||||||
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
|
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
|
||||||
|
|
||||||
{/* ── CALL / ACTIVITY TAB ── */}
|
{/* ── DONE NOW TAB (Strategic) ── */}
|
||||||
{activeTab === 'call' && (
|
{activeTab === 'call' && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.section}>Activity Type</Text>
|
<Text style={styles.section}>Strategic Activity Done</Text>
|
||||||
<View style={styles.typeGrid}>
|
<View style={styles.typeGrid}>
|
||||||
{ACTIVITY_TYPES.map(a => (
|
{STRATEGIC_TYPES.map(a => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={a.id}
|
key={a.id}
|
||||||
style={[styles.typeCard, actType === a.id && styles.typeCardActive]}
|
style={[styles.typeCard, actType === a.id && styles.typeCardActive]}
|
||||||
|
|
@ -215,7 +335,6 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
<View style={styles.statusSection}>
|
<View style={styles.statusSection}>
|
||||||
<View style={{ marginBottom: 10 }}>
|
<View style={{ marginBottom: 10 }}>
|
||||||
<Text style={styles.switchLabel}>Update Client Status</Text>
|
<Text style={styles.switchLabel}>Update Client Status</Text>
|
||||||
<Text style={styles.switchSub}>Optional: Automatically change status after this call.</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, paddingBottom: 5 }}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, paddingBottom: 5 }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -234,74 +353,155 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.demoToggle}
|
||||||
|
onPress={() => setIsDemoDone(!isDemoDone)}
|
||||||
|
>
|
||||||
|
<View style={[styles.checkbox, isDemoDone && styles.checkboxChecked]} />
|
||||||
|
<Text style={styles.checkboxLabel}>Mark Demo as Done</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text style={styles.section}>Description *</Text>
|
<Text style={styles.section}>Notes / Description *</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.textArea}
|
style={styles.textArea}
|
||||||
placeholder="What did you do? e.g. Called 20 leads from the database list..."
|
placeholder="What did you do? Details here..."
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={4}
|
numberOfLines={4}
|
||||||
value={description}
|
value={description}
|
||||||
onChangeText={setDescription}
|
onChangeText={setDescription}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={styles.section}>Quantity (Leads Generated)</Text>
|
<Text style={styles.section}>Quantity (e.g. Leads Generated)</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="How many leads did this generate?"
|
placeholder="Enter number..."
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={quantity}
|
value={quantity}
|
||||||
onChangeText={setQuantity}
|
onChangeText={setQuantity}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.submitBtn, loading && { opacity: 0.6 }]} onPress={handleSubmitCall} disabled={loading}>
|
<TouchableOpacity style={[styles.submitBtn, loading && { opacity: 0.6 }]} onPress={handleSubmitCall} disabled={loading}>
|
||||||
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📤 Log Activity</Text>}
|
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>✅ Submit Activity</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── FOLLOWUP TAB ── */}
|
{/* ── SCHEDULE TAB ── */}
|
||||||
{activeTab === 'followup' && (
|
{activeTab === 'followup' && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.section}>Client *</Text>
|
<Text style={styles.section}>Activity Type</Text>
|
||||||
<ClientPicker selected={fuClient} onSelect={setFuClient} />
|
<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>
|
||||||
|
|
||||||
<Text style={styles.section}>Notes / Task Description *</Text>
|
{fuType === 'QUOTE' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.section}>Link to Opportunity *</Text>
|
||||||
|
<OpportunityPicker />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text style={styles.section}>Client *</Text>
|
||||||
|
<ClientPicker selected={fuClient} onSelect={setFuClient} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{['ADMIN', 'GENERAL_MANAGER', 'MANAGER', 'TEAM_LEADER'].includes(userInfo?.role) && (
|
||||||
|
<>
|
||||||
|
<Text style={styles.section}>Assign To</Text>
|
||||||
|
<UserPicker />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text style={styles.section}>Task / Notes *</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.textArea}
|
style={styles.textArea}
|
||||||
placeholder="What needs to be done? e.g. Call back regarding demo pricing..."
|
placeholder="Details of what needs to be done..."
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={4}
|
numberOfLines={4}
|
||||||
value={fuNotes}
|
value={fuNotes}
|
||||||
onChangeText={setFuNotes}
|
onChangeText={setFuNotes}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={styles.section}>Follow-up Date *</Text>
|
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||||
<TextInput
|
<View style={{ flex: 1 }}>
|
||||||
style={styles.input}
|
<Text style={styles.section}>Date *</Text>
|
||||||
placeholder="YYYY-MM-DD"
|
<TouchableOpacity
|
||||||
value={fuDate}
|
style={styles.picker}
|
||||||
onChangeText={setFuDate}
|
onPress={() => setShowDatePicker(true)}
|
||||||
keyboardType="numeric"
|
>
|
||||||
/>
|
<Text style={fuDate ? styles.pickerSelected : styles.pickerPlaceholder}>
|
||||||
|
{fuDate || 'Select date...'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.pickerArrow}>📅</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Text style={styles.section}>Follow-up Time *</Text>
|
{showDatePicker && (
|
||||||
<TextInput
|
<DateTimePicker
|
||||||
style={styles.input}
|
value={fuDate ? new Date(fuDate) : new Date()}
|
||||||
placeholder="HH:MM (24h format, e.g. 14:30)"
|
mode="date"
|
||||||
value={fuTime}
|
display="default"
|
||||||
onChangeText={setFuTime}
|
onChange={(event, selectedDate) => {
|
||||||
keyboardType="numeric"
|
setShowDatePicker(false);
|
||||||
/>
|
if (selectedDate) {
|
||||||
|
setFuDate(selectedDate.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.section}>Time *</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.picker}
|
||||||
|
onPress={() => setShowTimePicker(true)}
|
||||||
|
>
|
||||||
|
<Text style={fuTime ? styles.pickerSelected : styles.pickerPlaceholder}>
|
||||||
|
{fuTime || 'Select time...'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.pickerArrow}>🕒</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.reminderBox}>
|
{showTimePicker && (
|
||||||
<Text style={styles.reminderText}>📲 You'll receive a mobile alert at the scheduled time to complete this follow-up.</Text>
|
<DateTimePicker
|
||||||
|
value={(() => {
|
||||||
|
const d = new Date();
|
||||||
|
if (fuTime) {
|
||||||
|
const [h, m] = fuTime.split(':');
|
||||||
|
d.setHours(parseInt(h), parseInt(m));
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
})()}
|
||||||
|
mode="time"
|
||||||
|
is24Hour={true}
|
||||||
|
display="default"
|
||||||
|
onChange={(event, selectedTime) => {
|
||||||
|
setShowTimePicker(false);
|
||||||
|
if (selectedTime) {
|
||||||
|
const h = selectedTime.getHours().toString().padStart(2, '0');
|
||||||
|
const m = selectedTime.getMinutes().toString().padStart(2, '0');
|
||||||
|
setFuTime(`${h}:${m}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.submitBtn, { backgroundColor: '#6366f1' }, loading && { opacity: 0.6 }]} onPress={handleSubmitFollowup} disabled={loading}>
|
<TouchableOpacity style={[styles.submitBtn, { backgroundColor: '#6366f1' }, loading && { opacity: 0.6 }]} onPress={handleSubmitSchedule} disabled={loading}>
|
||||||
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📅 Schedule Follow-up</Text>}
|
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>📅 Schedule Activity</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -329,7 +529,6 @@ const styles = StyleSheet.create({
|
||||||
typeCardActive: { borderColor: Colors.primary, backgroundColor: '#f0f4ff' },
|
typeCardActive: { borderColor: Colors.primary, backgroundColor: '#f0f4ff' },
|
||||||
typeIcon: { fontSize: 26, marginBottom: 6 },
|
typeIcon: { fontSize: 26, marginBottom: 6 },
|
||||||
typeLabel: { fontSize: 11, fontWeight: '700', color: '#64748b', textAlign: 'center' },
|
typeLabel: { fontSize: 11, fontWeight: '700', color: '#64748b', textAlign: 'center' },
|
||||||
typeLabelActive: { color: Colors.primary },
|
|
||||||
clientPickerContainer: { flexDirection: 'row', gap: 10, alignItems: 'center' },
|
clientPickerContainer: { flexDirection: 'row', gap: 10, alignItems: 'center' },
|
||||||
clientPicker: { flex: 1, backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
clientPicker: { flex: 1, backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
||||||
inlineCallBtn: { backgroundColor: '#eef2ff', width: 48, height: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
|
inlineCallBtn: { backgroundColor: '#eef2ff', width: 48, height: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
|
||||||
|
|
@ -338,12 +537,13 @@ const styles = StyleSheet.create({
|
||||||
clientPickerSelected: { color: '#1e293b', fontSize: 14, fontWeight: '700' },
|
clientPickerSelected: { color: '#1e293b', fontSize: 14, fontWeight: '700' },
|
||||||
clientPickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' },
|
clientPickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' },
|
||||||
input: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15 },
|
input: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15 },
|
||||||
|
picker: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
||||||
|
pickerPlaceholder: { color: '#94a3b8', fontSize: 14 },
|
||||||
|
pickerSelected: { color: '#1e293b', fontSize: 14, fontWeight: '700' },
|
||||||
|
pickerArrow: { color: '#94a3b8', fontSize: 20, fontWeight: '300' },
|
||||||
textArea: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15, minHeight: 100, textAlignVertical: 'top' },
|
textArea: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15, minHeight: 100, textAlignVertical: 'top' },
|
||||||
reminderBox: { backgroundColor: '#f0f4ff', borderRadius: 12, padding: 14, marginTop: 16, borderLeftWidth: 4, borderLeftColor: '#6366f1' },
|
|
||||||
reminderText: { fontSize: 12, color: '#6366f1', fontWeight: '600', lineHeight: 18 },
|
|
||||||
submitBtn: { backgroundColor: Colors.primary, borderRadius: 14, padding: 18, alignItems: 'center', marginTop: 24, elevation: 4, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
|
submitBtn: { backgroundColor: Colors.primary, borderRadius: 14, padding: 18, alignItems: 'center', marginTop: 24, elevation: 4, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
|
||||||
submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
|
submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
|
||||||
// Modal
|
|
||||||
modalContainer: { flex: 1, backgroundColor: '#f8f9fa' },
|
modalContainer: { flex: 1, backgroundColor: '#f8f9fa' },
|
||||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary },
|
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, backgroundColor: Colors.primary },
|
||||||
modalTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
|
modalTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
|
||||||
|
|
@ -360,7 +560,11 @@ const styles = StyleSheet.create({
|
||||||
statusPillActive: { backgroundColor: '#64748b', borderColor: '#64748b' },
|
statusPillActive: { backgroundColor: '#64748b', borderColor: '#64748b' },
|
||||||
statusPillText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
|
statusPillText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
|
||||||
switchLabel: { fontSize: 14, fontWeight: '700', color: '#374151' },
|
switchLabel: { fontSize: 14, fontWeight: '700', color: '#374151' },
|
||||||
switchSub: { fontSize: 11, color: '#6b7280', marginTop: 2 }
|
clientRowSub: { fontSize: 11, color: Colors.textMuted, fontStyle: 'italic' },
|
||||||
|
demoToggle: { flexDirection: 'row', alignItems: 'center', marginTop: 15, paddingTop: 15, borderTopWidth: 1, borderTopColor: '#f1f5f9' },
|
||||||
|
checkbox: { width: 18, height: 18, borderWidth: 2, borderColor: Colors.primary, borderRadius: 4, marginRight: 10 },
|
||||||
|
checkboxChecked: { backgroundColor: Colors.primary },
|
||||||
|
checkboxLabel: { fontSize: 13, fontWeight: '700', color: '#374151' }
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LogActivityScreen;
|
export default LogActivityScreen;
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,77 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useContext } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView, Alert, Modal, TextInput, ScrollView } from 'react-native';
|
import {
|
||||||
|
View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator,
|
||||||
|
SafeAreaView, Alert, Modal, TextInput, ScrollView
|
||||||
|
} from 'react-native';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { AuthContext } from '../context/AuthContext';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
|
||||||
const PipelineScreen = () => {
|
const PipelineScreen = ({ navigation }) => {
|
||||||
|
const { userInfo } = useContext(AuthContext);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [pipelineType, setPipelineType] = useState('DEALS'); // 'DEALS' or 'LEADS'
|
||||||
const [opportunities, setOpportunities] = useState([]);
|
const [opportunities, setOpportunities] = useState([]);
|
||||||
|
const [clients, setClients] = useState([]);
|
||||||
const [selectedStage, setSelectedStage] = useState('LEAD');
|
const [selectedStage, setSelectedStage] = useState('LEAD');
|
||||||
|
|
||||||
const stages = [
|
const dealStages = [
|
||||||
{ id: 'LEAD', label: 'Lead' },
|
{ id: 'LEAD', label: 'Lead' },
|
||||||
{ id: 'QUALIFIED', label: 'Qual' },
|
{ id: 'QUALIFIED', label: 'Qual' },
|
||||||
{ id: 'POTENTIAL', label: 'Poten' },
|
{ id: 'POTENTIAL', label: 'Poten' },
|
||||||
{ id: 'DEMO', label: 'Demo' },
|
|
||||||
{ id: 'WON', label: 'Won' },
|
{ id: 'WON', label: 'Won' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const leadStages = [
|
||||||
const [selectedOpp, setSelectedOpp] = useState(null);
|
{ id: 'LEAD', label: 'New Lead' },
|
||||||
const [updateData, setUpdateData] = useState({});
|
{ id: 'PROSPECT', label: 'Prospect' },
|
||||||
|
{ id: 'CLIENT', label: 'Client' },
|
||||||
|
];
|
||||||
|
|
||||||
const fetchOpportunities = useCallback(async () => {
|
const currentStages = pipelineType === 'DEALS' ? dealStages : leadStages;
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isClientModalOpen, setIsClientModalOpen] = useState(false);
|
||||||
|
const [selectedOpp, setSelectedOpp] = useState(null);
|
||||||
|
const [selectedClient, setSelectedClient] = useState(null);
|
||||||
|
const [updateData, setUpdateData] = useState({});
|
||||||
|
const [userModal, setUserModal] = useState(false);
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { data } = await api.get('/opportunities');
|
const [oppRes, clientRes] = await Promise.all([
|
||||||
setOpportunities(data);
|
api.get('/opportunities'),
|
||||||
|
api.get('/clients')
|
||||||
|
]);
|
||||||
|
setOpportunities(oppRes.data);
|
||||||
|
setClients(clientRes.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch opportunities', error);
|
console.error('Failed to fetch pipeline data', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
api.get('/users').then(r => setUsers(r.data)).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
fetchOpportunities();
|
fetchData();
|
||||||
}, [fetchOpportunities])
|
}, [fetchData])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenModal = (item) => {
|
const handleOpenModal = (item) => {
|
||||||
setSelectedOpp(item);
|
setSelectedOpp(item);
|
||||||
setUpdateData({
|
setUpdateData({
|
||||||
stage: item.stage,
|
stage: item.stage,
|
||||||
|
assignedTo: item.assignedTo,
|
||||||
demoPersonName: item.demoPersonName || '',
|
demoPersonName: item.demoPersonName || '',
|
||||||
demoContactDetails: item.demoContactDetails || '',
|
demoContactDetails: item.demoContactDetails || '',
|
||||||
expectedCloseDate: item.expectedCloseDate ? item.expectedCloseDate.split('T')[0] : '',
|
expectedCloseDate: item.expectedCloseDate ? item.expectedCloseDate.split('T')[0] : '',
|
||||||
|
|
@ -53,11 +81,40 @@ const PipelineScreen = () => {
|
||||||
specialRate: item.specialRate ? String(item.specialRate) : '',
|
specialRate: item.specialRate ? String(item.specialRate) : '',
|
||||||
freeOffers: item.freeOffers || '',
|
freeOffers: item.freeOffers || '',
|
||||||
negotiationRemarks: item.negotiationRemarks || '',
|
negotiationRemarks: item.negotiationRemarks || '',
|
||||||
value: String(item.value)
|
value: String(item.value),
|
||||||
|
isDemoDone: !!item.isDemoDone
|
||||||
});
|
});
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenClientModal = (item) => {
|
||||||
|
setSelectedClient(item);
|
||||||
|
setUpdateData({
|
||||||
|
status: item.status,
|
||||||
|
assignedTo: item.assignedTo
|
||||||
|
});
|
||||||
|
setIsClientModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateClientStatus = async () => {
|
||||||
|
try {
|
||||||
|
await api.patch(`/clients/${selectedClient.id}`, { status: updateData.status });
|
||||||
|
setIsClientModalOpen(false);
|
||||||
|
fetchData();
|
||||||
|
Alert.alert("Success", "Client status updated");
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert("Error", "Failed to update status");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConvertToDeal = () => {
|
||||||
|
setIsClientModalOpen(false);
|
||||||
|
navigation.navigate('AddOpportunity', {
|
||||||
|
client: selectedClient,
|
||||||
|
prefill: { title: `Opportunity for ${selectedClient.companyName || selectedClient.name}` }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
@ -68,7 +125,7 @@ const PipelineScreen = () => {
|
||||||
|
|
||||||
await api.patch(`/opportunities/${selectedOpp.id}`, payload);
|
await api.patch(`/opportunities/${selectedOpp.id}`, payload);
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
fetchOpportunities();
|
fetchData();
|
||||||
Alert.alert("Success", "Opportunity updated");
|
Alert.alert("Success", "Opportunity updated");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.response?.data?.message || error.message;
|
const msg = error.response?.data?.message || error.message;
|
||||||
|
|
@ -76,213 +133,265 @@ const PipelineScreen = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem = ({ item }) => (
|
const UserPicker = () => {
|
||||||
<TouchableOpacity style={styles.card} activeOpacity={0.7} onPress={() => handleOpenModal(item)}>
|
const currentAssignee = users.find(u => u.id === updateData.assignedTo);
|
||||||
<View style={styles.cardHeader}>
|
return (
|
||||||
<Text style={styles.cardTitle}>{item.title}</Text>
|
<>
|
||||||
<Text style={styles.cardValue}>₹{item.value.toLocaleString()}</Text>
|
<Text style={styles.modalLabel}>Primary Owner (Assigned To)</Text>
|
||||||
</View>
|
<TouchableOpacity style={styles.pickerBtn} onPress={() => setUserModal(true)}>
|
||||||
<View style={styles.cardFooter}>
|
<Text style={styles.pickerBtnText}>
|
||||||
<View style={styles.clientContainer}>
|
{currentAssignee ? `${currentAssignee.name} (${currentAssignee.role})` : 'Select Teammate'}
|
||||||
<View style={styles.avatar}>
|
|
||||||
<Text style={styles.avatarText}>{item.client?.name?.charAt(0)}</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.clientName}>{item.client?.name}</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'}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
<Text style={styles.pickerArrow}>›</Text>
|
||||||
</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>
|
</TouchableOpacity>
|
||||||
)}
|
</>
|
||||||
</TouchableOpacity>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
const filteredItems = opportunities.filter(item => item.stage === selectedStage);
|
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 || 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?.companyName || item.client?.name || item.name || item.companyName)?.charAt(0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.clientName}>{item.client?.companyName || item.client?.name || item.companyName || item.name}</Text>
|
||||||
|
{(item.client?.contactName || item.contactName) && <Text style={styles.contactSubText}>{item.client?.contactName || item.contactName}</Text>}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.priorityBadge, { backgroundColor: (item.priority === 'High' || isLead) ? '#fee2e2' : '#fef3c7' }]}>
|
||||||
|
<Text style={[styles.priorityText, { color: (item.priority === 'High' || isLead) ? '#ef4444' : '#f59e0b' }]}>
|
||||||
|
{isLead ? item.status : (item.priority || 'Normal')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.assignedContainer}>
|
||||||
|
<Text style={styles.assignedLabel}>Owner: </Text>
|
||||||
|
<Text style={styles.assignedName}>{item.user?.name || 'Unassigned'}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredData = (pipelineType === 'DEALS' ? opportunities : clients).filter(item => {
|
||||||
|
const matchesStage = (pipelineType === 'DEALS' ? item.stage : item.status) === selectedStage;
|
||||||
|
const matchesUser = item.assignedTo === userInfo?.id;
|
||||||
|
return matchesStage && matchesUser;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
{/* Stage Selector */}
|
{/* Pipeline Type Switcher */}
|
||||||
|
<View style={styles.switcherContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.switcherBtn, pipelineType === 'DEALS' && styles.activeSwitcherBtn]}
|
||||||
|
onPress={() => { setPipelineType('DEALS'); setSelectedStage('LEAD'); }}
|
||||||
|
>
|
||||||
|
<Text style={[styles.switcherText, pipelineType === 'DEALS' && styles.activeSwitcherText]}>DEALS</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.switcherBtn, pipelineType === 'LEADS' && styles.activeSwitcherBtn]}
|
||||||
|
onPress={() => { setPipelineType('LEADS'); setSelectedStage('LEAD'); }}
|
||||||
|
>
|
||||||
|
<Text style={[styles.switcherText, pipelineType === 'LEADS' && styles.activeSwitcherText]}>LEADS</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.stageBar}>
|
<View style={styles.stageBar}>
|
||||||
{stages.map((stage) => (
|
{currentStages.map(stage => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={stage.id}
|
key={stage.id}
|
||||||
|
style={[styles.stageItem, selectedStage === stage.id && styles.activeStageItem]}
|
||||||
onPress={() => setSelectedStage(stage.id)}
|
onPress={() => setSelectedStage(stage.id)}
|
||||||
style={[
|
|
||||||
styles.stageItem,
|
|
||||||
selectedStage === stage.id && styles.activeStageItem
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[styles.stageLabel, selectedStage === stage.id && styles.activeStageLabel]}>{stage.label}</Text>
|
||||||
styles.stageLabel,
|
<View style={[styles.stageIndicator, selectedStage === stage.id && { backgroundColor: Colors.primary }]} />
|
||||||
selectedStage === stage.id && styles.activeStageLabel
|
|
||||||
]}>
|
|
||||||
{stage.label}
|
|
||||||
</Text>
|
|
||||||
<View style={[
|
|
||||||
styles.stageIndicator,
|
|
||||||
selectedStage === stage.id && { backgroundColor: selectedStage === 'WON' ? Colors.secondary : Colors.primary }
|
|
||||||
]} />
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View style={styles.center}>
|
<View style={styles.center}><ActivityIndicator size="large" color={Colors.primary} /></View>
|
||||||
<ActivityIndicator size="large" color={Colors.primary} />
|
|
||||||
</View>
|
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={filteredItems}
|
data={filteredData}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
contentContainerStyle={styles.listContainer}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={<View style={styles.empty}><Text style={styles.emptyText}>No items in this stage</Text></View>}
|
||||||
<View style={styles.emptyContainer}>
|
onRefresh={fetchData}
|
||||||
<Text style={styles.emptyText}>No opportunities in this stage</Text>
|
refreshing={false}
|
||||||
</View>
|
|
||||||
}
|
|
||||||
onRefresh={fetchOpportunities}
|
|
||||||
refreshing={loading}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* UPDATE MODAL */}
|
{/* FAB - New Deal */}
|
||||||
<Modal
|
<TouchableOpacity
|
||||||
visible={isModalOpen}
|
style={styles.fab}
|
||||||
animationType="slide"
|
onPress={() => navigation.navigate('AddOpportunity')}
|
||||||
transparent={true}
|
activeOpacity={0.85}
|
||||||
onRequestClose={() => setIsModalOpen(false)}
|
|
||||||
>
|
>
|
||||||
<View style={styles.modalOverlay}>
|
<Text style={styles.fabText}>+</Text>
|
||||||
<View style={styles.modalContent}>
|
</TouchableOpacity>
|
||||||
<View style={styles.modalHeader}>
|
|
||||||
<Text style={styles.modalTitle}>Update Stage</Text>
|
|
||||||
<TouchableOpacity onPress={() => setIsModalOpen(false)}>
|
<Modal visible={isModalOpen} animationType="slide">
|
||||||
<Text style={styles.closeButton}>Cancel</Text>
|
<View style={styles.modalContainer}>
|
||||||
</TouchableOpacity>
|
<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]}
|
||||||
|
onPress={() => setUpdateData({ ...updateData, stage: s.id })}
|
||||||
|
>
|
||||||
|
<Text style={[styles.stageChipText, updateData.stage === s.id && styles.activeStageChipText]}>{s.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={styles.modalForm}>
|
<Text style={styles.modalLabel}>Expected Close Date</Text>
|
||||||
<Text style={styles.label}>Current Stage</Text>
|
<TouchableOpacity
|
||||||
<View style={styles.stagePicker}>
|
style={styles.pickerBtn}
|
||||||
{stages.map(s => (
|
onPress={() => setShowDatePicker(true)}
|
||||||
<TouchableOpacity
|
>
|
||||||
key={s.id}
|
<Text style={updateData.expectedCloseDate ? styles.pickerBtnText : styles.pickerPlaceholder}>
|
||||||
style={[styles.stageChip, updateData.stage === s.id && styles.activeStageChip]}
|
{updateData.expectedCloseDate || 'Select date...'}
|
||||||
onPress={() => setUpdateData({...updateData, stage: s.id})}
|
</Text>
|
||||||
>
|
<Text style={styles.pickerArrow}>📅</Text>
|
||||||
<Text style={[styles.stageChipText, updateData.stage === s.id && styles.activeStageChipText]}>{s.label}</Text>
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Expected Revenue (₹)</Text>
|
{showDatePicker && (
|
||||||
<TextInput
|
<DateTimePicker
|
||||||
style={styles.input}
|
value={updateData.expectedCloseDate ? new Date(updateData.expectedCloseDate) : new Date()}
|
||||||
value={updateData.value}
|
mode="date"
|
||||||
onChangeText={t => setUpdateData({...updateData, value: t})}
|
display="default"
|
||||||
keyboardType="numeric"
|
onChange={(event, selectedDate) => {
|
||||||
|
setShowDatePicker(false);
|
||||||
|
if (selectedDate) {
|
||||||
|
setUpdateData({ ...updateData, expectedCloseDate: selectedDate.toISOString().split('T')[0] });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(updateData.stage === 'DEMO' || updateData.stage === 'WON') && (
|
<Text style={styles.modalLabel}>Next Action / Remarks</Text>
|
||||||
<View style={styles.mandatorySection}>
|
<TextInput style={[styles.modalInput, { height: 80 }]} value={updateData.negotiationRemarks} onChangeText={v => setUpdateData({ ...updateData, negotiationRemarks: v })} multiline placeholder="Describe next steps..." />
|
||||||
<Text style={styles.sectionHeader}>DEMO DETAILS (MANDATORY)</Text>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Person Name</Text>
|
<TouchableOpacity style={styles.updateBtn} onPress={handleUpdate}>
|
||||||
<TextInput
|
<Text style={styles.updateBtnText}>Save Updates</Text>
|
||||||
style={styles.input}
|
</TouchableOpacity>
|
||||||
value={updateData.demoPersonName}
|
|
||||||
onChangeText={t => setUpdateData({...updateData, demoPersonName: t})}
|
|
||||||
placeholder="Name of person met"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Contact Details</Text>
|
<TouchableOpacity
|
||||||
<TextInput
|
style={styles.activityBtn}
|
||||||
style={styles.input}
|
onPress={() => {
|
||||||
value={updateData.demoContactDetails}
|
setIsModalOpen(false);
|
||||||
onChangeText={t => setUpdateData({...updateData, demoContactDetails: t})}
|
navigation.navigate('LogActivity', { client: selectedOpp?.client, tab: 'followup' });
|
||||||
placeholder="Phone or Email"
|
}}
|
||||||
/>
|
>
|
||||||
|
<Text style={styles.activityBtnText}>📅 Schedule Next Activity</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.label}>Expected Closing Date (YYYY-MM-DD)</Text>
|
{/* Nested User Modal */}
|
||||||
<TextInput
|
<Modal visible={userModal} animationType="fade" transparent={true}>
|
||||||
style={styles.input}
|
<View style={styles.modalOverlay}>
|
||||||
value={updateData.expectedCloseDate}
|
<View style={styles.userListContainer}>
|
||||||
onChangeText={t => setUpdateData({...updateData, expectedCloseDate: t})}
|
<View style={styles.modalHeader}>
|
||||||
placeholder="2024-12-31"
|
<Text style={styles.modalTitle}>Transfer To</Text>
|
||||||
/>
|
<TouchableOpacity onPress={() => setUserModal(false)}><Text style={styles.modalClose}>✕</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
<Text style={styles.label}>Competitor Mention</Text>
|
<FlatList
|
||||||
<TextInput
|
data={[{ id: userInfo?.id, name: 'Myself' }, ...users.filter(u => u.id !== userInfo?.id)]}
|
||||||
style={styles.input}
|
keyExtractor={item => item.id}
|
||||||
value={updateData.competitorMention}
|
renderItem={({ item }) => (
|
||||||
onChangeText={t => setUpdateData({...updateData, competitorMention: t})}
|
<TouchableOpacity
|
||||||
placeholder="None or Competitor Name"
|
style={styles.userRow}
|
||||||
/>
|
onPress={() => {
|
||||||
|
setUpdateData({ ...updateData, assignedTo: item.id });
|
||||||
<Text style={styles.label}>Queries / Objections</Text>
|
setUserModal(false);
|
||||||
<TextInput
|
}}
|
||||||
style={[styles.input, {height: 60}]}
|
>
|
||||||
multiline
|
<View style={styles.userAvatar}><Text style={styles.userAvatarText}>{item.name?.charAt(0)}</Text></View>
|
||||||
value={updateData.keyQueries}
|
<View>
|
||||||
onChangeText={t => setUpdateData({...updateData, keyQueries: t})}
|
<Text style={styles.userName}>{item.name}</Text>
|
||||||
/>
|
<Text style={styles.userRole}>{item.role}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
{updateData.stage === 'WON' && (
|
/>
|
||||||
<View style={[styles.mandatorySection, {backgroundColor: '#f0fdf4', borderColor: '#bbf7d0'}]}>
|
</View>
|
||||||
<Text style={[styles.sectionHeader, {color: '#166534'}]}>CLOSING DETAILS (MANDATORY)</Text>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Payment Mode</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={updateData.paymentMode}
|
|
||||||
onChangeText={t => setUpdateData({...updateData, paymentMode: t})}
|
|
||||||
placeholder="Cash / Bank Transfer / UPI"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Special Rate (Optional)</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={updateData.specialRate}
|
|
||||||
onChangeText={t => setUpdateData({...updateData, specialRate: t})}
|
|
||||||
placeholder="Final agreed rate"
|
|
||||||
keyboardType="numeric"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Negotiation Remarks</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={updateData.negotiationRemarks}
|
|
||||||
onChangeText={t => setUpdateData({...updateData, negotiationRemarks: t})}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TouchableOpacity style={styles.saveButton} onPress={handleUpdate}>
|
|
||||||
<Text style={styles.saveButtonText}>SAVE UPDATE</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={{height: 40}} />
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Client Update Modal */}
|
||||||
|
<Modal visible={isClientModalOpen} animationType="slide">
|
||||||
|
<View style={styles.modalContainer}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Manage Lead</Text>
|
||||||
|
<TouchableOpacity onPress={() => setIsClientModalOpen(false)}><Text style={styles.modalClose}>✕</Text></TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={styles.modalBody}>
|
||||||
|
<Text style={styles.modalLabel}>Lead / Client Name</Text>
|
||||||
|
<Text style={styles.clientNameBig}>{selectedClient?.companyName || selectedClient?.name}</Text>
|
||||||
|
|
||||||
|
<Text style={styles.modalLabel}>Update Status (Stage)</Text>
|
||||||
|
<View style={styles.stagePickerContainer}>
|
||||||
|
{leadStages.map(s => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={s.id}
|
||||||
|
style={[styles.stageChip, updateData.status === s.id && styles.activeStageChip]}
|
||||||
|
onPress={() => setUpdateData({ ...updateData, status: s.id })}
|
||||||
|
>
|
||||||
|
<Text style={[styles.stageChipText, updateData.status === s.id && styles.activeStageChipText]}>{s.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.updateBtn} onPress={handleUpdateClientStatus}>
|
||||||
|
<Text style={styles.updateBtnText}>Save Status</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<Text style={styles.modalLabel}>Pipeline Movement</Text>
|
||||||
|
<TouchableOpacity style={styles.convertBtn} onPress={handleConvertToDeal}>
|
||||||
|
<Text style={styles.convertBtnText}>🚀 Convert to Deal (Deals Pipeline)</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.activityBtn}
|
||||||
|
onPress={() => {
|
||||||
|
setIsClientModalOpen(false);
|
||||||
|
navigation.navigate('ClientDetails', { client: selectedClient });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.activityBtnText}>👁️ View Full Details</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
@ -290,242 +399,76 @@ const PipelineScreen = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: { flex: 1, backgroundColor: '#f8fafc' },
|
||||||
flex: 1,
|
switcherContainer: { flexDirection: 'row', backgroundColor: 'white', padding: 8, marginHorizontal: 16, marginTop: 10, borderRadius: 12, borderWidth: 1, borderColor: '#edf2f7' },
|
||||||
backgroundColor: Colors.background,
|
switcherBtn: { flex: 1, paddingVertical: 8, alignItems: 'center', borderRadius: 8 },
|
||||||
},
|
activeSwitcherBtn: { backgroundColor: Colors.primary },
|
||||||
stageBar: {
|
switcherText: { fontSize: 12, fontWeight: '800', color: '#94a3b8' },
|
||||||
flexDirection: 'row',
|
activeSwitcherText: { color: 'white' },
|
||||||
backgroundColor: 'white',
|
filterBar: { paddingVertical: 12 },
|
||||||
borderBottomWidth: 1,
|
filterScroll: { paddingHorizontal: 16, gap: 10 },
|
||||||
borderBottomColor: '#edf2f7',
|
filterChip: { paddingHorizontal: 16, paddingVertical: 8, backgroundColor: 'white', borderRadius: 20, borderWidth: 1, borderColor: '#e2e8f0' },
|
||||||
paddingTop: 10,
|
activeFilterChip: { backgroundColor: Colors.primary, borderColor: Colors.primary },
|
||||||
},
|
filterChipText: { fontSize: 13, fontWeight: '600', color: '#64748b' },
|
||||||
stageItem: {
|
activeFilterChipText: { color: 'white' },
|
||||||
flex: 1,
|
stagePickerContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 10 },
|
||||||
alignItems: 'center',
|
stageChip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, borderWidth: 1, borderColor: '#e2e8f0', backgroundColor: '#f8fafc' },
|
||||||
paddingVertical: 12,
|
activeStageChip: { backgroundColor: Colors.primary, borderColor: Colors.primary },
|
||||||
},
|
stageChipText: { fontSize: 12, fontWeight: '700', color: '#64748b' },
|
||||||
activeStageItem: {
|
activeStageChipText: { color: 'white' },
|
||||||
// backgroundColor: '#fdf2f8',
|
clientNameBig: { fontSize: 20, fontWeight: '800', color: Colors.primary, marginBottom: 10 },
|
||||||
},
|
divider: { height: 1, backgroundColor: '#f1f5f9', marginVertical: 25 },
|
||||||
stageLabel: {
|
convertBtn: { backgroundColor: '#f0f9ff', padding: 18, borderRadius: 14, alignItems: 'center', borderWidth: 1.5, borderColor: '#0ea5e9' },
|
||||||
fontSize: 12,
|
convertBtnText: { color: '#0369a1', fontSize: 15, fontWeight: '800' },
|
||||||
fontWeight: '600',
|
stageBar: { flexDirection: 'row', backgroundColor: 'white', borderBottomWidth: 1, borderBottomColor: '#edf2f7' },
|
||||||
color: Colors.textMuted,
|
stageItem: { flex: 1, alignItems: 'center', paddingVertical: 14 },
|
||||||
marginBottom: 8,
|
activeStageItem: { borderBottomWidth: 0 },
|
||||||
},
|
stageLabel: { fontSize: 11, fontWeight: '700', color: '#94a3b8', textTransform: 'uppercase' },
|
||||||
activeStageLabel: {
|
activeStageLabel: { color: Colors.primary },
|
||||||
color: Colors.text,
|
stageIndicator: { height: 3, width: '40%', marginTop: 8, borderRadius: 2 },
|
||||||
fontWeight: 'bold',
|
listContainer: { padding: 16 },
|
||||||
},
|
card: { backgroundColor: 'white', borderRadius: 16, padding: 16, marginBottom: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||||
stageIndicator: {
|
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
||||||
height: 3,
|
cardTitle: { fontSize: 15, fontWeight: '800', color: '#1e293b', flex: 1 },
|
||||||
width: '60%',
|
cardValue: { fontSize: 15, fontWeight: '900', color: Colors.primary },
|
||||||
borderRadius: 3,
|
cardFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' },
|
||||||
backgroundColor: 'transparent',
|
clientContainer: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||||
},
|
avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
|
||||||
listContainer: {
|
avatarText: { fontSize: 12, fontWeight: 'bold', color: '#64748b' },
|
||||||
padding: 16,
|
clientName: { fontSize: 13, fontWeight: '700', color: '#334155' },
|
||||||
},
|
contactSubText: { fontSize: 11, color: '#94a3b8', fontStyle: 'italic' },
|
||||||
center: {
|
priorityBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
|
||||||
flex: 1,
|
priorityText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' },
|
||||||
justifyContent: 'center',
|
assignedContainer: { flexDirection: 'row', marginTop: 12, paddingTop: 12, borderTopWidth: 1, borderTopColor: '#f1f5f9' },
|
||||||
alignItems: 'center',
|
assignedLabel: { fontSize: 11, color: '#94a3b8' },
|
||||||
},
|
assignedName: { fontSize: 11, fontWeight: 'bold', color: '#64748b' },
|
||||||
card: {
|
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||||
backgroundColor: 'white',
|
empty: { alignItems: 'center', marginTop: 100 },
|
||||||
borderRadius: 12,
|
emptyText: { color: '#94a3b8', fontWeight: '600' },
|
||||||
padding: 16,
|
modalContainer: { flex: 1, backgroundColor: 'white' },
|
||||||
marginBottom: 12,
|
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', backgroundColor: '#f8fafc' },
|
||||||
shadowColor: '#000',
|
modalTitle: { fontSize: 18, fontWeight: '900', color: '#1e293b' },
|
||||||
shadowOffset: { width: 0, height: 2 },
|
modalClose: { fontSize: 24, color: '#94a3b8' },
|
||||||
shadowOpacity: 0.05,
|
modalBody: { padding: 20 },
|
||||||
shadowRadius: 4,
|
modalLabel: { fontSize: 11, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', marginBottom: 8, marginTop: 20 },
|
||||||
elevation: 3,
|
modalInput: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 14, fontSize: 15, backgroundColor: '#f8fafc' },
|
||||||
borderLeftWidth: 4,
|
pickerBtn: { borderWidth: 1.5, borderColor: '#e2e8f0', borderRadius: 12, padding: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#f8fafc' },
|
||||||
borderLeftColor: Colors.primary,
|
pickerBtnText: { fontSize: 15, color: '#1e293b', fontWeight: '700' },
|
||||||
},
|
pickerPlaceholder: { color: '#94a3b8', fontSize: 15 },
|
||||||
cardHeader: {
|
pickerArrow: { fontSize: 20, color: '#94a3b8' },
|
||||||
flexDirection: 'row',
|
updateBtn: { backgroundColor: Colors.primary, padding: 18, borderRadius: 14, marginTop: 40, alignItems: 'center' },
|
||||||
justifyContent: 'space-between',
|
updateBtnText: { color: 'white', fontSize: 16, fontWeight: '900' },
|
||||||
alignItems: 'flex-start',
|
activityBtn: { padding: 18, borderRadius: 14, marginTop: 15, alignItems: 'center', borderWidth: 1.5, borderColor: Colors.primary },
|
||||||
marginBottom: 12,
|
activityBtnText: { color: Colors.primary, fontSize: 14, fontWeight: '800' },
|
||||||
},
|
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'flex-end' },
|
||||||
cardTitle: {
|
userListContainer: { backgroundColor: 'white', borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '60%' },
|
||||||
fontSize: 15,
|
userRow: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
|
||||||
fontWeight: 'bold',
|
userAvatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eef2ff', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
|
||||||
color: Colors.text,
|
userAvatarText: { color: Colors.primary, fontWeight: 'bold' },
|
||||||
flex: 1,
|
userName: { fontSize: 15, fontWeight: '700', color: '#1e293b' },
|
||||||
marginRight: 8,
|
userRole: { fontSize: 12, color: '#64748b' },
|
||||||
},
|
fab: { position: 'absolute', right: 20, bottom: 30, width: 56, height: 56, borderRadius: 28, backgroundColor: Colors.primary, alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: Colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 8 },
|
||||||
cardValue: {
|
fabText: { color: 'white', fontSize: 32, fontWeight: '300', lineHeight: 38 }
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: '800',
|
|
||||||
color: Colors.primary,
|
|
||||||
},
|
|
||||||
cardFooter: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
clientContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: Colors.border,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
avatarText: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: Colors.textMuted,
|
|
||||||
},
|
|
||||||
clientName: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.textMuted,
|
|
||||||
},
|
|
||||||
priorityBadge: {
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
priorityText: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
emptyContainer: {
|
|
||||||
padding: 40,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
color: '#a0aec0',
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
workOrderButton: {
|
|
||||||
marginTop: 15,
|
|
||||||
backgroundColor: Colors.secondary,
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
workOrderButtonText: {
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 13,
|
|
||||||
},
|
|
||||||
modalOverlay: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
modalContent: {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderTopLeftRadius: 24,
|
|
||||||
borderTopRightRadius: 24,
|
|
||||||
height: '85%',
|
|
||||||
paddingTop: 20,
|
|
||||||
},
|
|
||||||
modalHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingBottom: 20,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#f1f5f9',
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: Colors.text,
|
|
||||||
},
|
|
||||||
closeButton: {
|
|
||||||
color: Colors.textMuted,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
modalForm: {
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: Colors.textMuted,
|
|
||||||
marginBottom: 8,
|
|
||||||
marginTop: 15,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
backgroundColor: '#f8fafc',
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 12,
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.text,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#e2e8f0',
|
|
||||||
},
|
|
||||||
stagePicker: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
stageChip: {
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: '#f1f5f9',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#e2e8f0',
|
|
||||||
},
|
|
||||||
activeStageChip: {
|
|
||||||
backgroundColor: Colors.primary,
|
|
||||||
borderColor: Colors.primary,
|
|
||||||
},
|
|
||||||
stageChipText: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: Colors.textMuted,
|
|
||||||
},
|
|
||||||
activeStageChipText: {
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
mandatorySection: {
|
|
||||||
marginTop: 20,
|
|
||||||
padding: 15,
|
|
||||||
backgroundColor: '#f0f7ff',
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#bae6fd',
|
|
||||||
},
|
|
||||||
sectionHeader: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: 'black',
|
|
||||||
color: '#0369a1',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
backgroundColor: Colors.primary,
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingVertical: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 30,
|
|
||||||
shadowColor: Colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.2,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
saveButtonText: {
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 16,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PipelineScreen;
|
export default PipelineScreen;
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,26 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
|
||||||
|
const TYPE_ICONS = {
|
||||||
|
'FOLLOWUP': '📅',
|
||||||
|
'DEMO': '📽️',
|
||||||
|
'QUOTE': '📝',
|
||||||
|
'NEGOTIATION': '🤝',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_COLORS = {
|
||||||
|
'FOLLOWUP': '#6366f1',
|
||||||
|
'DEMO': '#3b82f6',
|
||||||
|
'QUOTE': '#a855f7',
|
||||||
|
'NEGOTIATION': '#f59e0b',
|
||||||
|
};
|
||||||
|
|
||||||
const TasksScreen = ({ navigation }) => {
|
const TasksScreen = ({ navigation }) => {
|
||||||
const { userInfo } = useContext(AuthContext);
|
const { userInfo } = useContext(AuthContext);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [sections, setSections] = useState([]);
|
const [sections, setSections] = useState([]);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [activeFilter, setActiveFilter] = useState('ALL'); // ALL, PENDING, DONE
|
const [activeFilter, setActiveFilter] = useState('PENDING'); // ALL, PENDING, DONE
|
||||||
|
|
||||||
const groupByDay = (followups) => {
|
const groupByDay = (followups) => {
|
||||||
const map = {};
|
const map = {};
|
||||||
|
|
@ -54,15 +68,15 @@ const TasksScreen = ({ navigation }) => {
|
||||||
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
|
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
|
||||||
|
|
||||||
const handleMarkDone = async (id) => {
|
const handleMarkDone = async (id) => {
|
||||||
Alert.alert('Mark as Done?', 'This will complete the task and dismiss the notification.', [
|
Alert.alert('Complete Activity?', 'This will mark the activity as done and remove it from pending.', [
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
{
|
{
|
||||||
text: 'Done ✓', onPress: async () => {
|
text: 'Complete ✓', onPress: async () => {
|
||||||
try {
|
try {
|
||||||
await api.patch(`/followups/${id}`, { status: 'DONE' });
|
await api.patch(`/followups/${id}`, { status: 'DONE' });
|
||||||
fetchTasks();
|
fetchTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('Error', 'Could not update task.');
|
Alert.alert('Error', 'Could not update activity.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,18 +91,25 @@ const TasksScreen = ({ navigation }) => {
|
||||||
const renderTask = ({ item }) => {
|
const renderTask = ({ item }) => {
|
||||||
const isPending = item.status === 'PENDING';
|
const isPending = item.status === 'PENDING';
|
||||||
const isOverdue = isPending && new Date(item.date) < new Date();
|
const isOverdue = isPending && new Date(item.date) < new Date();
|
||||||
|
const type = item.type || 'FOLLOWUP';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.card, isOverdue && styles.cardOverdue, !isPending && styles.cardDone]}>
|
<View style={[styles.card, isOverdue && styles.cardOverdue, !isPending && styles.cardDone]}>
|
||||||
<View style={[styles.dot, { backgroundColor: isOverdue ? '#ef4444' : isPending ? Colors.primary : '#10b981' }]} />
|
<View style={[styles.typeIconBadge, { backgroundColor: TYPE_COLORS[type] + '20' }]}>
|
||||||
|
<Text style={styles.typeIconText}>{TYPE_ICONS[type]}</Text>
|
||||||
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={styles.clientName}>{item.client?.name || 'Unknown Client'}</Text>
|
<Text style={styles.clientName}>{item.client?.companyName || item.client?.name || 'Unknown Client'}</Text>
|
||||||
{item.client?.phone && (
|
{item.client?.phone && (
|
||||||
<TouchableOpacity onPress={() => handleCall(item.client.phone)}>
|
<TouchableOpacity onPress={() => handleCall(item.client.phone)} style={styles.callCircle}>
|
||||||
<Text style={styles.callIcon}>📞</Text>
|
<Text style={styles.callIcon}>📞</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.typeBadge}>
|
||||||
|
<Text style={[styles.typeText, { color: TYPE_COLORS[type] }]}>{type}</Text>
|
||||||
|
</View>
|
||||||
<Text style={styles.notes} numberOfLines={2}>{item.notes}</Text>
|
<Text style={styles.notes} numberOfLines={2}>{item.notes}</Text>
|
||||||
<Text style={styles.time}>
|
<Text style={styles.time}>
|
||||||
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
{new Date(item.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
|
@ -96,7 +117,7 @@ const TasksScreen = ({ navigation }) => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<TouchableOpacity style={styles.doneBtn} onPress={() => handleMarkDone(item.id)}>
|
<TouchableOpacity style={[styles.doneBtn, { backgroundColor: TYPE_COLORS[type] }]} onPress={() => handleMarkDone(item.id)}>
|
||||||
<Text style={styles.doneBtnText}>Done</Text>
|
<Text style={styles.doneBtnText}>Done</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
@ -113,10 +134,20 @@ const TasksScreen = ({ navigation }) => {
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
<StatusBar backgroundColor={Colors.primary} barStyle="light-content" />
|
||||||
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||||
<Text style={styles.headerTitle}>My Tasks</Text>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Text style={styles.headerSub}>Sorted by date</Text>
|
<View>
|
||||||
|
<Text style={styles.headerTitle}>Activities</Text>
|
||||||
|
<Text style={styles.headerSub}>Manage your schedule</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.addBtn}
|
||||||
|
onPress={() => navigation.navigate('LogActivity', { tab: 'followup' })}
|
||||||
|
>
|
||||||
|
<Text style={styles.addBtnText}>+ New</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
<View style={styles.filterRow}>
|
<View style={styles.filterRow}>
|
||||||
{['ALL', 'PENDING', 'DONE'].map(f => (
|
{['PENDING', 'DONE', 'ALL'].map(f => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={f}
|
key={f}
|
||||||
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
|
style={[styles.filterBtn, activeFilter === f && styles.filterBtnActive]}
|
||||||
|
|
@ -135,16 +166,16 @@ const TasksScreen = ({ navigation }) => {
|
||||||
renderSectionHeader={({ section }) => (
|
renderSectionHeader={({ section }) => (
|
||||||
<View style={styles.sectionHeader}>
|
<View style={styles.sectionHeader}>
|
||||||
<Text style={styles.sectionTitle}>{section.title}</Text>
|
<Text style={styles.sectionTitle}>{section.title}</Text>
|
||||||
<Text style={styles.sectionCount}>{section.data.length} task{section.data.length !== 1 ? 's' : ''}</Text>
|
<Text style={styles.sectionCount}>{section.data.length} item{section.data.length !== 1 ? 's' : ''}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchTasks(); }} colors={[Colors.primary]} />}
|
||||||
contentContainerStyle={{ paddingBottom: 40 }}
|
contentContainerStyle={{ paddingBottom: 40 }}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.empty}>
|
<View style={styles.empty}>
|
||||||
<Text style={styles.emptyIcon}>🎉</Text>
|
<Text style={styles.emptyIcon}>✨</Text>
|
||||||
<Text style={styles.emptyTitle}>All Clear!</Text>
|
<Text style={styles.emptyTitle}>All Caught Up!</Text>
|
||||||
<Text style={styles.emptySub}>No tasks match this filter.</Text>
|
<Text style={styles.emptySub}>No activities scheduled here.</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -153,34 +184,40 @@ const TasksScreen = ({ navigation }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#f1f5f9' },
|
container: { flex: 1, backgroundColor: '#f8fafc' },
|
||||||
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 },
|
header: { backgroundColor: Colors.primary, paddingHorizontal: 20, paddingBottom: 20 },
|
||||||
headerTitle: { color: 'white', fontSize: 26, fontWeight: '900' },
|
headerTitle: { color: 'white', fontSize: 28, fontWeight: '900' },
|
||||||
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2, marginBottom: 14 },
|
headerSub: { color: 'rgba(255,255,255,0.7)', fontSize: 13, marginTop: 2, marginBottom: 16 },
|
||||||
filterRow: { flexDirection: 'row', gap: 8 },
|
filterRow: { flexDirection: 'row', gap: 8 },
|
||||||
filterBtn: { paddingHorizontal: 16, paddingVertical: 6, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' },
|
filterBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' },
|
||||||
filterBtnActive: { backgroundColor: 'white' },
|
filterBtnActive: { backgroundColor: 'white' },
|
||||||
filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '700' },
|
filterText: { color: 'rgba(255,255,255,0.8)', fontSize: 12, fontWeight: '800' },
|
||||||
filterTextActive: { color: Colors.primary },
|
filterTextActive: { color: Colors.primary },
|
||||||
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 20, paddingBottom: 8 },
|
addBtn: { backgroundColor: 'rgba(255,255,255,0.25)', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 12 },
|
||||||
sectionTitle: { fontSize: 13, fontWeight: '900', color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5 },
|
addBtnText: { color: 'white', fontWeight: '900', fontSize: 13 },
|
||||||
|
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 24, paddingBottom: 10 },
|
||||||
|
sectionTitle: { fontSize: 12, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 },
|
||||||
sectionCount: { fontSize: 11, color: '#94a3b8', fontWeight: '700' },
|
sectionCount: { fontSize: 11, color: '#94a3b8', fontWeight: '700' },
|
||||||
card: { backgroundColor: 'white', marginHorizontal: 16, marginBottom: 8, borderRadius: 14, padding: 14, flexDirection: 'row', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 4 },
|
card: { backgroundColor: 'white', marginHorizontal: 16, marginBottom: 10, borderRadius: 18, padding: 16, flexDirection: 'row', alignItems: 'center', elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||||
cardOverdue: { borderLeftWidth: 4, borderLeftColor: '#ef4444' },
|
cardOverdue: { borderLeftWidth: 5, borderLeftColor: '#ef4444' },
|
||||||
cardDone: { opacity: 0.65 },
|
cardDone: { opacity: 0.7 },
|
||||||
dot: { width: 10, height: 10, borderRadius: 5, marginRight: 12 },
|
typeIconBadge: { width: 44, height: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', marginRight: 14 },
|
||||||
clientName: { fontSize: 14, fontWeight: '800', color: '#1e293b', marginBottom: 3, flex: 1 },
|
typeIconText: { fontSize: 20 },
|
||||||
callIcon: { fontSize: 18, paddingHorizontal: 10 },
|
clientName: { fontSize: 15, fontWeight: '800', color: '#1e293b', marginBottom: 2, flex: 1 },
|
||||||
notes: { fontSize: 12, color: '#64748b', lineHeight: 17, marginBottom: 5 },
|
callCircle: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
|
||||||
time: { fontSize: 10, color: '#94a3b8', fontWeight: '600' },
|
callIcon: { fontSize: 14 },
|
||||||
doneBtn: { backgroundColor: Colors.primary, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10, marginLeft: 10 },
|
typeBadge: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, backgroundColor: '#f8fafc', marginBottom: 6 },
|
||||||
doneBtnText: { color: 'white', fontSize: 11, fontWeight: '900' },
|
typeText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
|
||||||
completedBadge: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 10 },
|
notes: { fontSize: 13, color: '#475569', lineHeight: 18, marginBottom: 8 },
|
||||||
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 14 },
|
time: { fontSize: 11, color: '#94a3b8', fontWeight: '600' },
|
||||||
empty: { alignItems: 'center', paddingTop: 80 },
|
doneBtn: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 12, marginLeft: 12 },
|
||||||
emptyIcon: { fontSize: 48, marginBottom: 12 },
|
doneBtnText: { color: 'white', fontSize: 12, fontWeight: '900' },
|
||||||
emptyTitle: { fontSize: 18, fontWeight: '800', color: '#1e293b' },
|
completedBadge: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#dcfce7', justifyContent: 'center', alignItems: 'center', marginLeft: 12 },
|
||||||
emptySub: { fontSize: 13, color: '#94a3b8', marginTop: 6 },
|
completedText: { color: '#16a34a', fontWeight: '900', fontSize: 16 },
|
||||||
|
empty: { alignItems: 'center', paddingTop: 100 },
|
||||||
|
emptyIcon: { fontSize: 56, marginBottom: 16 },
|
||||||
|
emptyTitle: { fontSize: 20, fontWeight: '900', color: '#1e293b' },
|
||||||
|
emptySub: { fontSize: 14, color: '#64748b', marginTop: 8 },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default TasksScreen;
|
export default TasksScreen;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue