feat(app): improve design
This commit is contained in:
parent
db1dc98300
commit
e96ea0a490
@ -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,
|
||||
},
|
||||
|
||||
@ -25,45 +25,53 @@ export const CustomerDetailHeader: React.FC<CustomerDetailHeaderProps> = ({
|
||||
const initials = getInitials(displayName);
|
||||
|
||||
return (
|
||||
<View style={styles.headerCard}>
|
||||
<View style={styles.card}>
|
||||
{/* Top Row: Avatar + Info + Active Badge */}
|
||||
<View style={styles.topRow}>
|
||||
<View style={styles.avatarCircle}>
|
||||
<Text style={styles.avatarInitial}>{initials}</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.companyName}>{displayName}</Text>
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.name} numberOfLines={1}>{displayName}</Text>
|
||||
{fullname && company ? (
|
||||
<Text style={styles.fullName}>{fullname}</Text>
|
||||
<Text style={styles.company} numberOfLines={1}>{fullname}</Text>
|
||||
) : null}
|
||||
|
||||
<View style={styles.badgeRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
active === '1' ? styles.activeBadge : styles.inactiveBadge,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.badgeText,
|
||||
active === '1' ? styles.activeBadgeText : styles.inactiveBadgeText,
|
||||
]}>
|
||||
{active === '1' ? 'Active Customer' : 'Inactive'}
|
||||
<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>
|
||||
</View>
|
||||
|
||||
{datecreated ? (
|
||||
<Text style={styles.dateText}>
|
||||
<Text style={styles.dateText} numberOfLines={1}>
|
||||
Added {formatDate(datecreated)}
|
||||
</Text>
|
||||
) : null}
|
||||
</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}>
|
||||
{phonenumber ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, styles.primaryBtn]}
|
||||
onPress={onPhonePress}>
|
||||
<Icon name="call" size={16} color="#FFFFFF" />
|
||||
<Icon name="call" size={14} color="#FFFFFF" />
|
||||
<Text style={styles.primaryBtnText}>Call</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
@ -72,7 +80,7 @@ export const CustomerDetailHeader: React.FC<CustomerDetailHeaderProps> = ({
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, styles.secondaryBtn]}
|
||||
onPress={onEmailPress}>
|
||||
<Icon name="mail" size={16} color={colors.text} />
|
||||
<Icon name="mail" size={14} color={colors.textSecondary} />
|
||||
<Text style={styles.secondaryBtnText}>Email</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
@ -81,7 +89,7 @@ export const CustomerDetailHeader: React.FC<CustomerDetailHeaderProps> = ({
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, styles.secondaryBtn]}
|
||||
onPress={onWebsitePress}>
|
||||
<Icon name="globe" size={16} color={colors.text} />
|
||||
<Icon name="globe" size={14} color={colors.textSecondary} />
|
||||
<Text style={styles.secondaryBtnText}>Website</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
@ -9,6 +9,8 @@ export interface FormInputProps extends Omit<TextInputProps, 'style'> {
|
||||
multiline?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
prefix?: string;
|
||||
prefixStyle?: StyleProp<TextStyle>;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inputContainerStyle?: StyleProp<ViewStyle>;
|
||||
inputStyle?: StyleProp<TextStyle>;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -14,6 +14,8 @@ export const FormInput: React.FC<FormInputProps> = ({
|
||||
keyboardType = 'default',
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
prefix,
|
||||
prefixStyle,
|
||||
containerStyle,
|
||||
inputContainerStyle,
|
||||
inputStyle,
|
||||
@ -32,6 +34,9 @@ export const FormInput: React.FC<FormInputProps> = ({
|
||||
) : null}
|
||||
<View style={[styles.inputWrapper, inputContainerStyle]}>
|
||||
{leftIcon ? <View style={styles.leftIconWrapper}>{leftIcon}</View> : null}
|
||||
{prefix ? (
|
||||
<Text style={[styles.prefix, prefixStyle]}>{prefix}</Text>
|
||||
) : null}
|
||||
<TextInput
|
||||
style={[styles.input, multiline && styles.multilineInput, inputStyle]}
|
||||
value={value}
|
||||
|
||||
@ -5,96 +5,99 @@ export const getStyles = (colors: ThemeColors) =>
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -26,20 +26,24 @@ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
{/* Top Row: Avatar + Info + Status */}
|
||||
<View style={styles.topRow}>
|
||||
<View style={[styles.avatar, { backgroundColor: accent }]}>
|
||||
<Text style={styles.avatarText}>{getInitials(name)}</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.name} numberOfLines={1}>
|
||||
{name || 'No Name'}
|
||||
<View style={styles.info}>
|
||||
<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>
|
||||
|
||||
{company && (
|
||||
<View style={styles.companyRow}>
|
||||
<Icon name="business-outline" size={13} color={colors.textMuted} />
|
||||
<Text style={styles.company}>{company}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onStatusPress}
|
||||
@ -52,27 +56,22 @@ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.quickActions}>
|
||||
{email && (
|
||||
{/* Bottom Row: Call + Email Buttons */}
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
onPress={onEmailPress}
|
||||
activeOpacity={0.7}>
|
||||
<Icon name="mail-outline" size={16} color={colors.icon} />
|
||||
<Text style={styles.actionBtnText}>Email</Text>
|
||||
style={[styles.actionBtn, { backgroundColor: `${accent}12`, borderColor: `${accent}30` }]}
|
||||
onPress={onPhonePress}>
|
||||
<Icon name="call" size={14} color={accent} />
|
||||
<Text style={[styles.btnText, { color: accent }]}>Call</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{phonenumber && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
onPress={onPhonePress}
|
||||
activeOpacity={0.7}>
|
||||
<Icon name="call-outline" size={16} color={colors.icon} />
|
||||
<Text style={styles.actionBtnText}>Call</Text>
|
||||
style={[styles.actionBtn, styles.secondaryBtn]}
|
||||
onPress={onEmailPress}>
|
||||
<Icon name="mail" size={14} color={colors.textSecondary} />
|
||||
<Text style={[styles.btnText, { color: colors.textSecondary }]}>Email</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -4,109 +4,81 @@ 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<LeadItemCardProps> = ({ 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 (
|
||||
<TouchableOpacity style={styles.cardWrapper} onPress={onPress} activeOpacity={0.8}>
|
||||
<View style={styles.card}>
|
||||
<View style={[styles.leftStrip, { backgroundColor: accent }]} />
|
||||
|
||||
<View style={styles.cardInner}>
|
||||
<View style={styles.topSection}>
|
||||
<View style={[styles.avatar, { backgroundColor: accent }]}>
|
||||
<Text style={styles.avatarText}>{getInitials(item.name)}</Text>
|
||||
<TouchableOpacity
|
||||
activeOpacity={onPress ? 0.8 : 1}
|
||||
onPress={onPress}
|
||||
style={styles.customerCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.cardHeaderLeft}>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: accent }]}>
|
||||
<Text style={styles.avatarInitial}>{initials}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.meta}>
|
||||
<Text style={styles.name} numberOfLines={1}>
|
||||
{item.name || 'No Name'}
|
||||
<View style={styles.cardInfo}>
|
||||
<Text style={styles.customerName} numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
{(item.company || item.title) && (
|
||||
<View style={styles.companyRow}>
|
||||
{item.company && (
|
||||
{item.company ? (
|
||||
<Text style={styles.contactPerson} numberOfLines={1}>
|
||||
{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} />
|
||||
<Text style={styles.companyText} numberOfLines={1}>{item.company}</Text>
|
||||
<Icon name="radio-outline" size={11} color={colors.textMuted} />
|
||||
{' '}{item.source_name || item.source}
|
||||
</>
|
||||
)}
|
||||
{item.company && item.title && <View style={styles.dot} />}
|
||||
{item.title && (
|
||||
<Text style={styles.titleText} numberOfLines={1}>{item.title}</Text>
|
||||
)}
|
||||
) : null}
|
||||
{item.dateadded ? ` • ${formatDate(item.dateadded)}` : ''}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[styles.statusBadge, { backgroundColor: tint(0.10) }]}>
|
||||
<View style={[styles.statusDot, { backgroundColor: accent }]} />
|
||||
<Text style={[styles.statusText, { color: accent }]}>
|
||||
{item.status_name || item.status || 'No Status'}
|
||||
<View style={styles.cardHeaderRight}>
|
||||
{item.status_name || item.status ? (
|
||||
<View style={[styles.badge, { backgroundColor: `${accent}15` }]}>
|
||||
<Text style={[styles.badgeText, { color: accent }]}>
|
||||
{item.status_name || item.status}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{(item.email || item.phonenumber) && (
|
||||
<>
|
||||
<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>
|
||||
<View style={styles.cardFooter}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, { backgroundColor: tint(0.08) }]}
|
||||
onPress={() => handleEmailPress(item.email)}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Icon name="send-outline" size={12} color={accent} />
|
||||
style={[styles.actionButton, styles.callButton, { backgroundColor: `${accent}10` }]}
|
||||
onPress={() => handlePhonePress(item.phonenumber)}>
|
||||
<Icon name="call" size={15} color={accent} />
|
||||
<Text style={[styles.callButtonText, { color: accent }]}>Call</Text>
|
||||
</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
|
||||
style={[styles.actionBtn, { backgroundColor: tint(0.08) }]}
|
||||
onPress={() => handlePhonePress(item.phonenumber)}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Icon name="call-outline" size={12} color={accent} />
|
||||
style={[styles.actionButton, styles.emailButton]}
|
||||
onPress={() => handleEmailPress(item.email)}>
|
||||
<Icon name="mail" size={15} color={colors.textSecondary} />
|
||||
<Text style={styles.emailButtonText}>Email</Text>
|
||||
</TouchableOpacity>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -16,5 +16,6 @@ export * from './tasks';
|
||||
export * from './tickets';
|
||||
export * from './customerDetails';
|
||||
export * from './addCustomer';
|
||||
export * from './notification';
|
||||
|
||||
|
||||
|
||||
@ -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<string | null>(null);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [deviceToken, setDeviceToken] = useState<string | null>(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 = () => {
|
||||
/>
|
||||
<View style={styles.overlay}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
||||
>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}
|
||||
keyboardDismissMode="none"
|
||||
>
|
||||
{/* Top Area Spacer displaying background illustration */}
|
||||
<View style={styles.topSpacer} />
|
||||
@ -173,7 +185,11 @@ export const LoginScreen = () => {
|
||||
<FormInput
|
||||
label="Tenancy Name"
|
||||
value={tenancy}
|
||||
onChangeText={setTenancy}
|
||||
onChangeText={(text) => {
|
||||
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={
|
||||
<Icon
|
||||
@ -211,7 +230,10 @@ export const LoginScreen = () => {
|
||||
styles.inputWrapper,
|
||||
focusedField === 'email' && styles.inputWrapperFocused,
|
||||
]}
|
||||
onFocus={() => setFocusedField('email')}
|
||||
onFocus={() => {
|
||||
setFocusedField('email');
|
||||
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
|
||||
}}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
leftIcon={
|
||||
<Icon
|
||||
@ -240,7 +262,10 @@ export const LoginScreen = () => {
|
||||
styles.inputWrapper,
|
||||
focusedField === 'password' && styles.inputWrapperFocused,
|
||||
]}
|
||||
onFocus={() => setFocusedField('password')}
|
||||
onFocus={() => {
|
||||
setFocusedField('password');
|
||||
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100);
|
||||
}}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
leftIcon={
|
||||
<Icon
|
||||
|
||||
@ -19,10 +19,9 @@ export const getStyles = (colors: ThemeColors, isDark: boolean) =>
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
topSpacer: {
|
||||
flex: 1,
|
||||
minHeight: 120,
|
||||
},
|
||||
bottomSheetCard: {
|
||||
|
||||
1
app/features/notification/index.ts
Normal file
1
app/features/notification/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './notification.screen';
|
||||
105
app/features/notification/notification.screen.tsx
Normal file
105
app/features/notification/notification.screen.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
94
app/features/notification/notification.styles.ts
Normal file
94
app/features/notification/notification.styles.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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 = () => {
|
||||
<Text style={styles.sectionHeader}>Personal Details</Text>
|
||||
<ProfileInfoRow
|
||||
icon="person-outline"
|
||||
label="Full Name"
|
||||
label="Name"
|
||||
value={fullName}
|
||||
/>
|
||||
<ProfileInfoRow
|
||||
@ -124,17 +125,17 @@ export const ProfileScreen = () => {
|
||||
/>
|
||||
<ProfileInfoRow
|
||||
icon="mail-outline"
|
||||
label="Email Address"
|
||||
label="Email"
|
||||
value={email}
|
||||
/>
|
||||
<ProfileInfoRow
|
||||
icon="call-outline"
|
||||
label="Phone Number"
|
||||
label="Phone"
|
||||
value={phone}
|
||||
/>
|
||||
<ProfileInfoRow
|
||||
icon="shield-checkmark-outline"
|
||||
label="Account Role"
|
||||
label="Role"
|
||||
value={roleText}
|
||||
isLast
|
||||
/>
|
||||
@ -227,7 +228,7 @@ export const ProfileScreen = () => {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.preferenceRow} activeOpacity={0.7}>
|
||||
{/* <TouchableOpacity style={styles.preferenceRow} activeOpacity={0.7}>
|
||||
<View style={styles.preferenceLabelGroup}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<Icon
|
||||
@ -239,23 +240,22 @@ export const ProfileScreen = () => {
|
||||
<Text style={styles.preferenceText}>Push Notifications</Text>
|
||||
</View>
|
||||
<Icon name="chevron-forward" size={18} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity> */}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.preferenceRow, styles.noBorder]}
|
||||
activeOpacity={0.7}>
|
||||
<View style={[styles.preferenceRow, styles.noBorder]}>
|
||||
<View style={styles.preferenceLabelGroup}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<Icon
|
||||
name="shield-checkmark-outline"
|
||||
size={18}
|
||||
color={colors.icon}
|
||||
<Icon name="notifications-outline" size={18} color={colors.icon} />
|
||||
</View>
|
||||
<Text style={styles.preferenceText}>Push Notifications</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={pushEnabled}
|
||||
onValueChange={setPushEnabled}
|
||||
trackColor={{ false: '#767577', true: colors.icon }}
|
||||
thumbColor={pushEnabled ? '#ffffff' : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.preferenceText}>Security & Privacy</Text>
|
||||
</View>
|
||||
<Icon name="chevron-forward" size={18} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* ── Sign Out ── */}
|
||||
|
||||
@ -2,11 +2,13 @@ import { route } from "../utils/route";
|
||||
|
||||
export const menuItems = [
|
||||
{ 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: '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: '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' },
|
||||
];
|
||||
@ -8,3 +8,4 @@ export * from './projects';
|
||||
export * from './proposal';
|
||||
export * from './tasks';
|
||||
export * from './tickets';
|
||||
export * from './notification'
|
||||
|
||||
48
app/mock-data/notification.ts
Normal file
48
app/mock-data/notification.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
@ -124,14 +124,37 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => {
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.menuContainer}>
|
||||
{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 (
|
||||
<DrawerItem
|
||||
key={item.route}
|
||||
label={item.label}
|
||||
iconName={item.icon}
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
16
app/navigation/customersStack.styles.ts
Normal file
16
app/navigation/customersStack.styles.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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<CustomersStackParamList>();
|
||||
|
||||
export const CustomersStack = () => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
@ -36,6 +39,21 @@ export const CustomersStack = () => {
|
||||
component={CustomersScreen}
|
||||
options={({ navigation }) => ({
|
||||
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: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate(route.addCustomer)}
|
||||
|
||||
@ -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<DrawerStackParamList>();
|
||||
|
||||
@ -53,17 +36,6 @@ export const DrawerStack = () => {
|
||||
component={TabStack}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
16
app/navigation/leadsStack.styles.ts
Normal file
16
app/navigation/leadsStack.styles.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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<LeadsStackParamList>();
|
||||
|
||||
export const LeadsStack = () => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
@ -38,7 +43,24 @@ export const LeadsStack = () => {
|
||||
<Stack.Screen
|
||||
name={route.leadsList}
|
||||
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
|
||||
name={route.leadDetails}
|
||||
|
||||
@ -68,9 +68,48 @@ export const getStyles = (colors: ThemeColors) =>
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -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<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 = () => {
|
||||
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,
|
||||
})}>
|
||||
<Tab.Screen
|
||||
name={route.dashboard}
|
||||
component={DashboardScreen}
|
||||
options={({ navigation }) => ({
|
||||
component={DashboardStack}
|
||||
options={{
|
||||
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
|
||||
name={route.leads}
|
||||
component={LeadsStack}
|
||||
options={{
|
||||
tabBarLabel: 'Leads',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={route.addLead}
|
||||
component={AddLeadScreen}
|
||||
component={AddLeadStack}
|
||||
options={{
|
||||
tabBarLabel: 'Add Lead',
|
||||
headerTitle: 'Add Lead',
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
@ -106,15 +252,13 @@ export const TabStack = () => {
|
||||
component={CustomersStack}
|
||||
options={{
|
||||
tabBarLabel: 'Customers',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={route.profile}
|
||||
component={ProfileScreen}
|
||||
component={ProfileStack}
|
||||
options={{
|
||||
tabBarLabel: 'Profile',
|
||||
headerTitle: 'Profile',
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
|
||||
@ -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<void> {
|
||||
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
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user