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) =>
|
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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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}>
|
||||||
<View style={styles.avatarCircle}>
|
{/* Top Row: Avatar + Info + Active Badge */}
|
||||||
<Text style={styles.avatarInitial}>{initials}</Text>
|
<View style={styles.topRow}>
|
||||||
</View>
|
<View style={styles.avatarCircle}>
|
||||||
|
<Text style={styles.avatarInitial}>{initials}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.companyName}>{displayName}</Text>
|
<View style={styles.info}>
|
||||||
{fullname && company ? (
|
<Text style={styles.name} numberOfLines={1}>{displayName}</Text>
|
||||||
<Text style={styles.fullName}>{fullname}</Text>
|
{fullname && company ? (
|
||||||
) : null}
|
<Text style={styles.company} numberOfLines={1}>{fullname}</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>
|
||||||
|
{datecreated ? (
|
||||||
|
<Text style={styles.dateText} numberOfLines={1}>
|
||||||
|
Added {formatDate(datecreated)}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.badgeRow}>
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.badge,
|
styles.statusBadge,
|
||||||
active === '1' ? styles.activeBadge : styles.inactiveBadge,
|
active === '1' ? styles.activeBadge : styles.inactiveBadge,
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.badgeText,
|
styles.statusText,
|
||||||
active === '1' ? styles.activeBadgeText : styles.inactiveBadgeText,
|
active === '1' ? styles.activeText : styles.inactiveText,
|
||||||
]}>
|
]}>
|
||||||
{active === '1' ? 'Active Customer' : 'Inactive'}
|
{active === '1' ? 'Active' : 'Inactive'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{datecreated ? (
|
|
||||||
<Text style={styles.dateText}>
|
|
||||||
Added {formatDate(datecreated)}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* 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}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,53 +26,52 @@ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={[styles.avatar, { backgroundColor: accent }]}>
|
{/* Top Row: Avatar + Info + Status */}
|
||||||
<Text style={styles.avatarText}>{getInitials(name)}</Text>
|
<View style={styles.topRow}>
|
||||||
</View>
|
<View style={[styles.avatar, { backgroundColor: accent }]}>
|
||||||
|
<Text style={styles.avatarText}>{getInitials(name)}</Text>
|
||||||
<Text style={styles.name} numberOfLines={1}>
|
|
||||||
{name || 'No Name'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{company && (
|
|
||||||
<View style={styles.companyRow}>
|
|
||||||
<Icon name="business-outline" size={13} color={colors.textMuted} />
|
|
||||||
<Text style={styles.company}>{company}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<View style={styles.info}>
|
||||||
onPress={onStatusPress}
|
<Text style={styles.name} numberOfLines={1}>{name || 'No Name'}</Text>
|
||||||
activeOpacity={onStatusPress ? 0.7 : 1}
|
{company ? (
|
||||||
disabled={!onStatusPress}>
|
<Text style={styles.company} numberOfLines={1}>{company}</Text>
|
||||||
<View style={[styles.statusBadge, { backgroundColor: badgeBg }]}>
|
) : null}
|
||||||
<View style={[styles.statusDot, { backgroundColor: accent }]} />
|
<Text style={styles.contact} numberOfLines={1}>
|
||||||
<Text style={[styles.statusText, { color: accent }]}>
|
<Icon name="call-outline" size={10} color={colors.textMuted} />
|
||||||
{statusName || 'No Status'}
|
{' '}{phonenumber || 'N/A'}{' '}
|
||||||
|
<Icon name="mail-outline" size={10} color={colors.textMuted} />
|
||||||
|
{' '}{email || 'N/A'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.quickActions}>
|
<TouchableOpacity
|
||||||
{email && (
|
onPress={onStatusPress}
|
||||||
<TouchableOpacity
|
activeOpacity={onStatusPress ? 0.7 : 1}
|
||||||
style={styles.actionBtn}
|
disabled={!onStatusPress}>
|
||||||
onPress={onEmailPress}
|
<View style={[styles.statusBadge, { backgroundColor: badgeBg }]}>
|
||||||
activeOpacity={0.7}>
|
<View style={[styles.statusDot, { backgroundColor: accent }]} />
|
||||||
<Icon name="mail-outline" size={16} color={colors.icon} />
|
<Text style={[styles.statusText, { color: accent }]}>
|
||||||
<Text style={styles.actionBtnText}>Email</Text>
|
{statusName || 'No Status'}
|
||||||
</TouchableOpacity>
|
</Text>
|
||||||
)}
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{phonenumber && (
|
{/* Bottom Row: Call + Email Buttons */}
|
||||||
<TouchableOpacity
|
<View style={styles.actionRow}>
|
||||||
style={styles.actionBtn}
|
<TouchableOpacity
|
||||||
onPress={onPhonePress}
|
style={[styles.actionBtn, { backgroundColor: `${accent}12`, borderColor: `${accent}30` }]}
|
||||||
activeOpacity={0.7}>
|
onPress={onPhonePress}>
|
||||||
<Icon name="call-outline" size={16} color={colors.icon} />
|
<Icon name="call" size={14} color={accent} />
|
||||||
<Text style={styles.actionBtnText}>Call</Text>
|
<Text style={[styles.btnText, { color: accent }]}>Call</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
<TouchableOpacity
|
||||||
|
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>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,108 +4,80 @@ 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 style={styles.meta}>
|
|
||||||
<Text style={styles.name} numberOfLines={1}>
|
|
||||||
{item.name || 'No Name'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{(item.company || item.title) && (
|
|
||||||
<View style={styles.companyRow}>
|
|
||||||
{item.company && (
|
|
||||||
<>
|
|
||||||
<Icon name="business-outline" size={11} color={colors.textMuted} />
|
|
||||||
<Text style={styles.companyText} numberOfLines={1}>{item.company}</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{item.company && item.title && <View style={styles.dot} />}
|
|
||||||
{item.title && (
|
|
||||||
<Text style={styles.titleText} numberOfLines={1}>{item.title}</Text>
|
|
||||||
)}
|
|
||||||
</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'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.cardInfo}>
|
||||||
{(item.email || item.phonenumber) && (
|
<Text style={styles.customerName} numberOfLines={1}>
|
||||||
<>
|
{displayName}
|
||||||
<View style={styles.separator} />
|
</Text>
|
||||||
<View style={styles.contacts}>
|
{item.company ? (
|
||||||
{item.email && (
|
<Text style={styles.contactPerson} numberOfLines={1}>
|
||||||
<View style={styles.contactRow}>
|
{item.company}
|
||||||
<View style={[styles.iconWrap, { backgroundColor: tint(0.08) }]}>
|
</Text>
|
||||||
<Icon name="mail-outline" size={13} color={accent} />
|
) : null}
|
||||||
</View>
|
<Text style={styles.location} numberOfLines={1}>
|
||||||
<Text style={styles.contactText} numberOfLines={1}>{item.email}</Text>
|
<Icon name="call-outline" size={11} color={colors.textMuted} />
|
||||||
<TouchableOpacity
|
{' '}{item.phonenumber || 'N/A'}{' • '}
|
||||||
style={[styles.actionBtn, { backgroundColor: tint(0.08) }]}
|
<Icon name="mail-outline" size={11} color={colors.textMuted} />
|
||||||
onPress={() => handleEmailPress(item.email)}
|
{' '}{item.email || 'N/A'}
|
||||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
</Text>
|
||||||
<Icon name="send-outline" size={12} color={accent} />
|
{item.source_name || item.source || item.dateadded ? (
|
||||||
</TouchableOpacity>
|
<Text style={styles.location} numberOfLines={1}>
|
||||||
</View>
|
{item.source_name || item.source ? (
|
||||||
)}
|
<>
|
||||||
|
<Icon name="radio-outline" size={11} color={colors.textMuted} />
|
||||||
{item.phonenumber && (
|
{' '}{item.source_name || item.source}
|
||||||
<View style={styles.contactRow}>
|
</>
|
||||||
<View style={[styles.iconWrap, { backgroundColor: tint(0.08) }]}>
|
) : null}
|
||||||
<Icon name="call-outline" size={13} color={accent} />
|
{item.dateadded ? ` • ${formatDate(item.dateadded)}` : ''}
|
||||||
</View>
|
</Text>
|
||||||
<Text style={styles.contactText} numberOfLines={1}>{item.phonenumber}</Text>
|
) : null}
|
||||||
<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} />
|
|
||||||
</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>
|
</View>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<View style={styles.cardFooter}>
|
||||||
|
<TouchableOpacity
|
||||||
|
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>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
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>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
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 {
|
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"
|
|
||||||
size={18}
|
|
||||||
color={colors.icon}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.preferenceText}>Security & Privacy</Text>
|
<Text style={styles.preferenceText}>Push Notifications</Text>
|
||||||
</View>
|
</View>
|
||||||
<Icon name="chevron-forward" size={18} color={colors.textMuted} />
|
<Switch
|
||||||
</TouchableOpacity>
|
value={pushEnabled}
|
||||||
|
onValueChange={setPushEnabled}
|
||||||
|
trackColor={{ false: '#767577', true: colors.icon }}
|
||||||
|
thumbColor={pushEnabled ? '#ffffff' : '#f4f3f4'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ── Sign Out ── */}
|
{/* ── Sign Out ── */}
|
||||||
|
|||||||
@ -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: 'Customers', route: route.customers, icon: 'business-outline' },
|
{ label: 'Leads', route: route.leads, icon: 'funnel-outline' },
|
||||||
{ label: 'Proposals', route: route.proposals, icon: 'document-text-outline' },
|
{ label: 'Add Lead', route: route.addLead, icon: 'person-add-outline' },
|
||||||
{ label: 'Estimates', route: route.estimates, icon: 'calculator-outline' },
|
{ label: 'Customers', route: route.customers, icon: 'business-outline' },
|
||||||
{ label: 'Invoices', route: route.invoices, icon: 'card-outline' },
|
// { label: 'Proposals', route: route.proposals, icon: 'document-text-outline' },
|
||||||
{ label: 'Projects', route: route.projects, icon: 'rocket-outline' },
|
// { label: 'Estimates', route: route.estimates, icon: 'calculator-outline' },
|
||||||
{ label: 'Tasks', route: route.tasks, icon: 'checkbox-outline' },
|
// { label: 'Invoices', route: route.invoices, icon: 'card-outline' },
|
||||||
{ label: 'Tickets', route: route.tickets, icon: 'bug-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 './proposal';
|
||||||
export * from './tasks';
|
export * from './tasks';
|
||||||
export * from './tickets';
|
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}>
|
<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 });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
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 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)}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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 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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user