feat(app): add, proposal, note desing
This commit is contained in:
parent
2a8c5f7e60
commit
395bed3a9c
7
app/components/actionButton/actionButton.props.ts
Normal file
7
app/components/actionButton/actionButton.props.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ActionButtonProps {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
variant?: 'primary' | 'secondary';
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
31
app/components/actionButton/actionButton.styles.ts
Normal file
31
app/components/actionButton/actionButton.styles.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
secondaryButton: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: '#3B82F6',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
primaryButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
29
app/components/actionButton/actionButton.tsx
Normal file
29
app/components/actionButton/actionButton.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, Text } from 'react-native';
|
||||
import { useTheme } from '@theme';
|
||||
import { getStyles } from './actionButton.styles';
|
||||
import { ActionButtonProps } from './actionButton.props';
|
||||
|
||||
export const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
label,
|
||||
onPress,
|
||||
variant = 'primary',
|
||||
loading = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const buttonStyle = variant === 'primary' ? styles.primaryButton : styles.secondaryButton;
|
||||
const textStyle = variant === 'primary' ? styles.primaryButtonText : styles.secondaryButtonText;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={buttonStyle}
|
||||
onPress={onPress}
|
||||
disabled={loading || disabled}
|
||||
>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
2
app/components/actionButton/index.ts
Normal file
2
app/components/actionButton/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './actionButton';
|
||||
export * from './actionButton.props';
|
||||
7
app/components/actionCard/actionCard.props.ts
Normal file
7
app/components/actionCard/actionCard.props.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ActionCardProps {
|
||||
title: string;
|
||||
icon: string;
|
||||
count?: number;
|
||||
onAddPress: () => void;
|
||||
onViewAllPress: () => void;
|
||||
}
|
||||
79
app/components/actionCard/actionCard.styles.ts
Normal file
79
app/components/actionCard/actionCard.styles.ts
Normal file
@ -0,0 +1,79 @@
|
||||
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,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
},
|
||||
leftSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.surface,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
titleSection: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: colors.text,
|
||||
},
|
||||
count: {
|
||||
fontSize: 12,
|
||||
color: colors.textMuted,
|
||||
marginTop: 2,
|
||||
},
|
||||
actionsRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: colors.icon,
|
||||
borderColor: colors.icon,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginLeft: 6,
|
||||
},
|
||||
addButtonText: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
59
app/components/actionCard/actionCard.tsx
Normal file
59
app/components/actionCard/actionCard.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
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 './actionCard.styles';
|
||||
import { ActionCardProps } from './actionCard.props';
|
||||
|
||||
export const ActionCard: React.FC<ActionCardProps> = ({
|
||||
title,
|
||||
icon,
|
||||
count,
|
||||
onAddPress,
|
||||
onViewAllPress,
|
||||
}) => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
{/* Header with Icon and Title */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.leftSection}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Icon name={icon} size={22} color={colors.icon} />
|
||||
</View>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
{count !== undefined && (
|
||||
<Text style={styles.count}>
|
||||
{count} {count === 1 ? 'item' : 'items'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.actionsRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.addButton]}
|
||||
onPress={onAddPress}
|
||||
activeOpacity={0.7}>
|
||||
<Icon name="add-circle-outline" size={18} color="#FFFFFF" />
|
||||
<Text style={[styles.actionButtonText, styles.addButtonText]}>
|
||||
Add
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={onViewAllPress}
|
||||
activeOpacity={0.7}>
|
||||
<Icon name="eye-outline" size={18} color={colors.icon} />
|
||||
<Text style={styles.actionButtonText}>View All</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
2
app/components/actionCard/index.ts
Normal file
2
app/components/actionCard/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './actionCard';
|
||||
export * from './actionCard.props';
|
||||
4
app/components/addItemButton/addItemButton.props.ts
Normal file
4
app/components/addItemButton/addItemButton.props.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface AddItemButtonProps {
|
||||
label?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
21
app/components/addItemButton/addItemButton.styles.ts
Normal file
21
app/components/addItemButton/addItemButton.styles.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
marginTop: 8,
|
||||
gap: 8,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#3B82F6',
|
||||
},
|
||||
});
|
||||
21
app/components/addItemButton/addItemButton.tsx
Normal file
21
app/components/addItemButton/addItemButton.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, Text } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { useTheme } from '@theme';
|
||||
import { getStyles } from './addItemButton.styles';
|
||||
import { AddItemButtonProps } from './addItemButton.props';
|
||||
|
||||
export const AddItemButton: React.FC<AddItemButtonProps> = ({
|
||||
label = 'Add Item',
|
||||
onPress,
|
||||
}) => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.container} onPress={onPress}>
|
||||
<Icon name="add-circle-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
2
app/components/addItemButton/index.ts
Normal file
2
app/components/addItemButton/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './addItemButton';
|
||||
export * from './addItemButton.props';
|
||||
12
app/components/checkboxWithLabel/checkboxWithLabel.props.ts
Normal file
12
app/components/checkboxWithLabel/checkboxWithLabel.props.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface CheckboxOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CheckboxWithLabelProps {
|
||||
label?: string;
|
||||
options: CheckboxOption[];
|
||||
selectedValues: string[];
|
||||
onValueChange: (selectedValues: string[]) => void;
|
||||
singleSelect?: boolean;
|
||||
}
|
||||
44
app/components/checkboxWithLabel/checkboxWithLabel.styles.ts
Normal file
44
app/components/checkboxWithLabel/checkboxWithLabel.styles.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginBottom: 12,
|
||||
},
|
||||
checkboxRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 24,
|
||||
},
|
||||
checkboxOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
checkbox: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 6,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
checkboxChecked: {
|
||||
borderColor: '#6366F1',
|
||||
backgroundColor: '#6366F1',
|
||||
},
|
||||
checkboxLabel: {
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
63
app/components/checkboxWithLabel/checkboxWithLabel.tsx
Normal file
63
app/components/checkboxWithLabel/checkboxWithLabel.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
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 './checkboxWithLabel.styles';
|
||||
import { CheckboxWithLabelProps } from './checkboxWithLabel.props';
|
||||
|
||||
export const CheckboxWithLabel: React.FC<CheckboxWithLabelProps> = ({
|
||||
label,
|
||||
options,
|
||||
selectedValues,
|
||||
onValueChange,
|
||||
singleSelect = false,
|
||||
}) => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const handlePress = (id: string) => {
|
||||
if (singleSelect) {
|
||||
// Single select mode - only one can be selected
|
||||
onValueChange([id]);
|
||||
} else {
|
||||
// Multiple select mode - can check all
|
||||
if (selectedValues.includes(id)) {
|
||||
// Remove from selection
|
||||
onValueChange(selectedValues.filter(val => val !== id));
|
||||
} else {
|
||||
// Add to selection
|
||||
onValueChange([...selectedValues, id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (id: string) => selectedValues.includes(id);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{label && <Text style={styles.label}>{label}</Text>}
|
||||
<View style={styles.checkboxRow}>
|
||||
{options.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option.id}
|
||||
style={styles.checkboxOption}
|
||||
onPress={() => handlePress(option.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.checkbox,
|
||||
isSelected(option.id) && styles.checkboxChecked,
|
||||
]}
|
||||
>
|
||||
{isSelected(option.id) && (
|
||||
<Icon name="checkmark" size={16} color="#FFFFFF" />
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.checkboxLabel}>{option.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
2
app/components/checkboxWithLabel/index.ts
Normal file
2
app/components/checkboxWithLabel/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './checkboxWithLabel';
|
||||
export * from './checkboxWithLabel.props';
|
||||
10
app/components/formInput/formInput.props.ts
Normal file
10
app/components/formInput/formInput.props.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TextInputProps } from 'react-native';
|
||||
|
||||
export interface FormInputProps extends Omit<TextInputProps, 'style'> {
|
||||
label: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
multiline?: boolean;
|
||||
}
|
||||
33
app/components/formInput/formInput.styles.ts
Normal file
33
app/components/formInput/formInput.styles.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginBottom: 8,
|
||||
},
|
||||
required: {
|
||||
color: '#EF4444',
|
||||
marginLeft: 2,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
},
|
||||
multilineInput: {
|
||||
height: 100,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
});
|
||||
39
app/components/formInput/formInput.tsx
Normal file
39
app/components/formInput/formInput.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TextInput } from 'react-native';
|
||||
import { useTheme } from '@theme';
|
||||
import { getStyles } from './formInput.styles';
|
||||
import { FormInputProps } from './formInput.props';
|
||||
|
||||
export const FormInput: React.FC<FormInputProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
required = false,
|
||||
multiline = false,
|
||||
keyboardType = 'default',
|
||||
...rest
|
||||
}) => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>
|
||||
{label}
|
||||
{required && <Text style={styles.required}>*</Text>}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, multiline && styles.multilineInput]}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
multiline={multiline}
|
||||
numberOfLines={multiline ? 4 : 1}
|
||||
keyboardType={keyboardType}
|
||||
{...rest}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
2
app/components/formInput/index.ts
Normal file
2
app/components/formInput/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './formInput';
|
||||
export * from './formInput.props';
|
||||
13
app/components/formPicker/formPicker.props.ts
Normal file
13
app/components/formPicker/formPicker.props.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface FormPickerOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface FormPickerProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
options: FormPickerOption[];
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
37
app/components/formPicker/formPicker.styles.ts
Normal file
37
app/components/formPicker/formPicker.styles.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginBottom: 8,
|
||||
},
|
||||
required: {
|
||||
color: '#EF4444',
|
||||
marginLeft: 2,
|
||||
},
|
||||
pickerContainer: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
pickerText: {
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
},
|
||||
placeholder: {
|
||||
color: colors.textMuted,
|
||||
},
|
||||
});
|
||||
44
app/components/formPicker/formPicker.tsx
Normal file
44
app/components/formPicker/formPicker.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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 './formPicker.styles';
|
||||
import { FormPickerProps } from './formPicker.props';
|
||||
|
||||
export const FormPicker: React.FC<FormPickerProps> = ({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
options,
|
||||
required = false,
|
||||
placeholder = 'Select...',
|
||||
}) => {
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const selectedOption = options.find(opt => opt.value === value);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>
|
||||
{label}
|
||||
{required && <Text style={styles.required}>*</Text>}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.pickerContainer}
|
||||
onPress={() => {
|
||||
// TODO: Implement modal picker or action sheet
|
||||
console.log('Picker pressed');
|
||||
}}>
|
||||
<Text
|
||||
style={[
|
||||
styles.pickerText,
|
||||
!selectedOption && styles.placeholder,
|
||||
]}>
|
||||
{selectedOption?.label || placeholder}
|
||||
</Text>
|
||||
<Icon name="chevron-down" size={20} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
2
app/components/formPicker/index.ts
Normal file
2
app/components/formPicker/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './formPicker';
|
||||
export * from './formPicker.props';
|
||||
@ -2,3 +2,9 @@ export * from './leadItemCard';
|
||||
export * from './searchInput';
|
||||
export * from './loader';
|
||||
export * from './leadDetailHeader';
|
||||
export * from './actionCard';
|
||||
export * from './formInput';
|
||||
export * from './formPicker';
|
||||
export * from './addItemButton';
|
||||
export * from './actionButton';
|
||||
export * from './checkboxWithLabel';
|
||||
|
||||
@ -3,13 +3,12 @@ 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';
|
||||
import { getInitials, handleEmailPress, handlePhonePress } from '@utils';
|
||||
|
||||
export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
||||
item,
|
||||
@ -18,38 +17,6 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
||||
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}
|
||||
@ -90,7 +57,7 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.contactIconButton}
|
||||
onPress={handleEmailPress}
|
||||
onPress={() => handleEmailPress(item.email)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||
<Icon name="mail" size={20} color={colors.icon} />
|
||||
</TouchableOpacity>
|
||||
@ -105,7 +72,7 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.contactIconButton}
|
||||
onPress={handlePhonePress}
|
||||
onPress={() => handlePhonePress(item.phonenumber)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||
<Icon name="call" size={20} color={colors.icon} />
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -5,6 +5,9 @@ export * from './estimates';
|
||||
export * from './invoices';
|
||||
export * from './leads';
|
||||
export * from './leadDetails';
|
||||
export * from './leadProposals';
|
||||
export * from './leadTasks';
|
||||
export * from './leadNotes';
|
||||
export * from './login';
|
||||
export * from './profile';
|
||||
export * from './projects';
|
||||
|
||||
@ -6,19 +6,26 @@ import {
|
||||
Linking,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
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';
|
||||
import { Loader, LeadDetailHeader, ActionCard } from '@components';
|
||||
import { handleEmailPress, handlePhonePress } from '@utils';
|
||||
import { route as routes } from '@utils';
|
||||
|
||||
type LeadDetailsRouteProp = RouteProp<LeadsStackParamList, 'leadDetails'>;
|
||||
type LeadDetailsNavigationProp = NativeStackNavigationProp<
|
||||
LeadsStackParamList,
|
||||
'leadDetails'
|
||||
>;
|
||||
|
||||
export const LeadDetailsScreen = () => {
|
||||
const route = useRoute<LeadDetailsRouteProp>();
|
||||
const navigation = useNavigation<LeadDetailsNavigationProp>();
|
||||
const dispatch = useAppDispatch();
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
@ -65,34 +72,42 @@ export const LeadDetailsScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// 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');
|
||||
};
|
||||
|
||||
// Proposals handlers
|
||||
const handleAddProposal = () => {
|
||||
// TODO: Navigate to add proposal screen
|
||||
navigation.navigate(routes.leadProposals, { leadId: lead.id });
|
||||
};
|
||||
|
||||
const handleViewAllProposals = () => {
|
||||
console.log('view Proposal pressed');
|
||||
};
|
||||
|
||||
// Tasks handlers
|
||||
const handleAddTask = () => {
|
||||
// TODO: Navigate to add task screen
|
||||
navigation.navigate(routes.leadTasks, { leadId: lead.id });
|
||||
};
|
||||
|
||||
const handleViewAllTasks = () => {
|
||||
console.log('view Add Task pressed');
|
||||
};
|
||||
|
||||
// Notes handlers
|
||||
const handleAddNote = () => {
|
||||
// TODO: Navigate to add note screen
|
||||
navigation.navigate(routes.leadNotes, { leadId: lead.id });
|
||||
};
|
||||
|
||||
const handleViewAllNotes = () => {
|
||||
console.log('view Add Note pressed');
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
||||
{/* Header with Avatar */}
|
||||
@ -104,9 +119,36 @@ export const LeadDetailsScreen = () => {
|
||||
statusName={lead.status_name || lead.status}
|
||||
statusColor={lead.color}
|
||||
onStatusPress={handleStatusPress}
|
||||
onEmailPress={handleEmailPress}
|
||||
onPhonePress={handlePhonePress}
|
||||
onEmailPress={() => handleEmailPress(lead.email)}
|
||||
onPhonePress={() => handlePhonePress(lead.phonenumber)}
|
||||
/>
|
||||
|
||||
{/* Action Cards Section */}
|
||||
<View style={styles.cardsContainer}>
|
||||
<ActionCard
|
||||
title="Proposals"
|
||||
icon="document-text-outline"
|
||||
count={0}
|
||||
onAddPress={handleAddProposal}
|
||||
onViewAllPress={handleViewAllProposals}
|
||||
/>
|
||||
|
||||
<ActionCard
|
||||
title="Tasks"
|
||||
icon="checkbox-outline"
|
||||
count={0}
|
||||
onAddPress={handleAddTask}
|
||||
onViewAllPress={handleViewAllTasks}
|
||||
/>
|
||||
|
||||
<ActionCard
|
||||
title="Notes"
|
||||
icon="create-outline"
|
||||
count={0}
|
||||
onAddPress={handleAddNote}
|
||||
onViewAllPress={handleViewAllNotes}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,6 +10,9 @@ export const getStyles = (colors: ThemeColors) =>
|
||||
scrollContent: {
|
||||
padding: 16,
|
||||
},
|
||||
cardsContainer: {
|
||||
marginTop: 16,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 24,
|
||||
|
||||
1
app/features/leadNotes/index.ts
Normal file
1
app/features/leadNotes/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './leadNotes.screen'
|
||||
49
app/features/leadNotes/leadNotes.screen.tsx
Normal file
49
app/features/leadNotes/leadNotes.screen.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, ScrollView, TextInput } from 'react-native';
|
||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||
import { useTheme } from '@theme';
|
||||
import { getStyles } from './leadNotes.styles';
|
||||
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
||||
import { ActionButton } from '@components';
|
||||
|
||||
type LeadNotesRouteProp = RouteProp<LeadsStackParamList, 'leadNotes'>;
|
||||
|
||||
export const LeadNotesScreen = () => {
|
||||
const route = useRoute<LeadNotesRouteProp>();
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const leadId = route.params?.leadId;
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Saving note:', note);
|
||||
// TODO: Implement save logic
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<TextInput
|
||||
style={styles.noteInput}
|
||||
value={note}
|
||||
onChangeText={setNote}
|
||||
placeholder="Write your note here..."
|
||||
placeholderTextColor={colors.textMuted}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
label="Add Note"
|
||||
onPress={handleSave}
|
||||
variant="primary"
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
29
app/features/leadNotes/leadNotes.styles.ts
Normal file
29
app/features/leadNotes/leadNotes.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: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
noteInput: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
minHeight: 200,
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
1
app/features/leadProposals/index.ts
Normal file
1
app/features/leadProposals/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './leadProposals.screen';
|
||||
293
app/features/leadProposals/leadProposals.screen.tsx
Normal file
293
app/features/leadProposals/leadProposals.screen.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
} from 'react-native';
|
||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { useTheme } from '@theme';
|
||||
import { getStyles } from './leadProposals.styles';
|
||||
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
||||
import { FormInput, FormPicker, AddItemButton, ActionButton } from '@components';
|
||||
|
||||
type LeadProposalsRouteProp = RouteProp<LeadsStackParamList, 'leadProposals'>;
|
||||
|
||||
export const LeadProposalsScreen = () => {
|
||||
const route = useRoute<LeadProposalsRouteProp>();
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const leadId = route.params?.leadId;
|
||||
|
||||
// Form state
|
||||
const [subject, setSubject] = useState('');
|
||||
const [relatedTo, setRelatedTo] = useState('lead');
|
||||
const [lead, setLead] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const [openTill, setOpenTill] = useState('');
|
||||
const [currency, setCurrency] = useState('USD');
|
||||
const [discountType, setDiscountType] = useState('before_tax');
|
||||
const [status, setStatus] = useState('revised');
|
||||
const [assignedTo, setAssignedTo] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
// Item fields
|
||||
const [showQuantityAs, setShowQuantityAs] = useState('qty');
|
||||
const [item, setItem] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [qty, setQty] = useState('1');
|
||||
const [rate, setRate] = useState('0');
|
||||
const [tax, setTax] = useState('no_tax');
|
||||
|
||||
// Calculation fields
|
||||
const [discount, setDiscount] = useState('0');
|
||||
const [discountUnit, setDiscountUnit] = useState('%');
|
||||
const [adjustment, setAdjustment] = useState('0');
|
||||
|
||||
const handleAddItem = () => {
|
||||
console.log('Add item pressed');
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Save pressed');
|
||||
};
|
||||
|
||||
const handleSaveAndSend = () => {
|
||||
console.log('Save and Send pressed');
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
||||
{/* Form Fields - Step 1 */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Basic Information</Text>
|
||||
|
||||
<FormInput
|
||||
label="Subject"
|
||||
value={subject}
|
||||
onChangeText={setSubject}
|
||||
placeholder="Enter subject"
|
||||
required
|
||||
/>
|
||||
|
||||
<FormPicker
|
||||
label="Related To"
|
||||
value={relatedTo}
|
||||
onValueChange={setRelatedTo}
|
||||
options={[
|
||||
{ label: 'Lead', value: 'lead' },
|
||||
{ label: 'Customer', value: 'customer' },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
|
||||
<FormPicker
|
||||
label="Lead"
|
||||
value={lead}
|
||||
onValueChange={setLead}
|
||||
options={[
|
||||
{ label: 'Select Lead', value: '' },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.halfWidth}>
|
||||
<FormInput
|
||||
label="Date"
|
||||
value={date}
|
||||
onChangeText={setDate}
|
||||
placeholder="Select date"
|
||||
required
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.halfWidth}>
|
||||
<FormInput
|
||||
label="Open Till"
|
||||
value={openTill}
|
||||
onChangeText={setOpenTill}
|
||||
placeholder="Select date"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<FormPicker
|
||||
label="Currency"
|
||||
value={currency}
|
||||
onValueChange={setCurrency}
|
||||
options={[
|
||||
{ label: 'USD', value: 'USD' },
|
||||
{ label: 'EUR', value: 'EUR' },
|
||||
{ label: 'GBP', value: 'GBP' },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
|
||||
<FormPicker
|
||||
label="Discount Type"
|
||||
value={discountType}
|
||||
onValueChange={setDiscountType}
|
||||
options={[
|
||||
{ label: 'Before Tax', value: 'before_tax' },
|
||||
{ label: 'After Tax', value: 'after_tax' },
|
||||
{ label: 'No Discount', value: 'no_discount' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormPicker
|
||||
label="Status"
|
||||
value={status}
|
||||
onValueChange={setStatus}
|
||||
options={[
|
||||
{ label: 'Draft', value: 'draft' },
|
||||
{ label: 'Sent', value: 'sent' },
|
||||
{ label: 'Revised', value: 'revised' },
|
||||
{ label: 'Declined', value: 'declined' },
|
||||
{ label: 'Accepted', value: 'accepted' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormPicker
|
||||
label="Assigned to"
|
||||
value={assignedTo}
|
||||
onValueChange={setAssignedTo}
|
||||
options={[
|
||||
{ label: 'Select User', value: '' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="Enter email"
|
||||
keyboardType="email-address"
|
||||
required
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Items Section */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Items</Text>
|
||||
|
||||
<FormPicker
|
||||
label="Show quantity as"
|
||||
value={showQuantityAs}
|
||||
onValueChange={setShowQuantityAs}
|
||||
options={[
|
||||
{ label: 'Qty', value: 'qty' },
|
||||
{ label: 'Hours', value: 'hours' },
|
||||
{ label: 'Qty/Hours', value: 'qty_hours' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Item"
|
||||
value={item}
|
||||
onChangeText={setItem}
|
||||
placeholder="Enter item name"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Description"
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="Long Description"
|
||||
multiline
|
||||
/>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.halfWidth}>
|
||||
<FormInput
|
||||
label="Qty"
|
||||
value={qty}
|
||||
onChangeText={setQty}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.halfWidth}>
|
||||
<FormInput
|
||||
label="Rate"
|
||||
value={rate}
|
||||
onChangeText={setRate}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<FormPicker
|
||||
label="Tax"
|
||||
value={tax}
|
||||
onValueChange={setTax}
|
||||
options={[
|
||||
{ label: 'No Tax', value: 'no_tax' },
|
||||
{ label: 'Tax 1', value: 'tax_1' },
|
||||
{ label: 'Tax 2', value: 'tax_2' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<AddItemButton onPress={handleAddItem} />
|
||||
</View>
|
||||
|
||||
{/* Calculation Section */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Summary</Text>
|
||||
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Sub Total:</Text>
|
||||
<Text style={styles.summaryValue}>0.0</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.discountRow}>
|
||||
<Text style={styles.summaryLabel}>Discount:</Text>
|
||||
<View style={styles.discountInputs}>
|
||||
<TextInput
|
||||
style={styles.discountInput}
|
||||
value={discount}
|
||||
onChangeText={setDiscount}
|
||||
keyboardType="numeric"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<TouchableOpacity style={styles.unitButton}>
|
||||
<Text style={styles.unitText}>{discountUnit}</Text>
|
||||
<Icon name="chevron-down" size={16} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.summaryValue}>0.00</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.adjustmentRow}>
|
||||
<Text style={styles.summaryLabel}>Adjustment:</Text>
|
||||
<TextInput
|
||||
style={styles.adjustmentInput}
|
||||
value={adjustment}
|
||||
onChangeText={setAdjustment}
|
||||
keyboardType="numeric"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={styles.summaryValue}>0</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.totalRow}>
|
||||
<Text style={styles.totalLabel}>Total:</Text>
|
||||
<Text style={styles.totalValue}>0.00</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ActionButton
|
||||
label="Save and Send"
|
||||
onPress={handleSaveAndSend}
|
||||
variant="secondary"
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
label="Save"
|
||||
onPress={handleSave}
|
||||
variant="primary"
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
190
app/features/leadProposals/leadProposals.styles.ts
Normal file
190
app/features/leadProposals/leadProposals.styles.ts
Normal file
@ -0,0 +1,190 @@
|
||||
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,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
|
||||
// Step Indicator
|
||||
stepIndicator: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
stepActive: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#6366F1',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
stepInactive: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.border,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
stepNumberActive: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
stepNumberInactive: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
stepLine: {
|
||||
flex: 1,
|
||||
height: 2,
|
||||
backgroundColor: colors.border,
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
|
||||
// Card
|
||||
card: {
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: colors.text,
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
||||
// Row Layout
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
halfWidth: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
// Summary Section
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: 15,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// Discount Row
|
||||
discountRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
discountInputs: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
gap: 8,
|
||||
},
|
||||
discountInput: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
},
|
||||
unitButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
gap: 4,
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// Adjustment Row
|
||||
adjustmentRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
adjustmentInput: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
|
||||
// Total Row
|
||||
totalRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: colors.border,
|
||||
marginTop: 8,
|
||||
},
|
||||
totalLabel: {
|
||||
fontSize: 18,
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
totalValue: {
|
||||
fontSize: 18,
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
1
app/features/leadTasks/index.ts
Normal file
1
app/features/leadTasks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './leadTasks.screen';
|
||||
198
app/features/leadTasks/leadTasks.screen.tsx
Normal file
198
app/features/leadTasks/leadTasks.screen.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { useTheme } from '@theme';
|
||||
import { getStyles } from './leadTasks.styles';
|
||||
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
||||
import { FormInput, FormPicker, ActionButton, CheckboxWithLabel } from '@components';
|
||||
|
||||
type LeadTasksRouteProp = RouteProp<LeadsStackParamList, 'leadTasks'>;
|
||||
|
||||
export const LeadTasksScreen = () => {
|
||||
const route = useRoute<LeadTasksRouteProp>();
|
||||
const navigation = useNavigation();
|
||||
const { theme: colors } = useTheme();
|
||||
const styles = getStyles(colors);
|
||||
|
||||
const leadId = route.params?.leadId;
|
||||
|
||||
// Multiple checkboxes can be checked at the same time
|
||||
const billableOptions = [
|
||||
{ id: 'public', label: 'Public' },
|
||||
{ id: 'billable', label: 'Billable' },
|
||||
];
|
||||
|
||||
// Form state
|
||||
const [selectedBillable, setSelectedBillable] = useState<string[]>(['billable']);
|
||||
const [attachment, setAttachment] = useState('');
|
||||
const [subject, setSubject] = useState('');
|
||||
const [hourlyRate, setHourlyRate] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [dueDate, setDueDate] = useState('');
|
||||
const [priority, setPriority] = useState('');
|
||||
const [repeatEvery, setRepeatEvery] = useState('');
|
||||
const [relatedTo, setRelatedTo] = useState('lead');
|
||||
const [lead, setLead] = useState('');
|
||||
const [assignees, setAssignees] = useState('');
|
||||
const [followers, setFollowers] = useState('');
|
||||
const [taskDescription, setTaskDescription] = useState('');
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Save task pressed');
|
||||
// TODO: Implement save logic
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
|
||||
{/* Public or Billable */}
|
||||
<CheckboxWithLabel
|
||||
label="Public or Billable"
|
||||
options={billableOptions}
|
||||
selectedValues={selectedBillable}
|
||||
onValueChange={setSelectedBillable}
|
||||
singleSelect={false}
|
||||
/>
|
||||
|
||||
{/* Attachment */}
|
||||
<FormInput
|
||||
label="Attachment"
|
||||
value={attachment}
|
||||
onChangeText={setAttachment}
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
{/* Subject */}
|
||||
<FormInput
|
||||
label="Subject"
|
||||
value={subject}
|
||||
onChangeText={setSubject}
|
||||
placeholder=""
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Hourly Rate */}
|
||||
<FormInput
|
||||
label="Hourly Rate"
|
||||
value={hourlyRate}
|
||||
onChangeText={setHourlyRate}
|
||||
placeholder=""
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
|
||||
{/* Start Date and Due Date */}
|
||||
<View style={styles.row}>
|
||||
<View style={styles.halfWidth}>
|
||||
<FormInput
|
||||
label="Start Date"
|
||||
value={startDate}
|
||||
onChangeText={setStartDate}
|
||||
placeholder=""
|
||||
required
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.halfWidth}>
|
||||
<FormInput
|
||||
label="Due Date"
|
||||
value={dueDate}
|
||||
onChangeText={setDueDate}
|
||||
placeholder=""
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Priority */}
|
||||
<FormPicker
|
||||
label="Priority"
|
||||
value={priority}
|
||||
onValueChange={setPriority}
|
||||
options={[
|
||||
{ label: 'Nothing selected', value: '' },
|
||||
{ label: 'Low', value: 'low' },
|
||||
{ label: 'Medium', value: 'medium' },
|
||||
{ label: 'High', value: 'high' },
|
||||
{ label: 'Urgent', value: 'urgent' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Repeat every */}
|
||||
<FormPicker
|
||||
label="Repeat every"
|
||||
value={repeatEvery}
|
||||
onValueChange={setRepeatEvery}
|
||||
options={[
|
||||
{ label: 'Nothing selected', value: '' },
|
||||
{ label: 'Daily', value: 'daily' },
|
||||
{ label: 'Weekly', value: 'weekly' },
|
||||
{ label: 'Monthly', value: 'monthly' },
|
||||
{ label: 'Yearly', value: 'yearly' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Related To */}
|
||||
<FormPicker
|
||||
label="Related To"
|
||||
value={relatedTo}
|
||||
onValueChange={setRelatedTo}
|
||||
options={[
|
||||
{ label: 'Lead', value: 'lead' },
|
||||
{ label: 'Customer', value: 'customer' },
|
||||
{ label: 'Project', value: 'project' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Lead */}
|
||||
<FormPicker
|
||||
label="Lead"
|
||||
value={lead}
|
||||
onValueChange={setLead}
|
||||
options={[
|
||||
{ label: 'Adan ltd - adantesr@gmail.com', value: 'adan_ltd' },
|
||||
{ label: 'Select Lead', value: '' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Assignees */}
|
||||
<FormPicker
|
||||
label="Assignees"
|
||||
value={assignees}
|
||||
onValueChange={setAssignees}
|
||||
options={[
|
||||
{ label: 'Rahul Roy', value: 'rahul_roy' },
|
||||
{ label: 'Select Assignee', value: '' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Followers */}
|
||||
<FormPicker
|
||||
label="Followers"
|
||||
value={followers}
|
||||
onValueChange={setFollowers}
|
||||
options={[
|
||||
{ label: 'Nothing selected', value: '' },
|
||||
{ label: 'User 1', value: 'user_1' },
|
||||
{ label: 'User 2', value: 'user_2' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Task Description */}
|
||||
<FormInput
|
||||
label="Task Description"
|
||||
value={taskDescription}
|
||||
onChangeText={setTaskDescription}
|
||||
placeholder=""
|
||||
multiline
|
||||
/>
|
||||
|
||||
{/* Save Button */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<ActionButton
|
||||
label="Save"
|
||||
onPress={handleSave}
|
||||
variant="primary"
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
60
app/features/leadTasks/leadTasks.styles.ts
Normal file
60
app/features/leadTasks/leadTasks.styles.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { ThemeColors } from '../../theme';
|
||||
|
||||
export const getStyles = (colors: ThemeColors) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
// Header
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
backgroundColor: colors.card,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
backButton: {
|
||||
fontSize: 24,
|
||||
color: colors.text,
|
||||
fontWeight: '400',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
},
|
||||
saveButton: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#3B82F6',
|
||||
},
|
||||
|
||||
// Scroll View
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
|
||||
// Row Layout
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
halfWidth: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
// Button Container
|
||||
buttonContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
@ -1,10 +1,19 @@
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { LeadsScreen, LeadDetailsScreen } from '@features';
|
||||
import {
|
||||
LeadsScreen,
|
||||
LeadDetailsScreen,
|
||||
LeadProposalsScreen,
|
||||
LeadTasksScreen,
|
||||
LeadNotesScreen,
|
||||
} from '@features';
|
||||
import { route, RouteParams } from '@utils';
|
||||
import { useTheme } from '@theme';
|
||||
|
||||
export type LeadsStackParamList = Pick<RouteParams, 'leads' | 'leadDetails'>;
|
||||
export type LeadsStackParamList = Pick<
|
||||
RouteParams,
|
||||
'leads' | 'leadDetails' | 'leadProposals' | 'leadTasks' | 'leadNotes'
|
||||
>;
|
||||
|
||||
const Stack = createNativeStackNavigator<LeadsStackParamList>();
|
||||
|
||||
@ -35,6 +44,21 @@ export const LeadsStack = () => {
|
||||
component={LeadDetailsScreen}
|
||||
options={{ headerTitle: 'Lead Details' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={route.leadProposals}
|
||||
component={LeadProposalsScreen}
|
||||
options={{ headerTitle: 'Proposals' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={route.leadTasks}
|
||||
component={LeadTasksScreen}
|
||||
options={{ headerTitle: 'Tasks' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={route.leadNotes}
|
||||
component={LeadNotesScreen}
|
||||
options={{ headerTitle: 'Notes' }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,70 +1,53 @@
|
||||
// import { Linking, Platform } from 'react-native';
|
||||
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;
|
||||
// }
|
||||
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),
|
||||
// );
|
||||
// };
|
||||
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;
|
||||
// }
|
||||
export const handlePhonePress = (phoneNumber?: string | null) => {
|
||||
if (!phoneNumber) {
|
||||
console.warn('No phone number provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// const phoneUrl =
|
||||
// Platform.OS === 'ios'
|
||||
// ? `telprompt:${phoneNumber}`
|
||||
// : `tel:${phoneNumber}`;
|
||||
const phoneUrl =
|
||||
Platform.OS === 'ios'
|
||||
? `telprompt:${phoneNumber}`
|
||||
: `tel:${phoneNumber}`;
|
||||
|
||||
// Linking.openURL(phoneUrl).catch(err =>
|
||||
// console.error('Error opening phone:', err),
|
||||
// );
|
||||
// };
|
||||
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;
|
||||
// }
|
||||
export const handleWebsitePress = (website?: string | null) => {
|
||||
if (!website) {
|
||||
console.warn('No website provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// const url = website.startsWith('http') ? website : `https://${website}`;
|
||||
const url = website.startsWith('http') ? website : `https://${website}`;
|
||||
|
||||
// Linking.openURL(url).catch(err =>
|
||||
// console.error('Error opening website:', err),
|
||||
// );
|
||||
// };
|
||||
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 '?';
|
||||
export const getInitials = (name?: string | null): string => {
|
||||
if (!name) return '?';
|
||||
|
||||
// const nameParts = name.trim().split(' ');
|
||||
const nameParts = name.trim().split(' ');
|
||||
|
||||
// if (nameParts.length >= 2) {
|
||||
// return (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase();
|
||||
// }
|
||||
if (nameParts.length >= 2) {
|
||||
return (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
// return name.substring(0, 2).toUpperCase();
|
||||
// };
|
||||
return name.substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from './route';
|
||||
export * from './assets';
|
||||
export * from './api';
|
||||
// export * from './helper';
|
||||
export * from './helper';
|
||||
|
||||
@ -17,6 +17,9 @@ export const route = {
|
||||
// Sub screens
|
||||
addLead: 'addLead',
|
||||
leadDetails: 'leadDetails',
|
||||
leadProposals: 'leadProposals',
|
||||
leadTasks: 'leadTasks',
|
||||
leadNotes: 'leadNotes',
|
||||
profile: 'profile',
|
||||
} as const;
|
||||
|
||||
@ -36,6 +39,9 @@ export type RouteParams = {
|
||||
tickets: undefined;
|
||||
addLead: undefined;
|
||||
leadDetails: { lead: any };
|
||||
leadProposals: { leadId: string };
|
||||
leadTasks: { leadId: string };
|
||||
leadNotes: { leadId: string };
|
||||
profile: undefined;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user