// odontox-app/components/booking/StaffBookingSheet.tsx
import { forwardRef, useCallback, useMemo, useState } from 'react';
import {
View, Text, StyleSheet, Pressable, TextInput,
FlatList, ActivityIndicator,
} from 'react-native';
import BottomSheet, { BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { Calendar } from 'react-native-calendars';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import Toast from 'react-native-toast-message';
import { Ionicons } from '@expo/vector-icons';
import { Colors, Typography, Spacing } from '@/constants/theme';
import { SlotGrid } from '@/components/ui/SlotGrid';
import { useRecentPatients, type PatientSummary } from '@/hooks/useRecentPatients';
import { papi } from '@/lib/api';
import { useAuthStore } from '@/store/auth';
const staffBookingSchema = z.object({
patientId: z.string().min(1, 'Select a patient'),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Select a date'),
time: z.string().min(1, 'Select a time'),
appointmentType: z.enum(['checkup', 'cleaning', 'emergency', 'consultation', 'other']),
notes: z.string().max(500).optional(),
});
type StaffBookingForm = z.infer<typeof staffBookingSchema>;
const TYPES = [
{ value: 'checkup', label: 'Checkup' },
{ value: 'cleaning', label: 'Cleaning' },
{ value: 'emergency', label: 'Emergency' },
{ value: 'consultation', label: 'Consultation' },
{ value: 'other', label: 'Other' },
] as const;
const today = new Date().toISOString().split('T')[0];
const CAL_THEME = {
selectedDayBackgroundColor: Colors.brand,
selectedDayTextColor: '#fff',
todayTextColor: Colors.brand,
arrowColor: Colors.brand,
};
interface StaffBookingSheetProps {
onSuccess?: () => void;
}
export const StaffBookingSheet = forwardRef<BottomSheet, StaffBookingSheetProps>(
function StaffBookingSheet({ onSuccess }, ref) {
const { user } = useAuthStore();
const snapPoints = useMemo(() => ['90%', '95%'], []);
const [searchQuery, setSearchQuery] = useState('');
const [selectedPatient, setSelectedPatient] = useState<PatientSummary | null>(null);
const { recentPatients, searchResults, searching, search, bumpFrequency } = useRecentPatients();
const { control, handleSubmit, watch, setValue, reset, formState: { isSubmitting, errors } } = useForm<StaffBookingForm>({
resolver: zodResolver(staffBookingSchema),
defaultValues: { patientId: '', date: '', time: '', appointmentType: 'checkup', notes: '' },
});
const selectedDate = watch('date');
const selectedTime = watch('time');
// Staff (doctor) see their own ID as doctorId for slot filtering
const doctorIdForSlots = user?.role === 'doctor' ? user.id : undefined;
const renderBackdrop = useCallback(
(props: any) => <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} />,
[],
);
function selectPatient(patient: PatientSummary) {
setSelectedPatient(patient);
setValue('patientId', patient.id, { shouldValidate: true });
setSearchQuery('');
}
async function onSubmit(data: StaffBookingForm) {
try {
await papi.post('/appointments', {
patientId: data.patientId,
appointmentDate: data.date,
appointmentTime: data.time,
appointmentType: data.appointmentType,
notes: data.notes || null,
status: 'scheduled',
...(user?.role === 'doctor' ? { doctorId: user.id } : {}),
});
await bumpFrequency(data.patientId);
Toast.show({ type: 'success', text1: 'Appointment scheduled' });
reset();
setSelectedPatient(null);
setSearchQuery('');
(ref as any)?.current?.close();
onSuccess?.();
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Failed to create appointment';
Toast.show({ type: 'error', text1: 'Could not schedule', text2: msg });
}
}
const displayList = searchQuery.length >= 2 ? searchResults : recentPatients;
const listLabel = searchQuery.length >= 2 ? 'Search results' : 'Recent patients';
return (
<BottomSheet
ref={ref}
index={-1}
snapPoints={snapPoints}
enablePanDownToClose
backdropComponent={renderBackdrop}
backgroundStyle={{ backgroundColor: Colors.surface }}
handleIndicatorStyle={{ backgroundColor: Colors.border }}
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"
>
<BottomSheetScrollView contentContainerStyle={s.container} keyboardShouldPersistTaps="handled">
<Text style={s.heading}>New Appointment</Text>
{/* Patient selection */}
<Text style={s.label}>Patient</Text>
{selectedPatient ? (
<Pressable style={s.selectedPatient} onPress={() => { setSelectedPatient(null); setValue('patientId', ''); }}>
<View style={{ flex: 1 }}>
<Text style={s.patientName}>{selectedPatient.name}</Text>
{selectedPatient.patientNumber && (
<Text style={s.patientNum}>#{selectedPatient.patientNumber}</Text>
)}
</View>
<Ionicons name="close-circle" size={20} color={Colors.textMuted} />
</Pressable>
) : (
<>
<TextInput
style={s.searchInput}
placeholder="Search by name…"
placeholderTextColor={Colors.textMuted}
value={searchQuery}
onChangeText={q => { setSearchQuery(q); search(q); }}
autoCapitalize="words"
returnKeyType="search"
/>
<Text style={s.listLabel}>{listLabel}</Text>
{searching && <ActivityIndicator color={Colors.brand} style={{ marginVertical: 8 }} />}
{displayList.map(p => (
<Pressable
key={p.id}
style={({ pressed }) => [s.patientRow, pressed && { opacity: 0.7 }]}
onPress={() => selectPatient(p)}
>
<Text style={s.patientRowName}>{p.name}</Text>
{p.patientNumber && <Text style={s.patientNum}>#{p.patientNumber}</Text>}
</Pressable>
))}
</>
)}
{errors.patientId && <Text style={s.error}>{errors.patientId.message}</Text>}
{/* Date */}
<Text style={[s.label, { marginTop: Spacing.md }]}>Date</Text>
<Controller
control={control}
name="date"
render={() => (
<Calendar
minDate={today}
onDayPress={day => {
setValue('date', day.dateString, { shouldValidate: true });
setValue('time', '');
}}
markedDates={selectedDate ? { [selectedDate]: { selected: true, selectedColor: Colors.brand } } : {}}
theme={CAL_THEME}
hideExtraDays
enableSwipeMonths
/>
)}
/>
{errors.date && <Text style={s.error}>{errors.date.message}</Text>}
{/* Slots */}
<Text style={[s.label, { marginTop: Spacing.md }]}>Time</Text>
<Controller
control={control}
name="time"
render={() => (
<SlotGrid
date={selectedDate}
doctorId={doctorIdForSlots}
selected={selectedTime}
onSelect={slot => setValue('time', slot, { shouldValidate: true })}
/>
)}
/>
{errors.time && <Text style={s.error}>{errors.time.message}</Text>}
{/* Type */}
<Text style={[s.label, { marginTop: Spacing.md }]}>Type</Text>
<Controller
control={control}
name="appointmentType"
render={({ field: { value, onChange } }) => (
<View style={s.typeRow}>
{TYPES.map(t => (
<Pressable
key={t.value}
onPress={() => onChange(t.value)}
style={[s.typeChip, value === t.value && s.typeChipSelected]}
>
<Text style={[s.typeText, value === t.value && s.typeTextSelected]}>{t.label}</Text>
</Pressable>
))}
</View>
)}
/>
<Pressable
onPress={handleSubmit(onSubmit)}
disabled={isSubmitting}
style={({ pressed }) => [s.btn, pressed && { opacity: 0.8 }, isSubmitting && { opacity: 0.6 }]}
>
{isSubmitting
? <ActivityIndicator color="#fff" />
: <Text style={s.btnText}>Schedule Appointment</Text>}
</Pressable>
</BottomSheetScrollView>
</BottomSheet>
);
}
);
const s = StyleSheet.create({
container: { padding: Spacing.lg, paddingBottom: 80 },
heading: { fontSize: Typography.xl, fontWeight: '700', color: Colors.text, marginBottom: Spacing.lg },
label: { fontSize: Typography.sm, fontWeight: '600', color: Colors.textSecondary, marginBottom: Spacing.sm },
error: { fontSize: Typography.xs, color: Colors.error, marginTop: 4 },
searchInput: {
backgroundColor: Colors.surface,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 10,
paddingHorizontal: Spacing.md,
paddingVertical: 10,
fontSize: Typography.base,
color: Colors.text,
marginBottom: Spacing.sm,
},
listLabel: { fontSize: Typography.xs, color: Colors.textMuted, marginBottom: 4 },
patientRow: {
paddingVertical: 10,
paddingHorizontal: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: Colors.border,
},
patientRowName: { fontSize: Typography.base, color: Colors.text },
patientNum: { fontSize: Typography.xs, color: Colors.textMuted },
selectedPatient: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.brandSurface,
borderRadius: 10,
padding: Spacing.md,
borderWidth: 1,
borderColor: Colors.brand + '40',
},
patientName: { fontSize: Typography.base, fontWeight: '600', color: Colors.text },
typeRow: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm },
typeChip: {
paddingHorizontal: 14, paddingVertical: 8,
borderRadius: 20, borderWidth: 1, borderColor: Colors.border,
backgroundColor: Colors.surface,
},
typeChipSelected: { backgroundColor: Colors.brand, borderColor: Colors.brand },
typeText: { fontSize: Typography.sm, color: Colors.text },
typeTextSelected: { color: '#fff', fontWeight: '600' },
btn: {
marginTop: Spacing.xl,
backgroundColor: Colors.brand,
borderRadius: 12,
paddingVertical: 16,
alignItems: 'center',
},
btnText: { color: '#fff', fontSize: Typography.base, fontWeight: '700' },
});