261 lines
8.7 KiB
TypeScript
261 lines
8.7 KiB
TypeScript
import React from 'react';
|
|
import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform, TouchableOpacity, Alert } from 'react-native';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { Header } from '@/components/Header';
|
|
import { TaskNameInput } from '@/components/TaskNameInput';
|
|
import { DateTimePickerComponent } from '@/components/DateTimePicker';
|
|
import { PrioritySelector } from '@/components/PrioritySelector';
|
|
import { RepeatSelector } from '@/components/RepeatSelector';
|
|
import { ReminderSelector } from '@/components/ReminderSelector';
|
|
import { DescriptionInput } from '@/components/DescriptionInput';
|
|
import { FormButtons } from '@/components/FormButtons';
|
|
import { AssigneeSelector } from '@/components/AssigneeSelector';
|
|
import { useForm, FormProvider, Controller } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { useDatabase } from '@/hooks/useDatabase';
|
|
import { collections } from '@/database';
|
|
import { SubtaskFormData, Reminder, parseReminders, toRemindersString } from '@/types';
|
|
import { useSettings } from '@/theme';
|
|
import { updateSubtask, deleteSubtask } from '@/utils/taskActions';
|
|
import { useFriends } from '@/hooks/useFriends';
|
|
import Svg, { Path } from 'react-native-svg';
|
|
|
|
const subtaskSchema = z.object({
|
|
title: z.string().trim().min(1, 'Subtask name is required').max(100),
|
|
description: z.string().max(1000).optional(),
|
|
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']),
|
|
dueDate: z.date().nullable().optional(),
|
|
dueTime: z.string().optional(),
|
|
endTime: z.string().optional(),
|
|
allDay: z.boolean().optional(),
|
|
repeat: z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']),
|
|
repeatInterval: z.number().int().min(1).max(30).optional(),
|
|
repeatDays: z.array(z.number().int().min(0).max(6)).optional(),
|
|
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']),
|
|
reminders: z.string().optional(),
|
|
assigneeId: z.string().nullable().optional(),
|
|
});
|
|
|
|
export default function SubtaskDetailScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { isReady } = useDatabase();
|
|
const router = useRouter();
|
|
const { theme } = useSettings();
|
|
const { friends } = useFriends();
|
|
const [loaded, setLoaded] = React.useState(false);
|
|
const [notFound, setNotFound] = React.useState(false);
|
|
|
|
const methods = useForm<SubtaskFormData>({
|
|
resolver: zodResolver(subtaskSchema),
|
|
defaultValues: {
|
|
title: '',
|
|
description: '',
|
|
priority: 'none',
|
|
dueDate: null,
|
|
dueTime: '',
|
|
endTime: '',
|
|
allDay: false,
|
|
repeat: 'none',
|
|
repeatInterval: 1,
|
|
repeatDays: [],
|
|
reminder: 'none',
|
|
reminders: '',
|
|
assigneeId: null,
|
|
},
|
|
});
|
|
|
|
const { handleSubmit, watch, setValue, control, reset, formState: { errors } } = methods;
|
|
|
|
const priority = watch('priority');
|
|
const repeat = watch('repeat');
|
|
const repeatInterval = watch('repeatInterval') ?? 1;
|
|
const repeatDays = watch('repeatDays') ?? [];
|
|
const reminder = watch('reminder');
|
|
const reminders = watch('reminders');
|
|
const dueDate = watch('dueDate');
|
|
const assigneeId = watch('assigneeId');
|
|
|
|
React.useEffect(() => {
|
|
if (!id || !isReady) return;
|
|
let mounted = true;
|
|
|
|
(async () => {
|
|
try {
|
|
const subtask = await collections.subtasks.find(id);
|
|
if (!mounted) return;
|
|
reset({
|
|
title: subtask.title,
|
|
description: subtask.description,
|
|
priority: subtask.priority,
|
|
dueDate: subtask.dueDate ? new Date(subtask.dueDate) : null,
|
|
dueTime: subtask.dueTime,
|
|
endTime: subtask.endTime || '',
|
|
allDay: subtask.allDay ?? false,
|
|
repeat: subtask.repeat,
|
|
repeatInterval: subtask.repeatInterval || 1,
|
|
repeatDays: (subtask.repeatDays || '').split(',').map(Number).filter((d) => !Number.isNaN(d)),
|
|
reminder: (subtask.reminder || 'none') as Reminder,
|
|
reminders: subtask.reminders || '',
|
|
assigneeId: subtask.assigneeId ?? null,
|
|
});
|
|
setLoaded(true);
|
|
} catch {
|
|
if (mounted) setNotFound(true);
|
|
}
|
|
})();
|
|
|
|
return () => { mounted = false; };
|
|
}, [id, isReady, reset]);
|
|
|
|
const onSubmit = async (data: SubtaskFormData) => {
|
|
if (!id || !isReady) return;
|
|
|
|
await updateSubtask(id, {
|
|
title: data.title,
|
|
description: data.description || '',
|
|
priority: data.priority,
|
|
dueDate: data.dueDate ? data.dueDate.getTime() : 0,
|
|
dueTime: data.dueTime || '',
|
|
endTime: data.endTime || '',
|
|
allDay: data.allDay ?? false,
|
|
repeat: data.repeat,
|
|
repeatInterval: data.repeatInterval || 1,
|
|
repeatDays: (data.repeatDays || []).join(','),
|
|
reminder: data.reminder || 'none',
|
|
assigneeId: data.assigneeId ?? null,
|
|
});
|
|
|
|
router.back();
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
Alert.alert('Delete Subtask', 'This action cannot be undone.', [
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
{
|
|
text: 'Delete',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
if (!id) return;
|
|
await deleteSubtask(id);
|
|
router.back();
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
if (!isReady || !loaded) {
|
|
return (
|
|
<View style={[styles.loadingContainer, { backgroundColor: theme.background }]}>
|
|
<Text style={{ color: theme.textFaint }}>{notFound ? 'Subtask not found' : 'Loading...'}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
|
<Header
|
|
title="Edit Subtask"
|
|
showLogo={true}
|
|
rightAction={
|
|
<TouchableOpacity onPress={confirmDelete} activeOpacity={0.7} style={[styles.deleteButton, { borderColor: theme.accentBorder, backgroundColor: theme.accentSoft }]}>
|
|
<Svg width={22} height={22} viewBox="0 0 24 24">
|
|
<Path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" stroke={theme.accent} strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
|
</Svg>
|
|
</TouchableOpacity>
|
|
}
|
|
/>
|
|
<FormProvider {...methods}>
|
|
<KeyboardAvoidingView
|
|
style={styles.keyboardAvoiding}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<Controller
|
|
control={control}
|
|
name="title"
|
|
render={({ field }) => (
|
|
<TaskNameInput
|
|
value={field.value}
|
|
onChangeText={field.onChange}
|
|
onBlur={field.onBlur}
|
|
error={errors.title?.message}
|
|
/>
|
|
)}
|
|
/>
|
|
<PrioritySelector
|
|
value={priority}
|
|
onChange={(value) => setValue('priority', value)}
|
|
/>
|
|
<DateTimePickerComponent control={control as any} />
|
|
<RepeatSelector
|
|
value={repeat}
|
|
interval={repeatInterval}
|
|
days={repeatDays}
|
|
onChange={(nextRepeat, nextInterval, nextDays) => {
|
|
setValue('repeat', nextRepeat);
|
|
setValue('repeatInterval', nextInterval);
|
|
setValue('repeatDays', nextDays);
|
|
}}
|
|
/>
|
|
<ReminderSelector
|
|
value={reminders}
|
|
hasDueDate={!!dueDate}
|
|
onChange={(value) => setValue('reminders', value)}
|
|
/>
|
|
<AssigneeSelector
|
|
value={assigneeId}
|
|
onChange={(value: string | null) => setValue('assigneeId', value)}
|
|
friends={friends.map((f) => f.username)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<DescriptionInput
|
|
value={field.value ?? ''}
|
|
onChangeText={field.onChange}
|
|
onBlur={field.onBlur}
|
|
/>
|
|
)}
|
|
/>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
<FormButtons onSubmit={handleSubmit(onSubmit)} submitLabel="Save" />
|
|
</FormProvider>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
keyboardAvoiding: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
paddingHorizontal: 16,
|
|
paddingTop: 8,
|
|
paddingBottom: 100,
|
|
gap: 24,
|
|
},
|
|
deleteButton: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
});
|