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 { RootNavigator } from './navigation/rootNavigator';
|
||||
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 { theme: colors, isDark } = useTheme();
|
||||
@ -22,17 +25,17 @@ const ThemedStatusBar = () => {
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<SafeAreaProvider>
|
||||
<ThemeProvider>
|
||||
<ThemedStatusBar />
|
||||
<RootNavigator />
|
||||
</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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 './invoices';
|
||||
export * from './leads';
|
||||
export * from './leadDetails';
|
||||
export * from './login';
|
||||
export * from './profile';
|
||||
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 {
|
||||
Text,
|
||||
View,
|
||||
FlatList,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
} 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 { 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 = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<LeadsScreenNavigationProp>();
|
||||
const { theme: colors } = useTheme();
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.searchWrapper}>
|
||||
<Icon
|
||||
name="search-outline"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Search leads, companies..."
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
placeholder="Search leads, companies, emails..."
|
||||
style={styles.searchInput}
|
||||
/>
|
||||
<TouchableOpacity style={styles.filterButton}>
|
||||
<Icon name="filter-outline" size={20} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={MOCK_LEADS}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={styles.listContent}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.leadCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View>
|
||||
<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>
|
||||
{loading ? (
|
||||
<Loader message="Loading leads..." />
|
||||
) : error ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredLeads}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={styles.listContent}
|
||||
renderItem={renderLead}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>No leads available.</Text>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,109 +1,55 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) => StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
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: {
|
||||
flex: 1,
|
||||
height: 44,
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
},
|
||||
filterButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
listContent: {
|
||||
padding: 16,
|
||||
paddingTop: 0,
|
||||
},
|
||||
leadCard: {
|
||||
backgroundColor: colors.card,
|
||||
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',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
},
|
||||
filterButton: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
listContent: {
|
||||
padding: 16,
|
||||
paddingTop: 0,
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: '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 {
|
||||
Text,
|
||||
View,
|
||||
@ -8,53 +8,60 @@ import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { getStyles } from './login.styles';
|
||||
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 = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<any>();
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
const [tenancy, setTenancy] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localError, setLocalError] = useState('');
|
||||
|
||||
const { loginLoading, loginError, loginSuccess, token, user_data } =
|
||||
useAppSelector((state: RootState) => state.auth);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError('');
|
||||
setLocalError('');
|
||||
if (!tenancy.trim()) {
|
||||
setError('Tenancy name is required');
|
||||
setLocalError('Tenancy name is required');
|
||||
return;
|
||||
}
|
||||
if (!email.trim()) {
|
||||
setError('Email address is required');
|
||||
setLocalError('Email address is required');
|
||||
return;
|
||||
}
|
||||
if (!password.trim()) {
|
||||
setError('Password is required');
|
||||
setLocalError('Password is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const mockToken = 'dummy-jwt-token-for-crm';
|
||||
await AsyncStorage.setItem('userToken', mockToken);
|
||||
await AsyncStorage.setItem('tenancyName', tenancy);
|
||||
setLoading(false);
|
||||
const payload: LoginRequest = {
|
||||
email,
|
||||
password,
|
||||
device_token: '',
|
||||
};
|
||||
|
||||
await dispatch(login(payload));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loginSuccess && token && user_data) {
|
||||
navigation.reset({
|
||||
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 (
|
||||
<KeyboardAvoidingView
|
||||
@ -76,10 +83,10 @@ export const LoginScreen = () => {
|
||||
</View>
|
||||
|
||||
<View style={styles.formCard}>
|
||||
{error ? (
|
||||
{localError || loginError ? (
|
||||
<View style={styles.errorBanner}>
|
||||
<Icon name="alert-circle" size={20} color="#EF4444" />
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<Text style={styles.errorText}>{localError || loginError}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@ -156,10 +163,10 @@ export const LoginScreen = () => {
|
||||
style={styles.button}
|
||||
activeOpacity={0.8}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
disabled={loginLoading}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{loading ? 'Signing In...' : 'Sign In'}
|
||||
{loginLoading ? 'Signing In...' : 'Sign In'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</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 './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 { NavigationContainer } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import {AuthStack} from './authStack';
|
||||
import {DrawerStack} from './drawerStack';
|
||||
import { AuthStack } from './authStack';
|
||||
import { DrawerStack } from './drawerStack';
|
||||
import { useAppSelector } from '../store/store';
|
||||
import { RootState } from '../store/rootReducer';
|
||||
|
||||
export type RootStackParamList = {
|
||||
AuthStack: undefined;
|
||||
@ -12,12 +14,19 @@ export type RootStackParamList = {
|
||||
const RootStack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
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 (
|
||||
<NavigationContainer>
|
||||
<RootStack.Navigator screenOptions={{ headerShown: false }} initialRouteName="AuthStack">
|
||||
<RootStack.Navigator
|
||||
screenOptions={{ headerShown: false }}
|
||||
initialRouteName={initialRouteName}
|
||||
>
|
||||
<RootStack.Screen name="AuthStack" component={AuthStack} />
|
||||
<RootStack.Screen name="DrawerStack" component={DrawerStack} />
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -6,11 +6,11 @@ import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { route, RouteParams } from '@utils';
|
||||
import {
|
||||
DashboardScreen,
|
||||
LeadsScreen,
|
||||
AddLeadScreen,
|
||||
CustomersScreen,
|
||||
ProfileScreen,
|
||||
} from '@features';
|
||||
import { LeadsStack } from './leadsStack';
|
||||
import { useTheme } from '@theme';
|
||||
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.customers} component={CustomersScreen} />
|
||||
<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 './assets';
|
||||
export * from './api';
|
||||
// export * from './helper';
|
||||
|
||||
@ -16,6 +16,7 @@ export const route = {
|
||||
|
||||
// Sub screens
|
||||
addLead: 'addLead',
|
||||
leadDetails: 'leadDetails',
|
||||
profile: 'profile',
|
||||
} as const;
|
||||
|
||||
@ -34,6 +35,7 @@ export type RouteParams = {
|
||||
tasks: undefined;
|
||||
tickets: undefined;
|
||||
addLead: undefined;
|
||||
leadDetails: { lead: any };
|
||||
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