From 8f2cdfff2f7b41d1aaeb92ddc3ed6422b436f5ad Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Wed, 19 Aug 2026 11:43:09 +0530 Subject: [PATCH] feat: implement service management module including UI, services, and navigation screens --- src/components/ui/Input.tsx | 2 + src/constants/theme.ts | 2 + src/navigation/AppNavigator.tsx | 7 + src/screens/AddEditServiceScreen.tsx | 472 +++++++++++++++++++++++++++ src/screens/LoginScreen.tsx | 34 +- src/screens/ManageServicesScreen.tsx | 249 ++++++++++++++ src/screens/SettingsScreen.tsx | 19 +- src/services/profileService.ts | 2 + src/services/serviceService.ts | 83 +++++ 9 files changed, 864 insertions(+), 6 deletions(-) create mode 100644 src/screens/AddEditServiceScreen.tsx create mode 100644 src/screens/ManageServicesScreen.tsx create mode 100644 src/services/serviceService.ts diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index d5118d0..942a57a 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -13,6 +13,8 @@ export interface InputProps extends TextInputProps { onChangeText?: (text: string) => void; style?: any; labelStyle?: any; + multiline?: boolean; + numberOfLines?: number; } export function Input({ label, error, style, labelStyle, ...props }: InputProps) { diff --git a/src/constants/theme.ts b/src/constants/theme.ts index f030e84..63c7604 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -1,6 +1,7 @@ export const THEME = { colors: { primary: '#1E3A8A', + primaryDark: '#172554', primaryLight: '#E0E7FF', primaryLighter: '#EEF2FF', background: '#F9FAFB', @@ -12,6 +13,7 @@ export const THEME = { }, border: '#F3F4F6', warning: '#EAB308', + error: '#EF4444', }, spacing: { xs: 4, diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index e365565..fdd55ee 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -16,6 +16,9 @@ import { AvailabilityListScreen } from '../screens/AvailabilityListScreen'; import { AddAvailabilityScreen } from '../screens/AddAvailabilityScreen'; import { EditAvailabilityScreen } from '../screens/EditAvailabilityScreen'; import { PaymentProfileScreen } from '../screens/PaymentProfileScreen'; +import { ManageServicesScreen } from '../screens/ManageServicesScreen'; +import { AddEditServiceScreen } from '../screens/AddEditServiceScreen'; +import { Service } from '../services/serviceService'; export type RootStackParamList = { Login: undefined; @@ -32,6 +35,8 @@ export type RootStackParamList = { end_time: string; }; PaymentProfile: undefined; + ManageServices: undefined; + AddEditService: { service?: Service }; }; export type TabParamList = { @@ -120,6 +125,8 @@ export const AppNavigator = () => { + + ); diff --git a/src/screens/AddEditServiceScreen.tsx b/src/screens/AddEditServiceScreen.tsx new file mode 100644 index 0000000..60a9a9b --- /dev/null +++ b/src/screens/AddEditServiceScreen.tsx @@ -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; + route: RouteProp; +} + +export const AddEditServiceScreen: React.FC = ({ navigation, route }) => { + const existingService = route.params?.service; + const isEditing = !!existingService; + + 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() || '', + is_active: existingService?.is_active ?? true, + }); + + 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); + + 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 = {}; + 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 ( + +
+ + + + + {isEditing ? 'Update the details of your service below.' : 'Add a new service that you offer to clients.'} + + + + handleChange('name', val)} + error={errors.name} + /> + handleChange('description', val)} + error={errors.description} + 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 + + + + +