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'; import { api } from '@utils';
export const getLeadsApi = async ( export const getLeadsApi = async (
@ -15,3 +15,25 @@ export const getLeadCountListApi = async (
const url = `/api/leadcountlist/${staffId}`; const url = `/api/leadcountlist/${staffId}`;
return await api.get<LeadCountItem[]>(url); 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[]; options: FormPickerOption[];
required?: boolean; required?: boolean;
placeholder?: string; 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'; import { ThemeColors } from '../../theme';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
export const getStyles = (colors: ThemeColors) => export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({ StyleSheet.create({
container: { container: {
@ -25,13 +27,81 @@ export const getStyles = (colors: ThemeColors) =>
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 13,
}, },
pickerText: { pickerText: {
fontSize: 15, fontSize: 15,
color: colors.text, color: colors.text,
flex: 1,
}, },
placeholder: { placeholder: {
color: colors.textMuted, 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 React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import {
View,
Text,
TouchableOpacity,
Modal,
FlatList,
TextInput,
SafeAreaView,
Platform,
StatusBar,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { getStyles } from './formPicker.styles'; import { getStyles } from './formPicker.styles';
import { FormPickerProps } from './formPicker.props'; import { FormPickerProps, FormPickerOption } from './formPicker.props';
export const FormPicker: React.FC<FormPickerProps> = ({ export const FormPicker: React.FC<FormPickerProps> = ({
label, label,
@ -12,33 +22,112 @@ export const FormPicker: React.FC<FormPickerProps> = ({
options, options,
required = false, required = false,
placeholder = 'Select...', placeholder = 'Select...',
searchable = false,
}) => { }) => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const [modalVisible, setModalVisible] = useState(false);
const [query, setQuery] = useState('');
const selectedOption = options.find(opt => opt.value === value); 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 ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.label}> <Text style={styles.label}>
{label} {label}
{required && <Text style={styles.required}>*</Text>} {required && <Text style={styles.required}> *</Text>}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={styles.pickerContainer} style={styles.pickerContainer}
onPress={() => { activeOpacity={0.7}
// TODO: Implement modal picker or action sheet onPress={() => setModalVisible(true)}>
console.log('Picker pressed');
}}>
<Text <Text
style={[ style={[styles.pickerText, !selectedOption && styles.placeholder]}>
styles.pickerText,
!selectedOption && styles.placeholder,
]}>
{selectedOption?.label || placeholder} {selectedOption?.label || placeholder}
</Text> </Text>
<Icon name="chevron-down" size={20} color={colors.textSecondary} /> <Icon name="chevron-down" size={18} color={colors.textSecondary} />
</TouchableOpacity> </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>
); );
}; };

View File

@ -9,3 +9,5 @@ export * from './addItemButton';
export * from './actionButton'; export * from './actionButton';
export * from './checkboxWithLabel'; export * from './checkboxWithLabel';
export * from './statCard'; 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 { import {
Text,
View, View,
TextInput, Text,
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
KeyboardAvoidingView, KeyboardAvoidingView,
@ -11,171 +10,243 @@ import {
} from 'react-native'; } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { getStyles } from './addLead.styles'; 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 = () => { export const AddLeadScreen = () => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors); 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 [company, setCompany] = useState('');
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [phone, setPhone] = 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 = () => { const handleSubmit = () => {
if (!name.trim() || !company.trim() || !email.trim()) { if (!fullName.trim() || !email.trim() || !source || !status) {
Alert.alert('Error', 'Please fill in Name, Company, and Email fields.'); Alert.alert('Error', 'Full Name, Email, Source and Status are required.');
return; return;
} }
Alert.alert( const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
'Success', if (!emailRegex.test(email.trim())) {
`Lead "${name}" for "${company}" has been created!`, Alert.alert('Error', 'Please enter a valid email address.');
[ return;
{ }
text: 'OK',
onPress: () => {
setName(''); const staffId = userData?.staffid || '';
setCompany(''); if (!staffId) {
setEmail(''); Alert.alert('Error', 'User session invalid. Please log in again.');
setPhone(''); return;
setValue(''); }
},
}, dispatch(
], addLead({
staff_id: staffId,
name: fullName,
company,
email,
phonenumber: phone,
website,
source,
status,
address,
city,
state,
zip,
country,
}),
); );
}; };
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container} style={styles.container}>
>
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled">
>
{/* Basic Info */}
<View style={styles.formCard}> <View style={styles.formCard}>
<Text style={styles.sectionHeader}>Lead Information</Text> <Text style={styles.sectionHeader}>Basic Information</Text>
{/* Name Field */} <FormInput
<View style={styles.inputContainer}> label="Company"
<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" placeholder="Acme Corp"
placeholderTextColor={colors.textMuted}
value={company} value={company}
onChangeText={setCompany} onChangeText={setCompany}
leftIcon={<Icon name="business-outline" size={18} color={colors.textSecondary} />}
/> />
</View> <FormInput
</View> label="Full Name"
required
{/* Email Field */} placeholder="John Doe"
<View style={styles.inputContainer}> value={fullName}
<Text style={styles.label}>Email Address *</Text> onChangeText={setFullName}
<View style={styles.inputWrapper}> leftIcon={<Icon name="person-outline" size={18} color={colors.textSecondary} />}
<Icon
name="mail-outline"
size={18}
color={colors.textSecondary}
style={styles.inputIcon}
/> />
<TextInput <FormInput
style={styles.input} label="Email"
placeholder="johndoe@acme.com" required
placeholderTextColor={colors.textMuted} placeholder="john@acme.com"
keyboardType="email-address"
autoCapitalize="none"
value={email} value={email}
onChangeText={setEmail} onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
leftIcon={<Icon name="mail-outline" size={18} color={colors.textSecondary} />}
/> />
</View> <FormInput
</View> label="Phone Number"
{/* 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" placeholder="+1 (555) 000-0000"
placeholderTextColor={colors.textMuted}
keyboardType="phone-pad"
value={phone} value={phone}
onChangeText={setPhone} 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>
</View> </View>
{/* Deal Value Field */} {/* Classification */}
<View style={styles.inputContainer}> <View style={[styles.formCard, styles.cardSpacing]}>
<Text style={styles.label}>Estimated Value ($)</Text> <Text style={styles.sectionHeader}>Classification</Text>
<View style={styles.inputWrapper}>
<Icon <FormPicker
name="cash-outline" label="Source"
size={18} required
color={colors.textSecondary} value={source}
style={styles.inputIcon} onValueChange={setSource}
options={sourceOptions}
placeholder="Select source..."
/> />
<TextInput <FormPicker
style={styles.input} label="Status"
placeholder="5,000" required
placeholderTextColor={colors.textMuted} value={status}
keyboardType="numeric" onValueChange={setStatus}
value={value} options={statusOptions}
onChangeText={setValue} placeholder="Select status..."
/> />
</View> </View>
</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 <TouchableOpacity
style={styles.submitButton} style={[styles.submitButton, loading && { opacity: 0.7 }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={handleSubmit} disabled={loading}
> onPress={handleSubmit}>
<Icon <Icon name="checkmark-circle-outline" size={20} color="#FFFFFF" style={styles.buttonIcon} />
name="checkmark" <Text style={styles.submitButtonText}>{loading ? 'Creating Lead...' : 'Create Lead'}</Text>
size={20}
color="#FFFFFF"
style={styles.buttonIcon}
/>
<Text style={styles.submitButtonText}>Create New Lead</Text>
</TouchableOpacity> </TouchableOpacity>
</View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );

View File

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

View File

@ -1 +1,3 @@
export * from './addLead.screen'; 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 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 { useNavigation } from '@react-navigation/native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { getStyles } from './profile.styles'; import { getStyles } from './profile.styles';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ProfileInfoRow } from '@components';
import { useAppDispatch, logout } from '@store'; import { useAppDispatch, useAppSelector, logout, RootState } from '@store';
export const ProfileScreen = () => { export const ProfileScreen = () => {
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
@ -13,6 +20,8 @@ export const ProfileScreen = () => {
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userData = useAppSelector((state: RootState) => state.auth.user_data);
const handleLogout = async () => { const handleLogout = async () => {
try { try {
dispatch(logout()); dispatch(logout());
@ -21,53 +30,193 @@ export const ProfileScreen = () => {
} }
navigation.reset({ navigation.reset({
index: 0, 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 ( return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Profile Header */} {/* ── Header Card ── */}
<View style={styles.profileHeader}> <View style={styles.headerCard}>
<View style={styles.avatarWrapper}>
{hasProfileImage ? (
<Image
source={{ uri: userData!.profile_image }}
style={styles.avatarImage}
/>
) : (
<View style={styles.avatarCircle}> <View style={styles.avatarCircle}>
<Text style={styles.avatarInitial}>A</Text> <Text style={styles.avatarInitial}>{initials}</Text>
</View> </View>
<Text style={styles.profileName}>Workspace Administrator</Text> )}
<Text style={styles.profileRole}>Owner / Administrator</Text> {isActive && <View style={styles.onlineDot} />}
</View> </View>
{/* Account Details list */} <Text style={styles.profileName}>{fullName}</Text>
<Text style={styles.profileEmail}>{email}</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleBadgeText}>{roleText}</Text>
</View>
</View>
{/* ── 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}> <View style={styles.sectionCard}>
<Text style={styles.sectionHeader}>Workspace Details</Text> <Text style={styles.sectionHeader}>Personal Details</Text>
<ProfileInfoRow
<View style={styles.infoRow}> icon="person-outline"
<Text style={styles.infoLabel}>Tenancy</Text> label="Full Name"
<Text style={styles.infoValue}>convex-crm-tenant</Text> 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> </View>
<View style={styles.infoRow}> {/* ── System & Account ── */}
<Text style={styles.infoLabel}>Email</Text> <View style={styles.sectionCard}>
<Text style={styles.infoValue}>admin@convex.com</Text> <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> </View>
<View style={styles.infoRow}> {/* ── Social Profiles ── */}
<Text style={styles.infoLabel}>Status</Text> {hasSocials && (
<Text style={styles.infoValueStatus}>Active</Text> <View style={styles.sectionCard}>
</View> <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> </View>
)}
{/* Settings list */} {/* ── Preferences ── */}
<View style={styles.sectionCard}> <View style={styles.sectionCard}>
<Text style={styles.sectionHeader}>Preferences</Text> <Text style={styles.sectionHeader}>Preferences</Text>
<View style={styles.preferenceRow}> <View style={styles.preferenceRow}>
<View style={styles.preferenceLabelGroup}> <View style={styles.preferenceLabelGroup}>
<View style={styles.iconWrapper}>
<Icon <Icon
name={isDark ? 'moon' : 'moon-outline'} name={isDark ? 'moon' : 'moon-outline'}
size={20} size={18}
color={colors.textSecondary} color={colors.icon}
style={styles.icon}
/> />
</View>
<Text style={styles.preferenceText}>Dark Mode</Text> <Text style={styles.preferenceText}>Dark Mode</Text>
</View> </View>
<Switch <Switch
@ -78,42 +227,49 @@ export const ProfileScreen = () => {
/> />
</View> </View>
<TouchableOpacity style={styles.preferenceRow}> <TouchableOpacity style={styles.preferenceRow} activeOpacity={0.7}>
<View style={styles.preferenceLabelGroup}> <View style={styles.preferenceLabelGroup}>
<View style={styles.iconWrapper}>
<Icon <Icon
name="notifications-outline" name="notifications-outline"
size={20} size={18}
color={colors.textSecondary} color={colors.icon}
style={styles.icon}
/> />
</View>
<Text style={styles.preferenceText}>Push Notifications</Text> <Text style={styles.preferenceText}>Push Notifications</Text>
</View> </View>
<Icon name="chevron-forward" size={18} color={colors.textMuted} /> <Icon name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.preferenceRow}> <TouchableOpacity
style={[styles.preferenceRow, styles.noBorder]}
activeOpacity={0.7}>
<View style={styles.preferenceLabelGroup}> <View style={styles.preferenceLabelGroup}>
<View style={styles.iconWrapper}>
<Icon <Icon
name="shield-checkmark-outline" name="shield-checkmark-outline"
size={20} size={18}
color={colors.textSecondary} color={colors.icon}
style={styles.icon}
/> />
</View>
<Text style={styles.preferenceText}>Security &amp; Privacy</Text> <Text style={styles.preferenceText}>Security &amp; Privacy</Text>
</View> </View>
<Icon name="chevron-forward" size={18} color={colors.textMuted} /> <Icon name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Sign Out Button */} {/* ── Sign Out ── */}
<TouchableOpacity style={styles.signOutButton} onPress={handleLogout}> <TouchableOpacity
style={styles.signOutButton}
activeOpacity={0.8}
onPress={handleLogout}>
<Icon <Icon
name="log-out-outline" name="log-out-outline"
size={20} size={20}
color="#FFFFFF" color="#FFFFFF"
style={styles.buttonIcon} style={styles.buttonIcon}
/> />
<Text style={styles.signOutButtonText}>Sign Out from Workspace</Text> <Text style={styles.signOutButtonText}>Sign Out</Text>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
); );

View File

@ -1,47 +1,132 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { ThemeColors } from '../../theme'; import { ThemeColors } from '../../theme';
export const getStyles = (colors: ThemeColors) => StyleSheet.create({ export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
content: { content: {
padding: 16, padding: 16,
paddingBottom: 36,
}, },
profileHeader: { headerCard: {
backgroundColor: colors.card,
borderRadius: 18,
padding: 20,
alignItems: 'center', alignItems: 'center',
marginVertical: 24, 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: { avatarCircle: {
width: 80, width: 84,
height: 80, height: 84,
borderRadius: 40, borderRadius: 42,
backgroundColor: colors.icon, backgroundColor: colors.icon,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 16, shadowColor: colors.icon,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 4,
}, },
avatarInitial: { avatarInitial: {
color: '#FFFFFF', color: '#FFFFFF',
fontSize: 32, fontSize: 34,
fontWeight: '800', fontWeight: '800',
}, },
onlineDot: {
position: 'absolute',
bottom: 2,
right: 4,
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#10B981',
borderWidth: 2.5,
borderColor: colors.card,
},
profileName: { 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, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: colors.text, color: colors.text,
marginBottom: 2,
}, },
profileRole: { statLabel: {
fontSize: 13, fontSize: 11,
fontWeight: '600',
color: colors.textSecondary, color: colors.textSecondary,
marginTop: 4, textTransform: 'uppercase',
letterSpacing: 0.3,
}, },
// Section Cards
sectionCard: { sectionCard: {
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: 14, borderRadius: 16,
padding: 16, paddingHorizontal: 16,
marginBottom: 20, paddingTop: 16,
paddingBottom: 6,
marginBottom: 16,
shadowColor: '#0F172A', shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05, shadowOpacity: 0.05,
@ -49,34 +134,12 @@ export const getStyles = (colors: ThemeColors) => StyleSheet.create({
elevation: 2, elevation: 2,
}, },
sectionHeader: { 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, fontSize: 13,
fontWeight: '700', fontWeight: '700',
color: '#10B981', color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 0.6,
marginBottom: 8,
}, },
preferenceRow: { preferenceRow: {
flexDirection: 'row', flexDirection: 'row',
@ -86,11 +149,20 @@ export const getStyles = (colors: ThemeColors) => StyleSheet.create({
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.border, borderBottomColor: colors.border,
}, },
noBorder: {
borderBottomWidth: 0,
},
preferenceLabelGroup: { preferenceLabelGroup: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
}, },
icon: { iconWrapper: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.surface,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12, marginRight: 12,
}, },
preferenceText: { preferenceText: {
@ -98,14 +170,20 @@ export const getStyles = (colors: ThemeColors) => StyleSheet.create({
color: colors.text, color: colors.text,
fontWeight: '500', fontWeight: '500',
}, },
// Sign Out Button
signOutButton: { signOutButton: {
backgroundColor: '#EF4444', backgroundColor: '#EF4444',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderRadius: 10, borderRadius: 12,
height: 48, height: 50,
marginTop: 10, marginTop: 8,
shadowColor: '#EF4444',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6,
elevation: 3,
}, },
buttonIcon: { buttonIcon: {
marginRight: 8, marginRight: 8,
@ -114,5 +192,6 @@ export const getStyles = (colors: ThemeColors) => StyleSheet.create({
color: '#FFFFFF', color: '#FFFFFF',
fontSize: 15, fontSize: 15,
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.3,
}, },
}); });

View File

@ -55,3 +55,25 @@ export interface LeadCountItem {
isdefault: string | number; isdefault: string | number;
count: 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"; import { route } from "../utils/route";
export const menuItems = [ 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: 'Customers', route: route.customers, icon: 'business-outline' },
{ label: 'Proposals', route: route.proposals, icon: 'document-text-outline' }, { label: 'Proposals', route: route.proposals, icon: 'document-text-outline' },
{ label: 'Estimates', route: route.estimates, icon: 'calculator-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'; import { ThemeColors } from '../theme';
export const getStyles = (colors: ThemeColors) => export const getStyles = (colors: ThemeColors) =>
@ -8,37 +7,90 @@ export const getStyles = (colors: ThemeColors) =>
flex: 1, flex: 1,
backgroundColor: colors.drawerBg, backgroundColor: colors.drawerBg,
}, },
header: { // Header
headerContainer: {
backgroundColor: colors.drawerHeader, backgroundColor: colors.drawerHeader,
padding: 24, paddingHorizontal: 20,
paddingTop: 48, paddingTop: Platform.OS === 'ios' ? 56 : 44,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
},
userHeaderTouchable: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
}, },
logoCircle: { avatarWrapper: {
width: 48, position: 'relative',
height: 48, marginRight: 14,
borderRadius: 24, },
avatarImage: {
width: 52,
height: 52,
borderRadius: 26,
borderWidth: 2,
borderColor: colors.icon,
},
avatarCircle: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: colors.icon, backgroundColor: colors.icon,
justifyContent: 'center', justifyContent: 'center',
alignItems: '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: { headerDetails: {
marginLeft: 14,
flex: 1, flex: 1,
}, },
tenantName: { userName: {
fontSize: 16, fontSize: 16,
fontWeight: '800', fontWeight: '700',
color: '#FFFFFF', color: '#FFFFFF',
marginBottom: 2,
}, },
adminEmail: { userEmail: {
fontSize: 12, fontSize: 12,
color: '#94A3B8', 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: { scrollContent: {
paddingVertical: 16, paddingVertical: 14,
}, },
menuContainer: { menuContainer: {
paddingHorizontal: 12, paddingHorizontal: 12,
@ -46,41 +98,58 @@ export const getStyles = (colors: ThemeColors) =>
itemWrapper: { itemWrapper: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: 12, paddingVertical: 11,
paddingHorizontal: 16, paddingHorizontal: 14,
borderRadius: 10, borderRadius: 12,
marginBottom: 6, marginBottom: 4,
}, },
itemWrapperActive: { 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: { itemIcon: {
marginRight: 14, marginRight: 12,
}, },
itemLabel: { itemLabel: {
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '500',
color: colors.textSecondary, color: colors.textSecondary,
flex: 1,
}, },
itemLabelActive: { itemLabelActive: {
color: colors.icon, color: colors.text,
fontWeight: '700', fontWeight: '700',
}, },
// Footer
footer: { footer: {
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.border, borderTopColor: colors.border,
padding: 16, paddingHorizontal: 16,
paddingBottom: 24, paddingVertical: 14,
backgroundColor: colors.drawerBg,
}, },
logoutButton: { logoutButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: 12, paddingVertical: 10,
paddingHorizontal: 16, paddingHorizontal: 14,
borderRadius: 10, borderRadius: 10,
backgroundColor: '#FEF2F2',
}, },
logoutIcon: { logoutIcon: {
marginRight: 14, marginRight: 12,
}, },
logoutText: { logoutText: {
fontSize: 14, fontSize: 14,

View File

@ -1,13 +1,20 @@
import React from 'react'; 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 { DrawerContentComponentProps } from '@react-navigation/drawer';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { menuItems } from '@mock-data'; import { menuItems } from '@mock-data';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { DrawerItemProps } from '@interfaces'; import { DrawerItemProps } from '@interfaces';
import { route } from '@utils';
import { getStyles } from './customDrawerContent.style'; 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 DrawerItem = ({ label, iconName, focused, onPress }: DrawerItemProps) => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
@ -17,11 +24,11 @@ const DrawerItem = ({ label, iconName, focused, onPress }: DrawerItemProps) => {
<TouchableOpacity <TouchableOpacity
style={[styles.itemWrapper, focused && styles.itemWrapperActive]} style={[styles.itemWrapper, focused && styles.itemWrapperActive]}
activeOpacity={0.7} activeOpacity={0.7}
onPress={onPress} onPress={onPress}>
> {focused && <View style={styles.activeIndicatorBar} />}
<Icon <Icon
name={iconName} name={iconName}
size={22} size={20}
color={focused ? colors.icon : colors.textSecondary} color={focused ? colors.icon : colors.textSecondary}
style={styles.itemIcon} style={styles.itemIcon}
/> />
@ -38,6 +45,8 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userData = useAppSelector((state: RootState) => state.auth.user_data);
const handleLogout = async () => { const handleLogout = async () => {
try { try {
dispatch(logout()); dispatch(logout());
@ -46,24 +55,72 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
} }
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: 'AuthStack' }], // root-level stack name stays as-is routes: [{ name: 'AuthStack' }],
}); });
}; };
const activeRouteName = state.routeNames[state.index]; 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 ( return (
<View style={styles.container}> <View style={styles.container}>
{/* Drawer Header Profile */} {/* ── Drawer Header (User Profile) ── */}
<View style={styles.header}> <View style={styles.headerContainer}>
<View style={styles.logoCircle}> <TouchableOpacity
<Icon name="cube" size={28} color="#FFFFFF" /> 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> </View>
)}
{isActive && <View style={styles.onlineDot} />}
</View>
<View style={styles.headerDetails}> <View style={styles.headerDetails}>
<Text style={styles.tenantName}>Convex CRM</Text> <Text style={styles.userName} numberOfLines={1}>
<Text style={styles.adminEmail}>admin@convex.com</Text> {fullName}
</Text>
<Text style={styles.userEmail} numberOfLines={1}>
{email}
</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleBadgeText}>{roleText}</Text>
</View> </View>
</View> </View>
<Icon name="chevron-forward" size={18} color="#94A3B8" />
</TouchableOpacity>
</View>
{/* ── Navigation Menu List ── */}
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.menuContainer}> <View style={styles.menuContainer}>
{menuItems.map(item => { {menuItems.map(item => {
@ -81,16 +138,19 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
</View> </View>
</ScrollView> </ScrollView>
{/* Drawer Footer / Sign Out */} {/* ── Drawer Footer (Logout) ── */}
<View style={styles.footer}> <View style={styles.footer}>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}> <TouchableOpacity
style={styles.logoutButton}
activeOpacity={0.7}
onPress={handleLogout}>
<Icon <Icon
name="log-out-outline" name="log-out-outline"
size={20} size={20}
color="#EF4444" color="#EF4444"
style={styles.logoutIcon} style={styles.logoutIcon}
/> />
<Text style={styles.logoutText}>Log Out</Text> <Text style={styles.logoutText}>Sign Out</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>

View File

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

View File

@ -7,25 +7,50 @@ export const getStyles = (colors: ThemeColors) =>
backgroundColor: colors.tabBar, backgroundColor: colors.tabBar,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.border, borderTopColor: colors.border,
height: Platform.OS === 'ios' ? 84 : 64, height: Platform.OS === 'ios' ? 86 : 68,
paddingBottom: Platform.OS === 'ios' ? 24 : 10, paddingBottom: Platform.OS === 'ios' ? 24 : 10,
// paddingTop: 8, paddingTop: 6,
shadowColor: '#5B4CF5', shadowColor: '#0F172A',
shadowOffset: { width: 0, height: -4 }, shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.06, shadowOpacity: 0.08,
shadowRadius: 12, shadowRadius: 12,
elevation: 8, elevation: 10,
}, },
tabBarLabel: { tabBarLabel: {
fontSize: 12, fontSize: 11,
fontWeight: '600', fontWeight: '600',
letterSpacing: 0.2, 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: { header: {
backgroundColor: colors.header, backgroundColor: colors.header,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.border, borderBottomColor: colors.border,
shadowColor: '#5B4CF5', shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04, shadowOpacity: 0.04,
shadowRadius: 6, shadowRadius: 6,

View File

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

View File

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

View File

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