diff --git a/src/config/env.js b/src/config/env.js
index 97ac92d..8ba1ad8 100644
--- a/src/config/env.js
+++ b/src/config/env.js
@@ -1,7 +1,7 @@
// Environment Configuration
const ENV = {
dev: {
- API_URL: 'http://192.168.29.100:3000', // Local Dev IP
+ API_URL: 'http://192.168.65.3:3000', // Local Dev IP
},
prod: {
API_URL: 'https://crmapi.ignosimoney.in', // Change this to your public IP/Domain
diff --git a/src/navigation/AppNav.js b/src/navigation/AppNav.js
index 7769427..0cc7dc9 100644
--- a/src/navigation/AppNav.js
+++ b/src/navigation/AppNav.js
@@ -24,6 +24,7 @@ import MyTargetScreen from '../screens/MyTargetScreen';
import TasksScreen from '../screens/TasksScreen';
import CallLogsScreen from '../screens/CallLogsScreen';
import ChangePasswordScreen from '../screens/ChangePasswordScreen';
+import FeedbackScreen from '../screens/FeedbackScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
@@ -84,6 +85,7 @@ const AppNav = () => {
+
>
) : (
diff --git a/src/screens/AddOpportunityScreen.js b/src/screens/AddOpportunityScreen.js
index 1b2f169..95e8cd7 100644
--- a/src/screens/AddOpportunityScreen.js
+++ b/src/screens/AddOpportunityScreen.js
@@ -10,10 +10,10 @@ import { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors';
const STAGES = [
- { id: 'NEW', label: 'New', color: '#6366f1' },
+ { id: 'LEAD', label: 'Lead', color: '#6366f1' },
{ id: 'QUALIFIED', label: 'Qualified', color: '#3b82f6' },
- { id: 'PROPOSITION', label: 'Proposition', color: '#f59e0b' },
- { id: 'WON', label: 'Won', color: '#10b981' },
+ { id: 'POTENTIAL', label: 'Potential', color: '#f59e0b' },
+ { id: 'SALES', label: 'Sales', color: '#10b981' },
{ id: 'LOST', label: 'Lost', color: '#ef4444' },
];
@@ -37,7 +37,7 @@ const AddOpportunityScreen = ({ navigation, route }) => {
const [form, setForm] = useState({
title: '',
value: '',
- stage: 'NEW',
+ stage: 'LEAD',
expectedClosingDate: '',
notes: '',
});
@@ -102,7 +102,7 @@ const AddOpportunityScreen = ({ navigation, route }) => {
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: 'Add Another', onPress: () => { setForm({ title: '', value: '', stage: 'LEAD', expectedClosingDate: '', notes: '' }); setSelectedClient(null); } },
{ text: 'View Pipeline', onPress: () => navigation.navigate('Pipeline') },
]);
} catch (e) {
diff --git a/src/screens/CallLogsScreen.js b/src/screens/CallLogsScreen.js
index 21f43f4..59dba7a 100644
--- a/src/screens/CallLogsScreen.js
+++ b/src/screens/CallLogsScreen.js
@@ -72,7 +72,7 @@ const CallLogsScreen = ({ navigation }) => {
);
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' },
DEMO: { label: 'Demo', color: '#a855f7', bg: '#f3e8ff' },
SALES: { label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
diff --git a/src/screens/ExpenseScreen.js b/src/screens/ExpenseScreen.js
index d2fb759..dd0d487 100644
--- a/src/screens/ExpenseScreen.js
+++ b/src/screens/ExpenseScreen.js
@@ -1,8 +1,10 @@
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 { AuthContext } from '../context/AuthContext';
import Colors from '../constants/Colors';
+import { Camera, Image as ImageIcon, X } from 'lucide-react-native';
const ExpenseScreen = ({ navigation }) => {
const { userInfo } = useContext(AuthContext);
@@ -11,6 +13,8 @@ const ExpenseScreen = ({ navigation }) => {
const [loading, setLoading] = useState(false);
const [expenses, setExpenses] = useState([]);
const [fetching, setFetching] = useState(true);
+ const [selectedImage, setSelectedImage] = useState(null);
+ const [uploading, setUploading] = useState(false);
useEffect(() => {
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 () => {
if (!amount || !description) {
Alert.alert("Error", "Amount and Description are required");
@@ -42,21 +80,31 @@ const ExpenseScreen = ({ navigation }) => {
setLoading(true);
try {
+ let imageUrl = null;
+ if (selectedImage) {
+ setUploading(true);
+ imageUrl = await uploadImage();
+ setUploading(false);
+ }
+
await api.post('/expenses', {
userId: userInfo.id,
amount: parsedAmount,
description,
+ imageUrl,
status: 'PENDING'
});
Alert.alert("Success", "Expense Submitted Successfully");
setAmount('');
setDescription('');
+ setSelectedImage(null);
fetchExpenses();
} catch (error) {
console.error(error);
- Alert.alert("Error", "Failed to submit expense");
+ Alert.alert("Error", error.message || "Failed to submit expense");
} finally {
setLoading(false);
+ setUploading(false);
}
};
@@ -84,7 +132,38 @@ const ExpenseScreen = ({ navigation }) => {
placeholder="Lunch, Travel, etc."
/>
-
+ Attach Bill (Optional)
+
+ {selectedImage ? (
+
+
+ {selectedImage.name}
+ setSelectedImage(null)} style={styles.removeBtn}>
+
+
+
+ ) : (
+
+
+ Tap to add bill image
+
+ )}
+
+
+
+ {loading || uploading ? (
+
+ ) : (
+ Submit Expense
+ )}
+
My Expenses
@@ -129,7 +208,64 @@ const styles = StyleSheet.create({
status: { fontSize: 12, fontWeight: 'bold', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, overflow: 'hidden' },
approved: { backgroundColor: Colors.accent, color: Colors.secondary },
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;
diff --git a/src/screens/FeedbackScreen.js b/src/screens/FeedbackScreen.js
new file mode 100644
index 0000000..4746e5c
--- /dev/null
+++ b/src/screens/FeedbackScreen.js
@@ -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 = '' }) => (
+
+ {label} *
+ setFeedback({ ...feedback, [keyName]: text })}
+ multiline={multiline}
+ placeholder={placeholder}
+ placeholderTextColor="#94a3b8"
+ />
+
+ );
+
+ return (
+
+
+ navigation.goBack()} style={styles.backBtn}>
+ ✕
+
+ Mandatory Feedback
+
+
+
+
+
+ {activity.type.replace('_', ' ')}
+ {activity.client?.companyName || activity.client?.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Initial Demo Info (Optional)
+
+
+
+
+
+ {loading ? : Complete Activity}
+
+
+
+ );
+};
+
+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;
diff --git a/src/screens/LogActivityScreen.js b/src/screens/LogActivityScreen.js
index 2ef76a5..090bb72 100644
--- a/src/screens/LogActivityScreen.js
+++ b/src/screens/LogActivityScreen.js
@@ -18,10 +18,16 @@ const STRATEGIC_TYPES = [
];
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: 'CALL', label: 'Call', icon: '📞', color: '#10b981' },
+ { id: 'MESSAGE', label: 'Message', icon: '💬', color: '#06b6d4' },
+ { 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: 'FOLLOWUP', label: 'Other', icon: '📅', color: '#64748b' },
];
const TABS = [
@@ -55,7 +61,7 @@ const LogActivityScreen = ({ navigation, route }) => {
const STATUS_OPTIONS = [
{ 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: 'SALES', label: 'Sales', color: '#0ea5e9', bg: '#e0f2fe' },
{ id: 'CLOSED', label: 'Closed', color: '#ef4444', bg: '#fee2e2' }
@@ -406,7 +412,7 @@ const LogActivityScreen = ({ navigation, route }) => {
))}
- {fuType === 'QUOTE' ? (
+ {['QUOTE', 'QUOTE_REQUEST', 'QUOTE_SEND'].includes(fuType) ? (
<>
Link to Opportunity *
diff --git a/src/screens/PipelineScreen.js b/src/screens/PipelineScreen.js
index a4c7170..d3b40cd 100644
--- a/src/screens/PipelineScreen.js
+++ b/src/screens/PipelineScreen.js
@@ -19,9 +19,9 @@ const PipelineScreen = ({ navigation }) => {
const dealStages = [
{ id: 'LEAD', label: 'Lead' },
- { id: 'QUALIFIED', label: 'Qual' },
- { id: 'POTENTIAL', label: 'Poten' },
- { id: 'WON', label: 'Won' },
+ { id: 'QUALIFIED', label: 'Qualified' },
+ { id: 'POTENTIAL', label: 'Potential' },
+ { id: 'SALES', label: 'Sales' },
];
const leadStages = [
@@ -150,7 +150,6 @@ const PipelineScreen = ({ navigation }) => {
const renderItem = ({ item }) => {
const isLead = !!item.status; // Clients have status, opportunities have stages
-
return (
{
@@ -67,13 +83,20 @@ const TasksScreen = ({ navigation }) => {
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.', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Complete ✓', onPress: async () => {
try {
- await api.patch(`/followups/${id}`, { status: 'DONE' });
+ await api.patch(`/followups/${item.id}`, { status: 'DONE' });
fetchTasks();
} catch (e) {
Alert.alert('Error', 'Could not update activity.');
@@ -108,7 +131,7 @@ const TasksScreen = ({ navigation }) => {
)}
- {type}
+ {type.replace('_', ' ')}
{item.notes}