feat(app): add lead and leadDetails
This commit is contained in:
parent
801218f692
commit
2a8c5f7e60
183062
android/app/src/main/assets/index.android.bundle
Normal file
183062
android/app/src/main/assets/index.android.bundle
Normal file
File diff suppressed because one or more lines are too long
11
app/App.tsx
11
app/App.tsx
@ -3,6 +3,9 @@ import { StyleSheet, StatusBar } from 'react-native';
|
|||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import { RootNavigator } from './navigation/rootNavigator';
|
import { RootNavigator } from './navigation/rootNavigator';
|
||||||
import { ThemeProvider, useTheme } from './theme';
|
import { ThemeProvider, useTheme } from './theme';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { store, persistor } from '@store';
|
||||||
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
|
|
||||||
const ThemedStatusBar = () => {
|
const ThemedStatusBar = () => {
|
||||||
const { theme: colors, isDark } = useTheme();
|
const { theme: colors, isDark } = useTheme();
|
||||||
@ -22,17 +25,17 @@ const ThemedStatusBar = () => {
|
|||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ThemedStatusBar />
|
<ThemedStatusBar />
|
||||||
<RootNavigator />
|
<RootNavigator />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
root: { flex: 1 },
|
|
||||||
});
|
|
||||||
|
|||||||
14
app/api/authApi.ts
Normal file
14
app/api/authApi.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { LoginRequest, LoginResponse } from '@interfaces';
|
||||||
|
import { api } from '@utils';
|
||||||
|
|
||||||
|
export const loginApi = async (
|
||||||
|
payload: LoginRequest,
|
||||||
|
): Promise<LoginResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('email', payload.email);
|
||||||
|
formData.append('password', payload.password);
|
||||||
|
if(payload.device_token) {
|
||||||
|
formData.append('device_token', String(payload.device_token));
|
||||||
|
}
|
||||||
|
return await api.post<LoginResponse>('/api/stafflogin', formData);
|
||||||
|
};
|
||||||
3
app/api/index.ts
Normal file
3
app/api/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './authApi';
|
||||||
|
export * from './leadsApi';
|
||||||
|
export * from './leadDetailsApi';
|
||||||
10
app/api/leadDetailsApi.ts
Normal file
10
app/api/leadDetailsApi.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { LeadDetailsItem } from '@interfaces';
|
||||||
|
import { api } from '@utils';
|
||||||
|
|
||||||
|
export const getLeadDetailsApi = async (
|
||||||
|
leadId: string,
|
||||||
|
staffId: string,
|
||||||
|
): Promise<LeadDetailsItem> => {
|
||||||
|
const url = `/api/leads/${leadId}/${staffId}`;
|
||||||
|
return await api.get<LeadDetailsItem>(url);
|
||||||
|
};
|
||||||
10
app/api/leadsApi.ts
Normal file
10
app/api/leadsApi.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { LeadItem } from '@interfaces';
|
||||||
|
import { api } from '@utils';
|
||||||
|
|
||||||
|
export const getLeadsApi = async (
|
||||||
|
leadId: string | null,
|
||||||
|
staffId: string,
|
||||||
|
): Promise<LeadItem[]> => {
|
||||||
|
const url = `/api/leads/${leadId}/${staffId}`;
|
||||||
|
return await api.get<LeadItem[]>(url);
|
||||||
|
};
|
||||||
4
app/components/index.ts
Normal file
4
app/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './leadItemCard';
|
||||||
|
export * from './searchInput';
|
||||||
|
export * from './loader';
|
||||||
|
export * from './leadDetailHeader';
|
||||||
2
app/components/leadDetailHeader/index.ts
Normal file
2
app/components/leadDetailHeader/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './leadDetailHeader';
|
||||||
|
export * from './leadDetailHeader.props';
|
||||||
12
app/components/leadDetailHeader/leadDetailHeader.props.ts
Normal file
12
app/components/leadDetailHeader/leadDetailHeader.props.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface LeadDetailHeaderProps {
|
||||||
|
name: string;
|
||||||
|
company?: string;
|
||||||
|
email?: string;
|
||||||
|
phonenumber?: string;
|
||||||
|
statusName: string;
|
||||||
|
statusColor: string;
|
||||||
|
avatarColor?: string;
|
||||||
|
onStatusPress?: () => void;
|
||||||
|
onEmailPress?: () => void;
|
||||||
|
onPhonePress?: () => void;
|
||||||
|
}
|
||||||
79
app/components/leadDetailHeader/leadDetailHeader.styles.ts
Normal file
79
app/components/leadDetailHeader/leadDetailHeader.styles.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { ThemeColors } from '../../theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: ThemeColors) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 24,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
avatarText: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
nameRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.text,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
},
|
||||||
|
company: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
contactRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
contactLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text,
|
||||||
|
marginRight: 8,
|
||||||
|
minWidth: 50,
|
||||||
|
},
|
||||||
|
contactText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
iconButton: {
|
||||||
|
marginLeft: 8,
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
103
app/components/leadDetailHeader/leadDetailHeader.tsx
Normal file
103
app/components/leadDetailHeader/leadDetailHeader.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, TouchableOpacity } from 'react-native';
|
||||||
|
import Icon from 'react-native-vector-icons/Ionicons';
|
||||||
|
import { useTheme } from '@theme';
|
||||||
|
import { getStyles } from './leadDetailHeader.styles';
|
||||||
|
import { LeadDetailHeaderProps } from './leadDetailHeader.props';
|
||||||
|
|
||||||
|
export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
|
||||||
|
name,
|
||||||
|
company,
|
||||||
|
email,
|
||||||
|
phonenumber,
|
||||||
|
statusName,
|
||||||
|
statusColor,
|
||||||
|
avatarColor,
|
||||||
|
onStatusPress,
|
||||||
|
onEmailPress,
|
||||||
|
onPhonePress,
|
||||||
|
}) => {
|
||||||
|
const { theme: colors } = useTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
|
||||||
|
// Get initials from name
|
||||||
|
const getInitials = (name: string): string => {
|
||||||
|
if (!name) return '?';
|
||||||
|
const nameParts = name.trim().split(' ');
|
||||||
|
if (nameParts.length >= 2) {
|
||||||
|
return (nameParts[0][0] + nameParts[1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
return name.substring(0, 2).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Avatar */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.avatar,
|
||||||
|
{ backgroundColor: avatarColor || statusColor || '#1565C0' },
|
||||||
|
]}>
|
||||||
|
<Text style={styles.avatarText}>{getInitials(name)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Name and Status Row */}
|
||||||
|
<View style={styles.nameRow}>
|
||||||
|
<Text style={styles.name}>{name || 'No Name'}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onStatusPress}
|
||||||
|
activeOpacity={onStatusPress ? 0.7 : 1}
|
||||||
|
disabled={!onStatusPress}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.statusBadge,
|
||||||
|
{ backgroundColor: `${statusColor || '#6B7280'}20` },
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.statusText,
|
||||||
|
{ color: statusColor || '#6B7280' },
|
||||||
|
]}>
|
||||||
|
{statusName || 'No Status'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Company */}
|
||||||
|
{company && <Text style={styles.company}>{company}</Text>}
|
||||||
|
|
||||||
|
{/* Email Row */}
|
||||||
|
{email && (
|
||||||
|
<View style={styles.contactRow}>
|
||||||
|
<Text style={styles.contactLabel}>Email:</Text>
|
||||||
|
<Text style={styles.contactText}>{email}</Text>
|
||||||
|
{onEmailPress && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.iconButton}
|
||||||
|
onPress={onEmailPress}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<Icon name="send" size={18} color={colors.icon} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Phone Row */}
|
||||||
|
{phonenumber && (
|
||||||
|
<View style={styles.contactRow}>
|
||||||
|
<Text style={styles.contactLabel}>Phone:</Text>
|
||||||
|
<Text style={styles.contactText}>{phonenumber}</Text>
|
||||||
|
{onPhonePress && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.iconButton}
|
||||||
|
onPress={onPhonePress}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<Icon name="call" size={18} color={colors.icon} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
app/components/leadItemCard/index.ts
Normal file
2
app/components/leadItemCard/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './leadItemCard';
|
||||||
|
export * from './leadItemCard.props';
|
||||||
6
app/components/leadItemCard/leadItemCard.props.ts
Normal file
6
app/components/leadItemCard/leadItemCard.props.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { LeadItem } from '@interfaces';
|
||||||
|
|
||||||
|
export interface LeadItemCardProps {
|
||||||
|
item: LeadItem;
|
||||||
|
onPress?: () => void;
|
||||||
|
}
|
||||||
74
app/components/leadItemCard/leadItemCard.styles.ts
Normal file
74
app/components/leadItemCard/leadItemCard.styles.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { ThemeColors } from '../../theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: ThemeColors) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
avatarContainer: {
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 28,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
avatarText: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.text,
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
},
|
||||||
|
contactRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
contactText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
contactIconButton: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
117
app/components/leadItemCard/leadItemCard.tsx
Normal file
117
app/components/leadItemCard/leadItemCard.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
Linking,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import Icon from 'react-native-vector-icons/Ionicons';
|
||||||
|
import { useTheme } from '@theme';
|
||||||
|
import { getStyles } from './leadItemCard.styles';
|
||||||
|
import { LeadItemCardProps } from './leadItemCard.props';
|
||||||
|
|
||||||
|
export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
||||||
|
item,
|
||||||
|
onPress,
|
||||||
|
}) => {
|
||||||
|
const { theme: colors } = useTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
|
||||||
|
// Get initials from name
|
||||||
|
const getInitials = (name: string): string => {
|
||||||
|
if (!name) return '?';
|
||||||
|
const nameParts = name.trim().split(' ');
|
||||||
|
if (nameParts.length >= 2) {
|
||||||
|
return (nameParts[0][0] + nameParts[1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
return name.substring(0, 2).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle email press
|
||||||
|
const handleEmailPress = () => {
|
||||||
|
if (item.email) {
|
||||||
|
Linking.openURL(`mailto:${item.email}`).catch(err =>
|
||||||
|
console.error('Error opening email:', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle phone press
|
||||||
|
const handlePhonePress = () => {
|
||||||
|
if (item.phonenumber) {
|
||||||
|
const phoneUrl =
|
||||||
|
Platform.OS === 'ios'
|
||||||
|
? `telprompt:${item.phonenumber}`
|
||||||
|
: `tel:${item.phonenumber}`;
|
||||||
|
Linking.openURL(phoneUrl).catch(err =>
|
||||||
|
console.error('Error opening phone:', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.card}
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={0.7}>
|
||||||
|
{/* Left side - Avatar/Initials */}
|
||||||
|
<View style={[styles.avatarContainer, { backgroundColor: item.color || '#1565C0' }]}>
|
||||||
|
<Text style={styles.avatarText}>{getInitials(item.name)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Right side - Content */}
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
{/* Name and Status Row */}
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<Text style={styles.name} numberOfLines={1}>
|
||||||
|
{item.name || 'No Name'}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.statusBadge,
|
||||||
|
{ backgroundColor: `${item.color || '#6B7280'}20` },
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.statusText,
|
||||||
|
{ color: item.color || '#6B7280' },
|
||||||
|
]}>
|
||||||
|
{item.status_name || item.status || 'No Status'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Email Row */}
|
||||||
|
{item.email ? (
|
||||||
|
<View style={styles.contactRow}>
|
||||||
|
<Text style={styles.contactText} numberOfLines={1}>
|
||||||
|
{item.email}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.contactIconButton}
|
||||||
|
onPress={handleEmailPress}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<Icon name="mail" size={20} color={colors.icon} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Phone Row */}
|
||||||
|
{item.phonenumber ? (
|
||||||
|
<View style={styles.contactRow}>
|
||||||
|
<Text style={styles.contactText} numberOfLines={1}>
|
||||||
|
{item.phonenumber}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.contactIconButton}
|
||||||
|
onPress={handlePhonePress}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<Icon name="call" size={20} color={colors.icon} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
app/components/loader/index.ts
Normal file
2
app/components/loader/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './loader';
|
||||||
|
export * from './loader.props';
|
||||||
5
app/components/loader/loader.props.ts
Normal file
5
app/components/loader/loader.props.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface LoaderProps {
|
||||||
|
size?: 'small' | 'large' | number;
|
||||||
|
color?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
18
app/components/loader/loader.styles.ts
Normal file
18
app/components/loader/loader.styles.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { ThemeColors } from '../../theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: ThemeColors) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 32,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
marginTop: 16,
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
23
app/components/loader/loader.tsx
Normal file
23
app/components/loader/loader.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, ActivityIndicator, Text } from 'react-native';
|
||||||
|
import { useTheme } from '@theme';
|
||||||
|
import { getStyles } from './loader.styles';
|
||||||
|
import { LoaderProps } from './loader.props';
|
||||||
|
|
||||||
|
export const Loader: React.FC<LoaderProps> = ({
|
||||||
|
size = 'large',
|
||||||
|
color,
|
||||||
|
message,
|
||||||
|
}) => {
|
||||||
|
const { theme: colors } = useTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
|
||||||
|
const loaderColor = color || colors.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ActivityIndicator size={size} color={loaderColor} />
|
||||||
|
{message && <Text style={styles.message}>{message}</Text>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
app/components/searchInput/index.ts
Normal file
2
app/components/searchInput/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './searchInput';
|
||||||
|
export * from './searchInput.props';
|
||||||
9
app/components/searchInput/searchInput.props.ts
Normal file
9
app/components/searchInput/searchInput.props.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { TextInputProps, ViewStyle } from 'react-native';
|
||||||
|
|
||||||
|
export interface SearchInputProps extends Omit<TextInputProps, 'style'> {
|
||||||
|
value: string;
|
||||||
|
onChangeText: (text: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
onClear?: () => void;
|
||||||
|
style?: ViewStyle;
|
||||||
|
}
|
||||||
29
app/components/searchInput/searchInput.styles.ts
Normal file
29
app/components/searchInput/searchInput.styles.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { ThemeColors } from '../../theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: ThemeColors) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
height: 48,
|
||||||
|
},
|
||||||
|
searchIcon: {
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 15,
|
||||||
|
color: colors.text,
|
||||||
|
paddingVertical: 0,
|
||||||
|
},
|
||||||
|
clearButton: {
|
||||||
|
marginLeft: 8,
|
||||||
|
padding: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
55
app/components/searchInput/searchInput.tsx
Normal file
55
app/components/searchInput/searchInput.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, TextInput, TouchableOpacity, ViewStyle } from 'react-native';
|
||||||
|
import Icon from 'react-native-vector-icons/Ionicons';
|
||||||
|
import { useTheme } from '@theme';
|
||||||
|
import { getStyles } from './searchInput.styles';
|
||||||
|
import { SearchInputProps } from './searchInput.props';
|
||||||
|
|
||||||
|
export const SearchInput: React.FC<SearchInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChangeText,
|
||||||
|
placeholder = 'Search...',
|
||||||
|
onClear,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { theme: colors } = useTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onChangeText('');
|
||||||
|
if (onClear) {
|
||||||
|
onClear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, style as ViewStyle]}>
|
||||||
|
<Icon
|
||||||
|
name="search-outline"
|
||||||
|
size={20}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
style={styles.searchIcon}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
returnKeyType="search"
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{value.length > 0 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.clearButton}
|
||||||
|
onPress={handleClear}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
|
<Icon name="close-circle" size={20} color={colors.textMuted} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ export * from './dashboard';
|
|||||||
export * from './estimates';
|
export * from './estimates';
|
||||||
export * from './invoices';
|
export * from './invoices';
|
||||||
export * from './leads';
|
export * from './leads';
|
||||||
|
export * from './leadDetails';
|
||||||
export * from './login';
|
export * from './login';
|
||||||
export * from './profile';
|
export * from './profile';
|
||||||
export * from './projects';
|
export * from './projects';
|
||||||
|
|||||||
3
app/features/leadDetails/index.ts
Normal file
3
app/features/leadDetails/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './leadDetails.screen';
|
||||||
|
export * from './thunk';
|
||||||
|
export * from './reducers';
|
||||||
112
app/features/leadDetails/leadDetails.screen.tsx
Normal file
112
app/features/leadDetails/leadDetails.screen.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
Linking,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||||
|
import { useTheme } from '@theme';
|
||||||
|
import { useAppDispatch, useAppSelector, RootState } from '@store';
|
||||||
|
import { getLeadDetails } from './thunk';
|
||||||
|
import { getStyles } from './leadDetails.styles';
|
||||||
|
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
||||||
|
import { Loader } from '@components';
|
||||||
|
import { LeadDetailHeader } from '../../components/leadDetailHeader/leadDetailHeader';
|
||||||
|
|
||||||
|
type LeadDetailsRouteProp = RouteProp<LeadsStackParamList, 'leadDetails'>;
|
||||||
|
|
||||||
|
export const LeadDetailsScreen = () => {
|
||||||
|
const route = useRoute<LeadDetailsRouteProp>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { theme: colors } = useTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
|
||||||
|
// Get lead ID from route params
|
||||||
|
const leadId = route.params?.lead?.id;
|
||||||
|
|
||||||
|
// Get data from Redux store
|
||||||
|
const { item: lead, loading, error } = useAppSelector(
|
||||||
|
(state: RootState) => state.leadDetails,
|
||||||
|
);
|
||||||
|
const user_data = useAppSelector((state: RootState) => state.auth.user_data);
|
||||||
|
|
||||||
|
// Fetch lead details on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (leadId && user_data?.staffid) {
|
||||||
|
dispatch(getLeadDetails({ leadId, staffid: user_data.staffid }));
|
||||||
|
}
|
||||||
|
}, [dispatch, leadId, user_data]);
|
||||||
|
|
||||||
|
// Show loader while loading
|
||||||
|
if (loading) {
|
||||||
|
return <Loader message="Loading lead details..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error if any
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32 }}>
|
||||||
|
<Text style={{ color: '#EF4444', fontSize: 14, textAlign: 'center' }}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lead) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.infoValue}>No lead data available</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle email press
|
||||||
|
const handleEmailPress = () => {
|
||||||
|
if (lead.email) {
|
||||||
|
Linking.openURL(`mailto:${lead.email}`).catch(err =>
|
||||||
|
console.error('Error opening email:', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle phone press
|
||||||
|
const handlePhonePress = () => {
|
||||||
|
if (lead.phonenumber) {
|
||||||
|
const phoneUrl =
|
||||||
|
Platform.OS === 'ios'
|
||||||
|
? `telprompt:${lead.phonenumber}`
|
||||||
|
: `tel:${lead.phonenumber}`;
|
||||||
|
Linking.openURL(phoneUrl).catch(err =>
|
||||||
|
console.error('Error opening phone:', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle status press (for future status change functionality)
|
||||||
|
const handleStatusPress = () => {
|
||||||
|
// TODO: Show status list modal/picker
|
||||||
|
console.log('Status pressed - show status list');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
||||||
|
{/* Header with Avatar */}
|
||||||
|
<LeadDetailHeader
|
||||||
|
name={lead.name}
|
||||||
|
company={lead.company}
|
||||||
|
email={lead.email}
|
||||||
|
phonenumber={lead.phonenumber}
|
||||||
|
statusName={lead.status_name || lead.status}
|
||||||
|
statusColor={lead.color}
|
||||||
|
onStatusPress={handleStatusPress}
|
||||||
|
onEmailPress={handleEmailPress}
|
||||||
|
onPhonePress={handlePhonePress}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
119
app/features/leadDetails/leadDetails.styles.ts
Normal file
119
app/features/leadDetails/leadDetails.styles.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { ThemeColors } from '../../theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: ThemeColors) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 24,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
avatarText: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
company: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
infoIcon: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
infoContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
infoLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.textMuted,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
infoValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: colors.icon,
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
noteText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
37
app/features/leadDetails/reducers.ts
Normal file
37
app/features/leadDetails/reducers.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { getLeadDetails } from './thunk';
|
||||||
|
import { LeadDetailsItem } from '@interfaces';
|
||||||
|
|
||||||
|
export interface LeadDetailsState {
|
||||||
|
item: LeadDetailsItem | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: LeadDetailsState = {
|
||||||
|
item: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducers = createReducer(initialState, builder => {
|
||||||
|
builder
|
||||||
|
.addCase(getLeadDetails.pending, acc => {
|
||||||
|
acc.loading = true;
|
||||||
|
acc.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getLeadDetails.fulfilled, (acc, action) => {
|
||||||
|
acc.loading = false;
|
||||||
|
acc.item = action.payload;
|
||||||
|
acc.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getLeadDetails.rejected, (acc, action) => {
|
||||||
|
acc.loading = false;
|
||||||
|
acc.error =
|
||||||
|
(action.payload as string) ??
|
||||||
|
action.error.message ??
|
||||||
|
'Failed to load lead details';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reducers;
|
||||||
14
app/features/leadDetails/thunk.ts
Normal file
14
app/features/leadDetails/thunk.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { getLeadDetailsApi } from '@api';
|
||||||
|
import { LeadDetailsItem } from '@interfaces';
|
||||||
|
|
||||||
|
export const getLeadDetails = createAsyncThunk<
|
||||||
|
LeadDetailsItem,
|
||||||
|
{ leadId: string; staffid: string }
|
||||||
|
>('leadDetails/getLeadDetails', async (payload, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
return await getLeadDetailsApi(payload.leadId, payload.staffid);
|
||||||
|
} catch (error: any) {
|
||||||
|
return rejectWithValue(error.message || 'Failed to load lead details');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -1,79 +1,96 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import Icon from 'react-native-vector-icons/Ionicons';
|
import Icon from 'react-native-vector-icons/Ionicons';
|
||||||
import { getStyles } from './leads.styles';
|
import { getStyles } from './leads.styles';
|
||||||
import { MOCK_LEADS } from '../../mock-data/leads';
|
import { useTheme } from '@theme';
|
||||||
import { useTheme } from '../../theme';
|
import { useAppDispatch, useAppSelector, RootState } from '@store';
|
||||||
|
import { getLeads } from './thunk';
|
||||||
|
import { LeadItem } from '@interfaces';
|
||||||
|
import { LeadItemCard, SearchInput, Loader } from '@components';
|
||||||
|
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
||||||
|
import { route } from '@utils';
|
||||||
|
|
||||||
|
type LeadsScreenNavigationProp = NativeStackNavigationProp<
|
||||||
|
LeadsStackParamList,
|
||||||
|
'leads'
|
||||||
|
>;
|
||||||
|
|
||||||
export const LeadsScreen = () => {
|
export const LeadsScreen = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const navigation = useNavigation<LeadsScreenNavigationProp>();
|
||||||
const { theme: colors } = useTheme();
|
const { theme: colors } = useTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const { items, loading, error } = useAppSelector(
|
||||||
|
(state: RootState) => state.leads,
|
||||||
|
);
|
||||||
|
const user_data = useAppSelector((state: RootState) => state.auth.user_data);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user_data?.staffid) {
|
||||||
|
dispatch(getLeads({ leadId: null, staffid: user_data.staffid }));
|
||||||
|
}
|
||||||
|
}, [dispatch, user_data]);
|
||||||
|
|
||||||
|
const filteredLeads = searchTerm
|
||||||
|
? items.filter(
|
||||||
|
lead =>
|
||||||
|
lead.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
lead.company.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
lead.email.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
)
|
||||||
|
: items;
|
||||||
|
|
||||||
|
const handleLeadPress = (lead: LeadItem) => {
|
||||||
|
// Navigate to lead details screen
|
||||||
|
navigation.navigate(route.leadDetails, { lead });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLead = ({ item }: { item: LeadItem }) => (
|
||||||
|
<LeadItemCard item={item} onPress={() => handleLeadPress(item)} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.searchWrapper}>
|
<SearchInput
|
||||||
<Icon
|
value={searchTerm}
|
||||||
name="search-outline"
|
onChangeText={setSearchTerm}
|
||||||
size={20}
|
placeholder="Search leads, companies, emails..."
|
||||||
color={colors.textSecondary}
|
|
||||||
style={styles.searchIcon}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
style={styles.searchInput}
|
style={styles.searchInput}
|
||||||
placeholder="Search leads, companies..."
|
|
||||||
placeholderTextColor={colors.textMuted}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
|
||||||
<TouchableOpacity style={styles.filterButton}>
|
<TouchableOpacity style={styles.filterButton}>
|
||||||
<Icon name="filter-outline" size={20} color={colors.text} />
|
<Icon name="filter-outline" size={20} color={colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Loader message="Loading leads..." />
|
||||||
|
) : error ? (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={MOCK_LEADS}
|
data={filteredLeads}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
renderItem={({ item }) => (
|
renderItem={renderLead}
|
||||||
<View style={styles.leadCard}>
|
ListEmptyComponent={() => (
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.emptyContainer}>
|
||||||
<View>
|
<Text style={styles.emptyText}>No leads available.</Text>
|
||||||
<Text style={styles.leadName}>{item.name}</Text>
|
|
||||||
<Text style={styles.leadCompany}>{item.company}</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.statusBadge,
|
|
||||||
{ backgroundColor: `${item.color}15` },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.statusText, { color: item.color }]}>
|
|
||||||
{item.status}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.cardFooter}>
|
|
||||||
<View style={styles.infoRow}>
|
|
||||||
<Icon
|
|
||||||
name="mail-outline"
|
|
||||||
size={16}
|
|
||||||
color={colors.textSecondary}
|
|
||||||
/>
|
|
||||||
<Text style={styles.infoValue}>{item.email}</Text>
|
|
||||||
</View>
|
|
||||||
<TouchableOpacity style={styles.actionButton}>
|
|
||||||
<Icon name="chevron-forward" size={18} color={colors.icon} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { ThemeColors } from '../../theme';
|
import { ThemeColors } from '../../theme';
|
||||||
|
|
||||||
export const getStyles = (colors: ThemeColors) => StyleSheet.create({
|
export const getStyles = (colors: ThemeColors) =>
|
||||||
|
StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
@ -10,34 +11,18 @@ export const getStyles = (colors: ThemeColors) => StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
gap: 10,
|
||||||
searchWrapper: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
borderRadius: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
searchIcon: {
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
},
|
||||||
searchInput: {
|
searchInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 44,
|
|
||||||
color: colors.text,
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
},
|
||||||
filterButton: {
|
filterButton: {
|
||||||
width: 44,
|
width: 48,
|
||||||
height: 44,
|
height: 48,
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
@ -45,65 +30,26 @@ export const getStyles = (colors: ThemeColors) => StyleSheet.create({
|
|||||||
padding: 16,
|
padding: 16,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
leadCard: {
|
errorContainer: {
|
||||||
backgroundColor: colors.card,
|
flex: 1,
|
||||||
borderRadius: 12,
|
|
||||||
padding: 16,
|
|
||||||
marginBottom: 16,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
leadName: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
leadCompany: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
cardFooter: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: 12,
|
|
||||||
},
|
|
||||||
infoRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
infoValue: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
actionButton: {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 8,
|
|
||||||
backgroundColor: colors.border,
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
padding: 32,
|
||||||
},
|
},
|
||||||
});
|
errorText: {
|
||||||
|
color: '#EF4444',
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 32,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
37
app/features/leads/reducers.ts
Normal file
37
app/features/leads/reducers.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { getLeads } from './thunk';
|
||||||
|
import { LeadItem } from '@interfaces';
|
||||||
|
|
||||||
|
export interface LeadsState {
|
||||||
|
items: LeadItem[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: LeadsState = {
|
||||||
|
items: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducers = createReducer(initialState, builder => {
|
||||||
|
builder
|
||||||
|
.addCase(getLeads.pending, acc => {
|
||||||
|
acc.loading = true;
|
||||||
|
acc.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getLeads.fulfilled, (acc, action) => {
|
||||||
|
acc.loading = false;
|
||||||
|
acc.items = action.payload;
|
||||||
|
acc.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getLeads.rejected, (acc, action) => {
|
||||||
|
acc.loading = false;
|
||||||
|
acc.error =
|
||||||
|
(action.payload as string) ??
|
||||||
|
action.error.message ??
|
||||||
|
'Failed to load leads';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reducers;
|
||||||
14
app/features/leads/thunk.ts
Normal file
14
app/features/leads/thunk.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { getLeadsApi } from '@api';
|
||||||
|
import { LeadItem } from '@interfaces';
|
||||||
|
|
||||||
|
export const getLeads = createAsyncThunk<
|
||||||
|
LeadItem[],
|
||||||
|
{ leadId: string | null; staffid: string }
|
||||||
|
>('leads/getLeads', async (payload, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
return await getLeadsApi(payload.leadId, payload.staffid);
|
||||||
|
} catch (error: any) {
|
||||||
|
return rejectWithValue(error.message || 'Failed to load leads');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
@ -8,53 +8,60 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
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';
|
||||||
import { getStyles } from './login.styles';
|
import { getStyles } from './login.styles';
|
||||||
import { useTheme } from '../../theme';
|
import { useTheme } from '../../theme';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../store/store';
|
||||||
|
import { login } from '../../store/commonReducers/auth/thunk';
|
||||||
|
import { LoginRequest } from '@interfaces';
|
||||||
|
import { RootState } from '../../store/rootReducer';
|
||||||
|
|
||||||
export const LoginScreen = () => {
|
export const LoginScreen = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<any>();
|
const navigation = useNavigation<any>();
|
||||||
const { theme: colors } = useTheme();
|
const { theme: colors } = useTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const [tenancy, setTenancy] = useState('');
|
const [tenancy, setTenancy] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [localError, setLocalError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
const { loginLoading, loginError, loginSuccess, token, user_data } =
|
||||||
|
useAppSelector((state: RootState) => state.auth);
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
setError('');
|
setLocalError('');
|
||||||
if (!tenancy.trim()) {
|
if (!tenancy.trim()) {
|
||||||
setError('Tenancy name is required');
|
setLocalError('Tenancy name is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!email.trim()) {
|
if (!email.trim()) {
|
||||||
setError('Email address is required');
|
setLocalError('Email address is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!password.trim()) {
|
if (!password.trim()) {
|
||||||
setError('Password is required');
|
setLocalError('Password is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
const payload: LoginRequest = {
|
||||||
try {
|
email,
|
||||||
const mockToken = 'dummy-jwt-token-for-crm';
|
password,
|
||||||
await AsyncStorage.setItem('userToken', mockToken);
|
device_token: '',
|
||||||
await AsyncStorage.setItem('tenancyName', tenancy);
|
};
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
|
await dispatch(login(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginSuccess && token && user_data) {
|
||||||
navigation.reset({
|
navigation.reset({
|
||||||
index: 0,
|
index: 0,
|
||||||
routes: [{ name: 'DrawerStack' }], // root-level stack name stays as-is
|
routes: [{ name: 'DrawerStack' }],
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
setLoading(false);
|
|
||||||
setError('Failed to sign in. Please try again.');
|
|
||||||
}
|
}
|
||||||
};
|
}, [loginSuccess, token, user_data, navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
@ -76,10 +83,10 @@ export const LoginScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.formCard}>
|
<View style={styles.formCard}>
|
||||||
{error ? (
|
{localError || loginError ? (
|
||||||
<View style={styles.errorBanner}>
|
<View style={styles.errorBanner}>
|
||||||
<Icon name="alert-circle" size={20} color="#EF4444" />
|
<Icon name="alert-circle" size={20} color="#EF4444" />
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
<Text style={styles.errorText}>{localError || loginError}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -156,10 +163,10 @@ export const LoginScreen = () => {
|
|||||||
style={styles.button}
|
style={styles.button}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
disabled={loading}
|
disabled={loginLoading}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>
|
<Text style={styles.buttonText}>
|
||||||
{loading ? 'Signing In...' : 'Sign In'}
|
{loginLoading ? 'Signing In...' : 'Sign In'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
57
app/interfaces/auth.ts
Normal file
57
app/interfaces/auth.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
export interface LoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
device_token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
staff_id: string;
|
||||||
|
feature: string;
|
||||||
|
capability: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserData {
|
||||||
|
staffid: string;
|
||||||
|
email: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
facebook: string;
|
||||||
|
linkedin: string;
|
||||||
|
phonenumber: string;
|
||||||
|
skype: string;
|
||||||
|
password: string;
|
||||||
|
datecreated: string;
|
||||||
|
profile_image: string;
|
||||||
|
last_ip: string;
|
||||||
|
last_login: string;
|
||||||
|
last_activity: string | null;
|
||||||
|
last_password_change: string | null;
|
||||||
|
new_pass_key: string | null;
|
||||||
|
new_pass_key_requested: string | null;
|
||||||
|
admin: string;
|
||||||
|
role: string;
|
||||||
|
active: string;
|
||||||
|
default_language: string;
|
||||||
|
direction: string;
|
||||||
|
media_path_slug: string;
|
||||||
|
is_not_staff: string;
|
||||||
|
hourly_rate: string;
|
||||||
|
two_factor_auth_enabled: string;
|
||||||
|
two_factor_auth_code: string | null;
|
||||||
|
two_factor_auth_code_requested: string | null;
|
||||||
|
email_signature: string;
|
||||||
|
google_auth_secret: string | null;
|
||||||
|
fcm_token: string;
|
||||||
|
device_os: string;
|
||||||
|
full_name: string;
|
||||||
|
total_unread_notifications: string;
|
||||||
|
total_unfinished_todos: string;
|
||||||
|
permissions: Permission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
status: boolean;
|
||||||
|
is_twofactor: boolean;
|
||||||
|
user_data: UserData;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
@ -1 +1,4 @@
|
|||||||
export * from './drawerItem';
|
export * from './drawerItem';
|
||||||
|
export * from './auth';
|
||||||
|
export * from './leads';
|
||||||
|
export * from './leadDetails';
|
||||||
|
|||||||
56
app/interfaces/leadDetails.ts
Normal file
56
app/interfaces/leadDetails.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
export interface LeadDetailsCustomField {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeadDetailsItem {
|
||||||
|
id: string;
|
||||||
|
hash: string | null;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
company: string;
|
||||||
|
description: string;
|
||||||
|
country: string;
|
||||||
|
zip: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
address: string;
|
||||||
|
assigned: string;
|
||||||
|
dateadded: string;
|
||||||
|
from_form_id: string;
|
||||||
|
status: string;
|
||||||
|
source: string;
|
||||||
|
source_type: string;
|
||||||
|
campaign_id: string | null;
|
||||||
|
lastcontact: string;
|
||||||
|
dateassigned: string | null;
|
||||||
|
last_status_change: string | null;
|
||||||
|
addedfrom: string;
|
||||||
|
email: string;
|
||||||
|
website: string;
|
||||||
|
leadorder: string;
|
||||||
|
phonenumber: string;
|
||||||
|
date_converted: string | null;
|
||||||
|
lost: string;
|
||||||
|
junk: string;
|
||||||
|
last_lead_status: string;
|
||||||
|
is_imported_from_email_integration: string;
|
||||||
|
email_integration_uid: string | null;
|
||||||
|
is_public: string;
|
||||||
|
default_language: string;
|
||||||
|
client_id: string;
|
||||||
|
lead_value: string | null;
|
||||||
|
followup_date: string | null;
|
||||||
|
remarks: string | null;
|
||||||
|
is_followup: string;
|
||||||
|
assign_type: string;
|
||||||
|
budget: string | null;
|
||||||
|
statusorder: string;
|
||||||
|
color: string;
|
||||||
|
isdefault: string;
|
||||||
|
status_name: string;
|
||||||
|
source_name: string;
|
||||||
|
attachments: any[];
|
||||||
|
public_url: string;
|
||||||
|
customfields: LeadDetailsCustomField[];
|
||||||
|
}
|
||||||
48
app/interfaces/leads.ts
Normal file
48
app/interfaces/leads.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export interface LeadItem {
|
||||||
|
id: string;
|
||||||
|
hash: string | null;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
company: string;
|
||||||
|
description: string;
|
||||||
|
country: string;
|
||||||
|
zip: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
address: string;
|
||||||
|
assigned: string;
|
||||||
|
dateadded: string;
|
||||||
|
from_form_id: string;
|
||||||
|
status: string;
|
||||||
|
source: string;
|
||||||
|
source_type: string;
|
||||||
|
campaign_id: string | null;
|
||||||
|
lastcontact: string;
|
||||||
|
dateassigned: string | null;
|
||||||
|
last_status_change: string | null;
|
||||||
|
addedfrom: string;
|
||||||
|
email: string;
|
||||||
|
website: string;
|
||||||
|
leadorder: string;
|
||||||
|
phonenumber: string;
|
||||||
|
date_converted: string | null;
|
||||||
|
lost: string;
|
||||||
|
junk: string;
|
||||||
|
last_lead_status: string;
|
||||||
|
is_imported_from_email_integration: string;
|
||||||
|
email_integration_uid: string | null;
|
||||||
|
is_public: string;
|
||||||
|
default_language: string;
|
||||||
|
client_id: string;
|
||||||
|
lead_value: string | null;
|
||||||
|
followup_date: string | null;
|
||||||
|
remarks: string | null;
|
||||||
|
is_followup: string;
|
||||||
|
assign_type: string;
|
||||||
|
budget: string | null;
|
||||||
|
statusorder: string;
|
||||||
|
color: string;
|
||||||
|
isdefault: string;
|
||||||
|
status_name: string;
|
||||||
|
source_name: string;
|
||||||
|
}
|
||||||
40
app/navigation/leadsStack.tsx
Normal file
40
app/navigation/leadsStack.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
|
import { LeadsScreen, LeadDetailsScreen } from '@features';
|
||||||
|
import { route, RouteParams } from '@utils';
|
||||||
|
import { useTheme } from '@theme';
|
||||||
|
|
||||||
|
export type LeadsStackParamList = Pick<RouteParams, 'leads' | 'leadDetails'>;
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<LeadsStackParamList>();
|
||||||
|
|
||||||
|
export const LeadsStack = () => {
|
||||||
|
const { theme: colors } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: colors.header,
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
headerTitleAlign: 'center',
|
||||||
|
headerTintColor: colors.text,
|
||||||
|
}}>
|
||||||
|
<Stack.Screen
|
||||||
|
name={route.leads}
|
||||||
|
component={LeadsScreen}
|
||||||
|
options={{ headerTitle: 'Leads' }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name={route.leadDetails}
|
||||||
|
component={LeadDetailsScreen}
|
||||||
|
options={{ headerTitle: 'Lead Details' }}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
import {AuthStack} from './authStack';
|
import { AuthStack } from './authStack';
|
||||||
import {DrawerStack} from './drawerStack';
|
import { DrawerStack } from './drawerStack';
|
||||||
|
import { useAppSelector } from '../store/store';
|
||||||
|
import { RootState } from '../store/rootReducer';
|
||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
AuthStack: undefined;
|
AuthStack: undefined;
|
||||||
@ -12,12 +14,19 @@ export type RootStackParamList = {
|
|||||||
const RootStack = createNativeStackNavigator<RootStackParamList>();
|
const RootStack = createNativeStackNavigator<RootStackParamList>();
|
||||||
|
|
||||||
export const RootNavigator = () => {
|
export const RootNavigator = () => {
|
||||||
|
const { token } = useAppSelector((state: RootState) => state.auth);
|
||||||
|
console.log('RootNavigator token:', token); // Debugging line to check the token value
|
||||||
|
const initialRouteName = token ? 'DrawerStack' : 'AuthStack';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<RootStack.Navigator screenOptions={{ headerShown: false }} initialRouteName="AuthStack">
|
<RootStack.Navigator
|
||||||
|
screenOptions={{ headerShown: false }}
|
||||||
|
initialRouteName={initialRouteName}
|
||||||
|
>
|
||||||
<RootStack.Screen name="AuthStack" component={AuthStack} />
|
<RootStack.Screen name="AuthStack" component={AuthStack} />
|
||||||
<RootStack.Screen name="DrawerStack" component={DrawerStack} />
|
<RootStack.Screen name="DrawerStack" component={DrawerStack} />
|
||||||
</RootStack.Navigator>
|
</RootStack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import Icon from 'react-native-vector-icons/Ionicons';
|
|||||||
import { route, RouteParams } from '@utils';
|
import { route, RouteParams } from '@utils';
|
||||||
import {
|
import {
|
||||||
DashboardScreen,
|
DashboardScreen,
|
||||||
LeadsScreen,
|
|
||||||
AddLeadScreen,
|
AddLeadScreen,
|
||||||
CustomersScreen,
|
CustomersScreen,
|
||||||
ProfileScreen,
|
ProfileScreen,
|
||||||
} from '@features';
|
} from '@features';
|
||||||
|
import { LeadsStack } from './leadsStack';
|
||||||
import { useTheme } from '@theme';
|
import { useTheme } from '@theme';
|
||||||
import { getStyles } from './tabStack.styles';
|
import { getStyles } from './tabStack.styles';
|
||||||
|
|
||||||
@ -76,7 +76,11 @@ export const TabStack = () => {
|
|||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen name={route.leads} component={LeadsScreen} />
|
<Tab.Screen
|
||||||
|
name={route.leads}
|
||||||
|
component={LeadsStack}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
<Tab.Screen name={route.addLead} component={AddLeadScreen} />
|
<Tab.Screen name={route.addLead} component={AddLeadScreen} />
|
||||||
<Tab.Screen name={route.customers} component={CustomersScreen} />
|
<Tab.Screen name={route.customers} component={CustomersScreen} />
|
||||||
<Tab.Screen name={route.profile} component={ProfileScreen} />
|
<Tab.Screen name={route.profile} component={ProfileScreen} />
|
||||||
|
|||||||
2
app/store/commonReducers/auth/index.ts
Normal file
2
app/store/commonReducers/auth/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './thunk';
|
||||||
|
export * from './reducers';
|
||||||
52
app/store/commonReducers/auth/reducers.ts
Normal file
52
app/store/commonReducers/auth/reducers.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { login } from './thunk';
|
||||||
|
import { UserData } from '@interfaces';
|
||||||
|
|
||||||
|
export interface LoginState {
|
||||||
|
loginSuccess: boolean;
|
||||||
|
loginLoading: boolean;
|
||||||
|
status: boolean;
|
||||||
|
is_twofactor: boolean;
|
||||||
|
user_data: UserData | null;
|
||||||
|
token: string | null;
|
||||||
|
loginError: string | null;
|
||||||
|
loginMessage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: LoginState = {
|
||||||
|
loginSuccess: false,
|
||||||
|
loginLoading: false,
|
||||||
|
status: false,
|
||||||
|
is_twofactor: false,
|
||||||
|
user_data: null,
|
||||||
|
token: null,
|
||||||
|
loginError: null,
|
||||||
|
loginMessage: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducers = createReducer(initialState, builder => {
|
||||||
|
builder
|
||||||
|
.addCase(login.pending, acc => {
|
||||||
|
acc.loginLoading = true;
|
||||||
|
acc.loginError = null;
|
||||||
|
acc.loginMessage = null;
|
||||||
|
})
|
||||||
|
.addCase(login.fulfilled, (acc, action) => {
|
||||||
|
acc.loginLoading = false;
|
||||||
|
acc.status = action.payload.status;
|
||||||
|
acc.is_twofactor = action.payload.is_twofactor;
|
||||||
|
acc.user_data = action.payload.user_data;
|
||||||
|
acc.token = action.payload.token;
|
||||||
|
acc.loginSuccess = true;
|
||||||
|
acc.loginError = null;
|
||||||
|
acc.loginMessage = null;
|
||||||
|
})
|
||||||
|
.addCase(login.rejected, (acc, { error, payload }) => {
|
||||||
|
acc.loginLoading = false;
|
||||||
|
acc.loginError = (payload as string) ?? error.message ?? 'Login failed';
|
||||||
|
acc.loginMessage = (payload as string) ?? error.message ?? 'Login failed';
|
||||||
|
acc.loginSuccess = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reducers;
|
||||||
14
app/store/commonReducers/auth/thunk.ts
Normal file
14
app/store/commonReducers/auth/thunk.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { LoginRequest, LoginResponse } from '@interfaces';
|
||||||
|
import { loginApi } from '@api';
|
||||||
|
|
||||||
|
export const login = createAsyncThunk<LoginResponse, LoginRequest>(
|
||||||
|
'auth/login',
|
||||||
|
async (payload, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
return await loginApi(payload);
|
||||||
|
} catch (error: any) {
|
||||||
|
return rejectWithValue(error.message || 'Failed to login');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
1
app/store/commonReducers/index.ts
Normal file
1
app/store/commonReducers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './auth';
|
||||||
4
app/store/index.ts
Normal file
4
app/store/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './commonReducers';
|
||||||
|
export * from './migration';
|
||||||
|
export * from './rootReducer';
|
||||||
|
export * from './store';
|
||||||
10
app/store/migration.ts
Normal file
10
app/store/migration.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
// Here we avoid typing things as the more migrations we have the more complex types we will have to create
|
||||||
|
|
||||||
|
export const migrations = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
1: (state: any) => state,
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrationVersions: number[] = Object.keys(migrations).map(k => Number(k));
|
||||||
|
export const persistVersion = migrationVersions[migrationVersions.length - 1];
|
||||||
13
app/store/rootReducer.ts
Normal file
13
app/store/rootReducer.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
|
import authReducer from './commonReducers/auth/reducers';
|
||||||
|
import leadsReducer from '../features/leads/reducers';
|
||||||
|
import leadDetailsReducer from '../features/leadDetails/reducers';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
auth: authReducer,
|
||||||
|
leads: leadsReducer,
|
||||||
|
leadDetails: leadDetailsReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
export default rootReducer;
|
||||||
53
app/store/store.ts
Normal file
53
app/store/store.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import {
|
||||||
|
createMigrate,
|
||||||
|
FLUSH,
|
||||||
|
PAUSE,
|
||||||
|
PERSIST,
|
||||||
|
persistReducer,
|
||||||
|
persistStore,
|
||||||
|
PURGE,
|
||||||
|
REGISTER,
|
||||||
|
REHYDRATE,
|
||||||
|
} from 'redux-persist';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { Action, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { migrations, persistVersion } from './migration';
|
||||||
|
import rootReducer, { RootState } from './rootReducer';
|
||||||
|
import { injectStore } from '@utils';
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: 'root',
|
||||||
|
version: persistVersion,
|
||||||
|
storage: AsyncStorage,
|
||||||
|
blacklist: [],
|
||||||
|
migrate: createMigrate(migrations, { debug: __DEV__ }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
devTools: __DEV__,
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
immutableCheck: false,
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
injectStore(store);
|
||||||
|
|
||||||
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|
||||||
|
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>;
|
||||||
|
|
||||||
|
export default store;
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
||||||
|
import config from 'react-native-config';
|
||||||
|
import {Store} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const axiosInstance: AxiosInstance = axios.create({
|
||||||
|
baseURL: config.BASE_URL,
|
||||||
|
timeout: 15000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authtoken: config.AUTH_TOKEN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
request => {
|
||||||
|
if (request.data instanceof FormData) {
|
||||||
|
delete request.headers?.['Content-Type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
(error: AxiosError) => Promise.reject(error),
|
||||||
|
);
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
(error: AxiosError) => {
|
||||||
|
if (error.response) {
|
||||||
|
const status = error.response.status;
|
||||||
|
const data = error.response.data;
|
||||||
|
console.error('[API] response error', status, data);
|
||||||
|
} else {
|
||||||
|
console.error('[API] network error', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let storeInstance: Store | null = null;
|
||||||
|
export const injectStore = (_store: Store | null): void => {
|
||||||
|
storeInstance = _store;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
const response = await axiosInstance.get<T>(url, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async post<T>(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await axiosInstance.post<T>(url, body, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async put<T>(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await axiosInstance.put<T>(url, body, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async patch<T>(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await axiosInstance.patch<T>(url, body, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
const response = await axiosInstance.delete<T>(url, config);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { axiosInstance };
|
||||||
70
app/utils/helper.ts
Normal file
70
app/utils/helper.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// import { Linking, Platform } from 'react-native';
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Opens the default email client with the provided email address
|
||||||
|
// * @param email - The email address to send to
|
||||||
|
// */
|
||||||
|
// export const handleEmailPress = (email?: string | null) => {
|
||||||
|
// if (!email) {
|
||||||
|
// console.warn('No email provided');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Linking.openURL(`mailto:${email}`).catch(err =>
|
||||||
|
// console.error('Error opening email:', err),
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Opens the phone dialer with the provided phone number
|
||||||
|
// * @param phoneNumber - The phone number to call
|
||||||
|
// */
|
||||||
|
// export const handlePhonePress = (phoneNumber?: string | null) => {
|
||||||
|
// if (!phoneNumber) {
|
||||||
|
// console.warn('No phone number provided');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const phoneUrl =
|
||||||
|
// Platform.OS === 'ios'
|
||||||
|
// ? `telprompt:${phoneNumber}`
|
||||||
|
// : `tel:${phoneNumber}`;
|
||||||
|
|
||||||
|
// Linking.openURL(phoneUrl).catch(err =>
|
||||||
|
// console.error('Error opening phone:', err),
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Opens the browser with the provided website URL
|
||||||
|
// * @param website - The website URL to open
|
||||||
|
// */
|
||||||
|
// export const handleWebsitePress = (website?: string | null) => {
|
||||||
|
// if (!website) {
|
||||||
|
// console.warn('No website provided');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const url = website.startsWith('http') ? website : `https://${website}`;
|
||||||
|
|
||||||
|
// Linking.openURL(url).catch(err =>
|
||||||
|
// console.error('Error opening website:', err),
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Generates initials from a full name
|
||||||
|
// * @param name - The full name to extract initials from
|
||||||
|
// * @returns The initials (e.g., "John Doe" returns "JD")
|
||||||
|
// */
|
||||||
|
// export const getInitials = (name?: string | null): string => {
|
||||||
|
// if (!name) return '?';
|
||||||
|
|
||||||
|
// const nameParts = name.trim().split(' ');
|
||||||
|
|
||||||
|
// if (nameParts.length >= 2) {
|
||||||
|
// return (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return name.substring(0, 2).toUpperCase();
|
||||||
|
// };
|
||||||
@ -1,2 +1,4 @@
|
|||||||
export * from './route';
|
export * from './route';
|
||||||
export * from './assets';
|
export * from './assets';
|
||||||
|
export * from './api';
|
||||||
|
// export * from './helper';
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export const route = {
|
|||||||
|
|
||||||
// Sub screens
|
// Sub screens
|
||||||
addLead: 'addLead',
|
addLead: 'addLead',
|
||||||
|
leadDetails: 'leadDetails',
|
||||||
profile: 'profile',
|
profile: 'profile',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ export type RouteParams = {
|
|||||||
tasks: undefined;
|
tasks: undefined;
|
||||||
tickets: undefined;
|
tickets: undefined;
|
||||||
addLead: undefined;
|
addLead: undefined;
|
||||||
|
leadDetails: { lead: any };
|
||||||
profile: undefined;
|
profile: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
207
tsconfig.effective.json
Normal file
207
tsconfig.effective.json
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"types": [
|
||||||
|
"jest"
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
"es2019",
|
||||||
|
"es2020.bigint",
|
||||||
|
"es2020.date",
|
||||||
|
"es2020.number",
|
||||||
|
"es2020.promise",
|
||||||
|
"es2020.string",
|
||||||
|
"es2020.symbol.wellknown",
|
||||||
|
"es2021.promise",
|
||||||
|
"es2021.string",
|
||||||
|
"es2021.weakref",
|
||||||
|
"es2022.array",
|
||||||
|
"es2022.object",
|
||||||
|
"es2022.string"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-native",
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"customConditions": [
|
||||||
|
"react-native"
|
||||||
|
],
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"allowArbitraryExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"resolvePackageJsonImports": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@components": [
|
||||||
|
"./app/components"
|
||||||
|
],
|
||||||
|
"@features": [
|
||||||
|
"./app/features"
|
||||||
|
],
|
||||||
|
"@features/*": [
|
||||||
|
"./app/features/*"
|
||||||
|
],
|
||||||
|
"@theme": [
|
||||||
|
"./app/theme"
|
||||||
|
],
|
||||||
|
"@theme/*": [
|
||||||
|
"./app/theme/*"
|
||||||
|
],
|
||||||
|
"@api": [
|
||||||
|
"./app/api"
|
||||||
|
],
|
||||||
|
"@api/*": [
|
||||||
|
"./app/api/*"
|
||||||
|
],
|
||||||
|
"@store": [
|
||||||
|
"./app/store"
|
||||||
|
],
|
||||||
|
"@store/*": [
|
||||||
|
"./app/store/*"
|
||||||
|
],
|
||||||
|
"@interfaces": [
|
||||||
|
"./app/interfaces"
|
||||||
|
],
|
||||||
|
"@interfaces/*": [
|
||||||
|
"./app/interfaces/*"
|
||||||
|
],
|
||||||
|
"@navigation": [
|
||||||
|
"./app/navigation"
|
||||||
|
],
|
||||||
|
"@navigation/*": [
|
||||||
|
"./app/navigation/*"
|
||||||
|
],
|
||||||
|
"@hooks/*": [
|
||||||
|
"./app/hooks/*"
|
||||||
|
],
|
||||||
|
"@utils": [
|
||||||
|
"./app/utils"
|
||||||
|
],
|
||||||
|
"@utils/*": [
|
||||||
|
"./app/utils/*"
|
||||||
|
],
|
||||||
|
"@services": [
|
||||||
|
"./app/services"
|
||||||
|
],
|
||||||
|
"@services/*": [
|
||||||
|
"./app/services/*"
|
||||||
|
],
|
||||||
|
"@mock-data": [
|
||||||
|
"./app/mock-data/index.ts"
|
||||||
|
],
|
||||||
|
"@mock-data/": [
|
||||||
|
"./app/mock-data"
|
||||||
|
],
|
||||||
|
"@mock-data/*": [
|
||||||
|
"./app/mock-data/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"strictBuiltinIteratorReturn": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"useUnknownInCatchVariables": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./app/api/authApi.ts",
|
||||||
|
"./app/api/index.ts",
|
||||||
|
"./app/features/index.ts",
|
||||||
|
"./app/features/addLead/addLead.styles.ts",
|
||||||
|
"./app/features/addLead/index.ts",
|
||||||
|
"./app/features/customers/customers.styles.ts",
|
||||||
|
"./app/features/customers/index.ts",
|
||||||
|
"./app/features/dashboard/dashboard.styles.ts",
|
||||||
|
"./app/features/dashboard/index.ts",
|
||||||
|
"./app/features/estimates/estimates.styles.ts",
|
||||||
|
"./app/features/estimates/index.ts",
|
||||||
|
"./app/features/invoices/index.ts",
|
||||||
|
"./app/features/invoices/invoices.styles.ts",
|
||||||
|
"./app/features/leads/index.ts",
|
||||||
|
"./app/features/leads/leads.styles.ts",
|
||||||
|
"./app/features/login/index.ts",
|
||||||
|
"./app/features/login/login.styles.ts",
|
||||||
|
"./app/features/profile/index.ts",
|
||||||
|
"./app/features/profile/profile.styles.ts",
|
||||||
|
"./app/features/projects/index.ts",
|
||||||
|
"./app/features/projects/projects.styles.ts",
|
||||||
|
"./app/features/proposals/index.ts",
|
||||||
|
"./app/features/proposals/proposals.styles.ts",
|
||||||
|
"./app/features/tasks/index.ts",
|
||||||
|
"./app/features/tasks/tasks.styles.ts",
|
||||||
|
"./app/features/tickets/index.ts",
|
||||||
|
"./app/features/tickets/tickets.styles.ts",
|
||||||
|
"./app/interfaces/auth.ts",
|
||||||
|
"./app/interfaces/drawerItem.ts",
|
||||||
|
"./app/interfaces/index.ts",
|
||||||
|
"./app/mock-data/customDrawer.ts",
|
||||||
|
"./app/mock-data/customers.ts",
|
||||||
|
"./app/mock-data/dashboard.ts",
|
||||||
|
"./app/mock-data/estimates.ts",
|
||||||
|
"./app/mock-data/index.ts",
|
||||||
|
"./app/mock-data/invoices.ts",
|
||||||
|
"./app/mock-data/leads.ts",
|
||||||
|
"./app/mock-data/projects.ts",
|
||||||
|
"./app/mock-data/proposal.ts",
|
||||||
|
"./app/mock-data/tasks.ts",
|
||||||
|
"./app/mock-data/tickets.ts",
|
||||||
|
"./app/navigation/customDrawerContent.style.ts",
|
||||||
|
"./app/navigation/tabStack.styles.ts",
|
||||||
|
"./app/store/index.ts",
|
||||||
|
"./app/store/migration.ts",
|
||||||
|
"./app/store/rootReducer.ts",
|
||||||
|
"./app/store/store.ts",
|
||||||
|
"./app/store/commonStore/auth/index.ts",
|
||||||
|
"./app/store/commonStore/auth/reducers.ts",
|
||||||
|
"./app/store/commonStore/auth/thunk.ts",
|
||||||
|
"./app/theme/colors.ts",
|
||||||
|
"./app/theme/index.ts",
|
||||||
|
"./app/types/react-native-vector-icons.d.ts",
|
||||||
|
"./app/utils/api.ts",
|
||||||
|
"./app/utils/index.ts",
|
||||||
|
"./app/utils/route.ts",
|
||||||
|
"./app/utils/assets/index.ts",
|
||||||
|
"./__tests__/App.test.tsx",
|
||||||
|
"./app/App.tsx",
|
||||||
|
"./app/features/addLead/addLead.screen.tsx",
|
||||||
|
"./app/features/customers/customers.screen.tsx",
|
||||||
|
"./app/features/dashboard/dashboard.screen.tsx",
|
||||||
|
"./app/features/estimates/estimates.screen.tsx",
|
||||||
|
"./app/features/invoices/invoices.screen.tsx",
|
||||||
|
"./app/features/leads/leads.screen.tsx",
|
||||||
|
"./app/features/login/login.screen.tsx",
|
||||||
|
"./app/features/profile/profile.screen.tsx",
|
||||||
|
"./app/features/projects/projects.screen.tsx",
|
||||||
|
"./app/features/proposals/proposals.screen.tsx",
|
||||||
|
"./app/features/tasks/tasks.screen.tsx",
|
||||||
|
"./app/features/tickets/tickets.screen.tsx",
|
||||||
|
"./app/navigation/authStack.tsx",
|
||||||
|
"./app/navigation/customDrawerContent.tsx",
|
||||||
|
"./app/navigation/drawerStack.tsx",
|
||||||
|
"./app/navigation/rootNavigator.tsx",
|
||||||
|
"./app/navigation/tabStack.tsx",
|
||||||
|
"./app/theme/ThemeContext.tsx"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules",
|
||||||
|
"**/Pods"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user