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 './searchInput';
|
||||||
export * from './loader';
|
export * from './loader';
|
||||||
export * from './leadDetailHeader';
|
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,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Linking,
|
|
||||||
Platform,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Icon from 'react-native-vector-icons/Ionicons';
|
import Icon from 'react-native-vector-icons/Ionicons';
|
||||||
import { useTheme } from '@theme';
|
import { useTheme } from '@theme';
|
||||||
import { getStyles } from './leadItemCard.styles';
|
import { getStyles } from './leadItemCard.styles';
|
||||||
import { LeadItemCardProps } from './leadItemCard.props';
|
import { LeadItemCardProps } from './leadItemCard.props';
|
||||||
|
import { getInitials, handleEmailPress, handlePhonePress } from '@utils';
|
||||||
|
|
||||||
export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
||||||
item,
|
item,
|
||||||
@ -18,38 +17,6 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
|||||||
const { theme: colors } = useTheme();
|
const { theme: colors } = useTheme();
|
||||||
const styles = getStyles(colors);
|
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 (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.card}
|
style={styles.card}
|
||||||
@ -90,7 +57,7 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.contactIconButton}
|
style={styles.contactIconButton}
|
||||||
onPress={handleEmailPress}
|
onPress={() => handleEmailPress(item.email)}
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
<Icon name="mail" size={20} color={colors.icon} />
|
<Icon name="mail" size={20} color={colors.icon} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -105,7 +72,7 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.contactIconButton}
|
style={styles.contactIconButton}
|
||||||
onPress={handlePhonePress}
|
onPress={() => handlePhonePress(item.phonenumber)}
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
||||||
<Icon name="call" size={20} color={colors.icon} />
|
<Icon name="call" size={20} color={colors.icon} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@ -5,6 +5,9 @@ export * from './estimates';
|
|||||||
export * from './invoices';
|
export * from './invoices';
|
||||||
export * from './leads';
|
export * from './leads';
|
||||||
export * from './leadDetails';
|
export * from './leadDetails';
|
||||||
|
export * from './leadProposals';
|
||||||
|
export * from './leadTasks';
|
||||||
|
export * from './leadNotes';
|
||||||
export * from './login';
|
export * from './login';
|
||||||
export * from './profile';
|
export * from './profile';
|
||||||
export * from './projects';
|
export * from './projects';
|
||||||
|
|||||||
@ -6,19 +6,26 @@ import {
|
|||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} 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 { useTheme } from '@theme';
|
||||||
import { useAppDispatch, useAppSelector, RootState } from '@store';
|
import { useAppDispatch, useAppSelector, RootState } from '@store';
|
||||||
import { getLeadDetails } from './thunk';
|
import { getLeadDetails } from './thunk';
|
||||||
import { getStyles } from './leadDetails.styles';
|
import { getStyles } from './leadDetails.styles';
|
||||||
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
import { LeadsStackParamList } from '../../navigation/leadsStack';
|
||||||
import { Loader } from '@components';
|
import { Loader, LeadDetailHeader, ActionCard } from '@components';
|
||||||
import { LeadDetailHeader } from '../../components/leadDetailHeader/leadDetailHeader';
|
import { handleEmailPress, handlePhonePress } from '@utils';
|
||||||
|
import { route as routes } from '@utils';
|
||||||
|
|
||||||
type LeadDetailsRouteProp = RouteProp<LeadsStackParamList, 'leadDetails'>;
|
type LeadDetailsRouteProp = RouteProp<LeadsStackParamList, 'leadDetails'>;
|
||||||
|
type LeadDetailsNavigationProp = NativeStackNavigationProp<
|
||||||
|
LeadsStackParamList,
|
||||||
|
'leadDetails'
|
||||||
|
>;
|
||||||
|
|
||||||
export const LeadDetailsScreen = () => {
|
export const LeadDetailsScreen = () => {
|
||||||
const route = useRoute<LeadDetailsRouteProp>();
|
const route = useRoute<LeadDetailsRouteProp>();
|
||||||
|
const navigation = useNavigation<LeadDetailsNavigationProp>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { theme: colors } = useTheme();
|
const { theme: colors } = useTheme();
|
||||||
const styles = getStyles(colors);
|
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)
|
// Handle status press (for future status change functionality)
|
||||||
const handleStatusPress = () => {
|
const handleStatusPress = () => {
|
||||||
// TODO: Show status list modal/picker
|
// TODO: Show status list modal/picker
|
||||||
console.log('Status pressed - show status list');
|
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 (
|
return (
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
<ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
|
||||||
{/* Header with Avatar */}
|
{/* Header with Avatar */}
|
||||||
@ -104,9 +119,36 @@ export const LeadDetailsScreen = () => {
|
|||||||
statusName={lead.status_name || lead.status}
|
statusName={lead.status_name || lead.status}
|
||||||
statusColor={lead.color}
|
statusColor={lead.color}
|
||||||
onStatusPress={handleStatusPress}
|
onStatusPress={handleStatusPress}
|
||||||
onEmailPress={handleEmailPress}
|
onEmailPress={() => handleEmailPress(lead.email)}
|
||||||
onPhonePress={handlePhonePress}
|
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>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,9 @@ export const getStyles = (colors: ThemeColors) =>
|
|||||||
scrollContent: {
|
scrollContent: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
|
cardsContainer: {
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 24,
|
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 React from 'react';
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
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 { route, RouteParams } from '@utils';
|
||||||
import { useTheme } from '@theme';
|
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>();
|
const Stack = createNativeStackNavigator<LeadsStackParamList>();
|
||||||
|
|
||||||
@ -35,6 +44,21 @@ export const LeadsStack = () => {
|
|||||||
component={LeadDetailsScreen}
|
component={LeadDetailsScreen}
|
||||||
options={{ headerTitle: 'Lead Details' }}
|
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>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,70 +1,53 @@
|
|||||||
// import { Linking, Platform } from 'react-native';
|
import { Linking, Platform } from 'react-native';
|
||||||
|
|
||||||
// /**
|
export const handleEmailPress = (email?: string | null) => {
|
||||||
// * Opens the default email client with the provided email address
|
if (!email) {
|
||||||
// * @param email - The email address to send to
|
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 =>
|
Linking.openURL(`mailto:${email}`).catch(err =>
|
||||||
// console.error('Error opening email:', err),
|
console.error('Error opening email:', err),
|
||||||
// );
|
);
|
||||||
// };
|
};
|
||||||
|
|
||||||
// /**
|
export const handlePhonePress = (phoneNumber?: string | null) => {
|
||||||
// * Opens the phone dialer with the provided phone number
|
if (!phoneNumber) {
|
||||||
// * @param phoneNumber - The phone number to call
|
console.warn('No phone number provided');
|
||||||
// */
|
return;
|
||||||
// export const handlePhonePress = (phoneNumber?: string | null) => {
|
}
|
||||||
// if (!phoneNumber) {
|
|
||||||
// console.warn('No phone number provided');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const phoneUrl =
|
const phoneUrl =
|
||||||
// Platform.OS === 'ios'
|
Platform.OS === 'ios'
|
||||||
// ? `telprompt:${phoneNumber}`
|
? `telprompt:${phoneNumber}`
|
||||||
// : `tel:${phoneNumber}`;
|
: `tel:${phoneNumber}`;
|
||||||
|
|
||||||
// Linking.openURL(phoneUrl).catch(err =>
|
Linking.openURL(phoneUrl).catch(err =>
|
||||||
// console.error('Error opening phone:', err),
|
console.error('Error opening phone:', err),
|
||||||
// );
|
);
|
||||||
// };
|
};
|
||||||
|
|
||||||
// /**
|
export const handleWebsitePress = (website?: string | null) => {
|
||||||
// * Opens the browser with the provided website URL
|
if (!website) {
|
||||||
// * @param website - The website URL to open
|
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 =>
|
Linking.openURL(url).catch(err =>
|
||||||
// console.error('Error opening website:', err),
|
console.error('Error opening website:', err),
|
||||||
// );
|
);
|
||||||
// };
|
};
|
||||||
|
|
||||||
// /**
|
export const getInitials = (name?: string | null): string => {
|
||||||
// * Generates initials from a full name
|
if (!name) return '?';
|
||||||
// * @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(' ');
|
const nameParts = name.trim().split(' ');
|
||||||
|
|
||||||
// if (nameParts.length >= 2) {
|
if (nameParts.length >= 2) {
|
||||||
// return (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase();
|
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 './route';
|
||||||
export * from './assets';
|
export * from './assets';
|
||||||
export * from './api';
|
export * from './api';
|
||||||
// export * from './helper';
|
export * from './helper';
|
||||||
|
|||||||
@ -17,6 +17,9 @@ export const route = {
|
|||||||
// Sub screens
|
// Sub screens
|
||||||
addLead: 'addLead',
|
addLead: 'addLead',
|
||||||
leadDetails: 'leadDetails',
|
leadDetails: 'leadDetails',
|
||||||
|
leadProposals: 'leadProposals',
|
||||||
|
leadTasks: 'leadTasks',
|
||||||
|
leadNotes: 'leadNotes',
|
||||||
profile: 'profile',
|
profile: 'profile',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -36,6 +39,9 @@ export type RouteParams = {
|
|||||||
tickets: undefined;
|
tickets: undefined;
|
||||||
addLead: undefined;
|
addLead: undefined;
|
||||||
leadDetails: { lead: any };
|
leadDetails: { lead: any };
|
||||||
|
leadProposals: { leadId: string };
|
||||||
|
leadTasks: { leadId: string };
|
||||||
|
leadNotes: { leadId: string };
|
||||||
profile: undefined;
|
profile: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user