feat(app): improve design and integrate apis
This commit is contained in:
parent
168c9fd39c
commit
0535845362
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -10,4 +10,5 @@ export interface FormPickerProps {
|
||||
options: FormPickerOption[];
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
searchable?: boolean;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,3 +9,5 @@ export * from './addItemButton';
|
||||
export * from './actionButton';
|
||||
export * from './checkboxWithLabel';
|
||||
export * from './statCard';
|
||||
export * from './profileInfoRow';
|
||||
|
||||
|
||||
2
app/components/profileInfoRow/index.ts
Normal file
2
app/components/profileInfoRow/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './profileInfoRow';
|
||||
export * from './profileInfoRow.props';
|
||||
13
app/components/profileInfoRow/profileInfoRow.props.ts
Normal file
13
app/components/profileInfoRow/profileInfoRow.props.ts
Normal 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>;
|
||||
}
|
||||
52
app/components/profileInfoRow/profileInfoRow.styles.ts
Normal file
52
app/components/profileInfoRow/profileInfoRow.styles.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
73
app/components/profileInfoRow/profileInfoRow.tsx
Normal file
73
app/components/profileInfoRow/profileInfoRow.tsx
Normal 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;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from './addLead.screen';
|
||||
export * from './thunk';
|
||||
export * from './reducers';
|
||||
|
||||
38
app/features/addLead/reducers.ts
Normal file
38
app/features/addLead/reducers.ts
Normal 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;
|
||||
16
app/features/addLead/thunk.ts
Normal file
16
app/features/addLead/thunk.ts
Normal 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');
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -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 & 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 & 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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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' },
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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' }}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user