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';
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,4 +10,5 @@ export interface FormPickerProps {
|
|||||||
options: FormPickerOption[];
|
options: FormPickerOption[];
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
searchable?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
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 {
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export * from './addLead.screen';
|
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 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 & 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 & Privacy</Text>
|
<Text style={styles.preferenceText}>Security & 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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' },
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user