From cf82850d1f37edcfbc914255c6b67ae36371ba67 Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Wed, 2 Sep 2026 18:30:45 +0530 Subject: [PATCH] feat: implement full caregiver application feature set including services, availability, profile, and navigation modules --- App.tsx | 5 +- src/components/ui/Button.tsx | 8 +- src/components/ui/Dropdown.tsx | 153 +++++++ src/components/ui/Header.tsx | 2 - src/context/SnackbarContext.tsx | 137 ++++++ src/navigation/AppNavigator.tsx | 39 +- src/navigation/navigationRef.ts | 10 + src/screens/AddAvailabilityScreen.tsx | 15 +- ...en.tsx => AddEditPaymentProfileScreen.tsx} | 100 ++--- src/screens/AddEditServiceScreen.tsx | 404 ++++-------------- src/screens/AddSkillScreen.tsx | 15 +- src/screens/AvailabilityListScreen.tsx | 7 +- src/screens/EditAvailabilityScreen.tsx | 16 +- src/screens/HomeScreen.tsx | 61 ++- src/screens/ManageServicesScreen.tsx | 11 +- src/screens/ManageSkillsScreen.tsx | 47 +- src/screens/PaymentProfileListScreen.tsx | 234 ++++++++++ src/screens/PersonalInformationScreen.tsx | 10 +- src/screens/ProfessionalDocumentsScreen.tsx | 155 +++++++ src/screens/SelectionScreen.tsx | 207 +++++++++ src/screens/SettingsScreen.tsx | 21 +- src/screens/UploadDocumentScreen.tsx | 158 +++++++ src/screens/ViewDocumentScreen.tsx | 104 +++++ src/services/apiClient.ts | 15 +- src/services/documentService.ts | 58 +++ src/services/profileService.ts | 17 + src/services/serviceService.ts | 40 +- src/utils/formatters.ts | 14 +- 28 files changed, 1592 insertions(+), 471 deletions(-) create mode 100644 src/components/ui/Dropdown.tsx create mode 100644 src/context/SnackbarContext.tsx create mode 100644 src/navigation/navigationRef.ts rename src/screens/{PaymentProfileScreen.tsx => AddEditPaymentProfileScreen.tsx} (67%) create mode 100644 src/screens/PaymentProfileListScreen.tsx create mode 100644 src/screens/ProfessionalDocumentsScreen.tsx create mode 100644 src/screens/SelectionScreen.tsx create mode 100644 src/screens/UploadDocumentScreen.tsx create mode 100644 src/screens/ViewDocumentScreen.tsx create mode 100644 src/services/documentService.ts diff --git a/App.tsx b/App.tsx index d45bedd..a8caa06 100644 --- a/App.tsx +++ b/App.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { AppNavigator } from './src/navigation/AppNavigator'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { SnackbarProvider } from './src/context/SnackbarContext'; import { LogBox, StatusBar } from 'react-native'; // LogBox.ignoreAllLogs(true); @@ -16,7 +17,9 @@ function App(): React.JSX.Element { return ( - + + + ); } diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 0db643d..a633cde 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -57,7 +57,7 @@ export const Button: React.FC = ({ {...props} > {loading ? ( - + ) : ( {title} @@ -80,14 +80,14 @@ const styles = StyleSheet.create({ backgroundColor: '#D1D5DB', }, bgPrimary: { - backgroundColor: '#4F46E5', + backgroundColor: '#1E3A8A', }, bgSecondary: { backgroundColor: '#10B981', }, bgOutline: { backgroundColor: 'transparent', - borderColor: '#4F46E5', + borderColor: '#1E3A8A', borderWidth: 2, }, bgGhost: { @@ -100,6 +100,6 @@ const styles = StyleSheet.create({ color: '#FFFFFF', }, textPrimary: { - color: '#4F46E5', + color: '#1E3A8A', }, }); diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx new file mode 100644 index 0000000..d22b641 --- /dev/null +++ b/src/components/ui/Dropdown.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, +} from 'react-native'; +import { ChevronDown } from 'lucide-react-native'; +import { Typography } from './Typography'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../../navigation/AppNavigator'; + +export interface DropdownItem { + label: string; + value: string | number; +} + +interface DropdownProps { + data: DropdownItem[]; + value?: string | number | (string | number)[]; + onChange: (value: any) => void; + placeholder?: string; + searchPlaceholder?: string; + isMulti?: boolean; + searchable?: boolean; + label?: string; + error?: string; + disabled?: boolean; +} + +export const Dropdown: React.FC = ({ + data, + value, + onChange, + placeholder = 'Select an option', + searchPlaceholder = 'Search...', + isMulti = false, + searchable = true, + label, + error, + disabled = false, +}) => { + const navigation = useNavigation>(); + + const handlePress = () => { + if (disabled) return; + + navigation.navigate('Selection', { + data, + value, + isMulti, + searchable, + label, + searchPlaceholder, + onSelect: onChange, + }); + }; + + const renderSelectedItemText = () => { + if (isMulti) { + const currentValue = (Array.isArray(value) ? value : []) as (string | number)[]; + if (currentValue.length === 0) return placeholder; + + const selectedItems = data.filter((item) => currentValue.includes(item.value)); + return selectedItems.map((item) => item.label).join(', '); + } else { + if (value == null || value === '') return placeholder; + const selectedItem = data.find((item) => item.value === value); + return selectedItem ? selectedItem.label : placeholder; + } + }; + + return ( + + {label && ( + + {label} + + )} + + + + {renderSelectedItemText()} + + + + + {error ? ( + + {error} + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + width: '100%', + }, + label: { + marginBottom: 8, + color: '#374151', + }, + dropdownButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: '#FFFFFF', + borderWidth: 1, + borderColor: '#D1D5DB', + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 12, + minHeight: 52, + }, + dropdownButtonError: { + borderColor: '#EF4444', + }, + dropdownButtonDisabled: { + backgroundColor: '#F3F4F6', + borderColor: '#E5E7EB', + }, + dropdownButtonText: { + flex: 1, + fontSize: 16, + color: '#111827', + marginRight: 8, + }, + placeholderText: { + color: '#9CA3AF', + }, + errorText: { + color: '#EF4444', + marginTop: 4, + }, +}); diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index 47c99a8..9a4c541 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -62,8 +62,6 @@ const styles = StyleSheet.create({ marginRight: 12, width: 40, height: 40, - borderRadius: 20, - backgroundColor: 'rgba(255,255,255,0.2)', alignItems: 'center', justifyContent: 'center', }, diff --git a/src/context/SnackbarContext.tsx b/src/context/SnackbarContext.tsx new file mode 100644 index 0000000..3fd5147 --- /dev/null +++ b/src/context/SnackbarContext.tsx @@ -0,0 +1,137 @@ +import React, { createContext, useContext, useState, useRef, ReactNode, useEffect } from 'react'; +import { View, Text, Animated, StyleSheet, TouchableOpacity } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { THEME } from '../constants/theme'; +import { CheckCircle2, AlertCircle, X } from 'lucide-react-native'; + +type SnackbarType = 'success' | 'error'; + +interface SnackbarContextProps { + showSnackbar: (message: string, type?: SnackbarType) => void; +} + +const SnackbarContext = createContext(undefined); + +export const useSnackbar = () => { + const context = useContext(SnackbarContext); + if (!context) { + throw new Error('useSnackbar must be used within a SnackbarProvider'); + } + return context; +}; + +export const SnackbarProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [visible, setVisible] = useState(false); + const [message, setMessage] = useState(''); + const [type, setType] = useState('success'); + const slideAnim = useRef(new Animated.Value(100)).current; + const insets = useSafeAreaInsets(); + const timerRef = useRef(null); + + const showSnackbar = (msg: string, msgType: SnackbarType = 'success') => { + setMessage(msg); + setType(msgType); + setVisible(true); + + // Slide up + Animated.spring(slideAnim, { + toValue: 0, + useNativeDriver: true, + speed: 12, + bounciness: 8, + }).start(); + + // Auto hide + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => { + hideSnackbar(); + }, 3000); + }; + + const hideSnackbar = () => { + Animated.timing(slideAnim, { + toValue: 150, + duration: 250, + useNativeDriver: true, + }).start(() => { + setVisible(false); + }); + }; + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + return ( + + {children} + {visible && ( + + + {type === 'success' ? ( + + ) : ( + + )} + {message} + + + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + left: 16, + right: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 8, + zIndex: 9999, + }, + successContainer: { + backgroundColor: '#059669', // A nice green for success + }, + errorContainer: { + backgroundColor: '#EF4444', // Red for errors + }, + content: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + messageText: { + color: '#FFF', + fontSize: 16, + fontWeight: '500', + marginLeft: 12, + marginRight: 16, + flexShrink: 1, + }, + closeBtn: { + padding: 4, + } +}); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index fdd55ee..4445fee 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { NavigationContainer, DefaultTheme } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Home, Calendar, Settings } from 'lucide-react-native'; import { HomeScreen } from '../screens/HomeScreen'; @@ -15,13 +16,28 @@ import { AddSkillScreen } from '../screens/AddSkillScreen'; import { AvailabilityListScreen } from '../screens/AvailabilityListScreen'; import { AddAvailabilityScreen } from '../screens/AddAvailabilityScreen'; import { EditAvailabilityScreen } from '../screens/EditAvailabilityScreen'; -import { PaymentProfileScreen } from '../screens/PaymentProfileScreen'; +import { PaymentProfileListScreen } from '../screens/PaymentProfileListScreen'; +import { AddEditPaymentProfileScreen } from '../screens/AddEditPaymentProfileScreen'; import { ManageServicesScreen } from '../screens/ManageServicesScreen'; import { AddEditServiceScreen } from '../screens/AddEditServiceScreen'; +import { ProfessionalDocumentsScreen } from '../screens/ProfessionalDocumentsScreen'; +import { UploadDocumentScreen } from '../screens/UploadDocumentScreen'; +import { ViewDocumentScreen } from '../screens/ViewDocumentScreen'; +import { SelectionScreen } from '../screens/SelectionScreen'; import { Service } from '../services/serviceService'; +import { navigationRef } from './navigationRef'; export type RootStackParamList = { Login: undefined; + Selection: { + data: any[]; + value: any; + isMulti?: boolean; + searchable?: boolean; + label?: string; + searchPlaceholder?: string; + onSelect: (value: any) => void; + }; MainTabs: undefined; Profile: { caregiverId: string }; PersonalInformation: undefined; @@ -34,9 +50,13 @@ export type RootStackParamList = { start_time: string; end_time: string; }; - PaymentProfile: undefined; + PaymentProfileList: undefined; + AddEditPaymentProfile: { profile?: any }; ManageServices: undefined; AddEditService: { service?: Service }; + ProfessionalDocuments: undefined; + UploadDocument: undefined; + ViewDocument: { documentId: number }; }; export type TabParamList = { @@ -49,6 +69,8 @@ const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); function MainTabs() { + const insets = useSafeAreaInsets(); + return ( { return ( - + { - + + + + + + ); diff --git a/src/navigation/navigationRef.ts b/src/navigation/navigationRef.ts new file mode 100644 index 0000000..9b4ded0 --- /dev/null +++ b/src/navigation/navigationRef.ts @@ -0,0 +1,10 @@ +import { createNavigationContainerRef } from '@react-navigation/native'; +import { RootStackParamList } from './AppNavigator'; + +export const navigationRef = createNavigationContainerRef(); + +export function navigate(name: keyof RootStackParamList, params?: any) { + if (navigationRef.isReady()) { + navigationRef.navigate(name, params); + } +} diff --git a/src/screens/AddAvailabilityScreen.tsx b/src/screens/AddAvailabilityScreen.tsx index 03ab4db..71b624b 100644 --- a/src/screens/AddAvailabilityScreen.tsx +++ b/src/screens/AddAvailabilityScreen.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { View, StyleSheet, TouchableOpacity, Alert, Platform } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import DateTimePicker from '@react-native-community/datetimepicker'; import { Typography } from '../components/ui/Typography'; @@ -8,6 +8,7 @@ import { Header } from '../components/ui/Header'; import { profileService } from '../services/profileService'; import { THEME } from '../constants/theme'; import { Clock, Calendar, Check } from 'lucide-react-native'; +import { useSnackbar } from '../context/SnackbarContext'; const DAYS_OF_WEEK = [ { label: 'Monday', value: 'monday' }, @@ -21,6 +22,8 @@ const DAYS_OF_WEEK = [ export const AddAvailabilityScreen = () => { const navigation = useNavigation(); + const insets = useSafeAreaInsets(); + const { showSnackbar } = useSnackbar(); const [selectedDay, setSelectedDay] = useState('monday'); const [showDayPicker, setShowDayPicker] = useState(false); @@ -72,9 +75,8 @@ export const AddAvailabilityScreen = () => { // @ts-ignore await profileService.addDayAvailability(payload); - Alert.alert('Success', 'Availability added successfully.', [ - { text: 'OK', onPress: () => navigation.goBack() } - ]); + showSnackbar('Availability added successfully.'); + navigation.goBack(); } catch (e: any) { Alert.alert('Error', e.response?.data?.message || 'Failed to add availability.'); } finally { @@ -153,7 +155,7 @@ export const AddAvailabilityScreen = () => { - + ; +interface AddEditPaymentProfileScreenProps { + navigation: NativeStackNavigationProp; + route: RouteProp; } -export const PaymentProfileScreen: React.FC = ({ navigation }) => { - const [isLoading, setIsLoading] = useState(true); +export const AddEditPaymentProfileScreen: React.FC = ({ navigation, route }) => { + const { profile } = route.params || {}; + const isEditing = !!profile; const [isSaving, setIsSaving] = useState(false); - const [existingId, setExistingId] = useState(undefined); + const insets = useSafeAreaInsets(); + const { showSnackbar } = useSnackbar(); const [form, setForm] = useState>({ - payment_method: 'bank', - account_holder_name: '', - account_number: '', - ifsc_code: '', - bank_name: '', - branch_name: '', - account_type: 'savings', + payment_method: profile?.payment_method || 'bank', + account_holder_name: profile?.account_holder_name || '', + account_number: profile?.account_number || '', + ifsc_code: profile?.ifsc_code || '', + bank_name: profile?.bank_name || '', + branch_name: profile?.branch_name || '', + account_type: profile?.account_type || 'savings', }); const [errors, setErrors] = useState>({}); - useEffect(() => { - fetchPaymentProfile(); - }, []); - - const fetchPaymentProfile = async () => { - try { - setIsLoading(true); - const response = await profileService.getPaymentProfile(); - if (response.success && response.data && response.data.id) { - setExistingId(response.data.id); - setForm({ - payment_method: response.data.payment_method || 'bank', - account_holder_name: response.data.account_holder_name || '', - account_number: response.data.account_number || '', - ifsc_code: response.data.ifsc_code || '', - bank_name: response.data.bank_name || '', - branch_name: response.data.branch_name || '', - account_type: response.data.account_type || 'savings', - }); - } - } catch (error: any) { - // If 404 or not found, it means no profile exists yet. - console.log('No existing payment profile found or error fetching.'); - } finally { - setIsLoading(false); - } - }; - const handleChange = (field: keyof PaymentProfile, value: string) => { setForm(prev => ({ ...prev, [field]: value })); if (errors[field]) { @@ -88,15 +65,13 @@ export const PaymentProfileScreen: React.FC = ({ navi setIsSaving(true); const payload = form as PaymentProfile; - if (existingId) { + if (isEditing && profile?.id) { + payload.id = profile.id; await profileService.updatePaymentProfile(payload); - Alert.alert('Success', 'Payment profile updated successfully.'); + showSnackbar('Payment profile updated successfully.'); } else { - const response = await profileService.addPaymentProfile(payload); - if (response.data && response.data.id) { - setExistingId(response.data.id); - } - Alert.alert('Success', 'Payment profile added successfully.'); + await profileService.addPaymentProfile(payload); + showSnackbar('Payment profile added successfully.'); } navigation.goBack(); } catch (error: any) { @@ -110,27 +85,21 @@ export const PaymentProfileScreen: React.FC = ({ navi return ( // @ts-ignore - TS types for SafeAreaView in this version incorrectly omit the style prop -
navigation.goBack()} /> +
navigation.goBack()} /> - - {isLoading ? ( - - Loading... + + + + - ) : ( - <> - - - - - - Add your bank details to receive payments. - - + + Add your bank details to receive payments. + + = ({ navi