feat: implement service management module including UI, services, and navigation screens
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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) {
|
||||||
|
if (response.data.accessToken) {
|
||||||
await AsyncStorage.setItem('token', 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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' }]}>
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user