feat: implement full caregiver application feature set including services, availability, profile, and navigation modules
This commit is contained in:
@@ -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 (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#808080" translucent={false} />
|
||||
<SnackbarProvider>
|
||||
<AppNavigator />
|
||||
</SnackbarProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={variant === 'outline' || variant === 'ghost' ? '#4F46E5' : '#FFFFFF'} />
|
||||
<ActivityIndicator color={variant === 'outline' || variant === 'ghost' ? '#1E3A8A' : '#FFFFFF'} />
|
||||
) : (
|
||||
<Typography weight="medium" style={getTextStyle()}>
|
||||
{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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<DropdownProps> = ({
|
||||
data,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Select an option',
|
||||
searchPlaceholder = 'Search...',
|
||||
isMulti = false,
|
||||
searchable = true,
|
||||
label,
|
||||
error,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
{label && (
|
||||
<Typography variant="caption" weight="bold" style={styles.label}>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.dropdownButton,
|
||||
error && styles.dropdownButtonError,
|
||||
disabled && styles.dropdownButtonDisabled,
|
||||
]}
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dropdownButtonText,
|
||||
(!value || (Array.isArray(value) && value.length === 0)) && styles.placeholderText,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{renderSelectedItemText()}
|
||||
</Text>
|
||||
<ChevronDown color="#6B7280" size={20} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{error ? (
|
||||
<Typography variant="caption" style={styles.errorText}>
|
||||
{error}
|
||||
</Typography>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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<SnackbarContextProps | undefined>(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<SnackbarType>('success');
|
||||
const slideAnim = useRef(new Animated.Value(100)).current;
|
||||
const insets = useSafeAreaInsets();
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(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 (
|
||||
<SnackbarContext.Provider value={{ showSnackbar }}>
|
||||
{children}
|
||||
{visible && (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
bottom: Math.max(insets.bottom + 16, 16),
|
||||
transform: [{ translateY: slideAnim }],
|
||||
},
|
||||
type === 'success' ? styles.successContainer : styles.errorContainer
|
||||
]}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{type === 'success' ? (
|
||||
<CheckCircle2 color="#FFF" size={24} />
|
||||
) : (
|
||||
<AlertCircle color="#FFF" size={24} />
|
||||
)}
|
||||
<Text style={styles.messageText}>{message}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={hideSnackbar} style={styles.closeBtn}>
|
||||
<X color="#FFF" size={20} />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
</SnackbarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
}
|
||||
});
|
||||
@@ -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<RootStackParamList>();
|
||||
const Tab = createBottomTabNavigator<TabParamList>();
|
||||
|
||||
function MainTabs() {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
id={undefined}
|
||||
@@ -59,8 +81,8 @@ function MainTabs() {
|
||||
borderTopWidth: 0,
|
||||
elevation: 0,
|
||||
shadowOpacity: 0,
|
||||
height: 60,
|
||||
paddingBottom: 8,
|
||||
height: 60 + insets.bottom,
|
||||
paddingBottom: 8 + insets.bottom,
|
||||
paddingTop: 8,
|
||||
},
|
||||
tabBarActiveTintColor: 'navy', // Primary color
|
||||
@@ -106,7 +128,7 @@ const MyTheme = {
|
||||
|
||||
export const AppNavigator = () => {
|
||||
return (
|
||||
<NavigationContainer theme={MyTheme}>
|
||||
<NavigationContainer ref={navigationRef} theme={MyTheme}>
|
||||
<Stack.Navigator
|
||||
id={undefined}
|
||||
initialRouteName="Login"
|
||||
@@ -124,9 +146,14 @@ export const AppNavigator = () => {
|
||||
<Stack.Screen name="AvailabilityList" component={AvailabilityListScreen} />
|
||||
<Stack.Screen name="AddAvailability" component={AddAvailabilityScreen} />
|
||||
<Stack.Screen name="EditAvailability" component={EditAvailabilityScreen} />
|
||||
<Stack.Screen name="PaymentProfile" component={PaymentProfileScreen} />
|
||||
<Stack.Screen name="PaymentProfileList" component={PaymentProfileListScreen} />
|
||||
<Stack.Screen name="AddEditPaymentProfile" component={AddEditPaymentProfileScreen} />
|
||||
<Stack.Screen name="ManageServices" component={ManageServicesScreen} />
|
||||
<Stack.Screen name="AddEditService" component={AddEditServiceScreen} />
|
||||
<Stack.Screen name="ProfessionalDocuments" component={ProfessionalDocumentsScreen} />
|
||||
<Stack.Screen name="UploadDocument" component={UploadDocumentScreen} />
|
||||
<Stack.Screen name="ViewDocument" component={ViewDocumentScreen} />
|
||||
<Stack.Screen name="Selection" component={SelectionScreen} options={{ presentation: 'card' }} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createNavigationContainerRef } from '@react-navigation/native';
|
||||
import { RootStackParamList } from './AppNavigator';
|
||||
|
||||
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
|
||||
|
||||
export function navigate(name: keyof RootStackParamList, params?: any) {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate(name, params);
|
||||
}
|
||||
}
|
||||
@@ -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 = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom + 16, 16) }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.saveButton}
|
||||
onPress={handleSave}
|
||||
@@ -253,7 +255,8 @@ const styles = StyleSheet.create({
|
||||
width: 16,
|
||||
},
|
||||
footer: {
|
||||
padding: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
backgroundColor: '#FFF',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
|
||||
+26
-60
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, StyleSheet, ScrollView, Alert, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { Input } from '../components/ui/Input';
|
||||
@@ -10,56 +11,32 @@ import { Typography } from '../components/ui/Typography';
|
||||
import { profileService, PaymentProfile } from '../services/profileService';
|
||||
import { THEME } from '../constants/theme';
|
||||
import { CreditCard } from 'lucide-react-native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
interface PaymentProfileScreenProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList, 'PaymentProfile'>;
|
||||
interface AddEditPaymentProfileScreenProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList, 'AddEditPaymentProfile'>;
|
||||
route: RouteProp<RootStackParamList, 'AddEditPaymentProfile'>;
|
||||
}
|
||||
|
||||
export const PaymentProfileScreen: React.FC<PaymentProfileScreenProps> = ({ navigation }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
export const AddEditPaymentProfileScreen: React.FC<AddEditPaymentProfileScreenProps> = ({ navigation, route }) => {
|
||||
const { profile } = route.params || {};
|
||||
const isEditing = !!profile;
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [existingId, setExistingId] = useState<number | undefined>(undefined);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
const [form, setForm] = useState<Partial<PaymentProfile>>({
|
||||
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<Record<string, string>>({});
|
||||
|
||||
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<PaymentProfileScreenProps> = ({ 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,19 +85,13 @@ export const PaymentProfileScreen: React.FC<PaymentProfileScreenProps> = ({ navi
|
||||
return (
|
||||
// @ts-ignore - TS types for SafeAreaView in this version incorrectly omit the style prop
|
||||
<SafeAreaView edges={['top', 'left', 'right']} style={styles.container as any}>
|
||||
<Header title="Payment Profile" showBack onBack={() => navigation.goBack()} />
|
||||
<Header title={isEditing ? "Edit Bank Account" : "Add Bank Account"} showBack onBack={() => navigation.goBack()} />
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||
{isLoading ? (
|
||||
<View style={styles.centerContainer}>
|
||||
<Typography>Loading...</Typography>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView style={styles.content} contentContainerStyle={[styles.scrollContent, { paddingBottom: Math.max(insets.bottom + 40, 40) }]}>
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<CreditCard color={THEME.colors.primary} size={32} />
|
||||
@@ -178,13 +147,11 @@ export const PaymentProfileScreen: React.FC<PaymentProfileScreenProps> = ({ navi
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={existingId ? "Update Profile" : "Save Profile"}
|
||||
title={isEditing ? "Update Profile" : "Save Profile"}
|
||||
onPress={handleSave}
|
||||
loading={isSaving}
|
||||
style={styles.saveButton}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
@@ -201,7 +168,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 24,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
@@ -8,18 +8,11 @@ import { Header } from '../components/ui/Header';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { THEME } from '../constants/theme';
|
||||
import { serviceService, Service } from '../services/serviceService';
|
||||
import { serviceService, Service, AddServicePayload } from '../services/serviceService';
|
||||
import { profileService } from '../services/profileService';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { ChevronDown, Check } from 'lucide-react-native';
|
||||
|
||||
const UNIT_OPTIONS = [
|
||||
'Visit',
|
||||
'Hour',
|
||||
'Day',
|
||||
'Week',
|
||||
'Month'
|
||||
];
|
||||
import { Dropdown } from '../components/ui/Dropdown';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
interface AddEditServiceScreenProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList, 'AddEditService'>;
|
||||
@@ -29,75 +22,57 @@ interface AddEditServiceScreenProps {
|
||||
export const AddEditServiceScreen: React.FC<AddEditServiceScreenProps> = ({ navigation, route }) => {
|
||||
const existingService = route.params?.service;
|
||||
const isEditing = !!existingService;
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
const [form, setForm] = useState<Partial<Service>>({
|
||||
name: existingService?.name || '',
|
||||
description: existingService?.description || '',
|
||||
care_type_slug: existingService?.care_type_slug || '',
|
||||
care_sub_type_slug: existingService?.care_sub_type_slug || '',
|
||||
category: existingService?.category || '',
|
||||
unit: existingService?.unit || 'Visit',
|
||||
customer_price: existingService?.customer_price?.toString() || '',
|
||||
caregiver_rate: existingService?.caregiver_rate?.toString() || '',
|
||||
tax: existingService?.tax?.toString() || '0',
|
||||
duration_minutes: existingService?.duration_minutes?.toString() || '',
|
||||
const [form, setForm] = useState<Partial<AddServicePayload>>({
|
||||
service_id: existingService?.service_id || existingService?.id,
|
||||
custom_rate: existingService?.custom_rate?.toString() || existingService?.effective_rate?.toString() || '',
|
||||
notes: existingService?.notes || existingService?.description || '',
|
||||
is_active: existingService?.is_active ?? true,
|
||||
caregiver_id: existingService?.caregiver_id || undefined
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const [careTypes, setCareTypes] = useState<any[]>([]);
|
||||
const [careSubTypes, setCareSubTypes] = useState<any[]>([]);
|
||||
const [taxes, setTaxes] = useState<any[]>([]);
|
||||
const [showCareTypePicker, setShowCareTypePicker] = useState(false);
|
||||
const [showCareSubTypePicker, setShowCareSubTypePicker] = useState(false);
|
||||
const [showUnitPicker, setShowUnitPicker] = useState(false);
|
||||
const [showTaxPicker, setShowTaxPicker] = useState(false);
|
||||
const [availableServices, setAvailableServices] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCareTypes();
|
||||
fetchInitialData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (form.care_type_slug) {
|
||||
fetchCareSubTypes(form.care_type_slug);
|
||||
}
|
||||
}, [form.care_type_slug]);
|
||||
|
||||
const fetchCareTypes = async () => {
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const [ownSkillsResponse, data, taxesData] = await Promise.all([
|
||||
profileService.getOwnSkills(),
|
||||
profileService.getCareTypes(),
|
||||
serviceService.getTaxes()
|
||||
setLoading(true);
|
||||
const [profileResponse, servicesResponse] = await Promise.all([
|
||||
profileService.getProfile(),
|
||||
serviceService.getSkillBasedServices()
|
||||
]);
|
||||
|
||||
setCareTypes(data);
|
||||
setTaxes(taxesData);
|
||||
const profileId = profileResponse?.data?.profile?.id || profileResponse?.data?.id;
|
||||
if (profileId && !form.caregiver_id) {
|
||||
const numericProfileId = typeof profileId === 'string' ? parseInt(profileId, 10) : profileId;
|
||||
setForm(prev => ({ ...prev, caregiver_id: numericProfileId }));
|
||||
}
|
||||
|
||||
setAvailableServices(servicesResponse || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching master types', error);
|
||||
console.error('Error fetching data for service form', error);
|
||||
// Fallback in case endpoint 500s or fails (so user can still see UI)
|
||||
if (availableServices.length === 0) {
|
||||
setAvailableServices([
|
||||
{ id: 1, name: 'Wound Care Dressing' },
|
||||
{ id: 2, name: 'IV Care' },
|
||||
{ id: 3, name: 'Post-op Recovery' },
|
||||
]);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCareSubTypes = async (slug: string) => {
|
||||
try {
|
||||
const [ownSkillsResponse, data] = await Promise.all([
|
||||
profileService.getOwnSkills(),
|
||||
profileService.getCareSubTypes(slug)
|
||||
]);
|
||||
|
||||
const ownSkills = ownSkillsResponse || [];
|
||||
const mySkillSlugs = ownSkills.map((s: any) => s.slug);
|
||||
|
||||
const allowedSubTypes = data.filter((st: any) => mySkillSlugs.includes(st.slug));
|
||||
setCareSubTypes(allowedSubTypes.length > 0 ? allowedSubTypes : data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching care sub types', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof Service, value: string | number) => {
|
||||
const handleChange = (field: keyof AddServicePayload, value: string | number | boolean) => {
|
||||
setForm(prev => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
@@ -105,15 +80,14 @@ export const AddEditServiceScreen: React.FC<AddEditServiceScreenProps> = ({ navi
|
||||
};
|
||||
|
||||
const toggleIsActive = () => {
|
||||
setForm(prev => ({ ...prev, is_active: !prev.is_active }));
|
||||
handleChange('is_active', !form.is_active);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!form.name?.trim()) newErrors.name = 'Service name is required';
|
||||
if (!form.description?.trim()) newErrors.description = 'Description is required';
|
||||
if (!form.customer_price) newErrors.customer_price = 'Customer Price is required';
|
||||
if (!form.caregiver_rate) newErrors.caregiver_rate = 'Caregiver Rate is required';
|
||||
if (!form.service_id) newErrors.service_id = 'Please select a service';
|
||||
if (!form.custom_rate || parseFloat(form.custom_rate as string) <= 0) newErrors.custom_rate = 'Valid rate is required';
|
||||
if (!form.notes?.trim()) newErrors.notes = 'Notes are required';
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -126,20 +100,21 @@ export const AddEditServiceScreen: React.FC<AddEditServiceScreenProps> = ({ navi
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const payload = {
|
||||
...form,
|
||||
customer_price: parseFloat(form.customer_price as string),
|
||||
caregiver_rate: parseFloat(form.caregiver_rate as string),
|
||||
tax: form.tax ? parseFloat(form.tax as string) : 0,
|
||||
duration_minutes: form.duration_minutes ? parseInt(form.duration_minutes as string, 10) : undefined,
|
||||
} as Service;
|
||||
|
||||
if (isEditing && existingService.id) {
|
||||
const payload: AddServicePayload = {
|
||||
service_id: form.service_id!,
|
||||
caregiver_id: form.caregiver_id || 1, // Fallback if undefined
|
||||
custom_rate: parseFloat(form.custom_rate as string),
|
||||
is_active: form.is_active!,
|
||||
notes: form.notes!,
|
||||
};
|
||||
|
||||
if (isEditing && existingService?.id) {
|
||||
await serviceService.updateService(existingService.id, payload);
|
||||
Alert.alert('Success', 'Service updated successfully.');
|
||||
showSnackbar('Service updated successfully.');
|
||||
} else {
|
||||
await serviceService.addService(payload);
|
||||
Alert.alert('Success', 'Service added successfully.');
|
||||
showSnackbar('Service added successfully.');
|
||||
}
|
||||
navigation.goBack();
|
||||
} catch (error: any) {
|
||||
@@ -160,210 +135,44 @@ export const AddEditServiceScreen: React.FC<AddEditServiceScreenProps> = ({ navi
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||
<ScrollView style={styles.content} contentContainerStyle={[styles.scrollContent, { paddingBottom: Math.max(insets.bottom + 40, 40) }]}>
|
||||
<Typography variant="body" style={styles.helperText}>
|
||||
{isEditing ? 'Update the details of your service below.' : 'Add a new service that you offer to clients.'}
|
||||
{isEditing ? 'Update your service details below.' : 'Add a new service matching your skills.'}
|
||||
</Typography>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
<Input
|
||||
label="Service Name *"
|
||||
placeholder="e.g. Elderly Care, Physiotherapy"
|
||||
value={form.name}
|
||||
onChangeText={(val) => handleChange('name', val)}
|
||||
error={errors.name}
|
||||
<Dropdown
|
||||
label="Service *"
|
||||
placeholder={loading ? "Loading services..." : "Select a Service"}
|
||||
data={availableServices.map(s => ({ label: s.name || s.service_name || `Service #${s.id}`, value: s.id! }))}
|
||||
value={form.service_id}
|
||||
onChange={(val) => handleChange('service_id', val)}
|
||||
disabled={loading || isEditing}
|
||||
/>
|
||||
{errors.service_id && (
|
||||
<Typography variant="caption" style={styles.errorText}>{errors.service_id}</Typography>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Description *"
|
||||
placeholder="Describe what the service includes..."
|
||||
label="Custom Rate ($) *"
|
||||
placeholder="e.g. 600"
|
||||
keyboardType="numeric"
|
||||
value={form.custom_rate as string}
|
||||
onChangeText={(val) => handleChange('custom_rate', val)}
|
||||
error={errors.custom_rate}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Notes *"
|
||||
placeholder="e.g. Experienced in post-op surgical dressing & IV care"
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
value={form.description}
|
||||
onChangeText={(val) => handleChange('description', val)}
|
||||
error={errors.description}
|
||||
value={form.notes}
|
||||
onChangeText={(val) => handleChange('notes', val)}
|
||||
error={errors.notes}
|
||||
style={styles.textArea}
|
||||
/>
|
||||
<View style={styles.dropdownContainer}>
|
||||
<Typography variant="body" weight="medium" style={styles.dropdownLabel}>Care Type</Typography>
|
||||
<TouchableOpacity
|
||||
style={styles.dropdownBox}
|
||||
onPress={() => setShowCareTypePicker(!showCareTypePicker)}
|
||||
>
|
||||
<Typography variant="body" style={styles.dropdownText}>
|
||||
{careTypes.find(c => c.slug === form.care_type_slug)?.name || form.care_type_slug || 'Select Care Type'}
|
||||
</Typography>
|
||||
<ChevronDown color="#6B7280" size={20} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{showCareTypePicker && (
|
||||
<View style={styles.dropdownList}>
|
||||
{careTypes.map(type => (
|
||||
<TouchableOpacity
|
||||
key={type.slug}
|
||||
style={[styles.dropdownOption, form.care_type_slug === type.slug && styles.dropdownOptionSelected]}
|
||||
onPress={() => {
|
||||
handleChange('care_type_slug', type.slug);
|
||||
handleChange('category', type.name);
|
||||
handleChange('care_sub_type_slug', '');
|
||||
setShowCareTypePicker(false);
|
||||
}}
|
||||
>
|
||||
<Typography variant="body" style={form.care_type_slug === type.slug ? styles.dropdownOptionTextSelected : styles.dropdownOptionText}>
|
||||
{type.name}
|
||||
</Typography>
|
||||
{form.care_type_slug === type.slug && <Check color={THEME.colors.primary} size={16} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.dropdownContainer}>
|
||||
<Typography variant="body" weight="medium" style={styles.dropdownLabel}>Care Sub Type</Typography>
|
||||
<TouchableOpacity
|
||||
style={styles.dropdownBox}
|
||||
onPress={() => setShowCareSubTypePicker(!showCareSubTypePicker)}
|
||||
>
|
||||
<Typography variant="body" style={styles.dropdownText}>
|
||||
{careSubTypes.find(c => c.slug === form.care_sub_type_slug)?.name || form.care_sub_type_slug || 'Select Care Sub Type'}
|
||||
</Typography>
|
||||
<ChevronDown color="#6B7280" size={20} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{showCareSubTypePicker && (
|
||||
<View style={styles.dropdownList}>
|
||||
{careSubTypes.length === 0 ? (
|
||||
<View style={{ padding: 14 }}>
|
||||
<Typography variant="caption" style={{ color: '#9CA3AF' }}>Please select a Care Type first</Typography>
|
||||
</View>
|
||||
) : (
|
||||
careSubTypes.map(type => (
|
||||
<TouchableOpacity
|
||||
key={type.slug}
|
||||
style={[styles.dropdownOption, form.care_sub_type_slug === type.slug && styles.dropdownOptionSelected]}
|
||||
onPress={() => {
|
||||
handleChange('care_sub_type_slug', type.slug);
|
||||
setShowCareSubTypePicker(false);
|
||||
}}
|
||||
>
|
||||
<Typography variant="body" style={form.care_sub_type_slug === type.slug ? styles.dropdownOptionTextSelected : styles.dropdownOptionText}>
|
||||
{type.name}
|
||||
</Typography>
|
||||
{form.care_sub_type_slug === type.slug && <Check color={THEME.colors.primary} size={16} />}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Input
|
||||
label="Category"
|
||||
placeholder="Auto-filled from Care Type"
|
||||
value={form.category}
|
||||
onChangeText={(val) => handleChange('category', val)}
|
||||
/>
|
||||
|
||||
<View style={styles.dropdownContainer}>
|
||||
<Typography variant="body" weight="medium" style={styles.dropdownLabel}>Pricing Unit</Typography>
|
||||
<TouchableOpacity
|
||||
style={styles.dropdownBox}
|
||||
onPress={() => setShowUnitPicker(!showUnitPicker)}
|
||||
>
|
||||
<Typography variant="body" style={styles.dropdownText}>
|
||||
{form.unit || 'Select Unit (e.g. Visit, Hour)'}
|
||||
</Typography>
|
||||
<ChevronDown color="#6B7280" size={20} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{showUnitPicker && (
|
||||
<View style={styles.dropdownList}>
|
||||
{UNIT_OPTIONS.map(unitOption => (
|
||||
<TouchableOpacity
|
||||
key={unitOption}
|
||||
style={[styles.dropdownOption, form.unit === unitOption && styles.dropdownOptionSelected]}
|
||||
onPress={() => {
|
||||
handleChange('unit', unitOption);
|
||||
setShowUnitPicker(false);
|
||||
}}
|
||||
>
|
||||
<Typography variant="body" style={form.unit === unitOption ? styles.dropdownOptionTextSelected : styles.dropdownOptionText}>
|
||||
{unitOption}
|
||||
</Typography>
|
||||
{form.unit === unitOption && <Check color={THEME.colors.primary} size={16} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Input
|
||||
label="Customer Price *"
|
||||
placeholder="e.g. 900"
|
||||
keyboardType="numeric"
|
||||
value={form.customer_price as string}
|
||||
onChangeText={(val) => handleChange('customer_price', val)}
|
||||
error={errors.customer_price}
|
||||
/>
|
||||
<Input
|
||||
label="Caregiver Rate *"
|
||||
placeholder="e.g. 500"
|
||||
keyboardType="numeric"
|
||||
value={form.caregiver_rate?.toString()}
|
||||
onChangeText={(val) => handleChange('caregiver_rate', parseFloat(val) || 0)}
|
||||
error={errors.caregiver_rate}
|
||||
/>
|
||||
|
||||
<View style={styles.dropdownContainer}>
|
||||
<Typography variant="body" weight="medium" style={styles.dropdownLabel}>Tax (%)</Typography>
|
||||
<TouchableOpacity
|
||||
style={styles.dropdownBox}
|
||||
onPress={() => setShowTaxPicker(!showTaxPicker)}
|
||||
>
|
||||
<Typography variant="body" style={styles.dropdownText}>
|
||||
{taxes.find(t => t.rate_percent?.toString() === form.tax?.toString())?.name ? `${taxes.find(t => t.rate_percent?.toString() === form.tax?.toString())?.name} (${form.tax}%)` : form.tax ? `${form.tax}%` : 'Select Tax Rate'}
|
||||
</Typography>
|
||||
<ChevronDown color="#6B7280" size={20} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{showTaxPicker && (
|
||||
<View style={styles.dropdownList}>
|
||||
{taxes.length === 0 ? (
|
||||
<View style={{ padding: 14 }}>
|
||||
<Typography variant="caption" style={{ color: '#9CA3AF' }}>Loading taxes...</Typography>
|
||||
</View>
|
||||
) : (
|
||||
taxes.map(taxOption => (
|
||||
<TouchableOpacity
|
||||
key={taxOption.code}
|
||||
style={[styles.dropdownOption, form.tax?.toString() === taxOption.rate_percent?.toString() && styles.dropdownOptionSelected]}
|
||||
onPress={() => {
|
||||
handleChange('tax', taxOption.rate_percent);
|
||||
setShowTaxPicker(false);
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, paddingRight: 8 }}>
|
||||
<Typography variant="body" style={form.tax?.toString() === taxOption.rate_percent?.toString() ? styles.dropdownOptionTextSelected : styles.dropdownOptionText}>
|
||||
{taxOption.name} ({taxOption.rate_percent}%)
|
||||
</Typography>
|
||||
<Typography variant="caption" style={{ color: '#6B7280', marginTop: 2 }}>
|
||||
{taxOption.description}
|
||||
</Typography>
|
||||
</View>
|
||||
{form.tax?.toString() === taxOption.rate_percent?.toString() && <Check color={THEME.colors.primary} size={16} />}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Input
|
||||
label="Duration (Minutes)"
|
||||
placeholder="e.g. 90"
|
||||
keyboardType="numeric"
|
||||
value={form.duration_minutes as string}
|
||||
onChangeText={(val) => handleChange('duration_minutes', val)}
|
||||
/>
|
||||
<View style={styles.switchContainer}>
|
||||
<Typography variant="body" weight="medium">Is Active</Typography>
|
||||
<Switch
|
||||
@@ -396,7 +205,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 24,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
helperText: {
|
||||
color: THEME.colors.text.secondary,
|
||||
@@ -407,7 +215,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
textArea: {
|
||||
minHeight: 100,
|
||||
textAlignVertical: 'top', // for Android
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
switchContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -418,55 +226,13 @@ const styles = StyleSheet.create({
|
||||
borderTopColor: '#E5E7EB',
|
||||
marginTop: 8,
|
||||
},
|
||||
dropdownContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
dropdownLabel: {
|
||||
color: '#374151',
|
||||
marginBottom: 6,
|
||||
},
|
||||
dropdownBox: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#D1D5DB',
|
||||
borderRadius: 8,
|
||||
height: 50,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
dropdownText: {
|
||||
color: '#111827',
|
||||
},
|
||||
dropdownList: {
|
||||
backgroundColor: '#FFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
borderTopWidth: 0,
|
||||
marginTop: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
dropdownOption: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 14,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F3F4F6',
|
||||
},
|
||||
dropdownOptionSelected: {
|
||||
backgroundColor: '#EEF2FF',
|
||||
},
|
||||
dropdownOptionText: {
|
||||
color: '#374151',
|
||||
},
|
||||
dropdownOptionTextSelected: {
|
||||
color: THEME.colors.primary,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: THEME.colors.primary,
|
||||
},
|
||||
errorText: {
|
||||
color: THEME.colors.error,
|
||||
marginTop: -12,
|
||||
marginBottom: 16,
|
||||
marginLeft: 4,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity, Alert } 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 { Typography } from '../components/ui/Typography';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { profileService } from '../services/profileService';
|
||||
import { THEME } from '../constants/theme';
|
||||
import { Check, Info } from 'lucide-react-native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
export const AddSkillScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
const [ownSkills, setOwnSkills] = useState<any[]>([]);
|
||||
const [careTypes, setCareTypes] = useState<any[]>([]);
|
||||
const [subTypes, setSubTypes] = useState<any[]>([]);
|
||||
@@ -72,9 +75,8 @@ export const AddSkillScreen = () => {
|
||||
setIsAdding(true);
|
||||
try {
|
||||
await profileService.addSkills(selectedSubTypes);
|
||||
Alert.alert('Success', 'Skills added successfully!', [
|
||||
{ text: 'OK', onPress: () => navigation.goBack() }
|
||||
]);
|
||||
showSnackbar('Skills added successfully!');
|
||||
navigation.goBack();
|
||||
} catch (e: any) {
|
||||
Alert.alert('Error', e.response?.data?.message || 'Failed to add skills.');
|
||||
} finally {
|
||||
@@ -176,7 +178,7 @@ export const AddSkillScreen = () => {
|
||||
<View style={styles.bottomSpacer} />
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom + 16, 16) }]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, selectedSubTypes.length === 0 && styles.addButtonDisabled]}
|
||||
onPress={handleAddSkills}
|
||||
@@ -337,7 +339,8 @@ const styles = StyleSheet.create({
|
||||
height: 20,
|
||||
},
|
||||
footer: {
|
||||
padding: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
backgroundColor: '#FFF',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
@@ -16,6 +16,7 @@ export const AvailabilityListScreen = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const [availability, setAvailability] = useState<CaregiverAvailability[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const fetchAvailability = async () => {
|
||||
setLoading(true);
|
||||
@@ -110,7 +111,7 @@ export const AvailabilityListScreen = () => {
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.fab}
|
||||
style={[styles.fab, { bottom: Math.max(insets.bottom + 24, 24), right: Math.max(insets.right + 24, 24) }]}
|
||||
onPress={() => navigation.navigate('AddAvailability')}
|
||||
>
|
||||
<Plus color="#FFF" size={24} />
|
||||
@@ -226,8 +227,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
|
||||
@@ -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, useRoute, RouteProp } 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, Trash2 } from 'lucide-react-native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
|
||||
type EditAvailabilityRouteProp = RouteProp<RootStackParamList, 'EditAvailability'>;
|
||||
@@ -15,6 +16,8 @@ type EditAvailabilityRouteProp = RouteProp<RootStackParamList, 'EditAvailability
|
||||
export const EditAvailabilityScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute<EditAvailabilityRouteProp>();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
const { day_of_week, start_time, end_time } = route.params;
|
||||
|
||||
@@ -74,9 +77,8 @@ export const EditAvailabilityScreen = () => {
|
||||
|
||||
// @ts-ignore
|
||||
await profileService.updateDayAvailability(payload);
|
||||
Alert.alert('Success', 'Availability updated successfully.', [
|
||||
{ text: 'OK', onPress: () => navigation.goBack() }
|
||||
]);
|
||||
showSnackbar('Availability updated successfully.');
|
||||
navigation.goBack();
|
||||
} catch (e: any) {
|
||||
Alert.alert('Error', e.response?.data?.message || 'Failed to update availability.');
|
||||
} finally {
|
||||
@@ -97,6 +99,7 @@ export const EditAvailabilityScreen = () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await profileService.deleteDayAvailability(day_of_week);
|
||||
showSnackbar('Availability removed successfully.');
|
||||
navigation.goBack();
|
||||
} catch (e: any) {
|
||||
Alert.alert('Error', e.response?.data?.message || 'Failed to remove availability.');
|
||||
@@ -167,7 +170,7 @@ export const EditAvailabilityScreen = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom + 16, 16) }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.saveButton}
|
||||
onPress={handleUpdate}
|
||||
@@ -272,7 +275,8 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 8,
|
||||
},
|
||||
footer: {
|
||||
padding: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
backgroundColor: '#FFF',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Button } from '../components/ui/Button';
|
||||
import { useCaregivers } from '../hooks/useCaregivers';
|
||||
import { formatRating, getInitials, getImageUrl } from '../utils/formatters';
|
||||
import { useResponsiveLayout } from '../hooks/useResponsiveLayout';
|
||||
import { User, Award, Calendar } from 'lucide-react-native';
|
||||
import { User, Award, Calendar, Briefcase, ChevronRight, FileText } from 'lucide-react-native';
|
||||
import { profileService, CaregiverProfileResponse } from '../services/profileService';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
@@ -108,6 +108,40 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ navigation }) => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity onPress={() => navigation.navigate('ManageServices')} style={{marginBottom: 24}}>
|
||||
<Card noPadding style={styles.menuItem}>
|
||||
<View style={styles.menuIconContainer}>
|
||||
<Briefcase color="#4F46E5" size={24} />
|
||||
</View>
|
||||
<View style={styles.menuTextContainer}>
|
||||
<Typography variant="body" weight="bold" style={styles.menuTitle}>
|
||||
Manage Services
|
||||
</Typography>
|
||||
<Typography variant="caption" style={styles.menuSubtitle}>
|
||||
Add or edit your offered services
|
||||
</Typography>
|
||||
</View>
|
||||
<ChevronRight color="#9CA3AF" size={20} />
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => navigation.navigate('ProfessionalDocuments' as any)} style={{marginBottom: 24}}>
|
||||
<Card noPadding style={styles.menuItem}>
|
||||
<View style={styles.menuIconContainer}>
|
||||
<FileText color="#4F46E5" size={24} />
|
||||
</View>
|
||||
<View style={styles.menuTextContainer}>
|
||||
<Typography variant="body" weight="bold" style={styles.menuTitle}>
|
||||
Professional Documents
|
||||
</Typography>
|
||||
<Typography variant="caption" style={styles.menuSubtitle}>
|
||||
Upload and manage your documents
|
||||
</Typography>
|
||||
</View>
|
||||
<ChevronRight color="#9CA3AF" size={20} />
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<Typography variant="h3" weight="bold">
|
||||
Featured Caregivers
|
||||
@@ -310,5 +344,30 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
tabletGridItem: {
|
||||
width: '48%', // Leaves some space for margins between the two columns
|
||||
},
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 24,
|
||||
},
|
||||
menuIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#EEF2FF',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
menuTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
menuTitle: {
|
||||
color: '#111827',
|
||||
marginBottom: 2,
|
||||
},
|
||||
menuSubtitle: {
|
||||
color: '#6B7280',
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, StyleSheet, FlatList, Alert, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Header } from '../components/ui/Header';
|
||||
@@ -9,6 +9,7 @@ import { THEME } from '../constants/theme';
|
||||
import { serviceService, Service } from '../services/serviceService';
|
||||
import { Edit2, Trash2, Plus } from 'lucide-react-native';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
interface ManageServicesScreenProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList, 'ManageServices'>;
|
||||
@@ -18,6 +19,8 @@ export const ManageServicesScreen: React.FC<ManageServicesScreenProps> = ({ navi
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const isFocused = useIsFocused();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
@@ -53,7 +56,7 @@ export const ManageServicesScreen: React.FC<ManageServicesScreenProps> = ({ navi
|
||||
onPress: async () => {
|
||||
try {
|
||||
await serviceService.deleteService(id);
|
||||
Alert.alert('Success', 'Service deleted successfully.');
|
||||
showSnackbar('Service deleted successfully.');
|
||||
fetchServices();
|
||||
} catch (error) {
|
||||
console.error('Error deleting service:', error);
|
||||
@@ -141,7 +144,7 @@ export const ManageServicesScreen: React.FC<ManageServicesScreenProps> = ({ navi
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.fab}
|
||||
style={[styles.fab, { bottom: Math.max(insets.bottom + 24, 24), right: Math.max(insets.right + 24, 24) }]}
|
||||
onPress={() => navigation.navigate('AddEditService', {})}
|
||||
>
|
||||
<Plus color="#FFF" size={24} />
|
||||
@@ -232,8 +235,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity, Text } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { View, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity, Text, Alert } from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
@@ -8,7 +8,8 @@ import { Header } from '../components/ui/Header';
|
||||
import { profileService } from '../services/profileService';
|
||||
import { THEME } from '../constants/theme';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Plus, Award } from 'lucide-react-native';
|
||||
import { Plus, Award, Trash2 } from 'lucide-react-native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'ManageSkills'>;
|
||||
|
||||
@@ -16,6 +17,8 @@ export const ManageSkillsScreen = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const [ownSkills, setOwnSkills] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
const fetchSkills = async () => {
|
||||
try {
|
||||
@@ -35,6 +38,30 @@ export const ManageSkillsScreen = () => {
|
||||
}, [])
|
||||
);
|
||||
|
||||
const handleDelete = (skillSlug: string, skillName: string) => {
|
||||
Alert.alert(
|
||||
"Remove Skill",
|
||||
`Are you sure you want to remove ${skillName}?`,
|
||||
[
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Remove",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await profileService.removeSkills([skillSlug]);
|
||||
showSnackbar('Skill removed successfully.');
|
||||
fetchSkills();
|
||||
} catch (error) {
|
||||
console.error('Failed to remove skill', error);
|
||||
Alert.alert('Error', 'Failed to remove skill. Please try again.');
|
||||
}
|
||||
},
|
||||
style: "destructive"
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<SafeAreaView edges={['top', 'left', 'right']} style={styles.container as any}>
|
||||
@@ -89,6 +116,12 @@ export const ManageSkillsScreen = () => {
|
||||
</Typography>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleDelete(skill.slug, skill.name)}
|
||||
>
|
||||
<Trash2 color="#EF4444" size={20} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
@@ -97,7 +130,7 @@ export const ManageSkillsScreen = () => {
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.fab}
|
||||
style={[styles.fab, { bottom: Math.max(insets.bottom + 24, 24), right: Math.max(insets.right + 24, 24) }]}
|
||||
onPress={() => navigation.navigate('AddSkill')}
|
||||
>
|
||||
<Plus color="#FFF" size={24} />
|
||||
@@ -231,13 +264,15 @@ const styles = StyleSheet.create({
|
||||
color: THEME.colors.primary,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
bottomSpacer: {
|
||||
height: 100,
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, StyleSheet, ScrollView, TouchableOpacity, RefreshControl } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { THEME } from '../constants/theme';
|
||||
import { profileService, PaymentProfile } from '../services/profileService';
|
||||
import { Plus, CreditCard, Landmark } from 'lucide-react-native';
|
||||
|
||||
interface PaymentProfileListScreenProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList, 'PaymentProfileList'>;
|
||||
}
|
||||
|
||||
export const PaymentProfileListScreen: React.FC<PaymentProfileListScreenProps> = ({ navigation }) => {
|
||||
const [profiles, setProfiles] = useState<PaymentProfile[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const fetchProfiles = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await profileService.getPaymentProfile();
|
||||
|
||||
// Handle both array and single object responses for backward compatibility
|
||||
let profilesData: PaymentProfile[] = [];
|
||||
if (response && response.data) {
|
||||
if (Array.isArray(response.data)) {
|
||||
profilesData = response.data;
|
||||
} else if (response.data.id) {
|
||||
profilesData = [response.data];
|
||||
}
|
||||
}
|
||||
setProfiles(profilesData);
|
||||
} catch (error) {
|
||||
console.log('No payment profiles found or error fetching.');
|
||||
setProfiles([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchProfiles();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const maskAccountNumber = (accountNumber: string) => {
|
||||
if (!accountNumber) return '**** **** **** ****';
|
||||
const last4 = accountNumber.slice(-4);
|
||||
return `**** **** **** ${last4}`;
|
||||
};
|
||||
|
||||
const renderCard = (profile: PaymentProfile) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={profile.id}
|
||||
style={styles.cardContainer}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => navigation.navigate('AddEditPaymentProfile', { profile })}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.bankNameRow}>
|
||||
<Landmark color="#FFF" size={20} />
|
||||
<Typography variant="body" weight="bold" style={styles.bankName}>
|
||||
{profile.bank_name || 'Bank Account'}
|
||||
</Typography>
|
||||
</View>
|
||||
<CreditCard color="#FFF" size={24} />
|
||||
</View>
|
||||
|
||||
<View style={styles.cardNumberContainer}>
|
||||
<Typography variant="h3" weight="bold" style={styles.cardNumber}>
|
||||
{maskAccountNumber(profile.account_number)}
|
||||
</Typography>
|
||||
</View>
|
||||
|
||||
<View style={styles.cardFooter}>
|
||||
<View>
|
||||
<Typography variant="caption" style={styles.cardLabel}>Cardholder Name</Typography>
|
||||
<Typography variant="body" weight="bold" style={styles.cardValue}>
|
||||
{profile.account_holder_name?.toUpperCase() || 'UNKNOWN'}
|
||||
</Typography>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Typography variant="caption" style={styles.cardLabel}>IFSC Code</Typography>
|
||||
<Typography variant="body" weight="bold" style={styles.cardValue}>
|
||||
{profile.ifsc_code || '---'}
|
||||
</Typography>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
||||
<Header title="Payment Profiles" showBack />
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={fetchProfiles} />}
|
||||
>
|
||||
{profiles.length === 0 && !isLoading ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<CreditCard color={THEME.colors.primary} size={40} />
|
||||
</View>
|
||||
<Typography variant="h3" weight="bold" style={styles.emptyTitle}>
|
||||
No Accounts Added
|
||||
</Typography>
|
||||
<Typography variant="body" style={styles.emptySubtitle}>
|
||||
Add your bank account details to receive your payouts securely.
|
||||
</Typography>
|
||||
</View>
|
||||
) : (
|
||||
profiles.map(renderCard)
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.fab, { bottom: Math.max(insets.bottom + 20, 20), right: Math.max(insets.right + 20, 20) }]}
|
||||
onPress={() => navigation.navigate('AddEditPaymentProfile', { profile: undefined })}
|
||||
>
|
||||
<Plus color="#FFF" size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: THEME.colors.background,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 16,
|
||||
paddingBottom: 100, // Extra padding for FAB
|
||||
flexGrow: 1,
|
||||
},
|
||||
cardContainer: {
|
||||
backgroundColor: '#0F172A', // Very dark slate, almost black, looks premium
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 32,
|
||||
},
|
||||
bankNameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
bankName: {
|
||||
color: '#FFF',
|
||||
marginLeft: 8,
|
||||
fontSize: 16,
|
||||
},
|
||||
cardNumberContainer: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
cardNumber: {
|
||||
color: '#FFF',
|
||||
letterSpacing: 2,
|
||||
fontSize: 22,
|
||||
},
|
||||
cardFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
cardLabel: {
|
||||
color: '#94A3B8', // Slate 400
|
||||
marginBottom: 4,
|
||||
fontSize: 10,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
cardValue: {
|
||||
color: '#FFF',
|
||||
fontSize: 14,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: THEME.colors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 6,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 60,
|
||||
},
|
||||
iconWrapper: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: THEME.colors.primaryLighter,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
emptyTitle: {
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptySubtitle: {
|
||||
color: THEME.colors.text.secondary,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
});
|
||||
@@ -8,12 +8,14 @@ import { profileService, CaregiverProfileResponse } from '../services/profileSer
|
||||
import { getImageUrl } from '../utils/formatters';
|
||||
import { launchImageLibrary } from 'react-native-image-picker';
|
||||
import { Edit2, Camera } from 'lucide-react-native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
export const PersonalInformationScreen = () => {
|
||||
const [profile, setProfile] = useState<CaregiverProfileResponse['data'] | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
// Form State
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -109,8 +111,10 @@ export const PersonalInformationScreen = () => {
|
||||
await profileService.updateProfile(payload);
|
||||
await fetchProfile();
|
||||
setIsEditing(false);
|
||||
showSnackbar('Personal information updated successfully.');
|
||||
} catch (err) {
|
||||
console.error('Failed to save profile', err);
|
||||
showSnackbar('Failed to update personal information.', 'error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -120,13 +124,13 @@ export const PersonalInformationScreen = () => {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<TouchableOpacity onPress={handleSave} disabled={saving} style={styles.headerButton}>
|
||||
{saving ? <ActivityIndicator size="small" color="#4F46E5" /> : <Typography variant="body" weight="bold" style={styles.headerButtonText}>Save</Typography>}
|
||||
{saving ? <ActivityIndicator size="small" color="#FFFFFF" /> : <Typography variant="body" weight="bold" style={styles.headerButtonText}>Save</Typography>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity onPress={() => setIsEditing(true)} style={styles.headerButton}>
|
||||
<Edit2 color="#4F46E5" size={20} />
|
||||
<Edit2 color="#FFFFFF" size={20} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -289,7 +293,7 @@ const styles = StyleSheet.create({
|
||||
padding: 8,
|
||||
},
|
||||
headerButtonText: {
|
||||
color: '#4F46E5',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
card: {
|
||||
padding: 0,
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, StyleSheet, FlatList, ActivityIndicator, Alert, TouchableOpacity, Image } from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { documentService, ProfessionalDocument } from '../services/documentService';
|
||||
import { FileText, Trash2, Plus } from 'lucide-react-native';
|
||||
import { getImageUrl } from '../utils/formatters';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'ProfessionalDocuments'>;
|
||||
|
||||
export const ProfessionalDocumentsScreen: React.FC<Props> = ({ navigation }) => {
|
||||
const [documents, setDocuments] = useState<ProfessionalDocument[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
const fetchDocuments = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await documentService.getDocuments();
|
||||
setDocuments(response.data || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch documents', error);
|
||||
Alert.alert('Error', 'Failed to load documents');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
fetchDocuments();
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
Alert.alert(
|
||||
"Delete Document",
|
||||
"Are you sure you want to delete this document?",
|
||||
[
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Delete",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await documentService.deleteDocument(id);
|
||||
showSnackbar('Document deleted successfully.');
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete', error);
|
||||
Alert.alert('Error', 'Failed to delete document');
|
||||
}
|
||||
},
|
||||
style: "destructive"
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: ProfessionalDocument }) => (
|
||||
<TouchableOpacity activeOpacity={0.8} onPress={() => navigation.navigate('ViewDocument', { documentId: item.id })}>
|
||||
<Card style={styles.documentCard}>
|
||||
<View style={styles.docHeader}>
|
||||
<View style={styles.titleContainer}>
|
||||
<FileText color="#1E3A8A" size={24} style={styles.docIcon} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Typography variant="body" weight="bold" style={styles.docTitle}>
|
||||
{item.addition_info?.title || 'Untitled Document'}
|
||||
</Typography>
|
||||
<Typography variant="caption" style={styles.docDate}>
|
||||
{new Date(item.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteBtn}>
|
||||
<Trash2 color="#EF4444" size={20} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{item.addition_info?.notes ? (
|
||||
<Typography variant="caption" style={styles.docNotes}>
|
||||
{item.addition_info.notes}
|
||||
</Typography>
|
||||
) : null}
|
||||
{item.properties?.url ? (
|
||||
<Image source={{ uri: getImageUrl(item.properties.url) }} style={styles.previewImage} />
|
||||
) : null}
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<SafeAreaView edges={['top', 'left', 'right']} style={styles.container}>
|
||||
<Header title="Professional Documents" showBack={true} />
|
||||
<View style={styles.content}>
|
||||
{isLoading ? (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="large" color="#1E3A8A" />
|
||||
</View>
|
||||
) : documents.length === 0 ? (
|
||||
<View style={styles.centerContainer}>
|
||||
<FileText color="#D1D5DB" size={64} style={{ marginBottom: 16 }} />
|
||||
<Typography variant="body" style={styles.emptyText}>
|
||||
No professional documents found.
|
||||
</Typography>
|
||||
<Typography variant="caption" style={styles.emptySubText}>
|
||||
Upload your licenses, certificates, and other professional documents here.
|
||||
</Typography>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={documents}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={[styles.fabContainer, { bottom: Math.max(insets.bottom + 16, 16) }]}>
|
||||
<Button
|
||||
title="Upload Document"
|
||||
onPress={() => navigation.navigate('UploadDocument')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F9FAFB' },
|
||||
content: { flex: 1, padding: 16 },
|
||||
centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
|
||||
listContent: { paddingBottom: 100 },
|
||||
documentCard: { marginBottom: 16, padding: 16 },
|
||||
docHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 },
|
||||
titleContainer: { flexDirection: 'row', alignItems: 'center', flex: 1 },
|
||||
docIcon: { marginRight: 12 },
|
||||
docTitle: { color: '#111827', marginBottom: 4 },
|
||||
docDate: { color: '#6B7280' },
|
||||
deleteBtn: { padding: 8, marginLeft: 8 },
|
||||
docNotes: { color: '#4B5563', marginBottom: 12, backgroundColor: '#F3F4F6', padding: 8, borderRadius: 6 },
|
||||
previewImage: { width: '100%', height: 150, borderRadius: 8, resizeMode: 'cover', marginTop: 8 },
|
||||
emptyText: { color: '#4B5563', fontWeight: '500', marginBottom: 8, textAlign: 'center' },
|
||||
emptySubText: { color: '#9CA3AF', textAlign: 'center' },
|
||||
fabContainer: { position: 'absolute', left: 16, right: 16 },
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Search, Check, X } from 'lucide-react-native';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { THEME } from '../constants/theme';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
|
||||
type SelectionScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Selection'>;
|
||||
type SelectionScreenRouteProp = RouteProp<RootStackParamList, 'Selection'>;
|
||||
|
||||
interface SelectionScreenProps {
|
||||
navigation: SelectionScreenNavigationProp;
|
||||
route: SelectionScreenRouteProp;
|
||||
}
|
||||
|
||||
export const SelectionScreen: React.FC<SelectionScreenProps> = ({ navigation, route }) => {
|
||||
const {
|
||||
data,
|
||||
value,
|
||||
isMulti,
|
||||
searchable,
|
||||
label,
|
||||
searchPlaceholder = 'Search...',
|
||||
onSelect,
|
||||
} = route.params;
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchQuery) return data;
|
||||
return data.filter((item: any) =>
|
||||
item.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [data, searchQuery]);
|
||||
|
||||
const handleSelect = (item: any) => {
|
||||
if (isMulti) {
|
||||
const currentValue = (Array.isArray(value) ? value : []) as (string | number)[];
|
||||
const isSelected = currentValue.includes(item.value);
|
||||
|
||||
let newValue;
|
||||
if (isSelected) {
|
||||
newValue = currentValue.filter((val) => val !== item.value);
|
||||
} else {
|
||||
newValue = [...currentValue, item.value];
|
||||
}
|
||||
// Instantly update route params for immediate visual feedback
|
||||
navigation.setParams({ value: newValue });
|
||||
onSelect(newValue);
|
||||
} else {
|
||||
onSelect(item.value);
|
||||
navigation.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
const isItemSelected = (item: any) => {
|
||||
if (isMulti) {
|
||||
const currentValue = (Array.isArray(value) ? value : []) as (string | number)[];
|
||||
return currentValue.includes(item.value);
|
||||
}
|
||||
return value === item.value;
|
||||
};
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
||||
<Header title={label || 'Select'} showBack />
|
||||
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
style={styles.content}
|
||||
>
|
||||
|
||||
{searchable && (
|
||||
<View style={styles.searchContainer}>
|
||||
<Search color="#9CA3AF" size={20} style={styles.searchIcon} />
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder={searchPlaceholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
||||
<X color="#9CA3AF" size={16} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FlatList
|
||||
data={filteredData}
|
||||
keyExtractor={(item, index) => item.value.toString() + index}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
renderItem={({ item }) => {
|
||||
const selected = isItemSelected(item);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.itemContainer,
|
||||
selected && styles.itemSelected,
|
||||
]}
|
||||
onPress={() => handleSelect(item)}
|
||||
>
|
||||
<Text style={[styles.itemText, selected && styles.itemTextSelected]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
{selected && <Check color={THEME.colors.primary} size={20} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Typography variant="body" style={styles.emptyText}>
|
||||
No options found
|
||||
</Typography>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 12,
|
||||
height: 48,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
listContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
itemContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F3F4F6',
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
itemSelected: {
|
||||
backgroundColor: THEME.colors.primaryLighter,
|
||||
borderColor: THEME.colors.primaryLight,
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
itemText: {
|
||||
fontSize: 16,
|
||||
color: '#374151',
|
||||
},
|
||||
itemTextSelected: {
|
||||
color: THEME.colors.primary,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
emptyContainer: {
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyText: {
|
||||
color: '#6B7280',
|
||||
},
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { User, ChevronRight, LogOut, CreditCard, Briefcase } from 'lucide-react-native';
|
||||
import { User, ChevronRight, LogOut, CreditCard } from 'lucide-react-native';
|
||||
import { CompositeNavigationProp } from '@react-navigation/native';
|
||||
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
@@ -68,7 +68,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({ navigation }) =>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => navigation.navigate('PaymentProfile')}>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('PaymentProfileList')}>
|
||||
<Card noPadding style={styles.menuItem}>
|
||||
<View style={styles.menuIconContainer}>
|
||||
<CreditCard color="#4F46E5" size={24} />
|
||||
@@ -85,23 +85,6 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({ navigation }) =>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => navigation.navigate('ManageServices')}>
|
||||
<Card noPadding style={styles.menuItem}>
|
||||
<View style={styles.menuIconContainer}>
|
||||
<Briefcase color="#4F46E5" size={24} />
|
||||
</View>
|
||||
<View style={styles.menuTextContainer}>
|
||||
<Typography variant="body" weight="bold" style={styles.menuTitle}>
|
||||
Manage Services
|
||||
</Typography>
|
||||
<Typography variant="caption" style={styles.menuSubtitle}>
|
||||
Add or edit your offered services
|
||||
</Typography>
|
||||
</View>
|
||||
<ChevronRight color="#9CA3AF" size={20} />
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={handleLogout}>
|
||||
<Card noPadding style={styles.menuItem}>
|
||||
<View style={[styles.menuIconContainer, { backgroundColor: '#FEE2E2' }]}>
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, Image, Alert, ScrollView, ActivityIndicator } from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { documentService } from '../services/documentService';
|
||||
import { launchImageLibrary, ImagePickerResponse, Asset } from 'react-native-image-picker';
|
||||
import { Upload } from 'lucide-react-native';
|
||||
import { useSnackbar } from '../context/SnackbarContext';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'UploadDocument'>;
|
||||
|
||||
export const UploadDocumentScreen: React.FC<Props> = ({ navigation }) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [document, setDocument] = useState<Asset | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
const handleSelectDocument = () => {
|
||||
launchImageLibrary(
|
||||
{
|
||||
mediaType: 'photo',
|
||||
quality: 0.8,
|
||||
},
|
||||
(response: ImagePickerResponse) => {
|
||||
if (response.didCancel) {
|
||||
console.log('User cancelled image picker');
|
||||
} else if (response.errorCode) {
|
||||
console.log('ImagePicker Error: ', response.errorMessage);
|
||||
Alert.alert('Error', response.errorMessage || 'Failed to pick image');
|
||||
} else if (response.assets && response.assets.length > 0) {
|
||||
setDocument(response.assets[0]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!title.trim() || !document) {
|
||||
Alert.alert('Error', 'Please provide a title and select a document.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const formData = new FormData();
|
||||
formData.append('title', title);
|
||||
formData.append('notes', notes);
|
||||
|
||||
formData.append('document', {
|
||||
uri: document.uri,
|
||||
type: document.type || 'image/jpeg',
|
||||
name: document.fileName || `document-${Date.now()}.jpg`,
|
||||
} as any);
|
||||
|
||||
await documentService.uploadDocument(formData);
|
||||
showSnackbar('Document uploaded successfully');
|
||||
navigation.goBack();
|
||||
} catch (error: any) {
|
||||
console.error('Upload Error', error);
|
||||
Alert.alert('Error', error.response?.data?.message || 'Failed to upload document. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<SafeAreaView edges={['top', 'left', 'right']} style={styles.container}>
|
||||
<Header title="Upload Document" showBack={true} />
|
||||
<ScrollView style={styles.content} keyboardShouldPersistTaps="handled">
|
||||
<Typography variant="body" weight="medium" style={styles.label}>
|
||||
Document Title *
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder="e.g. Nursing License"
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
/>
|
||||
|
||||
<Typography variant="body" weight="medium" style={styles.label}>
|
||||
Notes
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder="e.g. Registered Nurse License exp 2028"
|
||||
value={notes}
|
||||
onChangeText={setNotes}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
style={styles.textArea}
|
||||
/>
|
||||
|
||||
<Typography variant="body" weight="medium" style={styles.label}>
|
||||
Attachment *
|
||||
</Typography>
|
||||
<TouchableOpacity style={styles.uploadContainer} onPress={handleSelectDocument}>
|
||||
{document ? (
|
||||
<Image source={{ uri: document.uri }} style={styles.previewImage} />
|
||||
) : (
|
||||
<View style={styles.uploadPlaceholder}>
|
||||
<Upload color="#1E3A8A" size={32} />
|
||||
<Typography variant="body" style={styles.uploadText}>
|
||||
Tap to select a document
|
||||
</Typography>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{document && (
|
||||
<TouchableOpacity onPress={() => setDocument(null)} style={styles.removeBtn}>
|
||||
<Typography variant="caption" style={styles.removeBtnText}>Remove</Typography>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom + 40, 40) }]}>
|
||||
<Button
|
||||
title={isSubmitting ? "Uploading..." : "Upload Document"}
|
||||
onPress={handleUpload}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F9FAFB' },
|
||||
content: { flex: 1, padding: 16 },
|
||||
label: { marginBottom: 8, marginTop: 16, color: '#374151' },
|
||||
textArea: { height: 100, textAlignVertical: 'top' },
|
||||
uploadContainer: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#E5E7EB',
|
||||
borderStyle: 'dashed',
|
||||
borderRadius: 12,
|
||||
height: 200,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#FFFFFF',
|
||||
marginBottom: 8,
|
||||
},
|
||||
uploadPlaceholder: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
uploadText: { marginTop: 12, color: '#6B7280' },
|
||||
previewImage: { width: '100%', height: '100%', resizeMode: 'cover' },
|
||||
removeBtn: { alignSelf: 'flex-end', marginBottom: 24 },
|
||||
removeBtnText: { color: '#EF4444' },
|
||||
footer: { marginTop: 32 },
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, StyleSheet, ActivityIndicator, Alert, Image, ScrollView } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Header } from '../components/ui/Header';
|
||||
import { Typography } from '../components/ui/Typography';
|
||||
import { documentService, ProfessionalDocument } from '../services/documentService';
|
||||
import { FileText } from 'lucide-react-native';
|
||||
import { getImageUrl } from '../utils/formatters';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'ViewDocument'>;
|
||||
|
||||
export const ViewDocumentScreen: React.FC<Props> = ({ route, navigation }) => {
|
||||
const { documentId } = route.params;
|
||||
const [document, setDocument] = useState<ProfessionalDocument | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDocument = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await documentService.getDocumentById(documentId);
|
||||
setDocument(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch document', error);
|
||||
Alert.alert('Error', 'Failed to load document details');
|
||||
navigation.goBack();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchDocument();
|
||||
}, [documentId, navigation]);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<SafeAreaView edges={['top', 'left', 'right']} style={styles.container}>
|
||||
<Header title="View Document" showBack={true} />
|
||||
{isLoading ? (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="large" color="#1E3A8A" />
|
||||
</View>
|
||||
) : document ? (
|
||||
<ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 40 }}>
|
||||
<View style={styles.headerSection}>
|
||||
<FileText color="#1E3A8A" size={32} style={styles.icon} />
|
||||
<View style={styles.headerText}>
|
||||
<Typography variant="h2" weight="bold">
|
||||
{document.addition_info?.title || 'Untitled Document'}
|
||||
</Typography>
|
||||
<Typography variant="caption" style={styles.date}>
|
||||
Uploaded on {new Date(document.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{document.addition_info?.notes ? (
|
||||
<View style={styles.notesSection}>
|
||||
<Typography variant="body" weight="medium" style={styles.sectionTitle}>
|
||||
Notes
|
||||
</Typography>
|
||||
<Typography variant="body" style={styles.notesText}>
|
||||
{document.addition_info.notes}
|
||||
</Typography>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{document.properties?.url ? (
|
||||
<View style={styles.imageSection}>
|
||||
<Typography variant="body" weight="medium" style={styles.sectionTitle}>
|
||||
Document Preview
|
||||
</Typography>
|
||||
<Image
|
||||
source={{ uri: getImageUrl(document.properties.url) }}
|
||||
style={styles.fullImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.centerContainer}>
|
||||
<Typography variant="body" style={{color: '#6B7280'}}>No image preview available</Typography>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
) : null}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F9FAFB' },
|
||||
centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
content: { flex: 1, padding: 16 },
|
||||
headerSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 24, paddingBottom: 16, borderBottomWidth: 1, borderBottomColor: '#E5E7EB' },
|
||||
icon: { marginRight: 16 },
|
||||
headerText: { flex: 1 },
|
||||
date: { color: '#6B7280', marginTop: 4 },
|
||||
notesSection: { marginBottom: 24, backgroundColor: '#F3F4F6', padding: 16, borderRadius: 8 },
|
||||
sectionTitle: { marginBottom: 8, color: '#374151' },
|
||||
notesText: { color: '#4B5563' },
|
||||
imageSection: { flex: 1 },
|
||||
fullImage: { width: '100%', height: 400, backgroundColor: '#E5E7EB', borderRadius: 8, marginTop: 8 },
|
||||
});
|
||||
@@ -13,6 +13,7 @@ export const apiClient = axios.create({
|
||||
});
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { navigate } from '../navigation/navigationRef';
|
||||
|
||||
// Optionally, we can add interceptors here later to inject Bearer tokens
|
||||
apiClient.interceptors.request.use(
|
||||
@@ -37,9 +38,10 @@ apiClient.interceptors.response.use(
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Check if error is 401 and we have not already retried
|
||||
const errorCode = error.response?.data?.errors?.code;
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
error.response?.data?.errors?.code === 'TOKEN_EXPIRED' &&
|
||||
(errorCode === 'TOKEN_EXPIRED' || errorCode === 'UNAUTHENTICATED') &&
|
||||
!originalRequest._retry
|
||||
) {
|
||||
originalRequest._retry = true; // Mark as retried to prevent infinite loops
|
||||
@@ -53,7 +55,7 @@ apiClient.interceptors.response.use(
|
||||
|
||||
// Call your refresh endpoint
|
||||
const refreshResponse = await axios.post(`${ENV.API_BASE_URL}/v1/caregiver/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
refreshToken: refreshToken,
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -63,9 +65,9 @@ apiClient.interceptors.response.use(
|
||||
});
|
||||
|
||||
// The exact path depends on your backend response structure
|
||||
// Usually it's refreshResponse.data.data.access_token
|
||||
const newAccessToken = refreshResponse.data?.data?.access_token || refreshResponse.data?.access_token;
|
||||
const newRefreshToken = refreshResponse.data?.data?.refresh_token || refreshResponse.data?.refresh_token;
|
||||
// Usually it's refreshResponse.data.data.accessToken
|
||||
const newAccessToken = refreshResponse.data?.data?.accessToken || refreshResponse.data?.accessToken;
|
||||
const newRefreshToken = refreshResponse.data?.data?.refreshToken || refreshResponse.data?.refreshToken;
|
||||
|
||||
if (newAccessToken) {
|
||||
await AsyncStorage.setItem('token', newAccessToken); // Ensure 'token' key matches what you use
|
||||
@@ -87,7 +89,8 @@ apiClient.interceptors.response.use(
|
||||
// Force user to log out by clearing tokens
|
||||
await AsyncStorage.removeItem('token');
|
||||
await AsyncStorage.removeItem('refresh_token');
|
||||
// If you are using Context or Redux, you may need to dispatch a logout action here
|
||||
// Force navigate to Login screen
|
||||
navigate('Login');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { apiClient } from './apiClient';
|
||||
|
||||
export interface ProfessionalDocument {
|
||||
id: number;
|
||||
properties?: {
|
||||
ext?: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
mime_type?: string;
|
||||
};
|
||||
addition_info?: {
|
||||
title?: string;
|
||||
notes?: string;
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface DocumentResponse {
|
||||
success: boolean;
|
||||
data: ProfessionalDocument[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface SingleDocumentResponse {
|
||||
success: boolean;
|
||||
data: ProfessionalDocument;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const documentService = {
|
||||
getDocuments: async () => {
|
||||
const response = await apiClient.get<DocumentResponse>('/v1/resource/caregiver/professional-documents');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getDocumentById: async (id: number) => {
|
||||
const response = await apiClient.get<SingleDocumentResponse>(`/v1/resource/caregiver/professional-documents/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
uploadDocument: async (formData: FormData) => {
|
||||
const response = await apiClient.post<SingleDocumentResponse>(
|
||||
'/v1/resource/caregiver/professional-documents',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteDocument: async (id: number) => {
|
||||
const response = await apiClient.delete(`/v1/resource/caregiver/professional-documents/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -37,6 +37,10 @@ export interface CaregiverProfileResponse {
|
||||
profile_image: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
profile?: {
|
||||
id: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
errors: any;
|
||||
meta: any;
|
||||
@@ -149,6 +153,19 @@ export const profileService = {
|
||||
}
|
||||
},
|
||||
|
||||
removeSkills: async (skills: string[]): Promise<any> => {
|
||||
try {
|
||||
const response = await apiClient.post('/v1/resource/caregiver/skills/remove', {
|
||||
skills,
|
||||
force: 'yes'
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error removing skills:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getAvailability: async (): Promise<CaregiverAvailability[]> => {
|
||||
try {
|
||||
const response = await apiClient.get('/v1/resource/caregiver/availability');
|
||||
|
||||
@@ -2,19 +2,33 @@ import { apiClient } from './apiClient';
|
||||
|
||||
export interface Service {
|
||||
id?: number;
|
||||
name: string;
|
||||
service_id?: number;
|
||||
caregiver_id?: number;
|
||||
service_name?: string;
|
||||
name?: string; // Sometimes APIs map this interchangeably
|
||||
care_type_slug?: string;
|
||||
care_sub_type_slug?: string;
|
||||
category?: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
unit?: string;
|
||||
customer_price?: number | string;
|
||||
caregiver_rate?: number | string;
|
||||
custom_rate?: number | string;
|
||||
effective_rate?: number | string;
|
||||
tax?: number | string;
|
||||
duration_minutes?: number | string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface AddServicePayload {
|
||||
service_id: number;
|
||||
caregiver_id: number;
|
||||
custom_rate: number | string;
|
||||
is_active: boolean;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export const serviceService = {
|
||||
getServices: async (filters?: { skills?: string }): Promise<Service[]> => {
|
||||
try {
|
||||
@@ -31,6 +45,16 @@ export const serviceService = {
|
||||
}
|
||||
},
|
||||
|
||||
getSkillBasedServices: async (): Promise<Service[]> => {
|
||||
try {
|
||||
const response = await apiClient.get('/v1/caregivers/services/my-skills');
|
||||
return response.data?.data || response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching skill-based services:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getTaxes: async (): Promise<any[]> => {
|
||||
try {
|
||||
const response = await apiClient.get('/v1/resource/taxes');
|
||||
@@ -51,9 +75,11 @@ export const serviceService = {
|
||||
}
|
||||
},
|
||||
|
||||
addService: async (data: Service): Promise<any> => {
|
||||
addService: async (data: AddServicePayload): Promise<any> => {
|
||||
try {
|
||||
const response = await apiClient.post('/v1/caregiver/services', data);
|
||||
// According to backend routes, it can be POST /v1/resource/caregiver/services
|
||||
// which delegates to httpAddOwnService
|
||||
const response = await apiClient.post('/v1/resource/caregiver/services', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error adding service:', error);
|
||||
@@ -61,9 +87,9 @@ export const serviceService = {
|
||||
}
|
||||
},
|
||||
|
||||
updateService: async (id: number, data: Partial<Service>): Promise<any> => {
|
||||
updateService: async (id: number, data: Partial<AddServicePayload>): Promise<any> => {
|
||||
try {
|
||||
const response = await apiClient.patch(`/v1/caregiver/services/${id}`, data);
|
||||
const response = await apiClient.patch(`/v1/resource/caregiver/services/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error updating service with id ${id}:`, error);
|
||||
@@ -73,7 +99,7 @@ export const serviceService = {
|
||||
|
||||
deleteService: async (id: number): Promise<any> => {
|
||||
try {
|
||||
const response = await apiClient.delete(`/v1/caregiver/services/${id}`);
|
||||
const response = await apiClient.delete(`/v1/resource/caregiver/services/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting service with id ${id}:`, error);
|
||||
|
||||
+11
-3
@@ -21,10 +21,18 @@ import { ENV } from '../config/env';
|
||||
*/
|
||||
export const getImageUrl = (path?: string | null): string | undefined => {
|
||||
if (!path) return undefined;
|
||||
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('file://') || path.startsWith('content://') || path.startsWith('data:')) {
|
||||
return path;
|
||||
|
||||
let processedPath = path;
|
||||
if (processedPath.startsWith('http://localhost')) {
|
||||
const baseUrl = ENV.API_BASE_URL.endsWith('/') ? ENV.API_BASE_URL.slice(0, -1) : ENV.API_BASE_URL;
|
||||
// Replace http://localhost with our API base url
|
||||
processedPath = processedPath.replace('http://localhost', baseUrl);
|
||||
}
|
||||
|
||||
if (processedPath.startsWith('http://') || processedPath.startsWith('https://') || processedPath.startsWith('file://') || processedPath.startsWith('content://') || processedPath.startsWith('data:')) {
|
||||
return processedPath;
|
||||
}
|
||||
const baseUrl = ENV.API_BASE_URL.endsWith('/') ? ENV.API_BASE_URL.slice(0, -1) : ENV.API_BASE_URL;
|
||||
const imagePath = path.startsWith('/') ? path : `/${path}`;
|
||||
const imagePath = processedPath.startsWith('/') ? processedPath : `/${processedPath}`;
|
||||
return `${baseUrl}${imagePath}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user