From e96ea0a490ebd61621793c2fd69c4fd7dbe6bcf3 Mon Sep 17 00:00:00 2001 From: uttam Date: Fri, 24 Jul 2026 13:15:31 +0530 Subject: [PATCH] feat(app): improve design --- .../customerDetailHeader.styles.ts | 103 +++++---- .../customerDetailHeader.tsx | 54 +++-- app/components/formInput/formInput.props.ts | 2 + app/components/formInput/formInput.styles.ts | 5 + app/components/formInput/formInput.tsx | 5 + .../leadDetailHeader.styles.ts | 121 ++++++----- .../leadDetailHeader/leadDetailHeader.tsx | 81 ++++--- .../leadItemCard/leadItemCard.styles.ts | 195 ++++++----------- app/components/leadItemCard/leadItemCard.tsx | 158 ++++++-------- app/features/index.ts | 1 + app/features/login/login.screen.tsx | 43 +++- app/features/login/login.styles.ts | 3 +- app/features/notification/index.ts | 1 + .../notification/notification.screen.tsx | 105 +++++++++ .../notification/notification.styles.ts | 94 ++++++++ app/features/profile/profile.screen.tsx | 36 ++-- app/mock-data/customDrawer.ts | 16 +- app/mock-data/index.ts | 1 + app/mock-data/notification.ts | 48 +++++ app/navigation/customDrawerContent.tsx | 27 ++- app/navigation/customersStack.styles.ts | 16 ++ app/navigation/customersStack.tsx | 20 +- app/navigation/drawerStack.tsx | 34 +-- app/navigation/leadsStack.styles.ts | 16 ++ app/navigation/leadsStack.tsx | 24 ++- app/navigation/tabStack.styles.ts | 41 +++- app/navigation/tabStack.tsx | 202 +++++++++++++++--- app/services/notifications.ts | 28 +-- app/utils/route.ts | 2 + 29 files changed, 964 insertions(+), 518 deletions(-) create mode 100644 app/features/notification/index.ts create mode 100644 app/features/notification/notification.screen.tsx create mode 100644 app/features/notification/notification.styles.ts create mode 100644 app/mock-data/notification.ts create mode 100644 app/navigation/customersStack.styles.ts create mode 100644 app/navigation/leadsStack.styles.ts diff --git a/app/components/customerDetailHeader/customerDetailHeader.styles.ts b/app/components/customerDetailHeader/customerDetailHeader.styles.ts index 2cbc8d1..68f56eb 100644 --- a/app/components/customerDetailHeader/customerDetailHeader.styles.ts +++ b/app/components/customerDetailHeader/customerDetailHeader.styles.ts @@ -3,54 +3,70 @@ import { ThemeColors } from '../../theme'; export const getStyles = (colors: ThemeColors) => StyleSheet.create({ - headerCard: { + card: { backgroundColor: colors.card, - borderRadius: 16, - padding: 20, - alignItems: 'center', + borderRadius: 14, + padding: 14, marginBottom: 16, + borderWidth: 1, + borderColor: colors.border, shadowColor: '#0F172A', - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.08, - shadowRadius: 8, - elevation: 3, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, }, - avatarCircle: { - width: 64, - height: 64, - borderRadius: 32, - backgroundColor: colors.icon, - justifyContent: 'center', + topRow: { + flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, + avatarCircle: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: colors.icon, + justifyContent: 'center', + alignItems: 'center', + marginRight: 10, + flexShrink: 0, + }, avatarInitial: { color: '#FFFFFF', - fontSize: 26, + fontSize: 16, fontWeight: '700', }, - companyName: { - fontSize: 20, - fontWeight: '800', + info: { + flex: 1, + marginRight: 8, + }, + name: { + fontSize: 15, + fontWeight: '700', color: colors.text, - textAlign: 'center', + marginBottom: 1, }, - fullName: { - fontSize: 14, + company: { + fontSize: 12, color: colors.textSecondary, - marginTop: 4, - textAlign: 'center', + marginBottom: 2, }, - badgeRow: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - gap: 8, + contact: { + fontSize: 11, + color: colors.textMuted, + lineHeight: 16, }, - badge: { - paddingHorizontal: 10, + dateText: { + fontSize: 11, + color: colors.textMuted, + marginTop: 2, + }, + statusBadge: { + paddingHorizontal: 8, paddingVertical: 4, borderRadius: 20, + flexShrink: 0, + alignSelf: 'flex-start', }, activeBadge: { backgroundColor: '#D1FAE5', @@ -58,34 +74,31 @@ export const getStyles = (colors: ThemeColors) => inactiveBadge: { backgroundColor: '#FEE2E2', }, - badgeText: { - fontSize: 12, + statusText: { + fontSize: 10, fontWeight: '700', }, - activeBadgeText: { + activeText: { color: '#059669', }, - inactiveBadgeText: { + inactiveText: { color: '#DC2626', }, - dateText: { - fontSize: 12, - color: colors.textMuted, - }, actionRow: { flexDirection: 'row', gap: 10, - marginTop: 16, - width: '100%', + borderTopWidth: 1, + borderTopColor: colors.border, + paddingTop: 10, }, actionBtn: { flex: 1, flexDirection: 'row', - height: 40, - borderRadius: 10, + height: 36, + borderRadius: 8, justifyContent: 'center', alignItems: 'center', - gap: 6, + gap: 5, borderWidth: 1, }, primaryBtn: { @@ -98,11 +111,11 @@ export const getStyles = (colors: ThemeColors) => fontSize: 13, }, secondaryBtn: { - backgroundColor: colors.card, + backgroundColor: colors.background, borderColor: colors.border, }, secondaryBtnText: { - color: colors.text, + color: colors.textSecondary, fontWeight: '600', fontSize: 13, }, diff --git a/app/components/customerDetailHeader/customerDetailHeader.tsx b/app/components/customerDetailHeader/customerDetailHeader.tsx index 4da67b4..fa9c096 100644 --- a/app/components/customerDetailHeader/customerDetailHeader.tsx +++ b/app/components/customerDetailHeader/customerDetailHeader.tsx @@ -25,45 +25,53 @@ export const CustomerDetailHeader: React.FC = ({ const initials = getInitials(displayName); return ( - - - {initials} - + + {/* Top Row: Avatar + Info + Active Badge */} + + + {initials} + - {displayName} - {fullname && company ? ( - {fullname} - ) : null} + + {displayName} + {fullname && company ? ( + {fullname} + ) : null} + + + {' '}{phonenumber || 'N/A'}{' '} + + {' '}{email || 'N/A'} + + {datecreated ? ( + + Added {formatDate(datecreated)} + + ) : null} + - - {active === '1' ? 'Active Customer' : 'Inactive'} + {active === '1' ? 'Active' : 'Inactive'} - - {datecreated ? ( - - Added {formatDate(datecreated)} - - ) : null} - {/* Quick Actions */} + {/* Bottom Row: Action Buttons */} {phonenumber ? ( - + Call ) : null} @@ -72,7 +80,7 @@ export const CustomerDetailHeader: React.FC = ({ - + Email ) : null} @@ -81,7 +89,7 @@ export const CustomerDetailHeader: React.FC = ({ - + Website ) : null} diff --git a/app/components/formInput/formInput.props.ts b/app/components/formInput/formInput.props.ts index 8266252..8bd5ec8 100644 --- a/app/components/formInput/formInput.props.ts +++ b/app/components/formInput/formInput.props.ts @@ -9,6 +9,8 @@ export interface FormInputProps extends Omit { multiline?: boolean; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; + prefix?: string; + prefixStyle?: StyleProp; containerStyle?: StyleProp; inputContainerStyle?: StyleProp; inputStyle?: StyleProp; diff --git a/app/components/formInput/formInput.styles.ts b/app/components/formInput/formInput.styles.ts index 7522372..0343c8e 100644 --- a/app/components/formInput/formInput.styles.ts +++ b/app/components/formInput/formInput.styles.ts @@ -32,6 +32,11 @@ export const getStyles = (colors: ThemeColors) => justifyContent: 'center', alignItems: 'center', }, + prefix: { + fontSize: 15, + color: colors.textSecondary, + marginRight: 4, + }, rightIconWrapper: { marginLeft: 8, justifyContent: 'center', diff --git a/app/components/formInput/formInput.tsx b/app/components/formInput/formInput.tsx index 9d87ba8..740c70c 100644 --- a/app/components/formInput/formInput.tsx +++ b/app/components/formInput/formInput.tsx @@ -14,6 +14,8 @@ export const FormInput: React.FC = ({ keyboardType = 'default', leftIcon, rightIcon, + prefix, + prefixStyle, containerStyle, inputContainerStyle, inputStyle, @@ -32,6 +34,9 @@ export const FormInput: React.FC = ({ ) : null} {leftIcon ? {leftIcon} : null} + {prefix ? ( + {prefix} + ) : null} StyleSheet.create({ card: { backgroundColor: colors.card, - borderRadius: 20, - padding: 20, - alignItems: 'center', + borderRadius: 14, + padding: 14, marginBottom: 16, borderWidth: 1, borderColor: colors.border, - shadowColor: '#5B4CF5', - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.08, - shadowRadius: 16, - elevation: 4, + shadowColor: '#0F172A', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, }, - avatar: { - width: 76, - height: 76, - borderRadius: 38, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 14, - }, - avatarText: { - fontSize: 26, - fontWeight: '800', - color: '#FFFFFF', - letterSpacing: 0.5, - }, - name: { - fontSize: 20, - fontWeight: '700', - color: colors.text, - textAlign: 'center', - marginBottom: 4, - }, - companyRow: { + topRow: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', - gap: 6, marginBottom: 12, }, + avatar: { + width: 44, + height: 44, + borderRadius: 22, + justifyContent: 'center', + alignItems: 'center', + marginRight: 10, + flexShrink: 0, + }, + avatarText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '700', + }, + info: { + flex: 1, + marginRight: 8, + }, + name: { + fontSize: 15, + fontWeight: '700', + color: colors.text, + marginBottom: 1, + }, company: { - fontSize: 13, + fontSize: 12, color: colors.textSecondary, - fontWeight: '500', + marginBottom: 2, + }, + contact: { + fontSize: 11, + color: colors.textMuted, + lineHeight: 16, }, statusBadge: { flexDirection: 'row', alignItems: 'center', - gap: 6, - paddingHorizontal: 12, - paddingVertical: 5, + gap: 4, + paddingHorizontal: 8, + paddingVertical: 4, borderRadius: 20, - marginBottom: 16, + flexShrink: 0, }, statusDot: { - width: 6, - height: 6, - borderRadius: 3, + width: 5, + height: 5, + borderRadius: 2.5, }, statusText: { - fontSize: 11, + fontSize: 10, fontWeight: '700', textTransform: 'uppercase', - letterSpacing: 0.5, + letterSpacing: 0.3, }, - quickActions: { + actionRow: { flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 12, - width: '100%', - paddingTop: 14, - borderTopWidth: StyleSheet.hairlineWidth, + gap: 10, + borderTopWidth: 1, borderTopColor: colors.border, + paddingTop: 10, }, actionBtn: { flex: 1, flexDirection: 'row', - alignItems: 'center', + height: 36, + borderRadius: 8, justifyContent: 'center', - gap: 6, - paddingVertical: 10, - borderRadius: 12, + alignItems: 'center', + gap: 5, borderWidth: 1, - borderColor: colors.border, - backgroundColor: colors.surface, }, - actionBtnText: { - fontSize: 12, + secondaryBtn: { + backgroundColor: colors.background, + borderColor: colors.border, + }, + btnText: { + fontSize: 13, fontWeight: '600', - color: colors.text, }, }); diff --git a/app/components/leadDetailHeader/leadDetailHeader.tsx b/app/components/leadDetailHeader/leadDetailHeader.tsx index 67cb38b..6ce567e 100644 --- a/app/components/leadDetailHeader/leadDetailHeader.tsx +++ b/app/components/leadDetailHeader/leadDetailHeader.tsx @@ -26,53 +26,52 @@ export const LeadDetailHeader: React.FC = ({ return ( - - {getInitials(name)} - - - - {name || 'No Name'} - - - {company && ( - - - {company} + {/* Top Row: Avatar + Info + Status */} + + + {getInitials(name)} - )} - - - - - {statusName || 'No Status'} + + {name || 'No Name'} + {company ? ( + {company} + ) : null} + + + {' '}{phonenumber || 'N/A'}{' '} + + {' '}{email || 'N/A'} - - - {email && ( - - - Email - - )} + + + + + {statusName || 'No Status'} + + + + - {phonenumber && ( - - - Call - - )} + {/* Bottom Row: Call + Email Buttons */} + + + + Call + + + + Email + ); diff --git a/app/components/leadItemCard/leadItemCard.styles.ts b/app/components/leadItemCard/leadItemCard.styles.ts index f427deb..1e50e42 100644 --- a/app/components/leadItemCard/leadItemCard.styles.ts +++ b/app/components/leadItemCard/leadItemCard.styles.ts @@ -3,174 +3,109 @@ import { ThemeColors } from '../../theme'; export const getStyles = (colors: ThemeColors) => StyleSheet.create({ - cardWrapper: { - marginBottom: 16, - borderRadius: 20, - shadowColor: '#5B4CF5', - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.10, - shadowRadius: 18, - elevation: 6, - }, - card: { + customerCard: { backgroundColor: colors.card, - borderRadius: 20, - flexDirection: 'row', - overflow: 'hidden', - borderWidth: 1, - borderColor: colors.border, - }, - leftStrip: { - width: 4, - borderTopLeftRadius: 20, - borderBottomLeftRadius: 20, - }, - cardInner: { - flex: 1, - padding: 15, - paddingLeft: 14, - }, - topSection: { + borderRadius: 14, + padding: 16, + marginBottom: 12, + shadowColor: '#0F172A', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + cardHeader: { flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'flex-start', + borderBottomWidth: 1, + borderBottomColor: colors.border, + paddingBottom: 12, marginBottom: 12, }, - avatar: { - width: 48, - height: 48, - borderRadius: 24, + cardHeaderRight: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, + cardHeaderLeft: { + flexDirection: 'row', + alignItems: 'flex-start', + flex: 1, + marginRight: 10, + }, + avatarCircle: { + width: 42, + height: 42, + borderRadius: 21, + backgroundColor: colors.icon, justifyContent: 'center', alignItems: 'center', - marginRight: 12, + marginRight: 10, }, - avatarText: { - fontSize: 16, - fontWeight: '800', + avatarInitial: { color: '#FFFFFF', - letterSpacing: 0.5, + fontSize: 16, + fontWeight: '700', }, - meta: { + cardInfo: { flex: 1, - minWidth: 0, }, - name: { + customerName: { fontSize: 15, fontWeight: '700', color: colors.text, - marginBottom: 3, - letterSpacing: 0.05, }, - companyRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - gap: 5, - flexWrap: 'wrap', - }, - companyText: { + contactPerson: { fontSize: 12, color: colors.textSecondary, - fontWeight: '500', - flexShrink: 1, + marginTop: 2, }, - titleText: { - fontSize: 11, + location: { + fontSize: 12, color: colors.textMuted, - flexShrink: 1, + marginTop: 3, }, - dot: { - width: 3, - height: 3, - borderRadius: 2, - backgroundColor: colors.textMuted, - opacity: 0.6, - }, - statusBadge: { - alignSelf: 'flex-start', - flexDirection: 'row', - alignItems: 'center', - gap: 5, + badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 20, + backgroundColor: `${colors.icon}15`, }, - statusDot: { - width: 5, - height: 5, - borderRadius: 3, - }, - statusText: { - fontSize: 10, + badgeText: { + fontSize: 11, fontWeight: '700', - textTransform: 'uppercase', - letterSpacing: 0.7, + color: colors.icon, }, - separator: { - height: StyleSheet.hairlineWidth, - backgroundColor: colors.border, - marginVertical: 11, + cardFooter: { + flexDirection: 'row', + gap: 10, }, - contacts: { - gap: 8, - }, - contactRow: { + actionButton: { flexDirection: 'row', alignItems: 'center', - gap: 9, - }, - iconWrap: { - width: 28, - height: 28, - borderRadius: 9, justifyContent: 'center', - alignItems: 'center', - }, - contactText: { - fontSize: 12, - color: colors.textSecondary, + paddingVertical: 10, + borderRadius: 10, flex: 1, + gap: 6, }, - actionBtn: { - width: 28, - height: 28, - borderRadius: 9, - justifyContent: 'center', - alignItems: 'center', + callButton: { + backgroundColor: `${colors.icon}10`, }, - footer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginTop: 12, - paddingTop: 10, - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: colors.border, - }, - sourcePill: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.surface, - borderRadius: 20, - paddingHorizontal: 9, - paddingVertical: 4, - gap: 5, + emailButton: { + backgroundColor: colors.background, borderWidth: 1, borderColor: colors.border, }, - sourceText: { - fontSize: 10, - color: colors.textMuted, + callButtonText: { + fontSize: 13, fontWeight: '600', - textTransform: 'uppercase', - letterSpacing: 0.5, + color: colors.icon, }, - dateRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, - dateText: { - fontSize: 10, - color: colors.textMuted, + emailButtonText: { + fontSize: 13, + fontWeight: '600', + color: colors.textSecondary, }, }); diff --git a/app/components/leadItemCard/leadItemCard.tsx b/app/components/leadItemCard/leadItemCard.tsx index 2bcb78f..b52a9c2 100644 --- a/app/components/leadItemCard/leadItemCard.tsx +++ b/app/components/leadItemCard/leadItemCard.tsx @@ -4,108 +4,80 @@ import Icon from 'react-native-vector-icons/Ionicons'; import { useTheme } from '@theme'; import { getStyles } from './leadItemCard.styles'; import { LeadItemCardProps } from './leadItemCard.props'; -import { getInitials, handleEmailPress, handlePhonePress, toRgba, formatDate } from '@utils'; +import { getInitials, handleEmailPress, handlePhonePress, formatDate } from '@utils'; export const LeadItemCard: React.FC = ({ item, onPress }) => { - const { theme: colors, isDark } = useTheme(); + const { theme: colors } = useTheme(); const styles = getStyles(colors); - const accent = item.color?.startsWith('#') ? item.color : '#5B4CF5'; - const tint = (a: number) => toRgba(accent, isDark ? a + 0.08 : a); + const displayName = item.name || 'No Name'; + const initials = getInitials(displayName); + const accent = item.color?.startsWith('#') ? item.color : colors.icon; return ( - - - - - - - - {getInitials(item.name)} - - - - - {item.name || 'No Name'} - - - {(item.company || item.title) && ( - - {item.company && ( - <> - - {item.company} - - )} - {item.company && item.title && } - {item.title && ( - {item.title} - )} - - )} - - - - - {item.status_name || item.status || 'No Status'} - - - + + + + + {initials} - - {(item.email || item.phonenumber) && ( - <> - - - {item.email && ( - - - - - {item.email} - handleEmailPress(item.email)} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> - - - - )} - - {item.phonenumber && ( - - - - - {item.phonenumber} - handlePhonePress(item.phonenumber)} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> - - - - )} - - - )} - - - {(item.source_name || item.source) ? ( - - - {item.source_name || item.source} - - ) : } - - {item.dateadded && ( - - - {formatDate(item.dateadded)} - - )} + + + {displayName} + + {item.company ? ( + + {item.company} + + ) : null} + + + {' '}{item.phonenumber || 'N/A'}{' • '} + + {' '}{item.email || 'N/A'} + + {item.source_name || item.source || item.dateadded ? ( + + {item.source_name || item.source ? ( + <> + + {' '}{item.source_name || item.source} + + ) : null} + {item.dateadded ? ` • ${formatDate(item.dateadded)}` : ''} + + ) : null} + + + {item.status_name || item.status ? ( + + + {item.status_name || item.status} + + + ) : null} + + + + + handlePhonePress(item.phonenumber)}> + + Call + + + handleEmailPress(item.email)}> + + Email + ); diff --git a/app/features/index.ts b/app/features/index.ts index a54f07c..1958a96 100644 --- a/app/features/index.ts +++ b/app/features/index.ts @@ -16,5 +16,6 @@ export * from './tasks'; export * from './tickets'; export * from './customerDetails'; export * from './addCustomer'; +export * from './notification'; diff --git a/app/features/login/login.screen.tsx b/app/features/login/login.screen.tsx index f243aac..036bd8d 100644 --- a/app/features/login/login.screen.tsx +++ b/app/features/login/login.screen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Text, View, @@ -9,6 +9,7 @@ import { ImageBackground, ActivityIndicator, StatusBar, + Keyboard, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import Icon from 'react-native-vector-icons/Ionicons'; @@ -33,6 +34,7 @@ export const LoginScreen = () => { const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [focusedField, setFocusedField] = useState(null); + const scrollViewRef = useRef(null); const [localError, setLocalError] = useState(''); const [deviceToken, setDeviceToken] = useState(null); @@ -68,11 +70,13 @@ export const LoginScreen = () => { password, device_token: fcmToken ?? '', }; + const cleanTenancy = tenancy.trim().replace(/^(https?:\/\/)?/, ''); + const fullTenancy = `https://${cleanTenancy}`; console.log('payload-', payload) console.log('tenancy-', config.BASE_URL) - if (tenancy == config.BASE_URL) { + if (fullTenancy === config.BASE_URL) { try { - await AsyncStorage.setItem('base_url', tenancy); + await AsyncStorage.setItem('base_url', fullTenancy); await AsyncStorage.setItem('email', email); await AsyncStorage.setItem('password', password); } catch (e) { @@ -91,7 +95,10 @@ export const LoginScreen = () => { const savedEmail = await AsyncStorage.getItem('email'); const savedPassword = await AsyncStorage.getItem('password'); - if (savedBaseUrl) setTenancy(savedBaseUrl); + if (savedBaseUrl) { + const cleanBaseUrl = savedBaseUrl.replace(/^(https?:\/\/)?/, ''); + setTenancy(cleanBaseUrl); + } if (savedEmail) setEmail(savedEmail); if (savedPassword) setPassword(savedPassword); } catch (e) { @@ -123,6 +130,8 @@ export const LoginScreen = () => { index: 0, routes: [{ name: 'DrawerStack' }], }); + // Show permission popup after the user has landed in the app + setTimeout(() => NotificationService.requestPermission(), 1000); } }, [loginSuccess, token, user_data, navigation]); @@ -139,14 +148,17 @@ export const LoginScreen = () => { /> {/* Top Area Spacer displaying background illustration */} @@ -173,7 +185,11 @@ export const LoginScreen = () => { { + const cleaned = text.replace(/^(https?:\/\/)?/, ''); + setTenancy(cleaned); + }} + prefix="https://" placeholder="company-name" autoCapitalize="none" autoCorrect={false} @@ -182,7 +198,10 @@ export const LoginScreen = () => { styles.inputWrapper, focusedField === 'tenancy' && styles.inputWrapperFocused, ]} - onFocus={() => setFocusedField('tenancy')} + onFocus={() => { + setFocusedField('tenancy'); + setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100); + }} onBlur={() => setFocusedField(null)} leftIcon={ { styles.inputWrapper, focusedField === 'email' && styles.inputWrapperFocused, ]} - onFocus={() => setFocusedField('email')} + onFocus={() => { + setFocusedField('email'); + setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100); + }} onBlur={() => setFocusedField(null)} leftIcon={ { styles.inputWrapper, focusedField === 'password' && styles.inputWrapperFocused, ]} - onFocus={() => setFocusedField('password')} + onFocus={() => { + setFocusedField('password'); + setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100); + }} onBlur={() => setFocusedField(null)} leftIcon={ }, scrollContainer: { flexGrow: 1, - justifyContent: 'space-between', + justifyContent: 'flex-end', }, topSpacer: { - flex: 1, minHeight: 120, }, bottomSheetCard: { diff --git a/app/features/notification/index.ts b/app/features/notification/index.ts new file mode 100644 index 0000000..f0458a7 --- /dev/null +++ b/app/features/notification/index.ts @@ -0,0 +1 @@ +export * from './notification.screen'; diff --git a/app/features/notification/notification.screen.tsx b/app/features/notification/notification.screen.tsx new file mode 100644 index 0000000..3528f32 --- /dev/null +++ b/app/features/notification/notification.screen.tsx @@ -0,0 +1,105 @@ +import React, { useLayoutEffect, useState } from 'react'; +import { + View, + Text, + FlatList, + TouchableOpacity, + StatusBar, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import Icon from 'react-native-vector-icons/Ionicons'; +import { useTheme } from '../../theme'; +import { getStyles } from './notification.styles'; +import { INITIAL_NOTIFICATIONS } from '@mock-data'; + +export const NotificationScreen = () => { + const navigation = useNavigation(); + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + + const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS); + + // Mark all as read header button action + const handleMarkAllRead = () => { + setNotifications(prev => + prev.map(item => ({ ...item, unread: false })) + ); + }; + + useLayoutEffect(() => { + navigation.setOptions({ + headerTitle: 'Notifications', + headerRight: () => { + const hasUnread = notifications.some(n => n.unread); + if (!hasUnread) return null; + return ( + + Read All + + ); + }, + }); + }, [navigation, notifications, styles]); + + const toggleSingleRead = (id: string) => { + setNotifications(prev => + prev.map(item => + item.id === id ? { ...item, unread: !item.unread } : item + ) + ); + }; + + const renderItem = ({ item }: { item: typeof INITIAL_NOTIFICATIONS[0] }) => { + return ( + toggleSingleRead(item.id)} + > + {/* Left Side Styled Icon Circle */} + + + + + {/* Content Section */} + + + + {item.title} + + {item.unread && } + + {item.description} + {item.time} + + + ); + }; + + return ( + + {/* */} + {notifications.length > 0 ? ( + item.id} + renderItem={renderItem} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + /> + ) : ( + + + All caught up! + + When you get new updates or alerts, they will show up here. + + + )} + + ); +}; diff --git a/app/features/notification/notification.styles.ts b/app/features/notification/notification.styles.ts new file mode 100644 index 0000000..c5ca3e5 --- /dev/null +++ b/app/features/notification/notification.styles.ts @@ -0,0 +1,94 @@ +import { StyleSheet } from 'react-native'; +import { ThemeColors } from '../../theme'; + +export const getStyles = (colors: ThemeColors) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + headerRightBtn: { + paddingHorizontal: 12, + paddingVertical: 6, + }, + clearAllText: { + fontSize: 14, + fontWeight: '600', + color: colors.icon, + }, + listContent: { + paddingVertical: 8, + }, + card: { + backgroundColor: colors.surface, + paddingVertical: 14, + paddingHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: colors.border, + flexDirection: 'row', + alignItems: 'flex-start', + }, + unreadCard: { + backgroundColor: `${colors.icon}06`, + }, + iconWrapper: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + marginTop: 2, + }, + contentContainer: { + flex: 1, + }, + titleRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 3, + }, + title: { + fontSize: 14, + fontWeight: '600', + color: colors.text, + flex: 1, + }, + unreadDot: { + width: 6, + height: 6, + borderRadius: 3, + backgroundColor: '#EF4444', + marginLeft: 6, + }, + description: { + fontSize: 13, + color: colors.textSecondary, + lineHeight: 18, + marginBottom: 6, + }, + time: { + fontSize: 11, + color: colors.textMuted, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 32, + paddingTop: 100, + }, + emptyTitle: { + fontSize: 16, + fontWeight: '600', + color: colors.text, + marginTop: 16, + marginBottom: 6, + }, + emptySubtitle: { + fontSize: 13, + color: colors.textMuted, + textAlign: 'center', + lineHeight: 18, + }, + }); diff --git a/app/features/profile/profile.screen.tsx b/app/features/profile/profile.screen.tsx index 2dec883..eddbf15 100644 --- a/app/features/profile/profile.screen.tsx +++ b/app/features/profile/profile.screen.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Text, View, @@ -19,6 +19,7 @@ export const ProfileScreen = () => { const { theme: colors, isDark, toggleTheme } = useTheme(); const styles = getStyles(colors); const dispatch = useAppDispatch(); + const [pushEnabled, setPushEnabled] = useState(true); const userData = useAppSelector((state: RootState) => state.auth.user_data); @@ -114,7 +115,7 @@ export const ProfileScreen = () => { Personal Details { /> @@ -227,7 +228,7 @@ export const ProfileScreen = () => { /> - + {/* { Push Notifications - + */} - + - + - Security & Privacy + Push Notifications - - + + {/* ── Sign Out ── */} diff --git a/app/mock-data/customDrawer.ts b/app/mock-data/customDrawer.ts index da8c295..d4d714c 100644 --- a/app/mock-data/customDrawer.ts +++ b/app/mock-data/customDrawer.ts @@ -2,11 +2,13 @@ import { route } from "../utils/route"; export const menuItems = [ { 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' }, - { label: 'Invoices', route: route.invoices, icon: 'card-outline' }, - { label: 'Projects', route: route.projects, icon: 'rocket-outline' }, - { label: 'Tasks', route: route.tasks, icon: 'checkbox-outline' }, - { label: 'Tickets', route: route.tickets, icon: 'bug-outline' }, + { label: 'Leads', route: route.leads, icon: 'funnel-outline' }, + { label: 'Add Lead', route: route.addLead, icon: 'person-add-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' }, + // { label: 'Invoices', route: route.invoices, icon: 'card-outline' }, + // { label: 'Projects', route: route.projects, icon: 'rocket-outline' }, + // { label: 'Tasks', route: route.tasks, icon: 'checkbox-outline' }, + // { label: 'Tickets', route: route.tickets, icon: 'bug-outline' }, ]; \ No newline at end of file diff --git a/app/mock-data/index.ts b/app/mock-data/index.ts index 97218f2..a3190fc 100644 --- a/app/mock-data/index.ts +++ b/app/mock-data/index.ts @@ -8,3 +8,4 @@ export * from './projects'; export * from './proposal'; export * from './tasks'; export * from './tickets'; +export * from './notification' diff --git a/app/mock-data/notification.ts b/app/mock-data/notification.ts new file mode 100644 index 0000000..a911f93 --- /dev/null +++ b/app/mock-data/notification.ts @@ -0,0 +1,48 @@ +// Premium Mock Notification Data +export const INITIAL_NOTIFICATIONS = [ + { + id: '1', + title: 'New Lead Assigned', + description: 'A new high-priority lead "Acme Corp Ltd" has been successfully assigned to you.', + time: '2 hours ago', + unread: true, + iconName: 'person-add-outline', + iconColor: '#3B82F6', + }, + { + id: '2', + title: 'Invoice Payment Received', + description: 'Payment of $1,250.00 for Invoice #INV-1042 was received from Sarah Jenkins.', + time: '5 hours ago', + unread: true, + iconName: 'cash-outline', + iconColor: '#10B981', + }, + { + id: '3', + title: 'Proposal View Alert', + description: 'Workspace admin viewed your custom proposal "Website Redesign Quote".', + time: '1 day ago', + unread: false, + iconName: 'eye-outline', + iconColor: '#8B5CF6', + }, + { + id: '4', + title: 'Support Ticket #TCK-402', + description: 'Ticket resolved: "Cannot sign in to tenant workspace". Click to view details.', + time: '2 days ago', + unread: false, + iconName: 'alert-circle-outline', + iconColor: '#EF4444', + }, + { + id: '5', + title: 'Task Deadline Nearing', + description: 'Your project task "Perform complete audit" is due in 3 hours.', + time: '3 days ago', + unread: false, + iconName: 'time-outline', + iconColor: '#F59E0B', + }, +]; \ No newline at end of file diff --git a/app/navigation/customDrawerContent.tsx b/app/navigation/customDrawerContent.tsx index 9440db2..f4b6a78 100644 --- a/app/navigation/customDrawerContent.tsx +++ b/app/navigation/customDrawerContent.tsx @@ -124,14 +124,37 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => { {menuItems.map(item => { - const isFocused = activeRouteName === item.route; + // Find currently active nested tab route + let activeTabName = ''; + try { + let currentRoute = state.routes[state.index]; + if (currentRoute.name === 'home' && currentRoute.state) { + const nestedIndex = currentRoute.state.index ?? 0; + activeTabName = currentRoute.state.routes[nestedIndex].name; + } else { + activeTabName = currentRoute.name; + } + } catch (e) { + activeTabName = ''; + } + + const isFocused = + (item.route === 'home' && activeTabName === route.dashboard) || + (item.route !== 'home' && activeTabName === item.route); + return ( navigation.navigate(item.route)} + onPress={() => { + if (item.route === 'home') { + navigation.navigate('home', { screen: route.dashboard }); + } else { + navigation.navigate('home', { screen: item.route }); + } + }} /> ); })} diff --git a/app/navigation/customersStack.styles.ts b/app/navigation/customersStack.styles.ts new file mode 100644 index 0000000..12b970b --- /dev/null +++ b/app/navigation/customersStack.styles.ts @@ -0,0 +1,16 @@ +import { Platform, StyleSheet } from "react-native"; + +export const getStyles = (colors: any) => + StyleSheet.create({ + drawerButton: { + width: 38, + height: 38, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + marginLeft: Platform.OS === 'ios' ? 0 : 0, + backgroundColor: colors.surface, + borderWidth: 1, + borderColor: colors.border, + }, + }); \ No newline at end of file diff --git a/app/navigation/customersStack.tsx b/app/navigation/customersStack.tsx index 8af2de2..5befc47 100644 --- a/app/navigation/customersStack.tsx +++ b/app/navigation/customersStack.tsx @@ -1,10 +1,12 @@ import React from 'react'; -import { TouchableOpacity } from 'react-native'; +import { TouchableOpacity, StyleSheet, Platform } from 'react-native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { DrawerNavigationProp } from '@react-navigation/drawer'; import Icon from 'react-native-vector-icons/Ionicons'; import { CustomersScreen, CustomerDetailsScreen, AddCustomerScreen } from '@features'; import { route, RouteParams } from '@utils'; import { useTheme } from '@theme'; +import { getStyles } from './customersStack.styles'; export type CustomersStackParamList = Pick< RouteParams, @@ -15,6 +17,7 @@ const Stack = createNativeStackNavigator(); export const CustomersStack = () => { const { theme: colors } = useTheme(); + const styles = getStyles(colors); return ( { component={CustomersScreen} options={({ navigation }) => ({ headerTitle: 'Customers', + headerLeft: () => ( + { + const parentNav = navigation.getParent>(); + if (parentNav) { + parentNav.openDrawer(); + } else { + (navigation as any).openDrawer?.(); + } + }}> + + + ), headerRight: () => ( navigation.navigate(route.addCustomer)} diff --git a/app/navigation/drawerStack.tsx b/app/navigation/drawerStack.tsx index db811ab..db2430c 100644 --- a/app/navigation/drawerStack.tsx +++ b/app/navigation/drawerStack.tsx @@ -1,29 +1,12 @@ import React from 'react'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { TabStack } from './tabStack'; -import { CustomersStack } from './customersStack'; import { CustomDrawerContent } from './customDrawerContent'; -import { - ProposalsScreen, - EstimatesScreen, - InvoicesScreen, - ProjectsScreen, - TasksScreen, - TicketsScreen, -} from '@features'; -import { route, RouteParams } from '@utils'; import { useTheme } from '@theme'; -export type DrawerStackParamList = { home: undefined } & Pick< - RouteParams, - | 'customers' - | 'proposals' - | 'estimates' - | 'invoices' - | 'projects' - | 'tasks' - | 'tickets' ->; +export type DrawerStackParamList = { + home: undefined; +}; const Drawer = createDrawerNavigator(); @@ -53,17 +36,6 @@ export const DrawerStack = () => { component={TabStack} options={{ headerShown: false }} /> - - - - - - - ); }; diff --git a/app/navigation/leadsStack.styles.ts b/app/navigation/leadsStack.styles.ts new file mode 100644 index 0000000..12b970b --- /dev/null +++ b/app/navigation/leadsStack.styles.ts @@ -0,0 +1,16 @@ +import { Platform, StyleSheet } from "react-native"; + +export const getStyles = (colors: any) => + StyleSheet.create({ + drawerButton: { + width: 38, + height: 38, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + marginLeft: Platform.OS === 'ios' ? 0 : 0, + backgroundColor: colors.surface, + borderWidth: 1, + borderColor: colors.border, + }, + }); \ No newline at end of file diff --git a/app/navigation/leadsStack.tsx b/app/navigation/leadsStack.tsx index 08721cc..52c4903 100644 --- a/app/navigation/leadsStack.tsx +++ b/app/navigation/leadsStack.tsx @@ -1,5 +1,8 @@ import React from 'react'; +import { TouchableOpacity, StyleSheet, Platform } from 'react-native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { DrawerNavigationProp } from '@react-navigation/drawer'; +import Icon from 'react-native-vector-icons/Ionicons'; import { LeadsScreen, LeadDetailsScreen, @@ -9,6 +12,7 @@ import { } from '@features'; import { route, RouteParams } from '@utils'; import { useTheme } from '@theme'; +import { getStyles } from './leadsStack.styles'; export type LeadsStackParamList = Pick< RouteParams, @@ -19,6 +23,7 @@ const Stack = createNativeStackNavigator(); export const LeadsStack = () => { const { theme: colors } = useTheme(); + const styles = getStyles(colors); return ( { ({ + headerTitle: 'My Leads', + headerLeft: () => ( + { + const parentNav = navigation.getParent>(); + if (parentNav) { + parentNav.openDrawer(); + } else { + (navigation as any).openDrawer?.(); + } + }}> + + + ), + })} /> borderRadius: 12, justifyContent: 'center', alignItems: 'center', - marginLeft: 14, + marginLeft: 0, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, }, + notificationButton: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + marginRight: 8, + backgroundColor: colors.surface, + borderWidth: 1, + borderColor: colors.border, + position: 'relative', + // Subtle premium shadow + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.08, + shadowRadius: 2, + elevation: 2, + }, + badgeContainer: { + position: 'absolute', + top: -2, + right: -2, + backgroundColor: '#EF4444', + minWidth: 15, + height: 15, + borderRadius: 7.5, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 2, + borderWidth: 1.5, + borderColor: colors.surface, + }, + badgeText: { + color: '#FFFFFF', + fontSize: 7.5, + fontWeight: '800', + textAlign: 'center', + lineHeight: 11, + }, }); diff --git a/app/navigation/tabStack.tsx b/app/navigation/tabStack.tsx index e4b98ab..b7689a3 100644 --- a/app/navigation/tabStack.tsx +++ b/app/navigation/tabStack.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { TouchableOpacity, View } from 'react-native'; +import { TouchableOpacity, View, Text } from 'react-native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { DrawerNavigationProp } from '@react-navigation/drawer'; import Icon from 'react-native-vector-icons/Ionicons'; import { route, RouteParams } from '@utils'; @@ -8,10 +9,12 @@ import { DashboardScreen, AddLeadScreen, ProfileScreen, + NotificationScreen, } from '@features'; import { LeadsStack } from './leadsStack'; import { CustomersStack } from './customersStack'; import { useTheme } from '@theme'; +import { useAppSelector, RootState } from '@store'; import { getStyles } from './tabStack.styles'; export type TabStackParamList = Pick< @@ -21,6 +24,169 @@ export type TabStackParamList = Pick< const Tab = createBottomTabNavigator(); +const DashboardStackNav = createNativeStackNavigator(); +const DashboardStack = () => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + const userData = useAppSelector((state: RootState) => state.auth.user_data); + const unreadCount = parseInt(userData?.total_unread_notifications ?? '0', 10); + + return ( + + ({ + headerTitle: 'Convex CRM', + headerLeft: () => ( + { + const parentNav = navigation.getParent>(); + if (parentNav) { + parentNav.openDrawer(); + } else { + (navigation as any).openDrawer?.(); + } + }}> + + + ), + headerRight: () => ( + { + // Navigate to notification screen if exists, or profile/other suitable route + navigation.navigate(route.notifications); + }}> + + {unreadCount > 0 ? ( + + + {unreadCount > 99 ? '99+' : unreadCount} + + + ) : null} + + ), + })} + /> + + + ); +}; + +const AddLeadStackNav = createNativeStackNavigator(); +const AddLeadStack = () => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + return ( + + ({ + headerTitle: 'Add Lead', + headerLeft: () => ( + { + const parentNav = navigation.getParent>(); + if (parentNav) { + parentNav.openDrawer(); + } else { + (navigation as any).openDrawer?.(); + } + }}> + + + ), + })} + /> + + ); +}; + +const ProfileStackNav = createNativeStackNavigator(); +const ProfileStack = () => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + return ( + + ({ + headerTitle: 'Profile', + headerLeft: () => ( + { + const parentNav = navigation.getParent>(); + if (parentNav) { + parentNav.openDrawer(); + } else { + (navigation as any).openDrawer?.(); + } + }}> + + + ), + })} + /> + + ); +}; + export const TabStack = () => { const { theme: colors } = useTheme(); const styles = getStyles(colors); @@ -58,47 +224,27 @@ export const TabStack = () => { tabBarInactiveTintColor: colors.textMuted, tabBarStyle: styles.tabBar, tabBarLabelStyle: styles.tabBarLabel, - headerStyle: styles.header, - headerTitleStyle: styles.headerTitle, - headerTitleAlign: 'center', + headerShown: false, })}> ({ + component={DashboardStack} + options={{ tabBarLabel: 'Dashboard', - headerTitle: 'Convex CRM', - headerLeft: () => ( - { - const parentNav = navigation.getParent>(); - if (parentNav) { - parentNav.openDrawer(); - } else { - (navigation as any).openDrawer?.(); - } - }}> - - - ), - })} + }} /> { component={CustomersStack} options={{ tabBarLabel: 'Customers', - headerShown: false, }} /> diff --git a/app/services/notifications.ts b/app/services/notifications.ts index 3901386..583d4b6 100644 --- a/app/services/notifications.ts +++ b/app/services/notifications.ts @@ -3,7 +3,6 @@ import messaging, { } from '@react-native-firebase/messaging'; import notifee, { AndroidImportance, - AndroidVisibility, AuthorizationStatus, EventType, } from '@notifee/react-native'; @@ -11,7 +10,6 @@ import { Platform } from 'react-native'; const CHANNEL_ID = 'convex_crm_default'; const CHANNEL_NAME = 'Convex CRM Notifications'; -const CHANNEL_DESCRIPTION = 'General push notifications for Convex CRM'; export interface NotificationData { [key: string]: string; @@ -45,16 +43,8 @@ export async function createDefaultChannel(): Promise { await notifee.createChannel({ id: CHANNEL_ID, name: CHANNEL_NAME, - description: CHANNEL_DESCRIPTION, importance: AndroidImportance.HIGH, - visibility: AndroidVisibility.PUBLIC, - sound: 'default', - vibration: true, - vibrationPattern: [300, 500], - lights: true, - lightColor: '#4F46E5', // indigo accent }); - console.log('[NotificationService] Default Android channel created:', CHANNEL_ID); } catch (error) { console.error('[NotificationService] createDefaultChannel error:', error); } @@ -101,18 +91,7 @@ export async function displayNotification( data, android: { channelId: CHANNEL_ID, - importance: AndroidImportance.HIGH, - smallIcon: 'ic_launcher', // use ic_notification once drawable is added to android/app/src/main/res/drawable pressAction: { id: 'default' }, - color: '#4F46E5', - }, - ios: { - sound: 'default', - foregroundPresentationOptions: { - alert: true, - badge: true, - sound: true, - }, }, }); } catch (error) { @@ -259,13 +238,10 @@ export async function initialize( ): Promise<() => void> { const { onNotificationOpen, onTokenRefreshed } = options; - // 1. Permission - await requestPermission(); - - // 2. Android channel + // 1. Android channel (permission is requested explicitly after login) await createDefaultChannel(); - // 3. FCM token + // 2. FCM token await getFCMToken(); // 4. Token refresh diff --git a/app/utils/route.ts b/app/utils/route.ts index 3eebd20..aea11ad 100644 --- a/app/utils/route.ts +++ b/app/utils/route.ts @@ -27,6 +27,7 @@ export const route = { customerDetails: 'customerDetails', addCustomer: 'addCustomer', profile: 'profile', + notifications: 'notifications', } as const; // ─── Route Param Types ──────────────────────────────────────────────────────── @@ -54,6 +55,7 @@ export type RouteParams = { customerDetails: { userid: string; customer?: CustomerItem }; addCustomer: undefined; profile: undefined; + notifications: undefined; };