feat: implement service management module including UI, services, and navigation screens

This commit is contained in:
2026-08-19 11:43:09 +05:30
parent 13656d4bb3
commit 8f2cdfff2f
9 changed files with 864 additions and 6 deletions
+2
View File
@@ -13,6 +13,8 @@ export interface InputProps extends TextInputProps {
onChangeText?: (text: string) => void; onChangeText?: (text: string) => void;
style?: any; style?: any;
labelStyle?: any; labelStyle?: any;
multiline?: boolean;
numberOfLines?: number;
} }
export function Input({ label, error, style, labelStyle, ...props }: InputProps) { export function Input({ label, error, style, labelStyle, ...props }: InputProps) {
+2
View File
@@ -1,6 +1,7 @@
export const THEME = { export const THEME = {
colors: { colors: {
primary: '#1E3A8A', primary: '#1E3A8A',
primaryDark: '#172554',
primaryLight: '#E0E7FF', primaryLight: '#E0E7FF',
primaryLighter: '#EEF2FF', primaryLighter: '#EEF2FF',
background: '#F9FAFB', background: '#F9FAFB',
@@ -12,6 +13,7 @@ export const THEME = {
}, },
border: '#F3F4F6', border: '#F3F4F6',
warning: '#EAB308', warning: '#EAB308',
error: '#EF4444',
}, },
spacing: { spacing: {
xs: 4, xs: 4,
+7
View File
@@ -16,6 +16,9 @@ import { AvailabilityListScreen } from '../screens/AvailabilityListScreen';
import { AddAvailabilityScreen } from '../screens/AddAvailabilityScreen'; import { AddAvailabilityScreen } from '../screens/AddAvailabilityScreen';
import { EditAvailabilityScreen } from '../screens/EditAvailabilityScreen'; import { EditAvailabilityScreen } from '../screens/EditAvailabilityScreen';
import { PaymentProfileScreen } from '../screens/PaymentProfileScreen'; import { PaymentProfileScreen } from '../screens/PaymentProfileScreen';
import { ManageServicesScreen } from '../screens/ManageServicesScreen';
import { AddEditServiceScreen } from '../screens/AddEditServiceScreen';
import { Service } from '../services/serviceService';
export type RootStackParamList = { export type RootStackParamList = {
Login: undefined; Login: undefined;
@@ -32,6 +35,8 @@ export type RootStackParamList = {
end_time: string; end_time: string;
}; };
PaymentProfile: undefined; PaymentProfile: undefined;
ManageServices: undefined;
AddEditService: { service?: Service };
}; };
export type TabParamList = { export type TabParamList = {
@@ -120,6 +125,8 @@ export const AppNavigator = () => {
<Stack.Screen name="AddAvailability" component={AddAvailabilityScreen} /> <Stack.Screen name="AddAvailability" component={AddAvailabilityScreen} />
<Stack.Screen name="EditAvailability" component={EditAvailabilityScreen} /> <Stack.Screen name="EditAvailability" component={EditAvailabilityScreen} />
<Stack.Screen name="PaymentProfile" component={PaymentProfileScreen} /> <Stack.Screen name="PaymentProfile" component={PaymentProfileScreen} />
<Stack.Screen name="ManageServices" component={ManageServicesScreen} />
<Stack.Screen name="AddEditService" component={AddEditServiceScreen} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
+472
View File
@@ -0,0 +1,472 @@
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, ScrollView, Alert, KeyboardAvoidingView, Platform, Switch, TouchableOpacity } from 'react-native';
import { 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';
import { Button } from '../components/ui/Button';
import { THEME } from '../constants/theme';
import { serviceService, Service } 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'
];
interface AddEditServiceScreenProps {
navigation: NativeStackNavigationProp<RootStackParamList, 'AddEditService'>;
route: RouteProp<RootStackParamList, 'AddEditService'>;
}
export const AddEditServiceScreen: React.FC<AddEditServiceScreenProps> = ({ navigation, route }) => {
const existingService = route.params?.service;
const isEditing = !!existingService;
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() || '',
is_active: existingService?.is_active ?? true,
});
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);
useEffect(() => {
fetchCareTypes();
}, []);
useEffect(() => {
if (form.care_type_slug) {
fetchCareSubTypes(form.care_type_slug);
}
}, [form.care_type_slug]);
const fetchCareTypes = async () => {
try {
const [ownSkillsResponse, data, taxesData] = await Promise.all([
profileService.getOwnSkills(),
profileService.getCareTypes(),
serviceService.getTaxes()
]);
setCareTypes(data);
setTaxes(taxesData);
} catch (error) {
console.error('Error fetching master types', error);
}
};
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) => {
setForm(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const toggleIsActive = () => {
setForm(prev => ({ ...prev, is_active: !prev.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';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSave = async () => {
if (!validate()) {
return;
}
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) {
await serviceService.updateService(existingService.id, payload);
Alert.alert('Success', 'Service updated successfully.');
} else {
await serviceService.addService(payload);
Alert.alert('Success', 'Service added successfully.');
}
navigation.goBack();
} catch (error: any) {
console.error('Error saving service:', error);
Alert.alert('Error', error?.response?.data?.message || 'Failed to save service.');
} finally {
setIsSaving(false);
}
};
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right }]}>
<Header title={isEditing ? "Edit Service" : "Add Service"} showBack />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
<Typography variant="body" style={styles.helperText}>
{isEditing ? 'Update the details of your service below.' : 'Add a new service that you offer to clients.'}
</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}
/>
<Input
label="Description *"
placeholder="Describe what the service includes..."
multiline
numberOfLines={4}
value={form.description}
onChangeText={(val) => handleChange('description', val)}
error={errors.description}
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
value={form.is_active}
onValueChange={toggleIsActive}
trackColor={{ false: '#D1D5DB', true: THEME.colors.primary }}
/>
</View>
</View>
<Button
title={isEditing ? "Update Service" : "Save Service"}
onPress={handleSave}
loading={isSaving}
style={styles.saveButton}
/>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: THEME.colors.background,
},
content: {
flex: 1,
},
scrollContent: {
padding: 24,
paddingBottom: 40,
},
helperText: {
color: THEME.colors.text.secondary,
marginBottom: 24,
},
formContainer: {
marginBottom: 32,
},
textArea: {
minHeight: 100,
textAlignVertical: 'top', // for Android
},
switchContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderTopWidth: 1,
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,
},
});
+29 -5
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity } from 'react-native'; import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { Typography } from '../components/ui/Typography'; import { Typography } from '../components/ui/Typography';
@@ -23,6 +23,20 @@ export const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
useEffect(() => {
const checkAuthStatus = async () => {
try {
const token = await AsyncStorage.getItem('token');
if (token) {
navigation.replace('MainTabs');
}
} catch (e) {
console.error('Error checking auth status:', e);
}
};
checkAuthStatus();
}, [navigation]);
const handleLogin = async () => { const handleLogin = async () => {
if (step === 'PHONE') { if (step === 'PHONE') {
if (!phone || phone.length < 10) { if (!phone || phone.length < 10) {
@@ -34,9 +48,14 @@ export const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
try { try {
const response = await authService.login(phone); const response = await authService.login(phone);
console.log('OTP sent:', response); console.log('OTP sent:', response);
if (response.success === false) {
Alert.alert('API Error', response.message || 'Unknown error');
return;
}
setStep('OTP'); setStep('OTP');
} catch (err: any) { } catch (err: any) {
console.error('Login failed:', err); console.error('Login failed:', err);
Alert.alert('Frontend Error', err?.response?.data?.message || err.message || 'Failed to connect');
setError('Failed to send OTP. Please check your number and try again.'); setError('Failed to send OTP. Please check your number and try again.');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -51,9 +70,14 @@ export const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
try { try {
const response = await authService.verifyOtp(phone, otp); const response = await authService.verifyOtp(phone, otp);
console.log('OTP verified successfully:', response); console.log('OTP verified successfully:', response);
// Save the token // Save the token and refresh token
if (response.data && response.data.accessToken) { if (response.data) {
await AsyncStorage.setItem('token', response.data.accessToken); if (response.data.accessToken) {
await AsyncStorage.setItem('token', response.data.accessToken);
}
if (response.data.refreshToken) {
await AsyncStorage.setItem('refresh_token', response.data.refreshToken);
}
} else if (response.token) { } else if (response.token) {
await AsyncStorage.setItem('token', response.token); await AsyncStorage.setItem('token', response.token);
} }
+249
View File
@@ -0,0 +1,249 @@
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 { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/AppNavigator';
import { Header } from '../components/ui/Header';
import { Typography } from '../components/ui/Typography';
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';
interface ManageServicesScreenProps {
navigation: NativeStackNavigationProp<RootStackParamList, 'ManageServices'>;
}
export const ManageServicesScreen: React.FC<ManageServicesScreenProps> = ({ navigation }) => {
const [services, setServices] = useState<Service[]>([]);
const [isLoading, setIsLoading] = useState(true);
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) {
fetchServices();
}
}, [isFocused]);
const fetchServices = async () => {
try {
setIsLoading(true);
const data = await serviceService.getServices();
// Ensure data is array
setServices(Array.isArray(data) ? data : []);
} catch (error) {
console.error('Error fetching services:', error);
Alert.alert('Error', 'Failed to load services. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleDelete = (id?: number) => {
if (!id) return;
Alert.alert(
'Delete Service',
'Are you sure you want to delete this service?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
try {
await serviceService.deleteService(id);
Alert.alert('Success', 'Service deleted successfully.');
fetchServices();
} catch (error) {
console.error('Error deleting service:', error);
Alert.alert('Error', 'Failed to delete service.');
}
}
}
]
);
};
const renderItem = ({ item }: { item: Service }) => (
<View style={styles.serviceCard}>
<View style={styles.serviceInfo}>
<Typography variant="h3" weight="bold" style={styles.serviceName}>{item.name}</Typography>
<Typography variant="body" style={styles.serviceDescription}>{item.description}</Typography>
<View style={styles.metaContainer}>
<Typography variant="caption" weight="bold" style={styles.price}>
Price: ${item.customer_price}
</Typography>
{item.duration_minutes && (
<Typography variant="caption" style={styles.duration}>
• Duration: {item.duration_minutes} mins
</Typography>
)}
</View>
<View style={styles.skillsContainer}>
{item.category && (
<View style={styles.skillBadge}>
<Typography variant="caption" style={styles.skillText}>{item.category}</Typography>
</View>
)}
{item.unit && (
<View style={styles.skillBadge}>
<Typography variant="caption" style={styles.skillText}>{item.unit}</Typography>
</View>
)}
<View style={[styles.skillBadge, { backgroundColor: item.is_active ? THEME.colors.primaryLighter : '#FEE2E2' }]}>
<Typography variant="caption" style={[styles.skillText, { color: item.is_active ? THEME.colors.primaryDark : '#EF4444' }]}>
{item.is_active ? 'Active' : 'Inactive'}
</Typography>
</View>
</View>
</View>
<View style={styles.actionButtons}>
<TouchableOpacity
style={styles.actionBtn}
onPress={() => navigation.navigate('AddEditService', { service: item })}
>
<Edit2 color={THEME.colors.primary} size={20} />
</TouchableOpacity>
<TouchableOpacity
style={styles.actionBtn}
onPress={() => handleDelete(item.id)}
>
<Trash2 color={THEME.colors.error} size={20} />
</TouchableOpacity>
</View>
</View>
);
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="Manage Services" showBack />
{isLoading ? (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color={THEME.colors.primary} />
</View>
) : services.length === 0 ? (
<View style={styles.centerContainer}>
<Typography style={styles.emptyText}>No services found. Add one to get started.</Typography>
</View>
) : (
<FlatList
data={services}
keyExtractor={(item, index) => item.id?.toString() || index.toString()}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
/>
)}
<TouchableOpacity
style={styles.fab}
onPress={() => navigation.navigate('AddEditService', {})}
>
<Plus color="#FFF" size={24} />
</TouchableOpacity>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: THEME.colors.background,
},
centerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
emptyText: {
color: THEME.colors.text.secondary,
textAlign: 'center',
},
listContent: {
padding: 16,
paddingBottom: 80, // for FAB
},
serviceCard: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'flex-start',
borderWidth: 1,
borderColor: '#E5E7EB',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
serviceInfo: {
flex: 1,
},
serviceName: {
marginBottom: 4,
},
serviceDescription: {
color: THEME.colors.text.secondary,
marginBottom: 8,
},
metaContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
price: {
color: THEME.colors.primary,
},
duration: {
color: THEME.colors.text.secondary,
marginLeft: 8,
},
skillsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
skillBadge: {
backgroundColor: THEME.colors.primaryLighter,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 16,
},
skillText: {
color: THEME.colors.primaryDark,
},
actionButtons: {
flexDirection: 'row',
marginLeft: 16,
},
actionBtn: {
padding: 8,
marginLeft: 4,
backgroundColor: '#F3F4F6',
borderRadius: 8,
},
fab: {
position: 'absolute',
bottom: 24,
right: 24,
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,
},
});
+18 -1
View File
@@ -4,7 +4,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { Typography } from '../components/ui/Typography'; import { Typography } from '../components/ui/Typography';
import { Header } from '../components/ui/Header'; import { Header } from '../components/ui/Header';
import { Card } from '../components/ui/Card'; import { Card } from '../components/ui/Card';
import { User, ChevronRight, LogOut, CreditCard } from 'lucide-react-native'; import { User, ChevronRight, LogOut, CreditCard, Briefcase } from 'lucide-react-native';
import { CompositeNavigationProp } from '@react-navigation/native'; import { CompositeNavigationProp } from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
@@ -85,6 +85,23 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({ navigation }) =>
</Card> </Card>
</TouchableOpacity> </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}> <TouchableOpacity onPress={handleLogout}>
<Card noPadding style={styles.menuItem}> <Card noPadding style={styles.menuItem}>
<View style={[styles.menuIconContainer, { backgroundColor: '#FEE2E2' }]}> <View style={[styles.menuIconContainer, { backgroundColor: '#FEE2E2' }]}>
+2
View File
@@ -137,6 +137,8 @@ export const profileService = {
} }
}, },
addSkills: async (skills: string[]): Promise<any> => { addSkills: async (skills: string[]): Promise<any> => {
try { try {
const response = await apiClient.post('/v1/resource/caregiver/skills/add', { skills }); const response = await apiClient.post('/v1/resource/caregiver/skills/add', { skills });
+83
View File
@@ -0,0 +1,83 @@
import { apiClient } from './apiClient';
export interface Service {
id?: number;
name: string;
care_type_slug?: string;
care_sub_type_slug?: string;
category?: string;
description: string;
unit?: string;
customer_price?: number | string;
caregiver_rate?: number | string;
tax?: number | string;
duration_minutes?: number | string;
is_active?: boolean;
}
export const serviceService = {
getServices: async (filters?: { skills?: string }): Promise<Service[]> => {
try {
const params = new URLSearchParams();
if (filters?.skills) {
params.append('skills', filters.skills);
}
const queryString = params.toString() ? `?${params.toString()}` : '';
const response = await apiClient.get(`/v1/caregiver/services${queryString}`);
return response.data?.data || response.data || [];
} catch (error) {
console.error('Error fetching services:', error);
throw error;
}
},
getTaxes: async (): Promise<any[]> => {
try {
const response = await apiClient.get('/v1/resource/taxes');
return response.data?.data?.standard_tax_rates || [];
} catch (error) {
console.error('Error fetching taxes:', error);
throw error;
}
},
getServiceById: async (id: number): Promise<Service> => {
try {
const response = await apiClient.get(`/v1/caregiver/services/${id}`);
return response.data?.data || response.data;
} catch (error) {
console.error(`Error fetching service with id ${id}:`, error);
throw error;
}
},
addService: async (data: Service): Promise<any> => {
try {
const response = await apiClient.post('/v1/caregiver/services', data);
return response.data;
} catch (error) {
console.error('Error adding service:', error);
throw error;
}
},
updateService: async (id: number, data: Partial<Service>): Promise<any> => {
try {
const response = await apiClient.patch(`/v1/caregiver/services/${id}`, data);
return response.data;
} catch (error) {
console.error(`Error updating service with id ${id}:`, error);
throw error;
}
},
deleteService: async (id: number): Promise<any> => {
try {
const response = await apiClient.delete(`/v1/caregiver/services/${id}`);
return response.data;
} catch (error) {
console.error(`Error deleting service with id ${id}:`, error);
throw error;
}
}
};