From 0535845362dbec3f1aeb6068d4b79084706847fe Mon Sep 17 00:00:00 2001 From: uttam Date: Wed, 22 Jul 2026 17:41:07 +0530 Subject: [PATCH] feat(app): improve design and integrate apis --- app/api/leadsApi.ts | 24 +- app/components/formPicker/formPicker.props.ts | 1 + .../formPicker/formPicker.styles.ts | 74 +++- app/components/formPicker/formPicker.tsx | 115 +++++- app/components/index.ts | 2 + app/components/profileInfoRow/index.ts | 2 + .../profileInfoRow/profileInfoRow.props.ts | 13 + .../profileInfoRow/profileInfoRow.styles.ts | 52 +++ .../profileInfoRow/profileInfoRow.tsx | 73 ++++ app/features/addLead/addLead.screen.tsx | 361 +++++++++++------- app/features/addLead/addLead.styles.ts | 49 +-- app/features/addLead/index.ts | 2 + app/features/addLead/reducers.ts | 38 ++ app/features/addLead/thunk.ts | 16 + app/features/profile/profile.screen.tsx | 258 ++++++++++--- app/features/profile/profile.styles.ts | 309 +++++++++------ app/interfaces/leads.ts | 22 ++ app/mock-data/customDrawer.ts | 2 +- app/navigation/customDrawerContent.style.ts | 125 ++++-- app/navigation/customDrawerContent.tsx | 96 ++++- app/navigation/leadsStack.tsx | 4 +- app/navigation/tabStack.styles.ts | 39 +- app/navigation/tabStack.tsx | 23 +- app/store/rootReducer.ts | 2 + app/utils/route.ts | 2 + 25 files changed, 1286 insertions(+), 418 deletions(-) create mode 100644 app/components/profileInfoRow/index.ts create mode 100644 app/components/profileInfoRow/profileInfoRow.props.ts create mode 100644 app/components/profileInfoRow/profileInfoRow.styles.ts create mode 100644 app/components/profileInfoRow/profileInfoRow.tsx create mode 100644 app/features/addLead/reducers.ts create mode 100644 app/features/addLead/thunk.ts diff --git a/app/api/leadsApi.ts b/app/api/leadsApi.ts index 2fd506d..09db948 100644 --- a/app/api/leadsApi.ts +++ b/app/api/leadsApi.ts @@ -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(url); }; + +export const addLeadApi = async ( + payload: AddLeadPayload, +): Promise => { + 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('/api/leads', formData); +}; + diff --git a/app/components/formPicker/formPicker.props.ts b/app/components/formPicker/formPicker.props.ts index 4cd4f5e..90bdd9a 100644 --- a/app/components/formPicker/formPicker.props.ts +++ b/app/components/formPicker/formPicker.props.ts @@ -10,4 +10,5 @@ export interface FormPickerProps { options: FormPickerOption[]; required?: boolean; placeholder?: string; + searchable?: boolean; } diff --git a/app/components/formPicker/formPicker.styles.ts b/app/components/formPicker/formPicker.styles.ts index 1dec404..260c508 100644 --- a/app/components/formPicker/formPicker.styles.ts +++ b/app/components/formPicker/formPicker.styles.ts @@ -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', + }, }); diff --git a/app/components/formPicker/formPicker.tsx b/app/components/formPicker/formPicker.tsx index 2ef7461..fb00ff4 100644 --- a/app/components/formPicker/formPicker.tsx +++ b/app/components/formPicker/formPicker.tsx @@ -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 = ({ label, @@ -12,33 +22,112 @@ export const FormPicker: React.FC = ({ 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 ( {label} - {required && *} + {required && *} + { - // TODO: Implement modal picker or action sheet - console.log('Picker pressed'); - }}> + activeOpacity={0.7} + onPress={() => setModalVisible(true)}> + style={[styles.pickerText, !selectedOption && styles.placeholder]}> {selectedOption?.label || placeholder} - + + + setModalVisible(false)}> + setModalVisible(false)} + /> + + {/* Header */} + + {label} + setModalVisible(false)}> + + + + + {/* Search */} + {searchable && ( + + + + + )} + + {/* Options */} + item.value} + ItemSeparatorComponent={() => } + renderItem={({ item }) => { + const isSelected = item.value === value; + return ( + handleSelect(item)}> + + {item.label} + + {isSelected && ( + + )} + + ); + }} + /> + + ); }; diff --git a/app/components/index.ts b/app/components/index.ts index ef4d718..5e2aa45 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -9,3 +9,5 @@ export * from './addItemButton'; export * from './actionButton'; export * from './checkboxWithLabel'; export * from './statCard'; +export * from './profileInfoRow'; + diff --git a/app/components/profileInfoRow/index.ts b/app/components/profileInfoRow/index.ts new file mode 100644 index 0000000..667548a --- /dev/null +++ b/app/components/profileInfoRow/index.ts @@ -0,0 +1,2 @@ +export * from './profileInfoRow'; +export * from './profileInfoRow.props'; diff --git a/app/components/profileInfoRow/profileInfoRow.props.ts b/app/components/profileInfoRow/profileInfoRow.props.ts new file mode 100644 index 0000000..a268945 --- /dev/null +++ b/app/components/profileInfoRow/profileInfoRow.props.ts @@ -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; +} diff --git a/app/components/profileInfoRow/profileInfoRow.styles.ts b/app/components/profileInfoRow/profileInfoRow.styles.ts new file mode 100644 index 0000000..76c244b --- /dev/null +++ b/app/components/profileInfoRow/profileInfoRow.styles.ts @@ -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', + }, + }); diff --git a/app/components/profileInfoRow/profileInfoRow.tsx b/app/components/profileInfoRow/profileInfoRow.tsx new file mode 100644 index 0000000..9ad02b3 --- /dev/null +++ b/app/components/profileInfoRow/profileInfoRow.tsx @@ -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 = ({ + icon, + label, + value, + valueColor, + isBadge = false, + badgeBgColor, + onPress, + isLast = false, + containerStyle, +}) => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + + const Content = ( + + + + + + {label} + + + {isBadge ? ( + + + {value} + + + ) : ( + + {value} + + )} + + ); + + if (onPress) { + return ( + + {Content} + + ); + } + + return Content; +}; diff --git a/app/features/addLead/addLead.screen.tsx b/app/features/addLead/addLead.screen.tsx index bd72fb5..011462d 100644 --- a/app/features/addLead/addLead.screen.tsx +++ b/app/features/addLead/addLead.screen.tsx @@ -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 ( + style={styles.container}> + keyboardShouldPersistTaps="handled"> + + {/* Basic Info */} - Lead Information + Basic Information - {/* Name Field */} - - Lead Name * - - - - - - - {/* Company Field */} - - Company * - - - - - - - {/* Email Field */} - - Email Address * - - - - - - - {/* Phone Field */} - - Phone Number - - - - - - - {/* Deal Value Field */} - - Estimated Value ($) - - - - - - - - - Create New Lead - + } + /> + } + /> + } + /> + } + /> + } + /> + + {/* Classification */} + + Classification + + + + + + {/* Address */} + + Address + + } + /> + + + + + + + + + } + /> + + + + {/* Submit */} + + + {loading ? 'Creating Lead...' : 'Create Lead'} + + ); diff --git a/app/features/addLead/addLead.styles.ts b/app/features/addLead/addLead.styles.ts index 413cda1..a26e75e 100644 --- a/app/features/addLead/addLead.styles.ts +++ b/app/features/addLead/addLead.styles.ts @@ -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, }, }); diff --git a/app/features/addLead/index.ts b/app/features/addLead/index.ts index 26f3965..98b4f66 100644 --- a/app/features/addLead/index.ts +++ b/app/features/addLead/index.ts @@ -1 +1,3 @@ export * from './addLead.screen'; +export * from './thunk'; +export * from './reducers'; diff --git a/app/features/addLead/reducers.ts b/app/features/addLead/reducers.ts new file mode 100644 index 0000000..e31610b --- /dev/null +++ b/app/features/addLead/reducers.ts @@ -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; diff --git a/app/features/addLead/thunk.ts b/app/features/addLead/thunk.ts new file mode 100644 index 0000000..8fed085 --- /dev/null +++ b/app/features/addLead/thunk.ts @@ -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( + 'addLead/addLead', + async (payload, { rejectWithValue }) => { + try { + return await addLeadApi(payload); + } catch (error: any) { + return rejectWithValue(error.message || 'Failed to add lead'); + } + }, +); diff --git a/app/features/profile/profile.screen.tsx b/app/features/profile/profile.screen.tsx index ee063fa..2dec883 100644 --- a/app/features/profile/profile.screen.tsx +++ b/app/features/profile/profile.screen.tsx @@ -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(); @@ -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 ( - {/* Profile Header */} - - - A + {/* ── Header Card ── */} + + + {hasProfileImage ? ( + + ) : ( + + {initials} + + )} + {isActive && } + + + {fullName} + {email} + + + {roleText} - Workspace Administrator - Owner / Administrator - {/* Account Details list */} + {/* ── Stats Summary Row ── */} + + + {userData?.staffid || '—'} + Staff ID + + + + {userData?.total_unfinished_todos ?? '0'} + + Pending Todos + + + + {userData?.total_unread_notifications ?? '0'} + + Unread Alerts + + + + {/* ── Personal Details ── */} - Workspace Details - - - Tenancy - convex-crm-tenant - - - - Email - admin@convex.com - - - - Status - Active - + Personal Details + + + + + - {/* Settings list */} + {/* ── System & Account ── */} + + System & Account + + + {userData?.default_language ? ( + + ) : null} + + + + {/* ── Social Profiles ── */} + {hasSocials && ( + + Social Profiles + {userData?.linkedin ? ( + + ) : null} + {userData?.facebook ? ( + + ) : null} + {userData?.skype ? ( + + ) : null} + + )} + + {/* ── Preferences ── */} Preferences - + + + Dark Mode { /> - + - + + + Push Notifications - + - + + + Security & Privacy - {/* Sign Out Button */} - + {/* ── Sign Out ── */} + - Sign Out from Workspace + Sign Out ); diff --git a/app/features/profile/profile.styles.ts b/app/features/profile/profile.styles.ts index 9a1bb95..fcc7cbf 100644 --- a/app/features/profile/profile.styles.ts +++ b/app/features/profile/profile.styles.ts @@ -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, + }, + }); diff --git a/app/interfaces/leads.ts b/app/interfaces/leads.ts index e5dfaae..b0a5dc0 100644 --- a/app/interfaces/leads.ts +++ b/app/interfaces/leads.ts @@ -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; +} + diff --git a/app/mock-data/customDrawer.ts b/app/mock-data/customDrawer.ts index 23b8f61..da8c295 100644 --- a/app/mock-data/customDrawer.ts +++ b/app/mock-data/customDrawer.ts @@ -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' }, diff --git a/app/navigation/customDrawerContent.style.ts b/app/navigation/customDrawerContent.style.ts index 6e97966..0440bff 100644 --- a/app/navigation/customDrawerContent.style.ts +++ b/app/navigation/customDrawerContent.style.ts @@ -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, diff --git a/app/navigation/customDrawerContent.tsx b/app/navigation/customDrawerContent.tsx index 90cc851..9440db2 100644 --- a/app/navigation/customDrawerContent.tsx +++ b/app/navigation/customDrawerContent.tsx @@ -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) => { + onPress={onPress}> + {focused && } @@ -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 ( - {/* Drawer Header Profile */} - - - - - - Convex CRM - admin@convex.com - + {/* ── Drawer Header (User Profile) ── */} + + navigation.navigate('home', { screen: route.profile })}> + + {hasProfileImage ? ( + + ) : ( + + {initials} + + )} + {isActive && } + + + + + {fullName} + + + {email} + + + {roleText} + + + + + + {/* ── Navigation Menu List ── */} {menuItems.map(item => { @@ -81,16 +138,19 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => { - {/* Drawer Footer / Sign Out */} + {/* ── Drawer Footer (Logout) ── */} - + - Log Out + Sign Out diff --git a/app/navigation/leadsStack.tsx b/app/navigation/leadsStack.tsx index 461d0c9..08721cc 100644 --- a/app/navigation/leadsStack.tsx +++ b/app/navigation/leadsStack.tsx @@ -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(); @@ -36,7 +36,7 @@ export const LeadsStack = () => { headerShadowVisible: false, }}> diff --git a/app/navigation/tabStack.styles.ts b/app/navigation/tabStack.styles.ts index 7fda12f..5ff0aac 100644 --- a/app/navigation/tabStack.styles.ts +++ b/app/navigation/tabStack.styles.ts @@ -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, diff --git a/app/navigation/tabStack.tsx b/app/navigation/tabStack.tsx index a8f3ec2..356585e 100644 --- a/app/navigation/tabStack.tsx +++ b/app/navigation/tabStack.tsx @@ -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 ( ({ - tabBarIcon: ({ focused, color, size }) => { + tabBarIcon: ({ focused, color }) => { + if (tabRoute.name === route.addLead) { + return ( + + + + ); + } + 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 ; + + return ( + + + + ); }, 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', }} /> diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts index 61b77c6..b2704a6 100644 --- a/app/store/rootReducer.ts +++ b/app/store/rootReducer.ts @@ -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) => { diff --git a/app/utils/route.ts b/app/utils/route.ts index 2b3bf22..e7b8e99 100644 --- a/app/utils/route.ts +++ b/app/utils/route.ts @@ -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 };