diff --git a/App.tsx b/App.tsx
index d45bedd..a8caa06 100644
--- a/App.tsx
+++ b/App.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { AppNavigator } from './src/navigation/AppNavigator';
import { SafeAreaProvider } from 'react-native-safe-area-context';
+import { SnackbarProvider } from './src/context/SnackbarContext';
import { LogBox, StatusBar } from 'react-native';
// LogBox.ignoreAllLogs(true);
@@ -16,7 +17,9 @@ function App(): React.JSX.Element {
return (
-
+
+
+
);
}
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 0db643d..a633cde 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -57,7 +57,7 @@ export const Button: React.FC = ({
{...props}
>
{loading ? (
-
+
) : (
{title}
@@ -80,14 +80,14 @@ const styles = StyleSheet.create({
backgroundColor: '#D1D5DB',
},
bgPrimary: {
- backgroundColor: '#4F46E5',
+ backgroundColor: '#1E3A8A',
},
bgSecondary: {
backgroundColor: '#10B981',
},
bgOutline: {
backgroundColor: 'transparent',
- borderColor: '#4F46E5',
+ borderColor: '#1E3A8A',
borderWidth: 2,
},
bgGhost: {
@@ -100,6 +100,6 @@ const styles = StyleSheet.create({
color: '#FFFFFF',
},
textPrimary: {
- color: '#4F46E5',
+ color: '#1E3A8A',
},
});
diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx
new file mode 100644
index 0000000..d22b641
--- /dev/null
+++ b/src/components/ui/Dropdown.tsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+} from 'react-native';
+import { ChevronDown } from 'lucide-react-native';
+import { Typography } from './Typography';
+import { useNavigation } from '@react-navigation/native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { RootStackParamList } from '../../navigation/AppNavigator';
+
+export interface DropdownItem {
+ label: string;
+ value: string | number;
+}
+
+interface DropdownProps {
+ data: DropdownItem[];
+ value?: string | number | (string | number)[];
+ onChange: (value: any) => void;
+ placeholder?: string;
+ searchPlaceholder?: string;
+ isMulti?: boolean;
+ searchable?: boolean;
+ label?: string;
+ error?: string;
+ disabled?: boolean;
+}
+
+export const Dropdown: React.FC = ({
+ data,
+ value,
+ onChange,
+ placeholder = 'Select an option',
+ searchPlaceholder = 'Search...',
+ isMulti = false,
+ searchable = true,
+ label,
+ error,
+ disabled = false,
+}) => {
+ const navigation = useNavigation>();
+
+ const handlePress = () => {
+ if (disabled) return;
+
+ navigation.navigate('Selection', {
+ data,
+ value,
+ isMulti,
+ searchable,
+ label,
+ searchPlaceholder,
+ onSelect: onChange,
+ });
+ };
+
+ const renderSelectedItemText = () => {
+ if (isMulti) {
+ const currentValue = (Array.isArray(value) ? value : []) as (string | number)[];
+ if (currentValue.length === 0) return placeholder;
+
+ const selectedItems = data.filter((item) => currentValue.includes(item.value));
+ return selectedItems.map((item) => item.label).join(', ');
+ } else {
+ if (value == null || value === '') return placeholder;
+ const selectedItem = data.find((item) => item.value === value);
+ return selectedItem ? selectedItem.label : placeholder;
+ }
+ };
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+
+ {renderSelectedItemText()}
+
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ marginBottom: 16,
+ width: '100%',
+ },
+ label: {
+ marginBottom: 8,
+ color: '#374151',
+ },
+ dropdownButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ backgroundColor: '#FFFFFF',
+ borderWidth: 1,
+ borderColor: '#D1D5DB',
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ minHeight: 52,
+ },
+ dropdownButtonError: {
+ borderColor: '#EF4444',
+ },
+ dropdownButtonDisabled: {
+ backgroundColor: '#F3F4F6',
+ borderColor: '#E5E7EB',
+ },
+ dropdownButtonText: {
+ flex: 1,
+ fontSize: 16,
+ color: '#111827',
+ marginRight: 8,
+ },
+ placeholderText: {
+ color: '#9CA3AF',
+ },
+ errorText: {
+ color: '#EF4444',
+ marginTop: 4,
+ },
+});
diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx
index 47c99a8..9a4c541 100644
--- a/src/components/ui/Header.tsx
+++ b/src/components/ui/Header.tsx
@@ -62,8 +62,6 @@ const styles = StyleSheet.create({
marginRight: 12,
width: 40,
height: 40,
- borderRadius: 20,
- backgroundColor: 'rgba(255,255,255,0.2)',
alignItems: 'center',
justifyContent: 'center',
},
diff --git a/src/context/SnackbarContext.tsx b/src/context/SnackbarContext.tsx
new file mode 100644
index 0000000..3fd5147
--- /dev/null
+++ b/src/context/SnackbarContext.tsx
@@ -0,0 +1,137 @@
+import React, { createContext, useContext, useState, useRef, ReactNode, useEffect } from 'react';
+import { View, Text, Animated, StyleSheet, TouchableOpacity } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { THEME } from '../constants/theme';
+import { CheckCircle2, AlertCircle, X } from 'lucide-react-native';
+
+type SnackbarType = 'success' | 'error';
+
+interface SnackbarContextProps {
+ showSnackbar: (message: string, type?: SnackbarType) => void;
+}
+
+const SnackbarContext = createContext(undefined);
+
+export const useSnackbar = () => {
+ const context = useContext(SnackbarContext);
+ if (!context) {
+ throw new Error('useSnackbar must be used within a SnackbarProvider');
+ }
+ return context;
+};
+
+export const SnackbarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+ const [visible, setVisible] = useState(false);
+ const [message, setMessage] = useState('');
+ const [type, setType] = useState('success');
+ const slideAnim = useRef(new Animated.Value(100)).current;
+ const insets = useSafeAreaInsets();
+ const timerRef = useRef(null);
+
+ const showSnackbar = (msg: string, msgType: SnackbarType = 'success') => {
+ setMessage(msg);
+ setType(msgType);
+ setVisible(true);
+
+ // Slide up
+ Animated.spring(slideAnim, {
+ toValue: 0,
+ useNativeDriver: true,
+ speed: 12,
+ bounciness: 8,
+ }).start();
+
+ // Auto hide
+ if (timerRef.current) clearTimeout(timerRef.current);
+ timerRef.current = setTimeout(() => {
+ hideSnackbar();
+ }, 3000);
+ };
+
+ const hideSnackbar = () => {
+ Animated.timing(slideAnim, {
+ toValue: 150,
+ duration: 250,
+ useNativeDriver: true,
+ }).start(() => {
+ setVisible(false);
+ });
+ };
+
+ useEffect(() => {
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ };
+ }, []);
+
+ return (
+
+ {children}
+ {visible && (
+
+
+ {type === 'success' ? (
+
+ ) : (
+
+ )}
+ {message}
+
+
+
+
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ left: 16,
+ right: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: 16,
+ borderRadius: 12,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.15,
+ shadowRadius: 12,
+ elevation: 8,
+ zIndex: 9999,
+ },
+ successContainer: {
+ backgroundColor: '#059669', // A nice green for success
+ },
+ errorContainer: {
+ backgroundColor: '#EF4444', // Red for errors
+ },
+ content: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ messageText: {
+ color: '#FFF',
+ fontSize: 16,
+ fontWeight: '500',
+ marginLeft: 12,
+ marginRight: 16,
+ flexShrink: 1,
+ },
+ closeBtn: {
+ padding: 4,
+ }
+});
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
index fdd55ee..4445fee 100644
--- a/src/navigation/AppNavigator.tsx
+++ b/src/navigation/AppNavigator.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Home, Calendar, Settings } from 'lucide-react-native';
import { HomeScreen } from '../screens/HomeScreen';
@@ -15,13 +16,28 @@ import { AddSkillScreen } from '../screens/AddSkillScreen';
import { AvailabilityListScreen } from '../screens/AvailabilityListScreen';
import { AddAvailabilityScreen } from '../screens/AddAvailabilityScreen';
import { EditAvailabilityScreen } from '../screens/EditAvailabilityScreen';
-import { PaymentProfileScreen } from '../screens/PaymentProfileScreen';
+import { PaymentProfileListScreen } from '../screens/PaymentProfileListScreen';
+import { AddEditPaymentProfileScreen } from '../screens/AddEditPaymentProfileScreen';
import { ManageServicesScreen } from '../screens/ManageServicesScreen';
import { AddEditServiceScreen } from '../screens/AddEditServiceScreen';
+import { ProfessionalDocumentsScreen } from '../screens/ProfessionalDocumentsScreen';
+import { UploadDocumentScreen } from '../screens/UploadDocumentScreen';
+import { ViewDocumentScreen } from '../screens/ViewDocumentScreen';
+import { SelectionScreen } from '../screens/SelectionScreen';
import { Service } from '../services/serviceService';
+import { navigationRef } from './navigationRef';
export type RootStackParamList = {
Login: undefined;
+ Selection: {
+ data: any[];
+ value: any;
+ isMulti?: boolean;
+ searchable?: boolean;
+ label?: string;
+ searchPlaceholder?: string;
+ onSelect: (value: any) => void;
+ };
MainTabs: undefined;
Profile: { caregiverId: string };
PersonalInformation: undefined;
@@ -34,9 +50,13 @@ export type RootStackParamList = {
start_time: string;
end_time: string;
};
- PaymentProfile: undefined;
+ PaymentProfileList: undefined;
+ AddEditPaymentProfile: { profile?: any };
ManageServices: undefined;
AddEditService: { service?: Service };
+ ProfessionalDocuments: undefined;
+ UploadDocument: undefined;
+ ViewDocument: { documentId: number };
};
export type TabParamList = {
@@ -49,6 +69,8 @@ const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
function MainTabs() {
+ const insets = useSafeAreaInsets();
+
return (
{
return (
-
+
{
-
+
+
+
+
+
+
);
diff --git a/src/navigation/navigationRef.ts b/src/navigation/navigationRef.ts
new file mode 100644
index 0000000..9b4ded0
--- /dev/null
+++ b/src/navigation/navigationRef.ts
@@ -0,0 +1,10 @@
+import { createNavigationContainerRef } from '@react-navigation/native';
+import { RootStackParamList } from './AppNavigator';
+
+export const navigationRef = createNavigationContainerRef();
+
+export function navigate(name: keyof RootStackParamList, params?: any) {
+ if (navigationRef.isReady()) {
+ navigationRef.navigate(name, params);
+ }
+}
diff --git a/src/screens/AddAvailabilityScreen.tsx b/src/screens/AddAvailabilityScreen.tsx
index 03ab4db..71b624b 100644
--- a/src/screens/AddAvailabilityScreen.tsx
+++ b/src/screens/AddAvailabilityScreen.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Alert, Platform } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import DateTimePicker from '@react-native-community/datetimepicker';
import { Typography } from '../components/ui/Typography';
@@ -8,6 +8,7 @@ import { Header } from '../components/ui/Header';
import { profileService } from '../services/profileService';
import { THEME } from '../constants/theme';
import { Clock, Calendar, Check } from 'lucide-react-native';
+import { useSnackbar } from '../context/SnackbarContext';
const DAYS_OF_WEEK = [
{ label: 'Monday', value: 'monday' },
@@ -21,6 +22,8 @@ const DAYS_OF_WEEK = [
export const AddAvailabilityScreen = () => {
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+ const { showSnackbar } = useSnackbar();
const [selectedDay, setSelectedDay] = useState('monday');
const [showDayPicker, setShowDayPicker] = useState(false);
@@ -72,9 +75,8 @@ export const AddAvailabilityScreen = () => {
// @ts-ignore
await profileService.addDayAvailability(payload);
- Alert.alert('Success', 'Availability added successfully.', [
- { text: 'OK', onPress: () => navigation.goBack() }
- ]);
+ showSnackbar('Availability added successfully.');
+ navigation.goBack();
} catch (e: any) {
Alert.alert('Error', e.response?.data?.message || 'Failed to add availability.');
} finally {
@@ -153,7 +155,7 @@ export const AddAvailabilityScreen = () => {
-
+
;
+interface AddEditPaymentProfileScreenProps {
+ navigation: NativeStackNavigationProp;
+ route: RouteProp;
}
-export const PaymentProfileScreen: React.FC = ({ navigation }) => {
- const [isLoading, setIsLoading] = useState(true);
+export const AddEditPaymentProfileScreen: React.FC = ({ navigation, route }) => {
+ const { profile } = route.params || {};
+ const isEditing = !!profile;
const [isSaving, setIsSaving] = useState(false);
- const [existingId, setExistingId] = useState(undefined);
+ const insets = useSafeAreaInsets();
+ const { showSnackbar } = useSnackbar();
const [form, setForm] = useState>({
- payment_method: 'bank',
- account_holder_name: '',
- account_number: '',
- ifsc_code: '',
- bank_name: '',
- branch_name: '',
- account_type: 'savings',
+ payment_method: profile?.payment_method || 'bank',
+ account_holder_name: profile?.account_holder_name || '',
+ account_number: profile?.account_number || '',
+ ifsc_code: profile?.ifsc_code || '',
+ bank_name: profile?.bank_name || '',
+ branch_name: profile?.branch_name || '',
+ account_type: profile?.account_type || 'savings',
});
const [errors, setErrors] = useState>({});
- useEffect(() => {
- fetchPaymentProfile();
- }, []);
-
- const fetchPaymentProfile = async () => {
- try {
- setIsLoading(true);
- const response = await profileService.getPaymentProfile();
- if (response.success && response.data && response.data.id) {
- setExistingId(response.data.id);
- setForm({
- payment_method: response.data.payment_method || 'bank',
- account_holder_name: response.data.account_holder_name || '',
- account_number: response.data.account_number || '',
- ifsc_code: response.data.ifsc_code || '',
- bank_name: response.data.bank_name || '',
- branch_name: response.data.branch_name || '',
- account_type: response.data.account_type || 'savings',
- });
- }
- } catch (error: any) {
- // If 404 or not found, it means no profile exists yet.
- console.log('No existing payment profile found or error fetching.');
- } finally {
- setIsLoading(false);
- }
- };
-
const handleChange = (field: keyof PaymentProfile, value: string) => {
setForm(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
@@ -88,15 +65,13 @@ export const PaymentProfileScreen: React.FC = ({ navi
setIsSaving(true);
const payload = form as PaymentProfile;
- if (existingId) {
+ if (isEditing && profile?.id) {
+ payload.id = profile.id;
await profileService.updatePaymentProfile(payload);
- Alert.alert('Success', 'Payment profile updated successfully.');
+ showSnackbar('Payment profile updated successfully.');
} else {
- const response = await profileService.addPaymentProfile(payload);
- if (response.data && response.data.id) {
- setExistingId(response.data.id);
- }
- Alert.alert('Success', 'Payment profile added successfully.');
+ await profileService.addPaymentProfile(payload);
+ showSnackbar('Payment profile added successfully.');
}
navigation.goBack();
} catch (error: any) {
@@ -110,27 +85,21 @@ export const PaymentProfileScreen: React.FC = ({ navi
return (
// @ts-ignore - TS types for SafeAreaView in this version incorrectly omit the style prop
- navigation.goBack()} />
+
@@ -201,7 +168,6 @@ const styles = StyleSheet.create({
},
scrollContent: {
padding: 24,
- paddingBottom: 40,
},
centerContainer: {
flex: 1,
diff --git a/src/screens/AddEditServiceScreen.tsx b/src/screens/AddEditServiceScreen.tsx
index 60a9a9b..d60d98f 100644
--- a/src/screens/AddEditServiceScreen.tsx
+++ b/src/screens/AddEditServiceScreen.tsx
@@ -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;
@@ -29,75 +22,57 @@ interface AddEditServiceScreenProps {
export const AddEditServiceScreen: React.FC = ({ navigation, route }) => {
const existingService = route.params?.service;
const isEditing = !!existingService;
+ const { showSnackbar } = useSnackbar();
- const [form, setForm] = useState>({
- 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>({
+ 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>({});
const [isSaving, setIsSaving] = useState(false);
- const [careTypes, setCareTypes] = useState([]);
- const [careSubTypes, setCareSubTypes] = useState([]);
- const [taxes, setTaxes] = useState([]);
- const [showCareTypePicker, setShowCareTypePicker] = useState(false);
- const [showCareSubTypePicker, setShowCareSubTypePicker] = useState(false);
- const [showUnitPicker, setShowUnitPicker] = useState(false);
- const [showTaxPicker, setShowTaxPicker] = useState(false);
+ const [availableServices, setAvailableServices] = useState([]);
+ 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 = ({ navi
};
const toggleIsActive = () => {
- setForm(prev => ({ ...prev, is_active: !prev.is_active }));
+ handleChange('is_active', !form.is_active);
};
const validate = () => {
const newErrors: Record = {};
- 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 = ({ 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;
+
+ 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) {
+ 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 = ({ navi
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
-
+
- {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.'}
- handleChange('name', val)}
- error={errors.name}
+ ({ 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 && (
+ {errors.service_id}
+ )}
+
handleChange('custom_rate', val)}
+ error={errors.custom_rate}
+ />
+
+ handleChange('description', val)}
- error={errors.description}
+ value={form.notes}
+ onChangeText={(val) => handleChange('notes', val)}
+ error={errors.notes}
style={styles.textArea}
/>
-
- Care Type
- setShowCareTypePicker(!showCareTypePicker)}
- >
-
- {careTypes.find(c => c.slug === form.care_type_slug)?.name || form.care_type_slug || 'Select Care Type'}
-
-
-
-
- {showCareTypePicker && (
-
- {careTypes.map(type => (
- {
- handleChange('care_type_slug', type.slug);
- handleChange('category', type.name);
- handleChange('care_sub_type_slug', '');
- setShowCareTypePicker(false);
- }}
- >
-
- {type.name}
-
- {form.care_type_slug === type.slug && }
-
- ))}
-
- )}
-
-
-
- Care Sub Type
- setShowCareSubTypePicker(!showCareSubTypePicker)}
- >
-
- {careSubTypes.find(c => c.slug === form.care_sub_type_slug)?.name || form.care_sub_type_slug || 'Select Care Sub Type'}
-
-
-
-
- {showCareSubTypePicker && (
-
- {careSubTypes.length === 0 ? (
-
- Please select a Care Type first
-
- ) : (
- careSubTypes.map(type => (
- {
- handleChange('care_sub_type_slug', type.slug);
- setShowCareSubTypePicker(false);
- }}
- >
-
- {type.name}
-
- {form.care_sub_type_slug === type.slug && }
-
- ))
- )}
-
- )}
-
- handleChange('category', val)}
- />
-
- Pricing Unit
- setShowUnitPicker(!showUnitPicker)}
- >
-
- {form.unit || 'Select Unit (e.g. Visit, Hour)'}
-
-
-
-
- {showUnitPicker && (
-
- {UNIT_OPTIONS.map(unitOption => (
- {
- handleChange('unit', unitOption);
- setShowUnitPicker(false);
- }}
- >
-
- {unitOption}
-
- {form.unit === unitOption && }
-
- ))}
-
- )}
-
-
- handleChange('customer_price', val)}
- error={errors.customer_price}
- />
- handleChange('caregiver_rate', parseFloat(val) || 0)}
- error={errors.caregiver_rate}
- />
-
-
- Tax (%)
- setShowTaxPicker(!showTaxPicker)}
- >
-
- {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'}
-
-
-
-
- {showTaxPicker && (
-
- {taxes.length === 0 ? (
-
- Loading taxes...
-
- ) : (
- taxes.map(taxOption => (
- {
- handleChange('tax', taxOption.rate_percent);
- setShowTaxPicker(false);
- }}
- >
-
-
- {taxOption.name} ({taxOption.rate_percent}%)
-
-
- {taxOption.description}
-
-
- {form.tax?.toString() === taxOption.rate_percent?.toString() && }
-
- ))
- )}
-
- )}
-
-
- handleChange('duration_minutes', val)}
- />
Is Active
{
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+ const { showSnackbar } = useSnackbar();
const [ownSkills, setOwnSkills] = useState([]);
const [careTypes, setCareTypes] = useState([]);
const [subTypes, setSubTypes] = useState([]);
@@ -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 = () => {
-
+
{
const navigation = useNavigation();
const [availability, setAvailability] = useState([]);
const [loading, setLoading] = useState(true);
+ const insets = useSafeAreaInsets();
const fetchAvailability = async () => {
setLoading(true);
@@ -110,7 +111,7 @@ export const AvailabilityListScreen = () => {
navigation.navigate('AddAvailability')}
>
@@ -226,8 +227,6 @@ const styles = StyleSheet.create({
},
fab: {
position: 'absolute',
- bottom: 24,
- right: 24,
width: 60,
height: 60,
borderRadius: 30,
diff --git a/src/screens/EditAvailabilityScreen.tsx b/src/screens/EditAvailabilityScreen.tsx
index d6d8444..c3910cd 100644
--- a/src/screens/EditAvailabilityScreen.tsx
+++ b/src/screens/EditAvailabilityScreen.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Alert, Platform } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation, 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;
@@ -15,6 +16,8 @@ type EditAvailabilityRouteProp = RouteProp {
const navigation = useNavigation();
const route = useRoute();
+ 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 = () => {
-
+
= ({ navigation }) => {
+ navigation.navigate('ManageServices')} style={{marginBottom: 24}}>
+
+
+
+
+
+
+ Manage Services
+
+
+ Add or edit your offered services
+
+
+
+
+
+
+ navigation.navigate('ProfessionalDocuments' as any)} style={{marginBottom: 24}}>
+
+
+
+
+
+
+ Professional Documents
+
+
+ Upload and manage your documents
+
+
+
+
+
+
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',
}
});
diff --git a/src/screens/ManageServicesScreen.tsx b/src/screens/ManageServicesScreen.tsx
index ead2d22..8149507 100644
--- a/src/screens/ManageServicesScreen.tsx
+++ b/src/screens/ManageServicesScreen.tsx
@@ -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;
@@ -18,6 +19,8 @@ export const ManageServicesScreen: React.FC = ({ navi
const [services, setServices] = useState([]);
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 = ({ 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 = ({ navi
)}
navigation.navigate('AddEditService', {})}
>
@@ -232,8 +235,6 @@ const styles = StyleSheet.create({
},
fab: {
position: 'absolute',
- bottom: 24,
- right: 24,
width: 56,
height: 56,
borderRadius: 28,
diff --git a/src/screens/ManageSkillsScreen.tsx b/src/screens/ManageSkillsScreen.tsx
index 8609f86..4985c81 100644
--- a/src/screens/ManageSkillsScreen.tsx
+++ b/src/screens/ManageSkillsScreen.tsx
@@ -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;
@@ -16,6 +17,8 @@ export const ManageSkillsScreen = () => {
const navigation = useNavigation();
const [ownSkills, setOwnSkills] = useState([]);
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
@@ -89,6 +116,12 @@ export const ManageSkillsScreen = () => {
+ handleDelete(skill.slug, skill.name)}
+ >
+
+
))
)}
@@ -97,7 +130,7 @@ export const ManageSkillsScreen = () => {
navigation.navigate('AddSkill')}
>
@@ -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,
diff --git a/src/screens/PaymentProfileListScreen.tsx b/src/screens/PaymentProfileListScreen.tsx
new file mode 100644
index 0000000..e4e2114
--- /dev/null
+++ b/src/screens/PaymentProfileListScreen.tsx
@@ -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;
+}
+
+export const PaymentProfileListScreen: React.FC = ({ navigation }) => {
+ const [profiles, setProfiles] = useState([]);
+ 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 (
+ navigation.navigate('AddEditPaymentProfile', { profile })}
+ >
+
+
+
+
+ {profile.bank_name || 'Bank Account'}
+
+
+
+
+
+
+
+ {maskAccountNumber(profile.account_number)}
+
+
+
+
+
+ Cardholder Name
+
+ {profile.account_holder_name?.toUpperCase() || 'UNKNOWN'}
+
+
+
+ IFSC Code
+
+ {profile.ifsc_code || '---'}
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+ }
+ >
+ {profiles.length === 0 && !isLoading ? (
+
+
+
+
+
+ No Accounts Added
+
+
+ Add your bank account details to receive your payouts securely.
+
+
+ ) : (
+ profiles.map(renderCard)
+ )}
+
+
+ navigation.navigate('AddEditPaymentProfile', { profile: undefined })}
+ >
+
+
+
+ );
+};
+
+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,
+ },
+});
diff --git a/src/screens/PersonalInformationScreen.tsx b/src/screens/PersonalInformationScreen.tsx
index bf5724a..ee4d750 100644
--- a/src/screens/PersonalInformationScreen.tsx
+++ b/src/screens/PersonalInformationScreen.tsx
@@ -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(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 (
- {saving ? : Save}
+ {saving ? : Save}
);
}
return (
setIsEditing(true)} style={styles.headerButton}>
-
+
);
};
@@ -289,7 +293,7 @@ const styles = StyleSheet.create({
padding: 8,
},
headerButtonText: {
- color: '#4F46E5',
+ color: '#FFFFFF',
},
card: {
padding: 0,
diff --git a/src/screens/ProfessionalDocumentsScreen.tsx b/src/screens/ProfessionalDocumentsScreen.tsx
new file mode 100644
index 0000000..5825107
--- /dev/null
+++ b/src/screens/ProfessionalDocumentsScreen.tsx
@@ -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;
+
+export const ProfessionalDocumentsScreen: React.FC = ({ navigation }) => {
+ const [documents, setDocuments] = useState([]);
+ 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 }) => (
+ navigation.navigate('ViewDocument', { documentId: item.id })}>
+
+
+
+
+
+
+ {item.addition_info?.title || 'Untitled Document'}
+
+
+ {new Date(item.created_at).toLocaleDateString()}
+
+
+
+ handleDelete(item.id)} style={styles.deleteBtn}>
+
+
+
+ {item.addition_info?.notes ? (
+
+ {item.addition_info.notes}
+
+ ) : null}
+ {item.properties?.url ? (
+
+ ) : null}
+
+
+ );
+
+ return (
+ // @ts-ignore
+
+
+
+ {isLoading ? (
+
+
+
+ ) : documents.length === 0 ? (
+
+
+
+ No professional documents found.
+
+
+ Upload your licenses, certificates, and other professional documents here.
+
+
+ ) : (
+ item.id.toString()}
+ renderItem={renderItem}
+ contentContainerStyle={styles.listContent}
+ showsVerticalScrollIndicator={false}
+ />
+ )}
+
+
+
+
+
+ );
+};
+
+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 },
+});
diff --git a/src/screens/SelectionScreen.tsx b/src/screens/SelectionScreen.tsx
new file mode 100644
index 0000000..6f5a56a
--- /dev/null
+++ b/src/screens/SelectionScreen.tsx
@@ -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;
+type SelectionScreenRouteProp = RouteProp;
+
+interface SelectionScreenProps {
+ navigation: SelectionScreenNavigationProp;
+ route: SelectionScreenRouteProp;
+}
+
+export const SelectionScreen: React.FC = ({ 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 (
+
+
+
+
+
+ {searchable && (
+
+
+
+ {searchQuery.length > 0 && (
+ setSearchQuery('')}>
+
+
+ )}
+
+ )}
+
+ item.value.toString() + index}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContainer}
+ renderItem={({ item }) => {
+ const selected = isItemSelected(item);
+ return (
+ handleSelect(item)}
+ >
+
+ {item.label}
+
+ {selected && }
+
+ );
+ }}
+ ListEmptyComponent={() => (
+
+
+ No options found
+
+
+ )}
+ />
+
+
+ );
+};
+
+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',
+ },
+});
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index 314e907..369b010 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -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 = ({ navigation }) =>
- navigation.navigate('PaymentProfile')}>
+ navigation.navigate('PaymentProfileList')}>
@@ -85,23 +85,6 @@ export const SettingsScreen: React.FC = ({ navigation }) =>
- navigation.navigate('ManageServices')}>
-
-
-
-
-
-
- Manage Services
-
-
- Add or edit your offered services
-
-
-
-
-
-
diff --git a/src/screens/UploadDocumentScreen.tsx b/src/screens/UploadDocumentScreen.tsx
new file mode 100644
index 0000000..5ae7bff
--- /dev/null
+++ b/src/screens/UploadDocumentScreen.tsx
@@ -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;
+
+export const UploadDocumentScreen: React.FC = ({ navigation }) => {
+ const [title, setTitle] = useState('');
+ const [notes, setNotes] = useState('');
+ const [document, setDocument] = useState(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
+
+
+
+
+ Document Title *
+
+
+
+
+ Notes
+
+
+
+
+ Attachment *
+
+
+ {document ? (
+
+ ) : (
+
+
+
+ Tap to select a document
+
+
+ )}
+
+ {document && (
+ setDocument(null)} style={styles.removeBtn}>
+ Remove
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+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 },
+});
diff --git a/src/screens/ViewDocumentScreen.tsx b/src/screens/ViewDocumentScreen.tsx
new file mode 100644
index 0000000..fc5a1b2
--- /dev/null
+++ b/src/screens/ViewDocumentScreen.tsx
@@ -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;
+
+export const ViewDocumentScreen: React.FC = ({ route, navigation }) => {
+ const { documentId } = route.params;
+ const [document, setDocument] = useState(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
+
+
+ {isLoading ? (
+
+
+
+ ) : document ? (
+
+
+
+
+
+ {document.addition_info?.title || 'Untitled Document'}
+
+
+ Uploaded on {new Date(document.created_at).toLocaleDateString()}
+
+
+
+
+ {document.addition_info?.notes ? (
+
+
+ Notes
+
+
+ {document.addition_info.notes}
+
+
+ ) : null}
+
+ {document.properties?.url ? (
+
+
+ Document Preview
+
+
+
+ ) : (
+
+ No image preview available
+
+ )}
+
+ ) : null}
+
+ );
+};
+
+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 },
+});
diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts
index 855ce9d..77bcaac 100644
--- a/src/services/apiClient.ts
+++ b/src/services/apiClient.ts
@@ -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');
}
}
diff --git a/src/services/documentService.ts b/src/services/documentService.ts
new file mode 100644
index 0000000..d52a18c
--- /dev/null
+++ b/src/services/documentService.ts
@@ -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('/v1/resource/caregiver/professional-documents');
+ return response.data;
+ },
+
+ getDocumentById: async (id: number) => {
+ const response = await apiClient.get(`/v1/resource/caregiver/professional-documents/${id}`);
+ return response.data;
+ },
+
+ uploadDocument: async (formData: FormData) => {
+ const response = await apiClient.post(
+ '/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;
+ },
+};
diff --git a/src/services/profileService.ts b/src/services/profileService.ts
index 8e16439..8d49a33 100644
--- a/src/services/profileService.ts
+++ b/src/services/profileService.ts
@@ -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 => {
+ 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 => {
try {
const response = await apiClient.get('/v1/resource/caregiver/availability');
diff --git a/src/services/serviceService.ts b/src/services/serviceService.ts
index 21b17c5..3ef0ca9 100644
--- a/src/services/serviceService.ts
+++ b/src/services/serviceService.ts
@@ -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 => {
try {
@@ -31,6 +45,16 @@ export const serviceService = {
}
},
+ getSkillBasedServices: async (): Promise => {
+ 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 => {
try {
const response = await apiClient.get('/v1/resource/taxes');
@@ -51,9 +75,11 @@ export const serviceService = {
}
},
- addService: async (data: Service): Promise => {
+ addService: async (data: AddServicePayload): Promise => {
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): Promise => {
+ updateService: async (id: number, data: Partial): Promise => {
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 => {
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);
diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts
index 0a04d26..9f9d081 100644
--- a/src/utils/formatters.ts
+++ b/src/utils/formatters.ts
@@ -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}`;
};