feat(app): improve design and integrate apis

This commit is contained in:
uttam 2026-07-22 17:41:07 +05:30
parent 168c9fd39c
commit 0535845362
25 changed files with 1286 additions and 418 deletions

View File

@ -1,4 +1,4 @@
import { LeadItem, LeadCountItem } from '@interfaces';
import { LeadItem, LeadCountItem, AddLeadPayload, AddLeadResponse } from '@interfaces';
import { api } from '@utils';
export const getLeadsApi = async (
@ -15,3 +15,25 @@ export const getLeadCountListApi = async (
const url = `/api/leadcountlist/${staffId}`;
return await api.get<LeadCountItem[]>(url);
};
export const addLeadApi = async (
payload: AddLeadPayload,
): Promise<AddLeadResponse> => {
const formData = new FormData();
formData.append('staff_id', payload.staff_id);
formData.append('name', payload.name);
if (payload.company) formData.append('company', payload.company);
if (payload.email) formData.append('email', payload.email);
if (payload.phonenumber) formData.append('phonenumber', payload.phonenumber);
if (payload.website) formData.append('website', payload.website);
if (payload.source) formData.append('source', payload.source);
if (payload.status) formData.append('status', payload.status);
if (payload.address) formData.append('address', payload.address);
if (payload.city) formData.append('city', payload.city);
if (payload.state) formData.append('state', payload.state);
if (payload.zip) formData.append('zip', payload.zip);
if (payload.country) formData.append('country', payload.country);
return await api.post<AddLeadResponse>('/api/leads', formData);
};

View File

@ -10,4 +10,5 @@ export interface FormPickerProps {
options: FormPickerOption[];
required?: boolean;
placeholder?: string;
searchable?: boolean;
}

View File

@ -1,6 +1,8 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Platform, Dimensions } from 'react-native';
import { ThemeColors } from '../../theme';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({
container: {
@ -25,13 +27,81 @@ export const getStyles = (colors: ThemeColors) =>
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
paddingVertical: 13,
},
pickerText: {
fontSize: 15,
color: colors.text,
flex: 1,
},
placeholder: {
color: colors.textMuted,
},
// Modal
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.45)',
},
sheet: {
backgroundColor: colors.card,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: SCREEN_HEIGHT * 0.65,
paddingBottom: Platform.OS === 'ios' ? 0 : 16,
},
sheetHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
sheetTitle: {
fontSize: 16,
fontWeight: '700',
color: colors.text,
},
searchWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
borderRadius: 10,
marginHorizontal: 16,
marginTop: 12,
paddingHorizontal: 12,
borderWidth: 1,
borderColor: colors.border,
},
searchIcon: {
marginRight: 8,
},
searchInput: {
flex: 1,
height: 40,
fontSize: 14,
color: colors.text,
},
separator: {
height: 1,
backgroundColor: colors.border,
marginHorizontal: 16,
},
optionRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 14,
},
optionText: {
fontSize: 15,
color: colors.text,
flex: 1,
},
optionTextSelected: {
color: colors.icon,
fontWeight: '600',
},
});

View File

@ -1,9 +1,19 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
Modal,
FlatList,
TextInput,
SafeAreaView,
Platform,
StatusBar,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { useTheme } from '@theme';
import { getStyles } from './formPicker.styles';
import { FormPickerProps } from './formPicker.props';
import { FormPickerProps, FormPickerOption } from './formPicker.props';
export const FormPicker: React.FC<FormPickerProps> = ({
label,
@ -12,33 +22,112 @@ export const FormPicker: React.FC<FormPickerProps> = ({
options,
required = false,
placeholder = 'Select...',
searchable = false,
}) => {
const { theme: colors } = useTheme();
const styles = getStyles(colors);
const [modalVisible, setModalVisible] = useState(false);
const [query, setQuery] = useState('');
const selectedOption = options.find(opt => opt.value === value);
const filtered = searchable
? options.filter(opt =>
opt.label.toLowerCase().includes(query.toLowerCase()),
)
: options;
const handleSelect = (opt: FormPickerOption) => {
onValueChange(opt.value);
setModalVisible(false);
setQuery('');
};
return (
<View style={styles.container}>
<Text style={styles.label}>
{label}
{required && <Text style={styles.required}>*</Text>}
{required && <Text style={styles.required}> *</Text>}
</Text>
<TouchableOpacity
style={styles.pickerContainer}
onPress={() => {
// TODO: Implement modal picker or action sheet
console.log('Picker pressed');
}}>
activeOpacity={0.7}
onPress={() => setModalVisible(true)}>
<Text
style={[
styles.pickerText,
!selectedOption && styles.placeholder,
]}>
style={[styles.pickerText, !selectedOption && styles.placeholder]}>
{selectedOption?.label || placeholder}
</Text>
<Icon name="chevron-down" size={20} color={colors.textSecondary} />
<Icon name="chevron-down" size={18} color={colors.textSecondary} />
</TouchableOpacity>
<Modal
visible={modalVisible}
animationType="slide"
transparent
onRequestClose={() => setModalVisible(false)}>
<TouchableOpacity
style={styles.backdrop}
activeOpacity={1}
onPress={() => setModalVisible(false)}
/>
<SafeAreaView style={styles.sheet}>
{/* Header */}
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{label}</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Icon name="close" size={22} color={colors.text} />
</TouchableOpacity>
</View>
{/* Search */}
{searchable && (
<View style={styles.searchWrapper}>
<Icon
name="search-outline"
size={16}
color={colors.textSecondary}
style={styles.searchIcon}
/>
<TextInput
style={styles.searchInput}
placeholder="Search..."
placeholderTextColor={colors.textMuted}
value={query}
onChangeText={setQuery}
autoFocus
/>
</View>
)}
{/* Options */}
<FlatList
data={filtered}
keyExtractor={item => item.value}
ItemSeparatorComponent={() => <View style={styles.separator} />}
renderItem={({ item }) => {
const isSelected = item.value === value;
return (
<TouchableOpacity
style={styles.optionRow}
activeOpacity={0.6}
onPress={() => handleSelect(item)}>
<Text
style={[
styles.optionText,
isSelected && styles.optionTextSelected,
]}>
{item.label}
</Text>
{isSelected && (
<Icon name="checkmark" size={18} color={colors.icon} />
)}
</TouchableOpacity>
);
}}
/>
</SafeAreaView>
</Modal>
</View>
);
};

View File

@ -9,3 +9,5 @@ export * from './addItemButton';
export * from './actionButton';
export * from './checkboxWithLabel';
export * from './statCard';
export * from './profileInfoRow';

View File

@ -0,0 +1,2 @@
export * from './profileInfoRow';
export * from './profileInfoRow.props';

View File

@ -0,0 +1,13 @@
import { StyleProp, ViewStyle, TextStyle } from 'react-native';
export interface ProfileInfoRowProps {
icon: string;
label: string;
value: string;
valueColor?: string;
isBadge?: boolean;
badgeBgColor?: string;
onPress?: () => void;
isLast?: boolean;
containerStyle?: StyleProp<ViewStyle>;
}

View File

@ -0,0 +1,52 @@
import { StyleSheet } from 'react-native';
import { ThemeColors } from '../../theme';
export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
noBorder: {
borderBottomWidth: 0,
},
leftContent: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
marginRight: 12,
},
iconWrapper: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.surface,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
label: {
fontSize: 14,
color: colors.textSecondary,
fontWeight: '500',
},
value: {
fontSize: 14,
fontWeight: '600',
color: colors.text,
textAlign: 'right',
},
badgeContainer: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
badgeText: {
fontSize: 12,
fontWeight: '700',
},
});

View File

@ -0,0 +1,73 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { useTheme } from '@theme';
import { getStyles } from './profileInfoRow.styles';
import { ProfileInfoRowProps } from './profileInfoRow.props';
export const ProfileInfoRow: React.FC<ProfileInfoRowProps> = ({
icon,
label,
value,
valueColor,
isBadge = false,
badgeBgColor,
onPress,
isLast = false,
containerStyle,
}) => {
const { theme: colors } = useTheme();
const styles = getStyles(colors);
const Content = (
<View
style={[
styles.container,
isLast && styles.noBorder,
containerStyle,
]}>
<View style={styles.leftContent}>
<View style={styles.iconWrapper}>
<Icon name={icon} size={18} color={colors.icon} />
</View>
<Text style={styles.label}>{label}</Text>
</View>
{isBadge ? (
<View
style={[
styles.badgeContainer,
{ backgroundColor: badgeBgColor || `${colors.icon}20` },
]}>
<Text
style={[
styles.badgeText,
{ color: valueColor || colors.icon },
]}>
{value}
</Text>
</View>
) : (
<Text
style={[
styles.value,
valueColor ? { color: valueColor } : undefined,
]}
numberOfLines={1}
ellipsizeMode="tail">
{value}
</Text>
)}
</View>
);
if (onPress) {
return (
<TouchableOpacity activeOpacity={0.7} onPress={onPress}>
{Content}
</TouchableOpacity>
);
}
return Content;
};

View File

@ -1,8 +1,7 @@
import React, { useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Text,
View,
TextInput,
Text,
TouchableOpacity,
ScrollView,
KeyboardAvoidingView,
@ -11,171 +10,243 @@ import {
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { getStyles } from './addLead.styles';
import { useTheme } from '../../theme';
import { useTheme } from '@theme';
import { FormInput, FormPicker } from '@components';
import { RootState, useAppDispatch, useAppSelector } from '@store';
import { addLead, resetAddLeadState } from './thunk';
export const AddLeadScreen = () => {
const { theme: colors } = useTheme();
const styles = getStyles(colors);
const [name, setName] = useState('');
const dispatch = useAppDispatch();
const userData = useAppSelector((state: RootState) => state.auth.user_data);
const { loading, successMessage, error } = useAppSelector(
(state: RootState) => state.addLead,
);
const { items: sourceItems } = useAppSelector(
(state: RootState) => state.sourceList,
);
const { items: statusItems } = useAppSelector(
(state: RootState) => state.statusList,
);
const { items: countryItems } = useAppSelector(
(state: RootState) => state.countryList,
);
const [company, setCompany] = useState('');
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [value, setValue] = useState('');
const [website, setWebsite] = useState('');
const [source, setSource] = useState('');
const [status, setStatus] = useState('');
const [address, setAddress] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [zip, setZip] = useState('');
const [country, setCountry] = useState('');
const sourceOptions = useMemo(
() => sourceItems.map(s => ({ label: s.name, value: s.id })),
[sourceItems],
);
const statusOptions = useMemo(
() => statusItems.map(s => ({ label: s.name, value: s.id })),
[statusItems],
);
const countryOptions = useMemo(
() => countryItems.map(c => ({ label: c.short_name, value: c.country_id })),
[countryItems],
);
useEffect(() => {
if (successMessage) {
Alert.alert('Success', successMessage);
setCompany('');
setFullName('');
setEmail('');
setPhone('');
setWebsite('');
setSource('');
setStatus('');
setAddress('');
setCity('');
setState('');
setZip('');
setCountry('');
dispatch(resetAddLeadState());
} else if (error) {
Alert.alert('Error', error);
dispatch(resetAddLeadState());
}
}, [successMessage, error, dispatch]);
const handleSubmit = () => {
if (!name.trim() || !company.trim() || !email.trim()) {
Alert.alert('Error', 'Please fill in Name, Company, and Email fields.');
if (!fullName.trim() || !email.trim() || !source || !status) {
Alert.alert('Error', 'Full Name, Email, Source and Status are required.');
return;
}
Alert.alert(
'Success',
`Lead "${name}" for "${company}" has been created!`,
[
{
text: 'OK',
onPress: () => {
setName('');
setCompany('');
setEmail('');
setPhone('');
setValue('');
},
},
],
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.trim())) {
Alert.alert('Error', 'Please enter a valid email address.');
return;
}
const staffId = userData?.staffid || '';
if (!staffId) {
Alert.alert('Error', 'User session invalid. Please log in again.');
return;
}
dispatch(
addLead({
staff_id: staffId,
name: fullName,
company,
email,
phonenumber: phone,
website,
source,
status,
address,
city,
state,
zip,
country,
}),
);
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
style={styles.container}>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
>
keyboardShouldPersistTaps="handled">
{/* Basic Info */}
<View style={styles.formCard}>
<Text style={styles.sectionHeader}>Lead Information</Text>
<Text style={styles.sectionHeader}>Basic Information</Text>
{/* Name Field */}
<View style={styles.inputContainer}>
<Text style={styles.label}>Lead Name *</Text>
<View style={styles.inputWrapper}>
<Icon
name="person-outline"
size={18}
color={colors.textSecondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="John Doe"
placeholderTextColor={colors.textMuted}
value={name}
onChangeText={setName}
/>
</View>
</View>
{/* Company Field */}
<View style={styles.inputContainer}>
<Text style={styles.label}>Company *</Text>
<View style={styles.inputWrapper}>
<Icon
name="business-outline"
size={18}
color={colors.textSecondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="Acme Corp"
placeholderTextColor={colors.textMuted}
value={company}
onChangeText={setCompany}
/>
</View>
</View>
{/* Email Field */}
<View style={styles.inputContainer}>
<Text style={styles.label}>Email Address *</Text>
<View style={styles.inputWrapper}>
<Icon
name="mail-outline"
size={18}
color={colors.textSecondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="johndoe@acme.com"
placeholderTextColor={colors.textMuted}
keyboardType="email-address"
autoCapitalize="none"
value={email}
onChangeText={setEmail}
/>
</View>
</View>
{/* Phone Field */}
<View style={styles.inputContainer}>
<Text style={styles.label}>Phone Number</Text>
<View style={styles.inputWrapper}>
<Icon
name="call-outline"
size={18}
color={colors.textSecondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="+1 (555) 000-0000"
placeholderTextColor={colors.textMuted}
keyboardType="phone-pad"
value={phone}
onChangeText={setPhone}
/>
</View>
</View>
{/* Deal Value Field */}
<View style={styles.inputContainer}>
<Text style={styles.label}>Estimated Value ($)</Text>
<View style={styles.inputWrapper}>
<Icon
name="cash-outline"
size={18}
color={colors.textSecondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="5,000"
placeholderTextColor={colors.textMuted}
keyboardType="numeric"
value={value}
onChangeText={setValue}
/>
</View>
</View>
<TouchableOpacity
style={styles.submitButton}
activeOpacity={0.8}
onPress={handleSubmit}
>
<Icon
name="checkmark"
size={20}
color="#FFFFFF"
style={styles.buttonIcon}
/>
<Text style={styles.submitButtonText}>Create New Lead</Text>
</TouchableOpacity>
<FormInput
label="Company"
placeholder="Acme Corp"
value={company}
onChangeText={setCompany}
leftIcon={<Icon name="business-outline" size={18} color={colors.textSecondary} />}
/>
<FormInput
label="Full Name"
required
placeholder="John Doe"
value={fullName}
onChangeText={setFullName}
leftIcon={<Icon name="person-outline" size={18} color={colors.textSecondary} />}
/>
<FormInput
label="Email"
required
placeholder="john@acme.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
leftIcon={<Icon name="mail-outline" size={18} color={colors.textSecondary} />}
/>
<FormInput
label="Phone Number"
placeholder="+1 (555) 000-0000"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
leftIcon={<Icon name="call-outline" size={18} color={colors.textSecondary} />}
/>
<FormInput
label="Website"
placeholder="https://acme.com"
value={website}
onChangeText={setWebsite}
keyboardType="url"
autoCapitalize="none"
leftIcon={<Icon name="globe-outline" size={18} color={colors.textSecondary} />}
/>
</View>
{/* Classification */}
<View style={[styles.formCard, styles.cardSpacing]}>
<Text style={styles.sectionHeader}>Classification</Text>
<FormPicker
label="Source"
required
value={source}
onValueChange={setSource}
options={sourceOptions}
placeholder="Select source..."
/>
<FormPicker
label="Status"
required
value={status}
onValueChange={setStatus}
options={statusOptions}
placeholder="Select status..."
/>
</View>
{/* Address */}
<View style={[styles.formCard, styles.cardSpacing]}>
<Text style={styles.sectionHeader}>Address</Text>
<FormInput
label="Address"
placeholder="123 Main Street"
value={address}
onChangeText={setAddress}
leftIcon={<Icon name="location-outline" size={18} color={colors.textSecondary} />}
/>
<View style={styles.row}>
<View style={styles.rowHalf}>
<FormInput label="City" placeholder="New York" value={city} onChangeText={setCity} />
</View>
<View style={styles.rowHalf}>
<FormInput label="State" placeholder="NY" value={state} onChangeText={setState} />
</View>
</View>
<FormInput
label="ZIP / Postal Code"
placeholder="10001"
value={zip}
onChangeText={setZip}
keyboardType="number-pad"
leftIcon={<Icon name="map-outline" size={18} color={colors.textSecondary} />}
/>
<FormPicker
label="Country"
value={country}
onValueChange={setCountry}
options={countryOptions}
placeholder="Select country..."
searchable
/>
</View>
{/* Submit */}
<TouchableOpacity
style={[styles.submitButton, loading && { opacity: 0.7 }]}
activeOpacity={0.8}
disabled={loading}
onPress={handleSubmit}>
<Icon name="checkmark-circle-outline" size={20} color="#FFFFFF" style={styles.buttonIcon} />
<Text style={styles.submitButtonText}>{loading ? 'Creating Lead...' : 'Create Lead'}</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
);

View File

@ -1,5 +1,4 @@
import { StyleSheet } from 'react-native';
import { ThemeColors } from '../../theme';
export const getStyles = (colors: ThemeColors) =>
@ -10,6 +9,7 @@ export const getStyles = (colors: ThemeColors) =>
},
scrollContainer: {
padding: 16,
paddingBottom: 32,
},
formCard: {
backgroundColor: colors.card,
@ -21,8 +21,11 @@ export const getStyles = (colors: ThemeColors) =>
shadowRadius: 4,
elevation: 2,
},
cardSpacing: {
marginTop: 16,
},
sectionHeader: {
fontSize: 16,
fontSize: 15,
fontWeight: '700',
color: colors.text,
marginBottom: 20,
@ -30,48 +33,36 @@ export const getStyles = (colors: ThemeColors) =>
borderBottomColor: colors.border,
paddingBottom: 10,
},
inputContainer: {
marginBottom: 16,
},
label: {
fontSize: 13,
fontWeight: '600',
color: colors.textSecondary,
marginBottom: 6,
},
inputWrapper: {
// Two-column row layout
row: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 12,
gap: 12,
},
inputIcon: {
marginRight: 8,
},
input: {
rowHalf: {
flex: 1,
height: 46,
color: colors.text,
fontSize: 14,
},
// Submit
submitButton: {
backgroundColor: colors.icon,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
height: 48,
marginTop: 12,
borderRadius: 12,
height: 52,
marginTop: 24,
shadowColor: colors.icon,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
buttonIcon: {
marginRight: 8,
},
submitButtonText: {
color: '#FFFFFF',
fontSize: 15,
fontSize: 16,
fontWeight: '700',
letterSpacing: 0.3,
},
});

View File

@ -1 +1,3 @@
export * from './addLead.screen';
export * from './thunk';
export * from './reducers';

View File

@ -0,0 +1,38 @@
import { createReducer } from '@reduxjs/toolkit';
import { addLead, resetAddLeadState } from './thunk';
export interface AddLeadState {
loading: boolean;
error: string | null;
successMessage: string | null;
}
const initialState: AddLeadState = {
loading: false,
error: null,
successMessage: null,
};
export const addLeadReducer = createReducer(initialState, builder => {
builder
.addCase(resetAddLeadState, () => initialState)
.addCase(addLead.pending, acc => {
acc.loading = true;
acc.error = null;
acc.successMessage = null;
})
.addCase(addLead.fulfilled, (acc, action) => {
acc.loading = false;
acc.successMessage = action.payload.message;
acc.error = null;
})
.addCase(addLead.rejected, (acc, action) => {
acc.loading = false;
acc.error =
(action.payload as string) ??
action.error.message ??
'Failed to add lead';
});
});
export default addLeadReducer;

View File

@ -0,0 +1,16 @@
import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
import { addLeadApi } from '@api';
import { AddLeadPayload, AddLeadResponse } from '@interfaces';
export const resetAddLeadState = createAction('addLead/resetState');
export const addLead = createAsyncThunk<AddLeadResponse, AddLeadPayload>(
'addLead/addLead',
async (payload, { rejectWithValue }) => {
try {
return await addLeadApi(payload);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to add lead');
}
},
);

View File

@ -1,11 +1,18 @@
import React from 'react';
import { Text, View, TouchableOpacity, ScrollView, Switch } from 'react-native';
import {
Text,
View,
TouchableOpacity,
ScrollView,
Switch,
Image,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import Icon from 'react-native-vector-icons/Ionicons';
import { getStyles } from './profile.styles';
import { useTheme } from '../../theme';
import { useAppDispatch, logout } from '@store';
import { ProfileInfoRow } from '@components';
import { useAppDispatch, useAppSelector, logout, RootState } from '@store';
export const ProfileScreen = () => {
const navigation = useNavigation<any>();
@ -13,6 +20,8 @@ export const ProfileScreen = () => {
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const userData = useAppSelector((state: RootState) => state.auth.user_data);
const handleLogout = async () => {
try {
dispatch(logout());
@ -21,53 +30,193 @@ export const ProfileScreen = () => {
}
navigation.reset({
index: 0,
routes: [{ name: 'AuthStack' }], // root-level stack name stays as-is
routes: [{ name: 'AuthStack' }],
});
};
// Derive user info
const fullName =
userData?.full_name ||
`${userData?.firstname || ''} ${userData?.lastname || ''}`.trim() ||
'User Profile';
const email = userData?.email || 'N/A';
const phone = userData?.phonenumber || 'N/A';
const staffId = userData?.staffid ? `#${userData.staffid}` : 'N/A';
const isAdmin = userData?.admin === '1';
const roleText = isAdmin ? 'Administrator' : 'Staff Member';
const isActive = userData?.active === '1';
const hasProfileImage =
userData?.profile_image && userData.profile_image.startsWith('http');
// Derive initials
const initials = fullName
.split(' ')
.filter(Boolean)
.map(n => n[0])
.join('')
.substring(0, 2)
.toUpperCase() || 'U';
const hasSocials =
Boolean(userData?.linkedin) ||
Boolean(userData?.facebook) ||
Boolean(userData?.skype);
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Profile Header */}
<View style={styles.profileHeader}>
<View style={styles.avatarCircle}>
<Text style={styles.avatarInitial}>A</Text>
{/* ── Header Card ── */}
<View style={styles.headerCard}>
<View style={styles.avatarWrapper}>
{hasProfileImage ? (
<Image
source={{ uri: userData!.profile_image }}
style={styles.avatarImage}
/>
) : (
<View style={styles.avatarCircle}>
<Text style={styles.avatarInitial}>{initials}</Text>
</View>
)}
{isActive && <View style={styles.onlineDot} />}
</View>
<Text style={styles.profileName}>{fullName}</Text>
<Text style={styles.profileEmail}>{email}</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleBadgeText}>{roleText}</Text>
</View>
<Text style={styles.profileName}>Workspace Administrator</Text>
<Text style={styles.profileRole}>Owner / Administrator</Text>
</View>
{/* Account Details list */}
{/* ── Stats Summary Row ── */}
<View style={styles.statsRow}>
<View style={styles.statBox}>
<Text style={styles.statValue}>{userData?.staffid || '—'}</Text>
<Text style={styles.statLabel}>Staff ID</Text>
</View>
<View style={styles.statBox}>
<Text style={styles.statValue}>
{userData?.total_unfinished_todos ?? '0'}
</Text>
<Text style={styles.statLabel}>Pending Todos</Text>
</View>
<View style={styles.statBox}>
<Text style={styles.statValue}>
{userData?.total_unread_notifications ?? '0'}
</Text>
<Text style={styles.statLabel}>Unread Alerts</Text>
</View>
</View>
{/* ── Personal Details ── */}
<View style={styles.sectionCard}>
<Text style={styles.sectionHeader}>Workspace Details</Text>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Tenancy</Text>
<Text style={styles.infoValue}>convex-crm-tenant</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Email</Text>
<Text style={styles.infoValue}>admin@convex.com</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Status</Text>
<Text style={styles.infoValueStatus}>Active</Text>
</View>
<Text style={styles.sectionHeader}>Personal Details</Text>
<ProfileInfoRow
icon="person-outline"
label="Full Name"
value={fullName}
/>
<ProfileInfoRow
icon="card-outline"
label="Staff ID"
value={staffId}
/>
<ProfileInfoRow
icon="mail-outline"
label="Email Address"
value={email}
/>
<ProfileInfoRow
icon="call-outline"
label="Phone Number"
value={phone}
/>
<ProfileInfoRow
icon="shield-checkmark-outline"
label="Account Role"
value={roleText}
isLast
/>
</View>
{/* Settings list */}
{/* ── System & Account ── */}
<View style={styles.sectionCard}>
<Text style={styles.sectionHeader}>System &amp; Account</Text>
<ProfileInfoRow
icon="checkmark-circle-outline"
label="Account Status"
value={isActive ? 'Active' : 'Inactive'}
valueColor={isActive ? '#10B981' : '#EF4444'}
isBadge
badgeBgColor={isActive ? '#10B98120' : '#EF444420'}
/>
<ProfileInfoRow
icon="key-outline"
label="Two-Factor Auth"
value={
userData?.two_factor_auth_enabled === '1'
? 'Enabled'
: 'Disabled'
}
/>
{userData?.default_language ? (
<ProfileInfoRow
icon="language-outline"
label="Default Language"
value={userData.default_language.toUpperCase()}
/>
) : null}
<ProfileInfoRow
icon="time-outline"
label="Last Login"
value={userData?.last_login || 'N/A'}
isLast
/>
</View>
{/* ── Social Profiles ── */}
{hasSocials && (
<View style={styles.sectionCard}>
<Text style={styles.sectionHeader}>Social Profiles</Text>
{userData?.linkedin ? (
<ProfileInfoRow
icon="logo-linkedin"
label="LinkedIn"
value={userData.linkedin}
/>
) : null}
{userData?.facebook ? (
<ProfileInfoRow
icon="logo-facebook"
label="Facebook"
value={userData.facebook}
/>
) : null}
{userData?.skype ? (
<ProfileInfoRow
icon="logo-skype"
label="Skype"
value={userData.skype}
isLast
/>
) : null}
</View>
)}
{/* ── Preferences ── */}
<View style={styles.sectionCard}>
<Text style={styles.sectionHeader}>Preferences</Text>
<View style={styles.preferenceRow}>
<View style={styles.preferenceLabelGroup}>
<Icon
name={isDark ? 'moon' : 'moon-outline'}
size={20}
color={colors.textSecondary}
style={styles.icon}
/>
<View style={styles.iconWrapper}>
<Icon
name={isDark ? 'moon' : 'moon-outline'}
size={18}
color={colors.icon}
/>
</View>
<Text style={styles.preferenceText}>Dark Mode</Text>
</View>
<Switch
@ -78,42 +227,49 @@ export const ProfileScreen = () => {
/>
</View>
<TouchableOpacity style={styles.preferenceRow}>
<TouchableOpacity style={styles.preferenceRow} activeOpacity={0.7}>
<View style={styles.preferenceLabelGroup}>
<Icon
name="notifications-outline"
size={20}
color={colors.textSecondary}
style={styles.icon}
/>
<View style={styles.iconWrapper}>
<Icon
name="notifications-outline"
size={18}
color={colors.icon}
/>
</View>
<Text style={styles.preferenceText}>Push Notifications</Text>
</View>
<Icon name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity>
<TouchableOpacity style={styles.preferenceRow}>
<TouchableOpacity
style={[styles.preferenceRow, styles.noBorder]}
activeOpacity={0.7}>
<View style={styles.preferenceLabelGroup}>
<Icon
name="shield-checkmark-outline"
size={20}
color={colors.textSecondary}
style={styles.icon}
/>
<View style={styles.iconWrapper}>
<Icon
name="shield-checkmark-outline"
size={18}
color={colors.icon}
/>
</View>
<Text style={styles.preferenceText}>Security &amp; Privacy</Text>
</View>
<Icon name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
{/* Sign Out Button */}
<TouchableOpacity style={styles.signOutButton} onPress={handleLogout}>
{/* ── Sign Out ── */}
<TouchableOpacity
style={styles.signOutButton}
activeOpacity={0.8}
onPress={handleLogout}>
<Icon
name="log-out-outline"
size={20}
color="#FFFFFF"
style={styles.buttonIcon}
/>
<Text style={styles.signOutButtonText}>Sign Out from Workspace</Text>
<Text style={styles.signOutButtonText}>Sign Out</Text>
</TouchableOpacity>
</ScrollView>
);

View File

@ -1,118 +1,197 @@
import { StyleSheet } from 'react-native';
import { ThemeColors } from '../../theme';
export const getStyles = (colors: ThemeColors) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
padding: 16,
},
profileHeader: {
alignItems: 'center',
marginVertical: 24,
},
avatarCircle: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.icon,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16,
},
avatarInitial: {
color: '#FFFFFF',
fontSize: 32,
fontWeight: '800',
},
profileName: {
fontSize: 18,
fontWeight: '700',
color: colors.text,
},
profileRole: {
fontSize: 13,
color: colors.textSecondary,
marginTop: 4,
},
sectionCard: {
backgroundColor: colors.card,
borderRadius: 14,
padding: 16,
marginBottom: 20,
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
sectionHeader: {
fontSize: 14,
fontWeight: '700',
color: colors.text,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 16,
borderBottomWidth: 1,
borderBottomColor: colors.border,
paddingBottom: 8,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 14,
},
infoLabel: {
fontSize: 14,
color: colors.textSecondary,
},
infoValue: {
fontSize: 14,
fontWeight: '600',
color: colors.text,
},
infoValueStatus: {
fontSize: 13,
fontWeight: '700',
color: '#10B981',
},
preferenceRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
preferenceLabelGroup: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginRight: 12,
},
preferenceText: {
fontSize: 14,
color: colors.text,
fontWeight: '500',
},
signOutButton: {
backgroundColor: '#EF4444',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
height: 48,
marginTop: 10,
},
buttonIcon: {
marginRight: 8,
},
signOutButtonText: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '700',
},
});
export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
padding: 16,
paddingBottom: 36,
},
headerCard: {
backgroundColor: colors.card,
borderRadius: 18,
padding: 20,
alignItems: 'center',
marginBottom: 16,
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 6,
elevation: 3,
},
avatarWrapper: {
position: 'relative',
marginBottom: 12,
},
avatarImage: {
width: 84,
height: 84,
borderRadius: 42,
borderWidth: 3,
borderColor: colors.icon,
},
avatarCircle: {
width: 84,
height: 84,
borderRadius: 42,
backgroundColor: colors.icon,
justifyContent: 'center',
alignItems: 'center',
shadowColor: colors.icon,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 4,
},
avatarInitial: {
color: '#FFFFFF',
fontSize: 34,
fontWeight: '800',
},
onlineDot: {
position: 'absolute',
bottom: 2,
right: 4,
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#10B981',
borderWidth: 2.5,
borderColor: colors.card,
},
profileName: {
fontSize: 20,
fontWeight: '700',
color: colors.text,
textAlign: 'center',
marginBottom: 4,
},
profileEmail: {
fontSize: 13,
color: colors.textSecondary,
textAlign: 'center',
marginBottom: 10,
},
roleBadge: {
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 20,
backgroundColor: `${colors.icon}15`,
},
roleBadgeText: {
fontSize: 12,
fontWeight: '700',
color: colors.icon,
letterSpacing: 0.3,
},
// Stats Summary Row
statsRow: {
flexDirection: 'row',
gap: 12,
marginBottom: 16,
},
statBox: {
flex: 1,
backgroundColor: colors.card,
borderRadius: 14,
padding: 14,
alignItems: 'center',
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 4,
elevation: 2,
},
statValue: {
fontSize: 18,
fontWeight: '700',
color: colors.text,
marginBottom: 2,
},
statLabel: {
fontSize: 11,
fontWeight: '600',
color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 0.3,
},
// Section Cards
sectionCard: {
backgroundColor: colors.card,
borderRadius: 16,
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 6,
marginBottom: 16,
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
sectionHeader: {
fontSize: 13,
fontWeight: '700',
color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 0.6,
marginBottom: 8,
},
preferenceRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
noBorder: {
borderBottomWidth: 0,
},
preferenceLabelGroup: {
flexDirection: 'row',
alignItems: 'center',
},
iconWrapper: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.surface,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
preferenceText: {
fontSize: 14,
color: colors.text,
fontWeight: '500',
},
// Sign Out Button
signOutButton: {
backgroundColor: '#EF4444',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 12,
height: 50,
marginTop: 8,
shadowColor: '#EF4444',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6,
elevation: 3,
},
buttonIcon: {
marginRight: 8,
},
signOutButtonText: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '700',
letterSpacing: 0.3,
},
});

View File

@ -55,3 +55,25 @@ export interface LeadCountItem {
isdefault: string | number;
count: number;
}
export interface AddLeadPayload {
staff_id: string;
name: string;
company?: string;
email: string;
phonenumber?: string;
website?: string;
source: string;
status: string;
address?: string;
city?: string;
state?: string;
zip?: string;
country?: string;
}
export interface AddLeadResponse {
status: boolean;
message: string;
}

View File

@ -1,7 +1,7 @@
import { route } from "../utils/route";
export const menuItems = [
{ label: 'Home / Tabs', route: 'home', icon: 'home-outline' },
{ label: 'Dashboard', route: 'home', icon: 'grid-outline' },
{ label: 'Customers', route: route.customers, icon: 'business-outline' },
{ label: 'Proposals', route: route.proposals, icon: 'document-text-outline' },
{ label: 'Estimates', route: route.estimates, icon: 'calculator-outline' },

View File

@ -1,5 +1,4 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Platform } from 'react-native';
import { ThemeColors } from '../theme';
export const getStyles = (colors: ThemeColors) =>
@ -8,37 +7,90 @@ export const getStyles = (colors: ThemeColors) =>
flex: 1,
backgroundColor: colors.drawerBg,
},
header: {
// Header
headerContainer: {
backgroundColor: colors.drawerHeader,
padding: 24,
paddingTop: 48,
paddingHorizontal: 20,
paddingTop: Platform.OS === 'ios' ? 56 : 44,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
},
userHeaderTouchable: {
flexDirection: 'row',
alignItems: 'center',
},
logoCircle: {
width: 48,
height: 48,
borderRadius: 24,
avatarWrapper: {
position: 'relative',
marginRight: 14,
},
avatarImage: {
width: 52,
height: 52,
borderRadius: 26,
borderWidth: 2,
borderColor: colors.icon,
},
avatarCircle: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: colors.icon,
justifyContent: 'center',
alignItems: 'center',
shadowColor: colors.icon,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.3,
shadowRadius: 5,
elevation: 4,
},
avatarInitial: {
color: '#FFFFFF',
fontSize: 22,
fontWeight: '800',
},
onlineDot: {
position: 'absolute',
bottom: 0,
right: 0,
width: 13,
height: 13,
borderRadius: 6.5,
backgroundColor: '#10B981',
borderWidth: 2,
borderColor: colors.drawerHeader,
},
headerDetails: {
marginLeft: 14,
flex: 1,
},
tenantName: {
userName: {
fontSize: 16,
fontWeight: '800',
fontWeight: '700',
color: '#FFFFFF',
marginBottom: 2,
},
adminEmail: {
userEmail: {
fontSize: 12,
color: '#94A3B8',
marginTop: 2,
marginBottom: 6,
},
roleBadge: {
alignSelf: 'flex-start',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
backgroundColor: 'rgba(255, 255, 255, 0.12)',
},
roleBadgeText: {
fontSize: 10,
fontWeight: '700',
color: '#E2E8F0',
letterSpacing: 0.3,
},
// Navigation Menu
scrollContent: {
paddingVertical: 16,
paddingVertical: 14,
},
menuContainer: {
paddingHorizontal: 12,
@ -46,41 +98,58 @@ export const getStyles = (colors: ThemeColors) =>
itemWrapper: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 10,
marginBottom: 6,
paddingVertical: 11,
paddingHorizontal: 14,
borderRadius: 12,
marginBottom: 4,
},
itemWrapperActive: {
backgroundColor: colors.border,
backgroundColor: colors.card,
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 4,
elevation: 1,
},
activeIndicatorBar: {
width: 3.5,
height: 20,
borderRadius: 2,
backgroundColor: colors.icon,
marginRight: 10,
},
itemIcon: {
marginRight: 14,
marginRight: 12,
},
itemLabel: {
fontSize: 14,
fontWeight: '600',
fontWeight: '500',
color: colors.textSecondary,
flex: 1,
},
itemLabelActive: {
color: colors.icon,
color: colors.text,
fontWeight: '700',
},
// Footer
footer: {
borderTopWidth: 1,
borderTopColor: colors.border,
padding: 16,
paddingBottom: 24,
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: colors.drawerBg,
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 10,
backgroundColor: '#FEF2F2',
},
logoutIcon: {
marginRight: 14,
marginRight: 12,
},
logoutText: {
fontSize: 14,

View File

@ -1,13 +1,20 @@
import React from 'react';
import { Text, View, TouchableOpacity, ScrollView } from 'react-native';
import {
Text,
View,
TouchableOpacity,
ScrollView,
Image,
} from 'react-native';
import { DrawerContentComponentProps } from '@react-navigation/drawer';
import Icon from 'react-native-vector-icons/Ionicons';
import { menuItems } from '@mock-data';
import { useTheme } from '@theme';
import { DrawerItemProps } from '@interfaces';
import { route } from '@utils';
import { getStyles } from './customDrawerContent.style';
import { useAppDispatch, useAppSelector, logout, RootState } from '@store';
import { useAppDispatch, logout } from '@store';
const DrawerItem = ({ label, iconName, focused, onPress }: DrawerItemProps) => {
const { theme: colors } = useTheme();
@ -17,11 +24,11 @@ const DrawerItem = ({ label, iconName, focused, onPress }: DrawerItemProps) => {
<TouchableOpacity
style={[styles.itemWrapper, focused && styles.itemWrapperActive]}
activeOpacity={0.7}
onPress={onPress}
>
onPress={onPress}>
{focused && <View style={styles.activeIndicatorBar} />}
<Icon
name={iconName}
size={22}
size={20}
color={focused ? colors.icon : colors.textSecondary}
style={styles.itemIcon}
/>
@ -38,6 +45,8 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const userData = useAppSelector((state: RootState) => state.auth.user_data);
const handleLogout = async () => {
try {
dispatch(logout());
@ -46,24 +55,72 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
}
navigation.reset({
index: 0,
routes: [{ name: 'AuthStack' }], // root-level stack name stays as-is
routes: [{ name: 'AuthStack' }],
});
};
const activeRouteName = state.routeNames[state.index];
// User details
const fullName =
userData?.full_name ||
`${userData?.firstname || ''} ${userData?.lastname || ''}`.trim() ||
'User';
const email = userData?.email || 'N/A';
const isAdmin = userData?.admin === '1';
const roleText = isAdmin ? 'Administrator' : 'Staff Member';
const isActive = userData?.active === '1';
const hasProfileImage =
userData?.profile_image && userData.profile_image.startsWith('http');
const initials =
fullName
.split(' ')
.filter(Boolean)
.map(n => n[0])
.join('')
.substring(0, 2)
.toUpperCase() || 'U';
return (
<View style={styles.container}>
{/* Drawer Header Profile */}
<View style={styles.header}>
<View style={styles.logoCircle}>
<Icon name="cube" size={28} color="#FFFFFF" />
</View>
<View style={styles.headerDetails}>
<Text style={styles.tenantName}>Convex CRM</Text>
<Text style={styles.adminEmail}>admin@convex.com</Text>
</View>
{/* ── Drawer Header (User Profile) ── */}
<View style={styles.headerContainer}>
<TouchableOpacity
style={styles.userHeaderTouchable}
activeOpacity={0.8}
onPress={() => navigation.navigate('home', { screen: route.profile })}>
<View style={styles.avatarWrapper}>
{hasProfileImage ? (
<Image
source={{ uri: userData!.profile_image }}
style={styles.avatarImage}
/>
) : (
<View style={styles.avatarCircle}>
<Text style={styles.avatarInitial}>{initials}</Text>
</View>
)}
{isActive && <View style={styles.onlineDot} />}
</View>
<View style={styles.headerDetails}>
<Text style={styles.userName} numberOfLines={1}>
{fullName}
</Text>
<Text style={styles.userEmail} numberOfLines={1}>
{email}
</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleBadgeText}>{roleText}</Text>
</View>
</View>
<Icon name="chevron-forward" size={18} color="#94A3B8" />
</TouchableOpacity>
</View>
{/* ── Navigation Menu List ── */}
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.menuContainer}>
{menuItems.map(item => {
@ -81,16 +138,19 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
</View>
</ScrollView>
{/* Drawer Footer / Sign Out */}
{/* ── Drawer Footer (Logout) ── */}
<View style={styles.footer}>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<TouchableOpacity
style={styles.logoutButton}
activeOpacity={0.7}
onPress={handleLogout}>
<Icon
name="log-out-outline"
size={20}
color="#EF4444"
style={styles.logoutIcon}
/>
<Text style={styles.logoutText}>Log Out</Text>
<Text style={styles.logoutText}>Sign Out</Text>
</TouchableOpacity>
</View>
</View>

View File

@ -12,7 +12,7 @@ import { useTheme } from '@theme';
export type LeadsStackParamList = Pick<
RouteParams,
'leads' | 'leadDetails' | 'leadProposals' | 'leadTasks' | 'leadNotes'
'leadsList' | 'leadDetails' | 'leadProposals' | 'leadTasks' | 'leadNotes'
>;
const Stack = createNativeStackNavigator<LeadsStackParamList>();
@ -36,7 +36,7 @@ export const LeadsStack = () => {
headerShadowVisible: false,
}}>
<Stack.Screen
name={route.leads}
name={route.leadsList}
component={LeadsScreen}
options={{ headerTitle: 'My Leads' }}
/>

View File

@ -7,25 +7,50 @@ export const getStyles = (colors: ThemeColors) =>
backgroundColor: colors.tabBar,
borderTopWidth: 1,
borderTopColor: colors.border,
height: Platform.OS === 'ios' ? 84 : 64,
height: Platform.OS === 'ios' ? 86 : 68,
paddingBottom: Platform.OS === 'ios' ? 24 : 10,
// paddingTop: 8,
shadowColor: '#5B4CF5',
paddingTop: 6,
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.06,
shadowOpacity: 0.08,
shadowRadius: 12,
elevation: 8,
elevation: 10,
},
tabBarLabel: {
fontSize: 12,
fontSize: 11,
fontWeight: '600',
letterSpacing: 0.2,
marginTop: 2,
},
iconContainer: {
width: 42,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
},
iconContainerFocused: {
backgroundColor: `${colors.icon}18`,
},
addLeadButtonWrapper: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.icon,
justifyContent: 'center',
alignItems: 'center',
marginTop: -14,
shadowColor: colors.icon,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.4,
shadowRadius: 8,
elevation: 6,
},
header: {
backgroundColor: colors.header,
borderBottomWidth: 1,
borderBottomColor: colors.border,
shadowColor: '#5B4CF5',
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 6,

View File

@ -1,5 +1,5 @@
import React from 'react';
import { TouchableOpacity } from 'react-native';
import { TouchableOpacity, View } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { DrawerNavigationProp } from '@react-navigation/drawer';
import Icon from 'react-native-vector-icons/Ionicons';
@ -28,20 +28,31 @@ export const TabStack = () => {
return (
<Tab.Navigator
screenOptions={({ route: tabRoute }) => ({
tabBarIcon: ({ focused, color, size }) => {
tabBarIcon: ({ focused, color }) => {
if (tabRoute.name === route.addLead) {
return (
<View style={styles.addLeadButtonWrapper}>
<Icon name="add" size={24} color="#FFFFFF" />
</View>
);
}
let iconName = 'square';
if (tabRoute.name === route.dashboard) {
iconName = focused ? 'grid' : 'grid-outline';
} else if (tabRoute.name === route.leads) {
iconName = focused ? 'funnel' : 'funnel-outline';
} else if (tabRoute.name === route.addLead) {
iconName = focused ? 'add-circle' : 'add-circle-outline';
} else if (tabRoute.name === route.customers) {
iconName = focused ? 'people' : 'people-outline';
} else if (tabRoute.name === route.profile) {
iconName = focused ? 'person' : 'person-outline';
}
return <Icon name={iconName} size={size || 22} color={color} />;
return (
<View style={[styles.iconContainer, focused && styles.iconContainerFocused]}>
<Icon name={iconName} size={20} color={color} />
</View>
);
},
tabBarActiveTintColor: colors.icon,
tabBarInactiveTintColor: colors.textMuted,
@ -86,7 +97,7 @@ export const TabStack = () => {
name={route.addLead}
component={AddLeadScreen}
options={{
tabBarLabel: 'Add',
tabBarLabel: 'Add Lead',
headerTitle: 'Add Lead',
}}
/>

View File

@ -5,6 +5,7 @@ import sourceListReducer from './commonReducers/sourcelist/reducers';
import countryListReducer from './commonReducers/countrylist/reducers';
import leadsReducer from '../features/leads/reducers';
import leadDetailsReducer from '../features/leadDetails/reducers';
import addLeadReducer from '../features/addLead/reducers';
const appReducer = combineReducers({
auth: authReducer,
@ -13,6 +14,7 @@ const appReducer = combineReducers({
countryList: countryListReducer,
leads: leadsReducer,
leadDetails: leadDetailsReducer,
addLead: addLeadReducer,
});
const rootReducer = (state: any, action: any) => {

View File

@ -16,6 +16,7 @@ export const route = {
// Sub screens
addLead: 'addLead',
leadsList: 'leadsList',
leadDetails: 'leadDetails',
leadProposals: 'leadProposals',
leadTasks: 'leadTasks',
@ -38,6 +39,7 @@ export type RouteParams = {
tasks: undefined;
tickets: undefined;
addLead: undefined;
leadsList: undefined;
leadDetails: { lead: any };
leadProposals: { leadId: string };
leadTasks: { leadId: string };