From 395bed3a9cc3c49793c087f7527344768c929bc1 Mon Sep 17 00:00:00 2001 From: uttam Date: Thu, 16 Jul 2026 20:04:40 +0530 Subject: [PATCH] feat(app): add, proposal, note desing --- .../actionButton/actionButton.props.ts | 7 + .../actionButton/actionButton.styles.ts | 31 ++ app/components/actionButton/actionButton.tsx | 29 ++ app/components/actionButton/index.ts | 2 + app/components/actionCard/actionCard.props.ts | 7 + .../actionCard/actionCard.styles.ts | 79 +++++ app/components/actionCard/actionCard.tsx | 59 ++++ app/components/actionCard/index.ts | 2 + .../addItemButton/addItemButton.props.ts | 4 + .../addItemButton/addItemButton.styles.ts | 21 ++ .../addItemButton/addItemButton.tsx | 21 ++ app/components/addItemButton/index.ts | 2 + .../checkboxWithLabel.props.ts | 12 + .../checkboxWithLabel.styles.ts | 44 +++ .../checkboxWithLabel/checkboxWithLabel.tsx | 63 ++++ app/components/checkboxWithLabel/index.ts | 2 + app/components/formInput/formInput.props.ts | 10 + app/components/formInput/formInput.styles.ts | 33 ++ app/components/formInput/formInput.tsx | 39 +++ app/components/formInput/index.ts | 2 + app/components/formPicker/formPicker.props.ts | 13 + .../formPicker/formPicker.styles.ts | 37 +++ app/components/formPicker/formPicker.tsx | 44 +++ app/components/formPicker/index.ts | 2 + app/components/index.ts | 6 + app/components/leadItemCard/leadItemCard.tsx | 39 +-- app/features/index.ts | 3 + .../leadDetails/leadDetails.screen.tsx | 96 ++++-- .../leadDetails/leadDetails.styles.ts | 3 + app/features/leadNotes/index.ts | 1 + app/features/leadNotes/leadNotes.screen.tsx | 49 +++ app/features/leadNotes/leadNotes.styles.ts | 29 ++ app/features/leadProposals/index.ts | 1 + .../leadProposals/leadProposals.screen.tsx | 293 ++++++++++++++++++ .../leadProposals/leadProposals.styles.ts | 190 ++++++++++++ app/features/leadTasks/index.ts | 1 + app/features/leadTasks/leadTasks.screen.tsx | 198 ++++++++++++ app/features/leadTasks/leadTasks.styles.ts | 60 ++++ app/navigation/leadsStack.tsx | 28 +- app/utils/helper.ts | 99 +++--- app/utils/index.ts | 2 +- app/utils/route.ts | 6 + 42 files changed, 1545 insertions(+), 124 deletions(-) create mode 100644 app/components/actionButton/actionButton.props.ts create mode 100644 app/components/actionButton/actionButton.styles.ts create mode 100644 app/components/actionButton/actionButton.tsx create mode 100644 app/components/actionButton/index.ts create mode 100644 app/components/actionCard/actionCard.props.ts create mode 100644 app/components/actionCard/actionCard.styles.ts create mode 100644 app/components/actionCard/actionCard.tsx create mode 100644 app/components/actionCard/index.ts create mode 100644 app/components/addItemButton/addItemButton.props.ts create mode 100644 app/components/addItemButton/addItemButton.styles.ts create mode 100644 app/components/addItemButton/addItemButton.tsx create mode 100644 app/components/addItemButton/index.ts create mode 100644 app/components/checkboxWithLabel/checkboxWithLabel.props.ts create mode 100644 app/components/checkboxWithLabel/checkboxWithLabel.styles.ts create mode 100644 app/components/checkboxWithLabel/checkboxWithLabel.tsx create mode 100644 app/components/checkboxWithLabel/index.ts create mode 100644 app/components/formInput/formInput.props.ts create mode 100644 app/components/formInput/formInput.styles.ts create mode 100644 app/components/formInput/formInput.tsx create mode 100644 app/components/formInput/index.ts create mode 100644 app/components/formPicker/formPicker.props.ts create mode 100644 app/components/formPicker/formPicker.styles.ts create mode 100644 app/components/formPicker/formPicker.tsx create mode 100644 app/components/formPicker/index.ts create mode 100644 app/features/leadNotes/index.ts create mode 100644 app/features/leadNotes/leadNotes.screen.tsx create mode 100644 app/features/leadNotes/leadNotes.styles.ts create mode 100644 app/features/leadProposals/index.ts create mode 100644 app/features/leadProposals/leadProposals.screen.tsx create mode 100644 app/features/leadProposals/leadProposals.styles.ts create mode 100644 app/features/leadTasks/index.ts create mode 100644 app/features/leadTasks/leadTasks.screen.tsx create mode 100644 app/features/leadTasks/leadTasks.styles.ts diff --git a/app/components/actionButton/actionButton.props.ts b/app/components/actionButton/actionButton.props.ts new file mode 100644 index 0000000..9d39600 --- /dev/null +++ b/app/components/actionButton/actionButton.props.ts @@ -0,0 +1,7 @@ +export interface ActionButtonProps { + label: string; + onPress: () => void; + variant?: 'primary' | 'secondary'; + loading?: boolean; + disabled?: boolean; +} diff --git a/app/components/actionButton/actionButton.styles.ts b/app/components/actionButton/actionButton.styles.ts new file mode 100644 index 0000000..aadb020 --- /dev/null +++ b/app/components/actionButton/actionButton.styles.ts @@ -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', + }, + }); diff --git a/app/components/actionButton/actionButton.tsx b/app/components/actionButton/actionButton.tsx new file mode 100644 index 0000000..211d1e0 --- /dev/null +++ b/app/components/actionButton/actionButton.tsx @@ -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 = ({ + 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 ( + + {label} + + ); +}; diff --git a/app/components/actionButton/index.ts b/app/components/actionButton/index.ts new file mode 100644 index 0000000..32555e3 --- /dev/null +++ b/app/components/actionButton/index.ts @@ -0,0 +1,2 @@ +export * from './actionButton'; +export * from './actionButton.props'; diff --git a/app/components/actionCard/actionCard.props.ts b/app/components/actionCard/actionCard.props.ts new file mode 100644 index 0000000..5c946f8 --- /dev/null +++ b/app/components/actionCard/actionCard.props.ts @@ -0,0 +1,7 @@ +export interface ActionCardProps { + title: string; + icon: string; + count?: number; + onAddPress: () => void; + onViewAllPress: () => void; +} diff --git a/app/components/actionCard/actionCard.styles.ts b/app/components/actionCard/actionCard.styles.ts new file mode 100644 index 0000000..8954a99 --- /dev/null +++ b/app/components/actionCard/actionCard.styles.ts @@ -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', + }, + }); diff --git a/app/components/actionCard/actionCard.tsx b/app/components/actionCard/actionCard.tsx new file mode 100644 index 0000000..89b9a46 --- /dev/null +++ b/app/components/actionCard/actionCard.tsx @@ -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 = ({ + title, + icon, + count, + onAddPress, + onViewAllPress, +}) => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + + return ( + + {/* Header with Icon and Title */} + + + + + + + {title} + {count !== undefined && ( + + {count} {count === 1 ? 'item' : 'items'} + + )} + + + + + {/* Action Buttons */} + + + + + Add + + + + + + View All + + + + ); +}; diff --git a/app/components/actionCard/index.ts b/app/components/actionCard/index.ts new file mode 100644 index 0000000..7080674 --- /dev/null +++ b/app/components/actionCard/index.ts @@ -0,0 +1,2 @@ +export * from './actionCard'; +export * from './actionCard.props'; diff --git a/app/components/addItemButton/addItemButton.props.ts b/app/components/addItemButton/addItemButton.props.ts new file mode 100644 index 0000000..04e7764 --- /dev/null +++ b/app/components/addItemButton/addItemButton.props.ts @@ -0,0 +1,4 @@ +export interface AddItemButtonProps { + label?: string; + onPress: () => void; +} diff --git a/app/components/addItemButton/addItemButton.styles.ts b/app/components/addItemButton/addItemButton.styles.ts new file mode 100644 index 0000000..25ac3cc --- /dev/null +++ b/app/components/addItemButton/addItemButton.styles.ts @@ -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', + }, + }); diff --git a/app/components/addItemButton/addItemButton.tsx b/app/components/addItemButton/addItemButton.tsx new file mode 100644 index 0000000..736a376 --- /dev/null +++ b/app/components/addItemButton/addItemButton.tsx @@ -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 = ({ + label = 'Add Item', + onPress, +}) => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + + return ( + + + {label} + + ); +}; diff --git a/app/components/addItemButton/index.ts b/app/components/addItemButton/index.ts new file mode 100644 index 0000000..c55447f --- /dev/null +++ b/app/components/addItemButton/index.ts @@ -0,0 +1,2 @@ +export * from './addItemButton'; +export * from './addItemButton.props'; diff --git a/app/components/checkboxWithLabel/checkboxWithLabel.props.ts b/app/components/checkboxWithLabel/checkboxWithLabel.props.ts new file mode 100644 index 0000000..4a3e1cb --- /dev/null +++ b/app/components/checkboxWithLabel/checkboxWithLabel.props.ts @@ -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; +} diff --git a/app/components/checkboxWithLabel/checkboxWithLabel.styles.ts b/app/components/checkboxWithLabel/checkboxWithLabel.styles.ts new file mode 100644 index 0000000..f61c1fc --- /dev/null +++ b/app/components/checkboxWithLabel/checkboxWithLabel.styles.ts @@ -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', + }, + }); diff --git a/app/components/checkboxWithLabel/checkboxWithLabel.tsx b/app/components/checkboxWithLabel/checkboxWithLabel.tsx new file mode 100644 index 0000000..e3d65a6 --- /dev/null +++ b/app/components/checkboxWithLabel/checkboxWithLabel.tsx @@ -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 = ({ + 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 ( + + {label && {label}} + + {options.map((option) => ( + handlePress(option.id)} + activeOpacity={0.7} + > + + {isSelected(option.id) && ( + + )} + + {option.label} + + ))} + + + ); +}; diff --git a/app/components/checkboxWithLabel/index.ts b/app/components/checkboxWithLabel/index.ts new file mode 100644 index 0000000..d1915e3 --- /dev/null +++ b/app/components/checkboxWithLabel/index.ts @@ -0,0 +1,2 @@ +export * from './checkboxWithLabel'; +export * from './checkboxWithLabel.props'; diff --git a/app/components/formInput/formInput.props.ts b/app/components/formInput/formInput.props.ts new file mode 100644 index 0000000..7068b9f --- /dev/null +++ b/app/components/formInput/formInput.props.ts @@ -0,0 +1,10 @@ +import { TextInputProps } from 'react-native'; + +export interface FormInputProps extends Omit { + label: string; + value: string; + onChangeText: (text: string) => void; + placeholder?: string; + required?: boolean; + multiline?: boolean; +} diff --git a/app/components/formInput/formInput.styles.ts b/app/components/formInput/formInput.styles.ts new file mode 100644 index 0000000..d0f78a9 --- /dev/null +++ b/app/components/formInput/formInput.styles.ts @@ -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', + }, + }); diff --git a/app/components/formInput/formInput.tsx b/app/components/formInput/formInput.tsx new file mode 100644 index 0000000..100ba53 --- /dev/null +++ b/app/components/formInput/formInput.tsx @@ -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 = ({ + label, + value, + onChangeText, + placeholder, + required = false, + multiline = false, + keyboardType = 'default', + ...rest +}) => { + const { theme: colors } = useTheme(); + const styles = getStyles(colors); + + return ( + + + {label} + {required && *} + + + + ); +}; diff --git a/app/components/formInput/index.ts b/app/components/formInput/index.ts new file mode 100644 index 0000000..b711c0b --- /dev/null +++ b/app/components/formInput/index.ts @@ -0,0 +1,2 @@ +export * from './formInput'; +export * from './formInput.props'; diff --git a/app/components/formPicker/formPicker.props.ts b/app/components/formPicker/formPicker.props.ts new file mode 100644 index 0000000..4cd4f5e --- /dev/null +++ b/app/components/formPicker/formPicker.props.ts @@ -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; +} diff --git a/app/components/formPicker/formPicker.styles.ts b/app/components/formPicker/formPicker.styles.ts new file mode 100644 index 0000000..1dec404 --- /dev/null +++ b/app/components/formPicker/formPicker.styles.ts @@ -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, + }, + }); diff --git a/app/components/formPicker/formPicker.tsx b/app/components/formPicker/formPicker.tsx new file mode 100644 index 0000000..2ef7461 --- /dev/null +++ b/app/components/formPicker/formPicker.tsx @@ -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 = ({ + 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 ( + + + {label} + {required && *} + + { + // TODO: Implement modal picker or action sheet + console.log('Picker pressed'); + }}> + + {selectedOption?.label || placeholder} + + + + + ); +}; diff --git a/app/components/formPicker/index.ts b/app/components/formPicker/index.ts new file mode 100644 index 0000000..aa7d2b4 --- /dev/null +++ b/app/components/formPicker/index.ts @@ -0,0 +1,2 @@ +export * from './formPicker'; +export * from './formPicker.props'; diff --git a/app/components/index.ts b/app/components/index.ts index c456306..fa09fb6 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -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'; diff --git a/app/components/leadItemCard/leadItemCard.tsx b/app/components/leadItemCard/leadItemCard.tsx index eb9f3d5..9f76af7 100644 --- a/app/components/leadItemCard/leadItemCard.tsx +++ b/app/components/leadItemCard/leadItemCard.tsx @@ -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 = ({ item, @@ -18,38 +17,6 @@ export const LeadItemCard: React.FC = ({ 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 ( = ({ handleEmailPress(item.email)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> @@ -105,7 +72,7 @@ export const LeadItemCard: React.FC = ({ handlePhonePress(item.phonenumber)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> diff --git a/app/features/index.ts b/app/features/index.ts index 2a610ce..9b9cc91 100644 --- a/app/features/index.ts +++ b/app/features/index.ts @@ -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'; diff --git a/app/features/leadDetails/leadDetails.screen.tsx b/app/features/leadDetails/leadDetails.screen.tsx index 84236a3..306aaca 100644 --- a/app/features/leadDetails/leadDetails.screen.tsx +++ b/app/features/leadDetails/leadDetails.screen.tsx @@ -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; +type LeadDetailsNavigationProp = NativeStackNavigationProp< + LeadsStackParamList, + 'leadDetails' +>; export const LeadDetailsScreen = () => { const route = useRoute(); + const navigation = useNavigation(); 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 ( {/* 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 */} + + + + + + + ); }; diff --git a/app/features/leadDetails/leadDetails.styles.ts b/app/features/leadDetails/leadDetails.styles.ts index 2772fcc..70ad542 100644 --- a/app/features/leadDetails/leadDetails.styles.ts +++ b/app/features/leadDetails/leadDetails.styles.ts @@ -10,6 +10,9 @@ export const getStyles = (colors: ThemeColors) => scrollContent: { padding: 16, }, + cardsContainer: { + marginTop: 16, + }, header: { alignItems: 'center', paddingVertical: 24, diff --git a/app/features/leadNotes/index.ts b/app/features/leadNotes/index.ts new file mode 100644 index 0000000..4674f23 --- /dev/null +++ b/app/features/leadNotes/index.ts @@ -0,0 +1 @@ +export * from './leadNotes.screen' diff --git a/app/features/leadNotes/leadNotes.screen.tsx b/app/features/leadNotes/leadNotes.screen.tsx new file mode 100644 index 0000000..1e4a906 --- /dev/null +++ b/app/features/leadNotes/leadNotes.screen.tsx @@ -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; + +export const LeadNotesScreen = () => { + const route = useRoute(); + 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 ( + + + + + + + + ); +}; diff --git a/app/features/leadNotes/leadNotes.styles.ts b/app/features/leadNotes/leadNotes.styles.ts new file mode 100644 index 0000000..779c6b5 --- /dev/null +++ b/app/features/leadNotes/leadNotes.styles.ts @@ -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, + }, + }); diff --git a/app/features/leadProposals/index.ts b/app/features/leadProposals/index.ts new file mode 100644 index 0000000..0e3113d --- /dev/null +++ b/app/features/leadProposals/index.ts @@ -0,0 +1 @@ +export * from './leadProposals.screen'; \ No newline at end of file diff --git a/app/features/leadProposals/leadProposals.screen.tsx b/app/features/leadProposals/leadProposals.screen.tsx new file mode 100644 index 0000000..09f66dd --- /dev/null +++ b/app/features/leadProposals/leadProposals.screen.tsx @@ -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; + +export const LeadProposalsScreen = () => { + const route = useRoute(); + 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 ( + + {/* Form Fields - Step 1 */} + + Basic Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Items Section */} + + Items + + + + + + + + + + + + + + + + + + + + + + {/* Calculation Section */} + + Summary + + + Sub Total: + 0.0 + + + + Discount: + + + + {discountUnit} + + + + 0.00 + + + + Adjustment: + + 0 + + + + Total: + 0.00 + + + + + + + + ); +}; diff --git a/app/features/leadProposals/leadProposals.styles.ts b/app/features/leadProposals/leadProposals.styles.ts new file mode 100644 index 0000000..aff5600 --- /dev/null +++ b/app/features/leadProposals/leadProposals.styles.ts @@ -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', + }, + }); diff --git a/app/features/leadTasks/index.ts b/app/features/leadTasks/index.ts new file mode 100644 index 0000000..98de156 --- /dev/null +++ b/app/features/leadTasks/index.ts @@ -0,0 +1 @@ +export * from './leadTasks.screen'; diff --git a/app/features/leadTasks/leadTasks.screen.tsx b/app/features/leadTasks/leadTasks.screen.tsx new file mode 100644 index 0000000..4dbb075 --- /dev/null +++ b/app/features/leadTasks/leadTasks.screen.tsx @@ -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; + +export const LeadTasksScreen = () => { + const route = useRoute(); + 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(['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 ( + + + {/* Public or Billable */} + + + {/* Attachment */} + + + {/* Subject */} + + + {/* Hourly Rate */} + + + {/* Start Date and Due Date */} + + + + + + + + + + {/* Priority */} + + + {/* Repeat every */} + + + {/* Related To */} + + + {/* Lead */} + + + {/* Assignees */} + + + {/* Followers */} + + + {/* Task Description */} + + + {/* Save Button */} + + + + + + ); +}; diff --git a/app/features/leadTasks/leadTasks.styles.ts b/app/features/leadTasks/leadTasks.styles.ts new file mode 100644 index 0000000..ac3df8a --- /dev/null +++ b/app/features/leadTasks/leadTasks.styles.ts @@ -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, + }, + }); diff --git a/app/navigation/leadsStack.tsx b/app/navigation/leadsStack.tsx index fb30bc8..458187f 100644 --- a/app/navigation/leadsStack.tsx +++ b/app/navigation/leadsStack.tsx @@ -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; +export type LeadsStackParamList = Pick< + RouteParams, + 'leads' | 'leadDetails' | 'leadProposals' | 'leadTasks' | 'leadNotes' +>; const Stack = createNativeStackNavigator(); @@ -35,6 +44,21 @@ export const LeadsStack = () => { component={LeadDetailsScreen} options={{ headerTitle: 'Lead Details' }} /> + + + ); }; diff --git a/app/utils/helper.ts b/app/utils/helper.ts index 79a6178..19bc377 100644 --- a/app/utils/helper.ts +++ b/app/utils/helper.ts @@ -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(); +}; diff --git a/app/utils/index.ts b/app/utils/index.ts index c0dffd7..9a0bc25 100644 --- a/app/utils/index.ts +++ b/app/utils/index.ts @@ -1,4 +1,4 @@ export * from './route'; export * from './assets'; export * from './api'; -// export * from './helper'; +export * from './helper'; diff --git a/app/utils/route.ts b/app/utils/route.ts index 5f26dcf..2b3bf22 100644 --- a/app/utils/route.ts +++ b/app/utils/route.ts @@ -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; };