feat(app): improve design

This commit is contained in:
uttam 2026-07-24 13:15:31 +05:30
parent db1dc98300
commit e96ea0a490
29 changed files with 964 additions and 518 deletions

View File

@ -3,54 +3,70 @@ import { ThemeColors } from '../../theme';
export const getStyles = (colors: ThemeColors) => export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({ StyleSheet.create({
headerCard: { card: {
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: 16, borderRadius: 14,
padding: 20, padding: 14,
alignItems: 'center',
marginBottom: 16, marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
shadowColor: '#0F172A', shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 3 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08, shadowOpacity: 0.05,
shadowRadius: 8, shadowRadius: 4,
elevation: 3, elevation: 2,
}, },
avatarCircle: { topRow: {
width: 64, flexDirection: 'row',
height: 64,
borderRadius: 32,
backgroundColor: colors.icon,
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: 12,
}, },
avatarCircle: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.icon,
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
flexShrink: 0,
},
avatarInitial: { avatarInitial: {
color: '#FFFFFF', color: '#FFFFFF',
fontSize: 26, fontSize: 16,
fontWeight: '700', fontWeight: '700',
}, },
companyName: { info: {
fontSize: 20, flex: 1,
fontWeight: '800', marginRight: 8,
},
name: {
fontSize: 15,
fontWeight: '700',
color: colors.text, color: colors.text,
textAlign: 'center', marginBottom: 1,
}, },
fullName: { company: {
fontSize: 14, fontSize: 12,
color: colors.textSecondary, color: colors.textSecondary,
marginTop: 4, marginBottom: 2,
textAlign: 'center',
}, },
badgeRow: { contact: {
flexDirection: 'row', fontSize: 11,
alignItems: 'center', color: colors.textMuted,
marginTop: 10, lineHeight: 16,
gap: 8,
}, },
badge: { dateText: {
paddingHorizontal: 10, fontSize: 11,
color: colors.textMuted,
marginTop: 2,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 20, borderRadius: 20,
flexShrink: 0,
alignSelf: 'flex-start',
}, },
activeBadge: { activeBadge: {
backgroundColor: '#D1FAE5', backgroundColor: '#D1FAE5',
@ -58,34 +74,31 @@ export const getStyles = (colors: ThemeColors) =>
inactiveBadge: { inactiveBadge: {
backgroundColor: '#FEE2E2', backgroundColor: '#FEE2E2',
}, },
badgeText: { statusText: {
fontSize: 12, fontSize: 10,
fontWeight: '700', fontWeight: '700',
}, },
activeBadgeText: { activeText: {
color: '#059669', color: '#059669',
}, },
inactiveBadgeText: { inactiveText: {
color: '#DC2626', color: '#DC2626',
}, },
dateText: {
fontSize: 12,
color: colors.textMuted,
},
actionRow: { actionRow: {
flexDirection: 'row', flexDirection: 'row',
gap: 10, gap: 10,
marginTop: 16, borderTopWidth: 1,
width: '100%', borderTopColor: colors.border,
paddingTop: 10,
}, },
actionBtn: { actionBtn: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
height: 40, height: 36,
borderRadius: 10, borderRadius: 8,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
gap: 6, gap: 5,
borderWidth: 1, borderWidth: 1,
}, },
primaryBtn: { primaryBtn: {
@ -98,11 +111,11 @@ export const getStyles = (colors: ThemeColors) =>
fontSize: 13, fontSize: 13,
}, },
secondaryBtn: { secondaryBtn: {
backgroundColor: colors.card, backgroundColor: colors.background,
borderColor: colors.border, borderColor: colors.border,
}, },
secondaryBtnText: { secondaryBtnText: {
color: colors.text, color: colors.textSecondary,
fontWeight: '600', fontWeight: '600',
fontSize: 13, fontSize: 13,
}, },

View File

@ -25,45 +25,53 @@ export const CustomerDetailHeader: React.FC<CustomerDetailHeaderProps> = ({
const initials = getInitials(displayName); const initials = getInitials(displayName);
return ( return (
<View style={styles.headerCard}> <View style={styles.card}>
{/* Top Row: Avatar + Info + Active Badge */}
<View style={styles.topRow}>
<View style={styles.avatarCircle}> <View style={styles.avatarCircle}>
<Text style={styles.avatarInitial}>{initials}</Text> <Text style={styles.avatarInitial}>{initials}</Text>
</View> </View>
<Text style={styles.companyName}>{displayName}</Text> <View style={styles.info}>
<Text style={styles.name} numberOfLines={1}>{displayName}</Text>
{fullname && company ? ( {fullname && company ? (
<Text style={styles.fullName}>{fullname}</Text> <Text style={styles.company} numberOfLines={1}>{fullname}</Text>
) : null} ) : null}
<Text style={styles.contact} numberOfLines={1}>
<View style={styles.badgeRow}> <Icon name="call-outline" size={10} color={colors.textMuted} />
<View {' '}{phonenumber || 'N/A'}{' '}
style={[ <Icon name="mail-outline" size={10} color={colors.textMuted} />
styles.badge, {' '}{email || 'N/A'}
active === '1' ? styles.activeBadge : styles.inactiveBadge,
]}>
<Text
style={[
styles.badgeText,
active === '1' ? styles.activeBadgeText : styles.inactiveBadgeText,
]}>
{active === '1' ? 'Active Customer' : 'Inactive'}
</Text> </Text>
</View>
{datecreated ? ( {datecreated ? (
<Text style={styles.dateText}> <Text style={styles.dateText} numberOfLines={1}>
Added {formatDate(datecreated)} Added {formatDate(datecreated)}
</Text> </Text>
) : null} ) : null}
</View> </View>
{/* Quick Actions */} <View
style={[
styles.statusBadge,
active === '1' ? styles.activeBadge : styles.inactiveBadge,
]}>
<Text
style={[
styles.statusText,
active === '1' ? styles.activeText : styles.inactiveText,
]}>
{active === '1' ? 'Active' : 'Inactive'}
</Text>
</View>
</View>
{/* Bottom Row: Action Buttons */}
<View style={styles.actionRow}> <View style={styles.actionRow}>
{phonenumber ? ( {phonenumber ? (
<TouchableOpacity <TouchableOpacity
style={[styles.actionBtn, styles.primaryBtn]} style={[styles.actionBtn, styles.primaryBtn]}
onPress={onPhonePress}> onPress={onPhonePress}>
<Icon name="call" size={16} color="#FFFFFF" /> <Icon name="call" size={14} color="#FFFFFF" />
<Text style={styles.primaryBtnText}>Call</Text> <Text style={styles.primaryBtnText}>Call</Text>
</TouchableOpacity> </TouchableOpacity>
) : null} ) : null}
@ -72,7 +80,7 @@ export const CustomerDetailHeader: React.FC<CustomerDetailHeaderProps> = ({
<TouchableOpacity <TouchableOpacity
style={[styles.actionBtn, styles.secondaryBtn]} style={[styles.actionBtn, styles.secondaryBtn]}
onPress={onEmailPress}> onPress={onEmailPress}>
<Icon name="mail" size={16} color={colors.text} /> <Icon name="mail" size={14} color={colors.textSecondary} />
<Text style={styles.secondaryBtnText}>Email</Text> <Text style={styles.secondaryBtnText}>Email</Text>
</TouchableOpacity> </TouchableOpacity>
) : null} ) : null}
@ -81,7 +89,7 @@ export const CustomerDetailHeader: React.FC<CustomerDetailHeaderProps> = ({
<TouchableOpacity <TouchableOpacity
style={[styles.actionBtn, styles.secondaryBtn]} style={[styles.actionBtn, styles.secondaryBtn]}
onPress={onWebsitePress}> onPress={onWebsitePress}>
<Icon name="globe" size={16} color={colors.text} /> <Icon name="globe" size={14} color={colors.textSecondary} />
<Text style={styles.secondaryBtnText}>Website</Text> <Text style={styles.secondaryBtnText}>Website</Text>
</TouchableOpacity> </TouchableOpacity>
) : null} ) : null}

View File

@ -9,6 +9,8 @@ export interface FormInputProps extends Omit<TextInputProps, 'style'> {
multiline?: boolean; multiline?: boolean;
leftIcon?: React.ReactNode; leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode; rightIcon?: React.ReactNode;
prefix?: string;
prefixStyle?: StyleProp<TextStyle>;
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
inputContainerStyle?: StyleProp<ViewStyle>; inputContainerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>; inputStyle?: StyleProp<TextStyle>;

View File

@ -32,6 +32,11 @@ export const getStyles = (colors: ThemeColors) =>
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
prefix: {
fontSize: 15,
color: colors.textSecondary,
marginRight: 4,
},
rightIconWrapper: { rightIconWrapper: {
marginLeft: 8, marginLeft: 8,
justifyContent: 'center', justifyContent: 'center',

View File

@ -14,6 +14,8 @@ export const FormInput: React.FC<FormInputProps> = ({
keyboardType = 'default', keyboardType = 'default',
leftIcon, leftIcon,
rightIcon, rightIcon,
prefix,
prefixStyle,
containerStyle, containerStyle,
inputContainerStyle, inputContainerStyle,
inputStyle, inputStyle,
@ -32,6 +34,9 @@ export const FormInput: React.FC<FormInputProps> = ({
) : null} ) : null}
<View style={[styles.inputWrapper, inputContainerStyle]}> <View style={[styles.inputWrapper, inputContainerStyle]}>
{leftIcon ? <View style={styles.leftIconWrapper}>{leftIcon}</View> : null} {leftIcon ? <View style={styles.leftIconWrapper}>{leftIcon}</View> : null}
{prefix ? (
<Text style={[styles.prefix, prefixStyle]}>{prefix}</Text>
) : null}
<TextInput <TextInput
style={[styles.input, multiline && styles.multilineInput, inputStyle]} style={[styles.input, multiline && styles.multilineInput, inputStyle]}
value={value} value={value}

View File

@ -5,96 +5,99 @@ export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({ StyleSheet.create({
card: { card: {
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: 20, borderRadius: 14,
padding: 20, padding: 14,
alignItems: 'center',
marginBottom: 16, marginBottom: 16,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border, borderColor: colors.border,
shadowColor: '#5B4CF5', shadowColor: '#0F172A',
shadowOffset: { width: 0, height: 6 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08, shadowOpacity: 0.05,
shadowRadius: 16, shadowRadius: 4,
elevation: 4, elevation: 2,
}, },
avatar: { topRow: {
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: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
gap: 6,
marginBottom: 12, 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: { company: {
fontSize: 13, fontSize: 12,
color: colors.textSecondary, color: colors.textSecondary,
fontWeight: '500', marginBottom: 2,
},
contact: {
fontSize: 11,
color: colors.textMuted,
lineHeight: 16,
}, },
statusBadge: { statusBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 6, gap: 4,
paddingHorizontal: 12, paddingHorizontal: 8,
paddingVertical: 5, paddingVertical: 4,
borderRadius: 20, borderRadius: 20,
marginBottom: 16, flexShrink: 0,
}, },
statusDot: { statusDot: {
width: 6, width: 5,
height: 6, height: 5,
borderRadius: 3, borderRadius: 2.5,
}, },
statusText: { statusText: {
fontSize: 11, fontSize: 10,
fontWeight: '700', fontWeight: '700',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.3,
}, },
quickActions: { actionRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', gap: 10,
justifyContent: 'center', borderTopWidth: 1,
gap: 12,
width: '100%',
paddingTop: 14,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border, borderTopColor: colors.border,
paddingTop: 10,
}, },
actionBtn: { actionBtn: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', height: 36,
borderRadius: 8,
justifyContent: 'center', justifyContent: 'center',
gap: 6, alignItems: 'center',
paddingVertical: 10, gap: 5,
borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.surface,
}, },
actionBtnText: { secondaryBtn: {
fontSize: 12, backgroundColor: colors.background,
borderColor: colors.border,
},
btnText: {
fontSize: 13,
fontWeight: '600', fontWeight: '600',
color: colors.text,
}, },
}); });

View File

@ -26,20 +26,24 @@ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
return ( return (
<View style={styles.card}> <View style={styles.card}>
{/* Top Row: Avatar + Info + Status */}
<View style={styles.topRow}>
<View style={[styles.avatar, { backgroundColor: accent }]}> <View style={[styles.avatar, { backgroundColor: accent }]}>
<Text style={styles.avatarText}>{getInitials(name)}</Text> <Text style={styles.avatarText}>{getInitials(name)}</Text>
</View> </View>
<Text style={styles.name} numberOfLines={1}> <View style={styles.info}>
{name || 'No Name'} <Text style={styles.name} numberOfLines={1}>{name || 'No Name'}</Text>
{company ? (
<Text style={styles.company} numberOfLines={1}>{company}</Text>
) : null}
<Text style={styles.contact} numberOfLines={1}>
<Icon name="call-outline" size={10} color={colors.textMuted} />
{' '}{phonenumber || 'N/A'}{' '}
<Icon name="mail-outline" size={10} color={colors.textMuted} />
{' '}{email || 'N/A'}
</Text> </Text>
{company && (
<View style={styles.companyRow}>
<Icon name="business-outline" size={13} color={colors.textMuted} />
<Text style={styles.company}>{company}</Text>
</View> </View>
)}
<TouchableOpacity <TouchableOpacity
onPress={onStatusPress} onPress={onStatusPress}
@ -52,27 +56,22 @@ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
</Text> </Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View>
<View style={styles.quickActions}> {/* Bottom Row: Call + Email Buttons */}
{email && ( <View style={styles.actionRow}>
<TouchableOpacity <TouchableOpacity
style={styles.actionBtn} style={[styles.actionBtn, { backgroundColor: `${accent}12`, borderColor: `${accent}30` }]}
onPress={onEmailPress} onPress={onPhonePress}>
activeOpacity={0.7}> <Icon name="call" size={14} color={accent} />
<Icon name="mail-outline" size={16} color={colors.icon} /> <Text style={[styles.btnText, { color: accent }]}>Call</Text>
<Text style={styles.actionBtnText}>Email</Text>
</TouchableOpacity> </TouchableOpacity>
)}
{phonenumber && (
<TouchableOpacity <TouchableOpacity
style={styles.actionBtn} style={[styles.actionBtn, styles.secondaryBtn]}
onPress={onPhonePress} onPress={onEmailPress}>
activeOpacity={0.7}> <Icon name="mail" size={14} color={colors.textSecondary} />
<Icon name="call-outline" size={16} color={colors.icon} /> <Text style={[styles.btnText, { color: colors.textSecondary }]}>Email</Text>
<Text style={styles.actionBtnText}>Call</Text>
</TouchableOpacity> </TouchableOpacity>
)}
</View> </View>
</View> </View>
); );

View File

@ -3,174 +3,109 @@ import { ThemeColors } from '../../theme';
export const getStyles = (colors: ThemeColors) => export const getStyles = (colors: ThemeColors) =>
StyleSheet.create({ StyleSheet.create({
cardWrapper: { customerCard: {
marginBottom: 16,
borderRadius: 20,
shadowColor: '#5B4CF5',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.10,
shadowRadius: 18,
elevation: 6,
},
card: {
backgroundColor: colors.card, backgroundColor: colors.card,
borderRadius: 20, borderRadius: 14,
flexDirection: 'row', padding: 16,
overflow: 'hidden', marginBottom: 12,
borderWidth: 1, shadowColor: '#0F172A',
borderColor: colors.border, shadowOffset: { width: 0, height: 2 },
}, shadowOpacity: 0.05,
leftStrip: { shadowRadius: 4,
width: 4, elevation: 2,
borderTopLeftRadius: 20, },
borderBottomLeftRadius: 20, cardHeader: {
},
cardInner: {
flex: 1,
padding: 15,
paddingLeft: 14,
},
topSection: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
borderBottomWidth: 1,
borderBottomColor: colors.border,
paddingBottom: 12,
marginBottom: 12, marginBottom: 12,
}, },
avatar: { cardHeaderRight: {
width: 48, flexDirection: 'row',
height: 48, alignItems: 'center',
borderRadius: 24, gap: 10,
},
cardHeaderLeft: {
flexDirection: 'row',
alignItems: 'flex-start',
flex: 1,
marginRight: 10,
},
avatarCircle: {
width: 42,
height: 42,
borderRadius: 21,
backgroundColor: colors.icon,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, marginRight: 10,
}, },
avatarText: { avatarInitial: {
fontSize: 16,
fontWeight: '800',
color: '#FFFFFF', color: '#FFFFFF',
letterSpacing: 0.5, fontSize: 16,
fontWeight: '700',
}, },
meta: { cardInfo: {
flex: 1, flex: 1,
minWidth: 0,
}, },
name: { customerName: {
fontSize: 15, fontSize: 15,
fontWeight: '700', fontWeight: '700',
color: colors.text, color: colors.text,
marginBottom: 3,
letterSpacing: 0.05,
}, },
companyRow: { contactPerson: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
gap: 5,
flexWrap: 'wrap',
},
companyText: {
fontSize: 12, fontSize: 12,
color: colors.textSecondary, color: colors.textSecondary,
fontWeight: '500', marginTop: 2,
flexShrink: 1,
}, },
titleText: { location: {
fontSize: 11, fontSize: 12,
color: colors.textMuted, color: colors.textMuted,
flexShrink: 1, marginTop: 3,
}, },
dot: { badge: {
width: 3,
height: 3,
borderRadius: 2,
backgroundColor: colors.textMuted,
opacity: 0.6,
},
statusBadge: {
alignSelf: 'flex-start',
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 20, borderRadius: 20,
backgroundColor: `${colors.icon}15`,
}, },
statusDot: { badgeText: {
width: 5, fontSize: 11,
height: 5,
borderRadius: 3,
},
statusText: {
fontSize: 10,
fontWeight: '700', fontWeight: '700',
textTransform: 'uppercase', color: colors.icon,
letterSpacing: 0.7,
}, },
separator: { cardFooter: {
height: StyleSheet.hairlineWidth, flexDirection: 'row',
backgroundColor: colors.border, gap: 10,
marginVertical: 11,
}, },
contacts: { actionButton: {
gap: 8,
},
contactRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 9,
},
iconWrap: {
width: 28,
height: 28,
borderRadius: 9,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', paddingVertical: 10,
}, borderRadius: 10,
contactText: {
fontSize: 12,
color: colors.textSecondary,
flex: 1, flex: 1,
gap: 6,
}, },
actionBtn: { callButton: {
width: 28, backgroundColor: `${colors.icon}10`,
height: 28,
borderRadius: 9,
justifyContent: 'center',
alignItems: 'center',
}, },
footer: { emailButton: {
flexDirection: 'row', backgroundColor: colors.background,
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,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border, borderColor: colors.border,
}, },
sourceText: { callButtonText: {
fontSize: 10, fontSize: 13,
color: colors.textMuted,
fontWeight: '600', fontWeight: '600',
textTransform: 'uppercase', color: colors.icon,
letterSpacing: 0.5,
}, },
dateRow: { emailButtonText: {
flexDirection: 'row', fontSize: 13,
alignItems: 'center', fontWeight: '600',
gap: 4, color: colors.textSecondary,
},
dateText: {
fontSize: 10,
color: colors.textMuted,
}, },
}); });

View File

@ -4,109 +4,81 @@ import Icon from 'react-native-vector-icons/Ionicons';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { getStyles } from './leadItemCard.styles'; import { getStyles } from './leadItemCard.styles';
import { LeadItemCardProps } from './leadItemCard.props'; 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<LeadItemCardProps> = ({ item, onPress }) => { export const LeadItemCard: React.FC<LeadItemCardProps> = ({ item, onPress }) => {
const { theme: colors, isDark } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const accent = item.color?.startsWith('#') ? item.color : '#5B4CF5'; const displayName = item.name || 'No Name';
const tint = (a: number) => toRgba(accent, isDark ? a + 0.08 : a); const initials = getInitials(displayName);
const accent = item.color?.startsWith('#') ? item.color : colors.icon;
return ( return (
<TouchableOpacity style={styles.cardWrapper} onPress={onPress} activeOpacity={0.8}> <TouchableOpacity
<View style={styles.card}> activeOpacity={onPress ? 0.8 : 1}
<View style={[styles.leftStrip, { backgroundColor: accent }]} /> onPress={onPress}
style={styles.customerCard}>
<View style={styles.cardInner}> <View style={styles.cardHeader}>
<View style={styles.topSection}> <View style={styles.cardHeaderLeft}>
<View style={[styles.avatar, { backgroundColor: accent }]}> <View style={[styles.avatarCircle, { backgroundColor: accent }]}>
<Text style={styles.avatarText}>{getInitials(item.name)}</Text> <Text style={styles.avatarInitial}>{initials}</Text>
</View> </View>
<View style={styles.cardInfo}>
<View style={styles.meta}> <Text style={styles.customerName} numberOfLines={1}>
<Text style={styles.name} numberOfLines={1}> {displayName}
{item.name || 'No Name'}
</Text> </Text>
{item.company ? (
{(item.company || item.title) && ( <Text style={styles.contactPerson} numberOfLines={1}>
<View style={styles.companyRow}> {item.company}
{item.company && ( </Text>
) : null}
<Text style={styles.location} numberOfLines={1}>
<Icon name="call-outline" size={11} color={colors.textMuted} />
{' '}{item.phonenumber || 'N/A'}{' • '}
<Icon name="mail-outline" size={11} color={colors.textMuted} />
{' '}{item.email || 'N/A'}
</Text>
{item.source_name || item.source || item.dateadded ? (
<Text style={styles.location} numberOfLines={1}>
{item.source_name || item.source ? (
<> <>
<Icon name="business-outline" size={11} color={colors.textMuted} /> <Icon name="radio-outline" size={11} color={colors.textMuted} />
<Text style={styles.companyText} numberOfLines={1}>{item.company}</Text> {' '}{item.source_name || item.source}
</> </>
)} ) : null}
{item.company && item.title && <View style={styles.dot} />} {item.dateadded ? `${formatDate(item.dateadded)}` : ''}
{item.title && ( </Text>
<Text style={styles.titleText} numberOfLines={1}>{item.title}</Text> ) : null}
)} </View>
</View> </View>
)}
<View style={[styles.statusBadge, { backgroundColor: tint(0.10) }]}> <View style={styles.cardHeaderRight}>
<View style={[styles.statusDot, { backgroundColor: accent }]} /> {item.status_name || item.status ? (
<Text style={[styles.statusText, { color: accent }]}> <View style={[styles.badge, { backgroundColor: `${accent}15` }]}>
{item.status_name || item.status || 'No Status'} <Text style={[styles.badgeText, { color: accent }]}>
{item.status_name || item.status}
</Text> </Text>
</View> </View>
) : null}
</View> </View>
</View> </View>
{(item.email || item.phonenumber) && ( <View style={styles.cardFooter}>
<>
<View style={styles.separator} />
<View style={styles.contacts}>
{item.email && (
<View style={styles.contactRow}>
<View style={[styles.iconWrap, { backgroundColor: tint(0.08) }]}>
<Icon name="mail-outline" size={13} color={accent} />
</View>
<Text style={styles.contactText} numberOfLines={1}>{item.email}</Text>
<TouchableOpacity <TouchableOpacity
style={[styles.actionBtn, { backgroundColor: tint(0.08) }]} style={[styles.actionButton, styles.callButton, { backgroundColor: `${accent}10` }]}
onPress={() => handleEmailPress(item.email)} onPress={() => handlePhonePress(item.phonenumber)}>
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> <Icon name="call" size={15} color={accent} />
<Icon name="send-outline" size={12} color={accent} /> <Text style={[styles.callButtonText, { color: accent }]}>Call</Text>
</TouchableOpacity> </TouchableOpacity>
</View>
)}
{item.phonenumber && (
<View style={styles.contactRow}>
<View style={[styles.iconWrap, { backgroundColor: tint(0.08) }]}>
<Icon name="call-outline" size={13} color={accent} />
</View>
<Text style={styles.contactText} numberOfLines={1}>{item.phonenumber}</Text>
<TouchableOpacity <TouchableOpacity
style={[styles.actionBtn, { backgroundColor: tint(0.08) }]} style={[styles.actionButton, styles.emailButton]}
onPress={() => handlePhonePress(item.phonenumber)} onPress={() => handleEmailPress(item.email)}>
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> <Icon name="mail" size={15} color={colors.textSecondary} />
<Icon name="call-outline" size={12} color={accent} /> <Text style={styles.emailButtonText}>Email</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)}
</View>
</>
)}
<View style={styles.footer}>
{(item.source_name || item.source) ? (
<View style={styles.sourcePill}>
<Icon name="radio-outline" size={10} color={colors.textMuted} />
<Text style={styles.sourceText}>{item.source_name || item.source}</Text>
</View>
) : <View />}
{item.dateadded && (
<View style={styles.dateRow}>
<Icon name="time-outline" size={10} color={colors.textMuted} />
<Text style={styles.dateText}>{formatDate(item.dateadded)}</Text>
</View>
)}
</View>
</View>
</View>
</TouchableOpacity> </TouchableOpacity>
); );
}; };

View File

@ -16,5 +16,6 @@ export * from './tasks';
export * from './tickets'; export * from './tickets';
export * from './customerDetails'; export * from './customerDetails';
export * from './addCustomer'; export * from './addCustomer';
export * from './notification';

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { import {
Text, Text,
View, View,
@ -9,6 +9,7 @@ import {
ImageBackground, ImageBackground,
ActivityIndicator, ActivityIndicator,
StatusBar, StatusBar,
Keyboard,
} from 'react-native'; } 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';
@ -33,6 +34,7 @@ export const LoginScreen = () => {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [focusedField, setFocusedField] = useState<string | null>(null); const [focusedField, setFocusedField] = useState<string | null>(null);
const scrollViewRef = useRef<ScrollView>(null);
const [localError, setLocalError] = useState(''); const [localError, setLocalError] = useState('');
const [deviceToken, setDeviceToken] = useState<string | null>(null); const [deviceToken, setDeviceToken] = useState<string | null>(null);
@ -68,11 +70,13 @@ export const LoginScreen = () => {
password, password,
device_token: fcmToken ?? '', device_token: fcmToken ?? '',
}; };
const cleanTenancy = tenancy.trim().replace(/^(https?:\/\/)?/, '');
const fullTenancy = `https://${cleanTenancy}`;
console.log('payload-', payload) console.log('payload-', payload)
console.log('tenancy-', config.BASE_URL) console.log('tenancy-', config.BASE_URL)
if (tenancy == config.BASE_URL) { if (fullTenancy === config.BASE_URL) {
try { try {
await AsyncStorage.setItem('base_url', tenancy); await AsyncStorage.setItem('base_url', fullTenancy);
await AsyncStorage.setItem('email', email); await AsyncStorage.setItem('email', email);
await AsyncStorage.setItem('password', password); await AsyncStorage.setItem('password', password);
} catch (e) { } catch (e) {
@ -91,7 +95,10 @@ export const LoginScreen = () => {
const savedEmail = await AsyncStorage.getItem('email'); const savedEmail = await AsyncStorage.getItem('email');
const savedPassword = await AsyncStorage.getItem('password'); const savedPassword = await AsyncStorage.getItem('password');
if (savedBaseUrl) setTenancy(savedBaseUrl); if (savedBaseUrl) {
const cleanBaseUrl = savedBaseUrl.replace(/^(https?:\/\/)?/, '');
setTenancy(cleanBaseUrl);
}
if (savedEmail) setEmail(savedEmail); if (savedEmail) setEmail(savedEmail);
if (savedPassword) setPassword(savedPassword); if (savedPassword) setPassword(savedPassword);
} catch (e) { } catch (e) {
@ -123,6 +130,8 @@ export const LoginScreen = () => {
index: 0, index: 0,
routes: [{ name: 'DrawerStack' }], routes: [{ name: 'DrawerStack' }],
}); });
// Show permission popup after the user has landed in the app
setTimeout(() => NotificationService.requestPermission(), 1000);
} }
}, [loginSuccess, token, user_data, navigation]); }, [loginSuccess, token, user_data, navigation]);
@ -139,14 +148,17 @@ export const LoginScreen = () => {
/> />
<View style={styles.overlay}> <View style={styles.overlay}>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container} style={styles.container}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
> >
<ScrollView <ScrollView
ref={scrollViewRef}
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
bounces={false} bounces={false}
keyboardDismissMode="none"
> >
{/* Top Area Spacer displaying background illustration */} {/* Top Area Spacer displaying background illustration */}
<View style={styles.topSpacer} /> <View style={styles.topSpacer} />
@ -173,7 +185,11 @@ export const LoginScreen = () => {
<FormInput <FormInput
label="Tenancy Name" label="Tenancy Name"
value={tenancy} value={tenancy}
onChangeText={setTenancy} onChangeText={(text) => {
const cleaned = text.replace(/^(https?:\/\/)?/, '');
setTenancy(cleaned);
}}
prefix="https://"
placeholder="company-name" placeholder="company-name"
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -182,7 +198,10 @@ export const LoginScreen = () => {
styles.inputWrapper, styles.inputWrapper,
focusedField === 'tenancy' && styles.inputWrapperFocused, focusedField === 'tenancy' && styles.inputWrapperFocused,
]} ]}
onFocus={() => setFocusedField('tenancy')} onFocus={() => {
setFocusedField('tenancy');
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
}}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
leftIcon={ leftIcon={
<Icon <Icon
@ -211,7 +230,10 @@ export const LoginScreen = () => {
styles.inputWrapper, styles.inputWrapper,
focusedField === 'email' && styles.inputWrapperFocused, focusedField === 'email' && styles.inputWrapperFocused,
]} ]}
onFocus={() => setFocusedField('email')} onFocus={() => {
setFocusedField('email');
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
}}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
leftIcon={ leftIcon={
<Icon <Icon
@ -240,7 +262,10 @@ export const LoginScreen = () => {
styles.inputWrapper, styles.inputWrapper,
focusedField === 'password' && styles.inputWrapperFocused, focusedField === 'password' && styles.inputWrapperFocused,
]} ]}
onFocus={() => setFocusedField('password')} onFocus={() => {
setFocusedField('password');
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
}}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
leftIcon={ leftIcon={
<Icon <Icon

View File

@ -19,10 +19,9 @@ export const getStyles = (colors: ThemeColors, isDark: boolean) =>
}, },
scrollContainer: { scrollContainer: {
flexGrow: 1, flexGrow: 1,
justifyContent: 'space-between', justifyContent: 'flex-end',
}, },
topSpacer: { topSpacer: {
flex: 1,
minHeight: 120, minHeight: 120,
}, },
bottomSheetCard: { bottomSheetCard: {

View File

@ -0,0 +1 @@
export * from './notification.screen';

View File

@ -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 (
<TouchableOpacity
style={styles.headerRightBtn}
onPress={handleMarkAllRead}
activeOpacity={0.7}
>
<Text style={styles.clearAllText}>Read All</Text>
</TouchableOpacity>
);
},
});
}, [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 (
<TouchableOpacity
style={[styles.card, item.unread && styles.unreadCard]}
activeOpacity={0.7}
onPress={() => toggleSingleRead(item.id)}
>
{/* Left Side Styled Icon Circle */}
<View style={[styles.iconWrapper, { backgroundColor: `${item.iconColor}12` }]}>
<Icon name={item.iconName} size={18} color={item.iconColor} />
</View>
{/* Content Section */}
<View style={styles.contentContainer}>
<View style={styles.titleRow}>
<Text style={styles.title} numberOfLines={1}>
{item.title}
</Text>
{item.unread && <View style={styles.unreadDot} />}
</View>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.time}>{item.time}</Text>
</View>
</TouchableOpacity>
);
};
return (
<View style={styles.container}>
{/* <StatusBar barStyle="light-content" /> */}
{notifications.length > 0 ? (
<FlatList
data={notifications}
keyExtractor={item => item.id}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
) : (
<View style={styles.emptyContainer}>
<Icon name="notifications-off-outline" size={48} color={colors.textMuted} />
<Text style={styles.emptyTitle}>All caught up!</Text>
<Text style={styles.emptySubtitle}>
When you get new updates or alerts, they will show up here.
</Text>
</View>
)}
</View>
);
};

View File

@ -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,
},
});

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { import {
Text, Text,
View, View,
@ -19,6 +19,7 @@ export const ProfileScreen = () => {
const { theme: colors, isDark, toggleTheme } = useTheme(); const { theme: colors, isDark, toggleTheme } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [pushEnabled, setPushEnabled] = useState(true);
const userData = useAppSelector((state: RootState) => state.auth.user_data); const userData = useAppSelector((state: RootState) => state.auth.user_data);
@ -114,7 +115,7 @@ export const ProfileScreen = () => {
<Text style={styles.sectionHeader}>Personal Details</Text> <Text style={styles.sectionHeader}>Personal Details</Text>
<ProfileInfoRow <ProfileInfoRow
icon="person-outline" icon="person-outline"
label="Full Name" label="Name"
value={fullName} value={fullName}
/> />
<ProfileInfoRow <ProfileInfoRow
@ -124,17 +125,17 @@ export const ProfileScreen = () => {
/> />
<ProfileInfoRow <ProfileInfoRow
icon="mail-outline" icon="mail-outline"
label="Email Address" label="Email"
value={email} value={email}
/> />
<ProfileInfoRow <ProfileInfoRow
icon="call-outline" icon="call-outline"
label="Phone Number" label="Phone"
value={phone} value={phone}
/> />
<ProfileInfoRow <ProfileInfoRow
icon="shield-checkmark-outline" icon="shield-checkmark-outline"
label="Account Role" label="Role"
value={roleText} value={roleText}
isLast isLast
/> />
@ -227,7 +228,7 @@ export const ProfileScreen = () => {
/> />
</View> </View>
<TouchableOpacity style={styles.preferenceRow} activeOpacity={0.7}> {/* <TouchableOpacity style={styles.preferenceRow} activeOpacity={0.7}>
<View style={styles.preferenceLabelGroup}> <View style={styles.preferenceLabelGroup}>
<View style={styles.iconWrapper}> <View style={styles.iconWrapper}>
<Icon <Icon
@ -239,23 +240,22 @@ export const ProfileScreen = () => {
<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 <View style={[styles.preferenceRow, styles.noBorder]}>
style={[styles.preferenceRow, styles.noBorder]}
activeOpacity={0.7}>
<View style={styles.preferenceLabelGroup}> <View style={styles.preferenceLabelGroup}>
<View style={styles.iconWrapper}> <View style={styles.iconWrapper}>
<Icon <Icon name="notifications-outline" size={18} color={colors.icon} />
name="shield-checkmark-outline" </View>
size={18} <Text style={styles.preferenceText}>Push Notifications</Text>
color={colors.icon} </View>
<Switch
value={pushEnabled}
onValueChange={setPushEnabled}
trackColor={{ false: '#767577', true: colors.icon }}
thumbColor={pushEnabled ? '#ffffff' : '#f4f3f4'}
/> />
</View> </View>
<Text style={styles.preferenceText}>Security &amp; Privacy</Text>
</View>
<Icon name="chevron-forward" size={18} color={colors.textMuted} />
</TouchableOpacity>
</View> </View>
{/* ── Sign Out ── */} {/* ── Sign Out ── */}

View File

@ -2,11 +2,13 @@ import { route } from "../utils/route";
export const menuItems = [ export const menuItems = [
{ label: 'Dashboard', route: 'home', icon: 'grid-outline' }, { label: 'Dashboard', route: 'home', icon: 'grid-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: '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' },
{ label: 'Invoices', route: route.invoices, icon: 'card-outline' }, // { label: 'Invoices', route: route.invoices, icon: 'card-outline' },
{ label: 'Projects', route: route.projects, icon: 'rocket-outline' }, // { label: 'Projects', route: route.projects, icon: 'rocket-outline' },
{ label: 'Tasks', route: route.tasks, icon: 'checkbox-outline' }, // { label: 'Tasks', route: route.tasks, icon: 'checkbox-outline' },
{ label: 'Tickets', route: route.tickets, icon: 'bug-outline' }, // { label: 'Tickets', route: route.tickets, icon: 'bug-outline' },
]; ];

View File

@ -8,3 +8,4 @@ export * from './projects';
export * from './proposal'; export * from './proposal';
export * from './tasks'; export * from './tasks';
export * from './tickets'; export * from './tickets';
export * from './notification'

View File

@ -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',
},
];

View File

@ -124,14 +124,37 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.menuContainer}> <View style={styles.menuContainer}>
{menuItems.map(item => { {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 ( return (
<DrawerItem <DrawerItem
key={item.route} key={item.route}
label={item.label} label={item.label}
iconName={item.icon} iconName={item.icon}
focused={isFocused} focused={isFocused}
onPress={() => navigation.navigate(item.route)} onPress={() => {
if (item.route === 'home') {
navigation.navigate('home', { screen: route.dashboard });
} else {
navigation.navigate('home', { screen: item.route });
}
}}
/> />
); );
})} })}

View File

@ -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,
},
});

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity, StyleSheet, Platform } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { DrawerNavigationProp } from '@react-navigation/drawer';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { CustomersScreen, CustomerDetailsScreen, AddCustomerScreen } from '@features'; import { CustomersScreen, CustomerDetailsScreen, AddCustomerScreen } from '@features';
import { route, RouteParams } from '@utils'; import { route, RouteParams } from '@utils';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { getStyles } from './customersStack.styles';
export type CustomersStackParamList = Pick< export type CustomersStackParamList = Pick<
RouteParams, RouteParams,
@ -15,6 +17,7 @@ const Stack = createNativeStackNavigator<CustomersStackParamList>();
export const CustomersStack = () => { export const CustomersStack = () => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors);
return ( return (
<Stack.Navigator <Stack.Navigator
@ -36,6 +39,21 @@ export const CustomersStack = () => {
component={CustomersScreen} component={CustomersScreen}
options={({ navigation }) => ({ options={({ navigation }) => ({
headerTitle: 'Customers', headerTitle: 'Customers',
headerLeft: () => (
<TouchableOpacity
style={styles.drawerButton}
activeOpacity={0.7}
onPress={() => {
const parentNav = navigation.getParent<DrawerNavigationProp<any>>();
if (parentNav) {
parentNav.openDrawer();
} else {
(navigation as any).openDrawer?.();
}
}}>
<Icon name="menu-outline" size={20} color={colors.text} />
</TouchableOpacity>
),
headerRight: () => ( headerRight: () => (
<TouchableOpacity <TouchableOpacity
onPress={() => navigation.navigate(route.addCustomer)} onPress={() => navigation.navigate(route.addCustomer)}

View File

@ -1,29 +1,12 @@
import React from 'react'; import React from 'react';
import { createDrawerNavigator } from '@react-navigation/drawer'; import { createDrawerNavigator } from '@react-navigation/drawer';
import { TabStack } from './tabStack'; import { TabStack } from './tabStack';
import { CustomersStack } from './customersStack';
import { CustomDrawerContent } from './customDrawerContent'; import { CustomDrawerContent } from './customDrawerContent';
import {
ProposalsScreen,
EstimatesScreen,
InvoicesScreen,
ProjectsScreen,
TasksScreen,
TicketsScreen,
} from '@features';
import { route, RouteParams } from '@utils';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
export type DrawerStackParamList = { home: undefined } & Pick< export type DrawerStackParamList = {
RouteParams, home: undefined;
| 'customers' };
| 'proposals'
| 'estimates'
| 'invoices'
| 'projects'
| 'tasks'
| 'tickets'
>;
const Drawer = createDrawerNavigator<DrawerStackParamList>(); const Drawer = createDrawerNavigator<DrawerStackParamList>();
@ -53,17 +36,6 @@ export const DrawerStack = () => {
component={TabStack} component={TabStack}
options={{ headerShown: false }} options={{ headerShown: false }}
/> />
<Drawer.Screen
name={route.customers}
component={CustomersStack}
options={{ headerShown: false }}
/>
<Drawer.Screen name={route.proposals} component={ProposalsScreen} />
<Drawer.Screen name={route.estimates} component={EstimatesScreen} />
<Drawer.Screen name={route.invoices} component={InvoicesScreen} />
<Drawer.Screen name={route.projects} component={ProjectsScreen} />
<Drawer.Screen name={route.tasks} component={TasksScreen} />
<Drawer.Screen name={route.tickets} component={TicketsScreen} />
</Drawer.Navigator> </Drawer.Navigator>
); );
}; };

View File

@ -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,
},
});

View File

@ -1,5 +1,8 @@
import React from 'react'; import React from 'react';
import { TouchableOpacity, StyleSheet, Platform } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { DrawerNavigationProp } from '@react-navigation/drawer';
import Icon from 'react-native-vector-icons/Ionicons';
import { import {
LeadsScreen, LeadsScreen,
LeadDetailsScreen, LeadDetailsScreen,
@ -9,6 +12,7 @@ import {
} from '@features'; } from '@features';
import { route, RouteParams } from '@utils'; import { route, RouteParams } from '@utils';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { getStyles } from './leadsStack.styles';
export type LeadsStackParamList = Pick< export type LeadsStackParamList = Pick<
RouteParams, RouteParams,
@ -19,6 +23,7 @@ const Stack = createNativeStackNavigator<LeadsStackParamList>();
export const LeadsStack = () => { export const LeadsStack = () => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors);
return ( return (
<Stack.Navigator <Stack.Navigator
@ -38,7 +43,24 @@ export const LeadsStack = () => {
<Stack.Screen <Stack.Screen
name={route.leadsList} name={route.leadsList}
component={LeadsScreen} component={LeadsScreen}
options={{ headerTitle: 'My Leads' }} options={({ navigation }) => ({
headerTitle: 'My Leads',
headerLeft: () => (
<TouchableOpacity
style={styles.drawerButton}
activeOpacity={0.7}
onPress={() => {
const parentNav = navigation.getParent<DrawerNavigationProp<any>>();
if (parentNav) {
parentNav.openDrawer();
} else {
(navigation as any).openDrawer?.();
}
}}>
<Icon name="menu-outline" size={20} color={colors.text} />
</TouchableOpacity>
),
})}
/> />
<Stack.Screen <Stack.Screen
name={route.leadDetails} name={route.leadDetails}

View File

@ -68,9 +68,48 @@ export const getStyles = (colors: ThemeColors) =>
borderRadius: 12, borderRadius: 12,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginLeft: 14, marginLeft: 0,
backgroundColor: colors.surface, backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border, 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,
},
}); });

View File

@ -1,6 +1,7 @@
import React from 'react'; 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 { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
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';
import { route, RouteParams } from '@utils'; import { route, RouteParams } from '@utils';
@ -8,10 +9,12 @@ import {
DashboardScreen, DashboardScreen,
AddLeadScreen, AddLeadScreen,
ProfileScreen, ProfileScreen,
NotificationScreen,
} from '@features'; } from '@features';
import { LeadsStack } from './leadsStack'; import { LeadsStack } from './leadsStack';
import { CustomersStack } from './customersStack'; import { CustomersStack } from './customersStack';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { useAppSelector, RootState } from '@store';
import { getStyles } from './tabStack.styles'; import { getStyles } from './tabStack.styles';
export type TabStackParamList = Pick< export type TabStackParamList = Pick<
@ -21,6 +24,169 @@ export type TabStackParamList = Pick<
const Tab = createBottomTabNavigator<TabStackParamList>(); const Tab = createBottomTabNavigator<TabStackParamList>();
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 (
<DashboardStackNav.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.header,
},
headerTitleStyle: {
fontSize: 17,
fontWeight: '700',
color: colors.text,
},
headerTitleAlign: 'center',
headerTintColor: colors.text,
headerShadowVisible: false,
}}>
<DashboardStackNav.Screen
name="dashboardList"
component={DashboardScreen}
options={({ navigation }) => ({
headerTitle: 'Convex CRM',
headerLeft: () => (
<TouchableOpacity
style={styles.drawerButton}
activeOpacity={0.7}
onPress={() => {
const parentNav = navigation.getParent<DrawerNavigationProp<any>>();
if (parentNav) {
parentNav.openDrawer();
} else {
(navigation as any).openDrawer?.();
}
}}>
<Icon name="menu-outline" size={20} color={colors.text} />
</TouchableOpacity>
),
headerRight: () => (
<TouchableOpacity
style={styles.notificationButton}
activeOpacity={0.7}
onPress={() => {
// Navigate to notification screen if exists, or profile/other suitable route
navigation.navigate(route.notifications);
}}>
<Icon name="notifications-outline" size={20} color='#EF4444' />
{unreadCount > 0 ? (
<View style={styles.badgeContainer}>
<Text style={styles.badgeText}>
{unreadCount > 99 ? '99+' : unreadCount}
</Text>
</View>
) : null}
</TouchableOpacity>
),
})}
/>
<DashboardStackNav.Screen
name={route.notifications}
component={NotificationScreen}
options={{
headerTitle: 'Notifications',
}}
/>
</DashboardStackNav.Navigator>
);
};
const AddLeadStackNav = createNativeStackNavigator();
const AddLeadStack = () => {
const { theme: colors } = useTheme();
const styles = getStyles(colors);
return (
<AddLeadStackNav.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.header,
},
headerTitleStyle: {
fontSize: 17,
fontWeight: '700',
color: colors.text,
},
headerTitleAlign: 'center',
headerTintColor: colors.text,
headerShadowVisible: false,
}}>
<AddLeadStackNav.Screen
name="addLeadForm"
component={AddLeadScreen}
options={({ navigation }) => ({
headerTitle: 'Add Lead',
headerLeft: () => (
<TouchableOpacity
style={styles.drawerButton}
activeOpacity={0.7}
onPress={() => {
const parentNav = navigation.getParent<DrawerNavigationProp<any>>();
if (parentNav) {
parentNav.openDrawer();
} else {
(navigation as any).openDrawer?.();
}
}}>
<Icon name="menu-outline" size={20} color={colors.text} />
</TouchableOpacity>
),
})}
/>
</AddLeadStackNav.Navigator>
);
};
const ProfileStackNav = createNativeStackNavigator();
const ProfileStack = () => {
const { theme: colors } = useTheme();
const styles = getStyles(colors);
return (
<ProfileStackNav.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.header,
},
headerTitleStyle: {
fontSize: 17,
fontWeight: '700',
color: colors.text,
},
headerTitleAlign: 'center',
headerTintColor: colors.text,
headerShadowVisible: false,
}}>
<ProfileStackNav.Screen
name="profileForm"
component={ProfileScreen}
options={({ navigation }) => ({
headerTitle: 'Profile',
headerLeft: () => (
<TouchableOpacity
style={styles.drawerButton}
activeOpacity={0.7}
onPress={() => {
const parentNav = navigation.getParent<DrawerNavigationProp<any>>();
if (parentNav) {
parentNav.openDrawer();
} else {
(navigation as any).openDrawer?.();
}
}}>
<Icon name="menu-outline" size={20} color={colors.text} />
</TouchableOpacity>
),
})}
/>
</ProfileStackNav.Navigator>
);
};
export const TabStack = () => { export const TabStack = () => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
@ -58,47 +224,27 @@ export const TabStack = () => {
tabBarInactiveTintColor: colors.textMuted, tabBarInactiveTintColor: colors.textMuted,
tabBarStyle: styles.tabBar, tabBarStyle: styles.tabBar,
tabBarLabelStyle: styles.tabBarLabel, tabBarLabelStyle: styles.tabBarLabel,
headerStyle: styles.header, headerShown: false,
headerTitleStyle: styles.headerTitle,
headerTitleAlign: 'center',
})}> })}>
<Tab.Screen <Tab.Screen
name={route.dashboard} name={route.dashboard}
component={DashboardScreen} component={DashboardStack}
options={({ navigation }) => ({ options={{
tabBarLabel: 'Dashboard', tabBarLabel: 'Dashboard',
headerTitle: 'Convex CRM', }}
headerLeft: () => (
<TouchableOpacity
style={styles.drawerButton}
activeOpacity={0.7}
onPress={() => {
const parentNav = navigation.getParent<DrawerNavigationProp<any>>();
if (parentNav) {
parentNav.openDrawer();
} else {
(navigation as any).openDrawer?.();
}
}}>
<Icon name="menu-outline" size={20} color={colors.text} />
</TouchableOpacity>
),
})}
/> />
<Tab.Screen <Tab.Screen
name={route.leads} name={route.leads}
component={LeadsStack} component={LeadsStack}
options={{ options={{
tabBarLabel: 'Leads', tabBarLabel: 'Leads',
headerShown: false,
}} }}
/> />
<Tab.Screen <Tab.Screen
name={route.addLead} name={route.addLead}
component={AddLeadScreen} component={AddLeadStack}
options={{ options={{
tabBarLabel: 'Add Lead', tabBarLabel: 'Add Lead',
headerTitle: 'Add Lead',
}} }}
/> />
<Tab.Screen <Tab.Screen
@ -106,15 +252,13 @@ export const TabStack = () => {
component={CustomersStack} component={CustomersStack}
options={{ options={{
tabBarLabel: 'Customers', tabBarLabel: 'Customers',
headerShown: false,
}} }}
/> />
<Tab.Screen <Tab.Screen
name={route.profile} name={route.profile}
component={ProfileScreen} component={ProfileStack}
options={{ options={{
tabBarLabel: 'Profile', tabBarLabel: 'Profile',
headerTitle: 'Profile',
}} }}
/> />
</Tab.Navigator> </Tab.Navigator>

View File

@ -3,7 +3,6 @@ import messaging, {
} from '@react-native-firebase/messaging'; } from '@react-native-firebase/messaging';
import notifee, { import notifee, {
AndroidImportance, AndroidImportance,
AndroidVisibility,
AuthorizationStatus, AuthorizationStatus,
EventType, EventType,
} from '@notifee/react-native'; } from '@notifee/react-native';
@ -11,7 +10,6 @@ import { Platform } from 'react-native';
const CHANNEL_ID = 'convex_crm_default'; const CHANNEL_ID = 'convex_crm_default';
const CHANNEL_NAME = 'Convex CRM Notifications'; const CHANNEL_NAME = 'Convex CRM Notifications';
const CHANNEL_DESCRIPTION = 'General push notifications for Convex CRM';
export interface NotificationData { export interface NotificationData {
[key: string]: string; [key: string]: string;
@ -45,16 +43,8 @@ export async function createDefaultChannel(): Promise<void> {
await notifee.createChannel({ await notifee.createChannel({
id: CHANNEL_ID, id: CHANNEL_ID,
name: CHANNEL_NAME, name: CHANNEL_NAME,
description: CHANNEL_DESCRIPTION,
importance: AndroidImportance.HIGH, 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) { } catch (error) {
console.error('[NotificationService] createDefaultChannel error:', error); console.error('[NotificationService] createDefaultChannel error:', error);
} }
@ -101,18 +91,7 @@ export async function displayNotification(
data, data,
android: { android: {
channelId: CHANNEL_ID, 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' }, pressAction: { id: 'default' },
color: '#4F46E5',
},
ios: {
sound: 'default',
foregroundPresentationOptions: {
alert: true,
badge: true,
sound: true,
},
}, },
}); });
} catch (error) { } catch (error) {
@ -259,13 +238,10 @@ export async function initialize(
): Promise<() => void> { ): Promise<() => void> {
const { onNotificationOpen, onTokenRefreshed } = options; const { onNotificationOpen, onTokenRefreshed } = options;
// 1. Permission // 1. Android channel (permission is requested explicitly after login)
await requestPermission();
// 2. Android channel
await createDefaultChannel(); await createDefaultChannel();
// 3. FCM token // 2. FCM token
await getFCMToken(); await getFCMToken();
// 4. Token refresh // 4. Token refresh

View File

@ -27,6 +27,7 @@ export const route = {
customerDetails: 'customerDetails', customerDetails: 'customerDetails',
addCustomer: 'addCustomer', addCustomer: 'addCustomer',
profile: 'profile', profile: 'profile',
notifications: 'notifications',
} as const; } as const;
// ─── Route Param Types ──────────────────────────────────────────────────────── // ─── Route Param Types ────────────────────────────────────────────────────────
@ -54,6 +55,7 @@ export type RouteParams = {
customerDetails: { userid: string; customer?: CustomerItem }; customerDetails: { userid: string; customer?: CustomerItem };
addCustomer: undefined; addCustomer: undefined;
profile: undefined; profile: undefined;
notifications: undefined;
}; };