feat(app): add, proposal, note desing

This commit is contained in:
uttam 2026-07-16 20:04:40 +05:30
parent 2a8c5f7e60
commit 395bed3a9c
42 changed files with 1545 additions and 124 deletions

View File

@ -0,0 +1,7 @@
export interface ActionButtonProps {
label: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
loading?: boolean;
disabled?: boolean;
}

View 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',
},
});

View 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>
);
};

View File

@ -0,0 +1,2 @@
export * from './actionButton';
export * from './actionButton.props';

View File

@ -0,0 +1,7 @@
export interface ActionCardProps {
title: string;
icon: string;
count?: number;
onAddPress: () => void;
onViewAllPress: () => void;
}

View 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',
},
});

View 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>
);
};

View File

@ -0,0 +1,2 @@
export * from './actionCard';
export * from './actionCard.props';

View File

@ -0,0 +1,4 @@
export interface AddItemButtonProps {
label?: string;
onPress: () => void;
}

View 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',
},
});

View 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>
);
};

View File

@ -0,0 +1,2 @@
export * from './addItemButton';
export * from './addItemButton.props';

View 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;
}

View 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',
},
});

View 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>
);
};

View File

@ -0,0 +1,2 @@
export * from './checkboxWithLabel';
export * from './checkboxWithLabel.props';

View 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;
}

View 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',
},
});

View 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>
);
};

View File

@ -0,0 +1,2 @@
export * from './formInput';
export * from './formInput.props';

View 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;
}

View 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,
},
});

View 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>
);
};

View File

@ -0,0 +1,2 @@
export * from './formPicker';
export * from './formPicker.props';

View File

@ -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';

View File

@ -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>

View File

@ -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';

View File

@ -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>
);
};

View File

@ -10,6 +10,9 @@ export const getStyles = (colors: ThemeColors) =>
scrollContent: {
padding: 16,
},
cardsContainer: {
marginTop: 16,
},
header: {
alignItems: 'center',
paddingVertical: 24,

View File

@ -0,0 +1 @@
export * from './leadNotes.screen'

View 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>
);
};

View 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,
},
});

View File

@ -0,0 +1 @@
export * from './leadProposals.screen';

View 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>
);
};

View 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',
},
});

View File

@ -0,0 +1 @@
export * from './leadTasks.screen';

View 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>
);
};

View 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,
},
});

View File

@ -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>
);
};

View File

@ -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();
};

View File

@ -1,4 +1,4 @@
export * from './route';
export * from './assets';
export * from './api';
// export * from './helper';
export * from './helper';

View File

@ -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;
};