parent
de97592ded
commit
69f1ee64fb
|
|
@ -1,7 +1,7 @@
|
||||||
// Environment Configuration
|
// Environment Configuration
|
||||||
const ENV = {
|
const ENV = {
|
||||||
dev: {
|
dev: {
|
||||||
API_URL: 'http://192.168.29.100:3000', // Local Dev IP
|
API_URL: 'http://192.168.65.3:3000', // Local Dev IP
|
||||||
},
|
},
|
||||||
prod: {
|
prod: {
|
||||||
API_URL: 'https://crmapi.ignosimoney.in', // Change this to your public IP/Domain
|
API_URL: 'https://crmapi.ignosimoney.in', // Change this to your public IP/Domain
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import MyTargetScreen from '../screens/MyTargetScreen';
|
||||||
import TasksScreen from '../screens/TasksScreen';
|
import TasksScreen from '../screens/TasksScreen';
|
||||||
import CallLogsScreen from '../screens/CallLogsScreen';
|
import CallLogsScreen from '../screens/CallLogsScreen';
|
||||||
import ChangePasswordScreen from '../screens/ChangePasswordScreen';
|
import ChangePasswordScreen from '../screens/ChangePasswordScreen';
|
||||||
|
import FeedbackScreen from '../screens/FeedbackScreen';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator();
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
@ -84,6 +85,7 @@ const AppNav = () => {
|
||||||
<Stack.Screen name="TasksDetail" component={TasksScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="TasksDetail" component={TasksScreen} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="CallLogs" component={CallLogsScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="CallLogs" component={CallLogsScreen} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="ChangePassword" component={ChangePasswordScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="ChangePassword" component={ChangePasswordScreen} options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="Feedback" component={FeedbackScreen} options={{ headerShown: false }} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import { AuthContext } from '../context/AuthContext';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
|
||||||
const STAGES = [
|
const STAGES = [
|
||||||
{ id: 'NEW', label: 'New', color: '#6366f1' },
|
{ id: 'LEAD', label: 'Lead', color: '#6366f1' },
|
||||||
{ id: 'QUALIFIED', label: 'Qualified', color: '#3b82f6' },
|
{ id: 'QUALIFIED', label: 'Qualified', color: '#3b82f6' },
|
||||||
{ id: 'PROPOSITION', label: 'Proposition', color: '#f59e0b' },
|
{ id: 'POTENTIAL', label: 'Potential', color: '#f59e0b' },
|
||||||
{ id: 'WON', label: 'Won', color: '#10b981' },
|
{ id: 'SALES', label: 'Sales', color: '#10b981' },
|
||||||
{ id: 'LOST', label: 'Lost', color: '#ef4444' },
|
{ id: 'LOST', label: 'Lost', color: '#ef4444' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ const AddOpportunityScreen = ({ navigation, route }) => {
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
value: '',
|
value: '',
|
||||||
stage: 'NEW',
|
stage: 'LEAD',
|
||||||
expectedClosingDate: '',
|
expectedClosingDate: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
});
|
});
|
||||||
|
|
@ -102,7 +102,7 @@ const AddOpportunityScreen = ({ navigation, route }) => {
|
||||||
notes: form.notes || null,
|
notes: form.notes || null,
|
||||||
});
|
});
|
||||||
Alert.alert('✅ Deal Created!', `"${form.title}" has been added to your pipeline.`, [
|
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: 'Add Another', onPress: () => { setForm({ title: '', value: '', stage: 'LEAD', expectedClosingDate: '', notes: '' }); setSelectedClient(null); } },
|
||||||
{ text: 'View Pipeline', onPress: () => navigation.navigate('Pipeline') },
|
{ text: 'View Pipeline', onPress: () => navigation.navigate('Pipeline') },
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ const CallLogsScreen = ({ navigation }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
QUALITY: { label: 'Quality Lead', color: '#16a34a', bg: '#dcfce7' },
|
QUALIFIED: { label: 'Qualified Lead', color: '#16a34a', bg: '#dcfce7' },
|
||||||
POTENTIAL: { label: 'Potential', color: '#eab308', bg: '#fef9c3' },
|
POTENTIAL: { label: 'Potential', color: '#eab308', bg: '#fef9c3' },
|
||||||
DEMO: { label: 'Demo', color: '#a855f7', bg: '#f3e8ff' },
|
DEMO: { label: 'Demo', color: '#a855f7', bg: '#f3e8ff' },
|
||||||
SALES: { label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
|
SALES: { label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useState, useContext, useEffect } from 'react';
|
import React, { useState, useContext, useEffect } from 'react';
|
||||||
import { View, Text, TextInput, Button, StyleSheet, Alert, ScrollView } from 'react-native';
|
import { View, Text, TextInput, Button, StyleSheet, Alert, ScrollView, TouchableOpacity, ActivityIndicator, Image, Platform } from 'react-native';
|
||||||
|
import { pick } from '@react-native-documents/picker';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import { AuthContext } from '../context/AuthContext';
|
import { AuthContext } from '../context/AuthContext';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
import { Camera, Image as ImageIcon, X } from 'lucide-react-native';
|
||||||
|
|
||||||
const ExpenseScreen = ({ navigation }) => {
|
const ExpenseScreen = ({ navigation }) => {
|
||||||
const { userInfo } = useContext(AuthContext);
|
const { userInfo } = useContext(AuthContext);
|
||||||
|
|
@ -11,6 +13,8 @@ const ExpenseScreen = ({ navigation }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expenses, setExpenses] = useState([]);
|
const [expenses, setExpenses] = useState([]);
|
||||||
const [fetching, setFetching] = useState(true);
|
const [fetching, setFetching] = useState(true);
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchExpenses();
|
fetchExpenses();
|
||||||
|
|
@ -28,6 +32,40 @@ const ExpenseScreen = ({ navigation }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePickImage = async () => {
|
||||||
|
try {
|
||||||
|
const results = await pick({
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
if (results && results.length > 0) {
|
||||||
|
setSelectedImage(results[0]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadImage = async () => {
|
||||||
|
if (!selectedImage) return null;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', {
|
||||||
|
uri: Platform.OS === 'ios' ? selectedImage.uri.replace('file://', '') : selectedImage.uri,
|
||||||
|
type: selectedImage.type || 'image/jpeg',
|
||||||
|
name: selectedImage.name || 'bill.jpg',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post('/upload', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
return response.data.url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed', error);
|
||||||
|
throw new Error('Failed to upload bill image');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!amount || !description) {
|
if (!amount || !description) {
|
||||||
Alert.alert("Error", "Amount and Description are required");
|
Alert.alert("Error", "Amount and Description are required");
|
||||||
|
|
@ -42,21 +80,31 @@ const ExpenseScreen = ({ navigation }) => {
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
let imageUrl = null;
|
||||||
|
if (selectedImage) {
|
||||||
|
setUploading(true);
|
||||||
|
imageUrl = await uploadImage();
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
|
||||||
await api.post('/expenses', {
|
await api.post('/expenses', {
|
||||||
userId: userInfo.id,
|
userId: userInfo.id,
|
||||||
amount: parsedAmount,
|
amount: parsedAmount,
|
||||||
description,
|
description,
|
||||||
|
imageUrl,
|
||||||
status: 'PENDING'
|
status: 'PENDING'
|
||||||
});
|
});
|
||||||
Alert.alert("Success", "Expense Submitted Successfully");
|
Alert.alert("Success", "Expense Submitted Successfully");
|
||||||
setAmount('');
|
setAmount('');
|
||||||
setDescription('');
|
setDescription('');
|
||||||
|
setSelectedImage(null);
|
||||||
fetchExpenses();
|
fetchExpenses();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Alert.alert("Error", "Failed to submit expense");
|
Alert.alert("Error", error.message || "Failed to submit expense");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setUploading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -84,7 +132,38 @@ const ExpenseScreen = ({ navigation }) => {
|
||||||
placeholder="Lunch, Travel, etc."
|
placeholder="Lunch, Travel, etc."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button title={loading ? "Submitting..." : "Submit Expense"} onPress={handleSubmit} disabled={loading} color={Colors.primary} />
|
<Text style={styles.label}>Attach Bill (Optional)</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.imagePicker, selectedImage && styles.imagePickerSelected]}
|
||||||
|
onPress={handlePickImage}
|
||||||
|
>
|
||||||
|
{selectedImage ? (
|
||||||
|
<View style={styles.selectedImageInfo}>
|
||||||
|
<ImageIcon size={20} color={Colors.primary} />
|
||||||
|
<Text style={styles.imageName} numberOfLines={1}>{selectedImage.name}</Text>
|
||||||
|
<TouchableOpacity onPress={() => setSelectedImage(null)} style={styles.removeBtn}>
|
||||||
|
<X size={16} color="#ef4444" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.pickerPlaceholder}>
|
||||||
|
<Camera size={24} color="#94a3b8" />
|
||||||
|
<Text style={styles.pickerText}>Tap to add bill image</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.submitBtn, (loading || uploading) && styles.disabledBtn]}
|
||||||
|
onPress={handleSubmit}
|
||||||
|
disabled={loading || uploading}
|
||||||
|
>
|
||||||
|
{loading || uploading ? (
|
||||||
|
<ActivityIndicator color="white" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.submitBtnText}>Submit Expense</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.subTitle}>My Expenses</Text>
|
<Text style={styles.subTitle}>My Expenses</Text>
|
||||||
|
|
@ -129,7 +208,64 @@ const styles = StyleSheet.create({
|
||||||
status: { fontSize: 12, fontWeight: 'bold', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, overflow: 'hidden' },
|
status: { fontSize: 12, fontWeight: 'bold', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, overflow: 'hidden' },
|
||||||
approved: { backgroundColor: Colors.accent, color: Colors.secondary },
|
approved: { backgroundColor: Colors.accent, color: Colors.secondary },
|
||||||
rejected: { backgroundColor: Colors.backgroundSecondary, color: Colors.textMuted },
|
rejected: { backgroundColor: Colors.backgroundSecondary, color: Colors.textMuted },
|
||||||
pending: { backgroundColor: Colors.backgroundSecondary, color: Colors.textMuted }
|
pending: { backgroundColor: Colors.backgroundSecondary, color: Colors.textMuted },
|
||||||
|
imagePicker: {
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: '#e2e8f0',
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 15,
|
||||||
|
marginBottom: 20,
|
||||||
|
backgroundColor: '#f8fafc',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
imagePickerSelected: {
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: Colors.primary + '40',
|
||||||
|
backgroundColor: Colors.primary + '05',
|
||||||
|
},
|
||||||
|
pickerPlaceholder: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
pickerText: {
|
||||||
|
color: '#94a3b8',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
selectedImageInfo: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
imageName: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 10,
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#1e293b',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
removeBtn: {
|
||||||
|
padding: 5,
|
||||||
|
},
|
||||||
|
submitBtn: {
|
||||||
|
backgroundColor: Colors.primary,
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
submitBtnText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
disabledBtn: {
|
||||||
|
opacity: 0.6,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ExpenseScreen;
|
export default ExpenseScreen;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View, Text, StyleSheet, ScrollView, TextInput,
|
||||||
|
TouchableOpacity, ActivityIndicator, Alert, SafeAreaView
|
||||||
|
} from 'react-native';
|
||||||
|
import api from '../services/api';
|
||||||
|
import Colors from '../constants/Colors';
|
||||||
|
|
||||||
|
const FeedbackScreen = ({ navigation, route }) => {
|
||||||
|
const { activity } = route.params;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [feedback, setFeedback] = useState({
|
||||||
|
customerFeedback: '',
|
||||||
|
requirementDetails: '',
|
||||||
|
suggestions: '',
|
||||||
|
budget: '',
|
||||||
|
expectedClosingTimeline: '',
|
||||||
|
competitorInfo: '',
|
||||||
|
staffRemarks: '',
|
||||||
|
customerCommitments: '',
|
||||||
|
caCsDetails: '',
|
||||||
|
demoPersonName: '',
|
||||||
|
demoContactDetails: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const requiredFields = [
|
||||||
|
'customerFeedback', 'requirementDetails', 'budget',
|
||||||
|
'expectedClosingTimeline', 'competitorInfo', 'staffRemarks',
|
||||||
|
'customerCommitments', 'caCsDetails'
|
||||||
|
];
|
||||||
|
|
||||||
|
const missing = requiredFields.filter(f => !feedback[f]);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
Alert.alert('Incomplete Form', 'Please fill all mandatory fields to complete this activity.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await api.patch(`/followups/${activity.id}`, {
|
||||||
|
status: 'DONE',
|
||||||
|
...feedback
|
||||||
|
});
|
||||||
|
Alert.alert('Success ✅', 'Activity marked as completed with feedback.', [
|
||||||
|
{ text: 'OK', onPress: () => navigation.goBack() }
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
Alert.alert('Error', 'Failed to save feedback.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputField = ({ label, value, keyName, multiline = false, placeholder = '' }) => (
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<Text style={styles.label}>{label} *</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, multiline && styles.textArea]}
|
||||||
|
value={value}
|
||||||
|
onChangeText={(text) => setFeedback({ ...feedback, [keyName]: text })}
|
||||||
|
multiline={multiline}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor="#94a3b8"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
|
||||||
|
<Text style={styles.backBtnText}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Mandatory Feedback</Text>
|
||||||
|
<View style={{ width: 40 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
|
||||||
|
<View style={styles.infoBox}>
|
||||||
|
<Text style={styles.infoTitle}>{activity.type.replace('_', ' ')}</Text>
|
||||||
|
<Text style={styles.infoSub}>{activity.client?.companyName || activity.client?.name}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<InputField label="Customer Feedback" value={feedback.customerFeedback} keyName="customerFeedback" multiline placeholder="Reaction/Feedback from customer" />
|
||||||
|
<InputField label="Requirement Details" value={feedback.requirementDetails} keyName="requirementDetails" multiline placeholder="Specific needs identified" />
|
||||||
|
|
||||||
|
<View style={{ flexDirection: 'row', gap: 12 }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<InputField label="Budget" value={feedback.budget} keyName="budget" placeholder="e.g. 5L" />
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<InputField label="Exp. Timeline" value={feedback.expectedClosingTimeline} keyName="expectedClosingTimeline" placeholder="e.g. 1 month" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<InputField label="Competitor Info" value={feedback.competitorInfo} keyName="competitorInfo" placeholder="Other vendors considered" />
|
||||||
|
<InputField label="Staff Remarks" value={feedback.staffRemarks} keyName="staffRemarks" multiline placeholder="Internal observations" />
|
||||||
|
<InputField label="Customer Commitments" value={feedback.customerCommitments} keyName="customerCommitments" multiline placeholder="What did they promise?" />
|
||||||
|
<InputField label="CA / CS Details" value={feedback.caCsDetails} keyName="caCsDetails" placeholder="Chartered Accountant info" />
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
<Text style={styles.sectionHeader}>Initial Demo Info (Optional)</Text>
|
||||||
|
|
||||||
|
<InputField label="Person Met" value={feedback.demoPersonName} keyName="demoPersonName" placeholder="Name and Role" />
|
||||||
|
<InputField label="Contact Details" value={feedback.demoContactDetails} keyName="demoContactDetails" placeholder="Phone/Email" />
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.submitBtn, loading && { opacity: 0.7 }]}
|
||||||
|
onPress={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? <ActivityIndicator color="white" /> : <Text style={styles.submitBtnText}>Complete Activity</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: '#f8fafc' },
|
||||||
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, backgroundColor: Colors.primary },
|
||||||
|
headerTitle: { color: 'white', fontSize: 18, fontWeight: '900' },
|
||||||
|
backBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center' },
|
||||||
|
backBtnText: { color: 'white', fontSize: 20, fontWeight: 'bold' },
|
||||||
|
body: { padding: 20, paddingBottom: 60 },
|
||||||
|
infoBox: { backgroundColor: 'white', padding: 16, borderRadius: 16, marginBottom: 24, borderLeftWidth: 4, borderLeftColor: Colors.primary, elevation: 2 },
|
||||||
|
infoTitle: { fontSize: 12, fontWeight: '900', color: Colors.primary, textTransform: 'uppercase', letterSpacing: 1 },
|
||||||
|
infoSub: { fontSize: 16, fontWeight: '700', color: '#1e293b', marginTop: 4 },
|
||||||
|
inputGroup: { marginBottom: 20 },
|
||||||
|
label: { fontSize: 11, fontWeight: '900', color: '#64748b', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4 },
|
||||||
|
input: { backgroundColor: 'white', borderRadius: 12, borderWidth: 1.5, borderColor: '#e2e8f0', padding: 14, fontSize: 15, color: '#1e293b' },
|
||||||
|
textArea: { minHeight: 80, textAlignVertical: 'top' },
|
||||||
|
divider: { height: 1, backgroundColor: '#e2e8f0', marginVertical: 20 },
|
||||||
|
sectionHeader: { fontSize: 12, fontWeight: '800', color: '#94a3b8', marginBottom: 20 },
|
||||||
|
submitBtn: { backgroundColor: Colors.primary, borderRadius: 14, padding: 18, alignItems: 'center', marginTop: 20, elevation: 4 },
|
||||||
|
submitBtnText: { color: 'white', fontSize: 16, fontWeight: '900' }
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FeedbackScreen;
|
||||||
|
|
@ -18,10 +18,16 @@ const STRATEGIC_TYPES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const SCHEDULE_TYPES = [
|
const SCHEDULE_TYPES = [
|
||||||
{ id: 'FOLLOWUP', label: 'Follow-up', icon: '📅', color: '#6366f1' },
|
{ id: 'CALL', label: 'Call', icon: '📞', color: '#10b981' },
|
||||||
{ id: 'DEMO', label: 'Demo', icon: '📽️', color: '#3b82f6' },
|
{ id: 'MESSAGE', label: 'Message', icon: '💬', color: '#06b6d4' },
|
||||||
{ id: 'QUOTE', label: 'Quote', icon: '📝', color: '#a855f7' },
|
{ id: 'DEMO_SCHEDULED', label: 'Demo Sch', icon: '📅', color: '#3b82f6' },
|
||||||
|
{ id: 'DEMO_COMPLETED', label: 'Demo Done', icon: '✅', color: '#10b981' },
|
||||||
|
{ id: 'QUOTE_REQUEST', label: 'Quote Req', icon: '📋', color: '#a855f7' },
|
||||||
|
{ id: 'QUOTE_SEND', label: 'Quote Send', icon: '📤', color: '#6366f1' },
|
||||||
|
{ id: 'VISIT_SCHEDULED', label: 'Visit Sch', icon: '📍', color: '#f97316' },
|
||||||
|
{ id: 'VISIT_COMPLETED', label: 'Visit Done', icon: '🏁', color: '#ef4444' },
|
||||||
{ id: 'NEGOTIATION', label: 'Negotiate', icon: '🤝', color: '#f59e0b' },
|
{ id: 'NEGOTIATION', label: 'Negotiate', icon: '🤝', color: '#f59e0b' },
|
||||||
|
{ id: 'FOLLOWUP', label: 'Other', icon: '📅', color: '#64748b' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
|
|
@ -55,7 +61,7 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ id: 'LEAD', label: 'Lead', color: '#6366f1', bg: '#eef2ff' },
|
{ id: 'LEAD', label: 'Lead', color: '#6366f1', bg: '#eef2ff' },
|
||||||
{ id: 'QUALITY', label: 'Quality', color: '#16a34a', bg: '#dcfce7' },
|
{ id: 'QUALIFIED', label: 'Qualified', color: '#16a34a', bg: '#dcfce7' },
|
||||||
{ id: 'POTENTIAL', label: 'Potential', color: '#eab308', bg: '#fef9c3' },
|
{ id: 'POTENTIAL', label: 'Potential', color: '#eab308', bg: '#fef9c3' },
|
||||||
{ 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' }
|
||||||
|
|
@ -406,7 +412,7 @@ const LogActivityScreen = ({ navigation, route }) => {
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{fuType === 'QUOTE' ? (
|
{['QUOTE', 'QUOTE_REQUEST', 'QUOTE_SEND'].includes(fuType) ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.section}>Link to Opportunity *</Text>
|
<Text style={styles.section}>Link to Opportunity *</Text>
|
||||||
<OpportunityPicker />
|
<OpportunityPicker />
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ const PipelineScreen = ({ navigation }) => {
|
||||||
|
|
||||||
const dealStages = [
|
const dealStages = [
|
||||||
{ id: 'LEAD', label: 'Lead' },
|
{ id: 'LEAD', label: 'Lead' },
|
||||||
{ id: 'QUALIFIED', label: 'Qual' },
|
{ id: 'QUALIFIED', label: 'Qualified' },
|
||||||
{ id: 'POTENTIAL', label: 'Poten' },
|
{ id: 'POTENTIAL', label: 'Potential' },
|
||||||
{ id: 'WON', label: 'Won' },
|
{ id: 'SALES', label: 'Sales' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const leadStages = [
|
const leadStages = [
|
||||||
|
|
@ -150,7 +150,6 @@ const PipelineScreen = ({ navigation }) => {
|
||||||
|
|
||||||
const renderItem = ({ item }) => {
|
const renderItem = ({ item }) => {
|
||||||
const isLead = !!item.status; // Clients have status, opportunities have stages
|
const isLead = !!item.status; // Clients have status, opportunities have stages
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.card}
|
style={styles.card}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,33 @@ import api from '../services/api';
|
||||||
import Colors from '../constants/Colors';
|
import Colors from '../constants/Colors';
|
||||||
|
|
||||||
const TYPE_ICONS = {
|
const TYPE_ICONS = {
|
||||||
|
'CALL': '📞',
|
||||||
|
'MESSAGE': '💬',
|
||||||
|
'DEMO_SCHEDULED': '📅',
|
||||||
|
'DEMO_COMPLETED': '✅',
|
||||||
|
'QUOTE_REQUEST': '📋',
|
||||||
|
'QUOTE_SEND': '📤',
|
||||||
|
'VISIT_SCHEDULED': '📍',
|
||||||
|
'VISIT_COMPLETED': '🏁',
|
||||||
|
'NEGOTIATION': '🤝',
|
||||||
'FOLLOWUP': '📅',
|
'FOLLOWUP': '📅',
|
||||||
'DEMO': '📽️',
|
'DEMO': '📽️',
|
||||||
'QUOTE': '📝',
|
'QUOTE': '📝',
|
||||||
'NEGOTIATION': '🤝',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TYPE_COLORS = {
|
const TYPE_COLORS = {
|
||||||
'FOLLOWUP': '#6366f1',
|
'CALL': '#10b981',
|
||||||
|
'MESSAGE': '#06b6d4',
|
||||||
|
'DEMO_SCHEDULED': '#3b82f6',
|
||||||
|
'DEMO_COMPLETED': '#10b981',
|
||||||
|
'QUOTE_REQUEST': '#a855f7',
|
||||||
|
'QUOTE_SEND': '#6366f1',
|
||||||
|
'VISIT_SCHEDULED': '#f97316',
|
||||||
|
'VISIT_COMPLETED': '#ef4444',
|
||||||
|
'NEGOTIATION': '#f59e0b',
|
||||||
|
'FOLLOWUP': '#64748b',
|
||||||
'DEMO': '#3b82f6',
|
'DEMO': '#3b82f6',
|
||||||
'QUOTE': '#a855f7',
|
'QUOTE': '#a855f7',
|
||||||
'NEGOTIATION': '#f59e0b',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TasksScreen = ({ navigation }) => {
|
const TasksScreen = ({ navigation }) => {
|
||||||
|
|
@ -67,13 +83,20 @@ const TasksScreen = ({ navigation }) => {
|
||||||
|
|
||||||
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
|
useFocusEffect(useCallback(() => { fetchTasks(); }, [activeFilter]));
|
||||||
|
|
||||||
const handleMarkDone = async (id) => {
|
const handleMarkDone = async (item) => {
|
||||||
|
const isMandatory = ['DEMO_COMPLETED', 'VISIT_COMPLETED', 'DEMO'].includes(item.type);
|
||||||
|
|
||||||
|
if (isMandatory) {
|
||||||
|
navigation.navigate('Feedback', { activity: item });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Alert.alert('Complete Activity?', 'This will mark the activity as done and remove it from pending.', [
|
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: 'Complete ✓', onPress: async () => {
|
text: 'Complete ✓', onPress: async () => {
|
||||||
try {
|
try {
|
||||||
await api.patch(`/followups/${id}`, { status: 'DONE' });
|
await api.patch(`/followups/${item.id}`, { status: 'DONE' });
|
||||||
fetchTasks();
|
fetchTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('Error', 'Could not update activity.');
|
Alert.alert('Error', 'Could not update activity.');
|
||||||
|
|
@ -108,7 +131,7 @@ const TasksScreen = ({ navigation }) => {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.typeBadge}>
|
<View style={styles.typeBadge}>
|
||||||
<Text style={[styles.typeText, { color: TYPE_COLORS[type] }]}>{type}</Text>
|
<Text style={[styles.typeText, { color: TYPE_COLORS[type] || '#64748b' }]}>{type.replace('_', ' ')}</Text>
|
||||||
</View>
|
</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}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue