import React, { useRef } from 'react'; import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform, TouchableOpacity, Keyboard } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { Header } from '@/components/Header'; import { CategorySelector } from '@/components/CategorySelector'; import { TaskNameInput } from '@/components/TaskNameInput'; import { SubtasksSection } from '@/components/SubtasksSection'; 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 { TaskDeleteModal } from '@/components/TaskDeleteModal'; 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 { database, collections } from '@/database'; import { Q } from '@nozbe/watermelondb'; import { TaskFormData, parseTaskTags, tagsToString } from '@/types'; import { useSettings } from '@/theme'; import { deleteTaskOccurrences } from '@/utils/taskActions'; import { scheduleTaskReminder } from '@/services/notifications'; import { ReminderPreference } from '@/types'; import { recordTombstonesInBatch } from '@/database/tombstones'; import { useFriends } from '@/hooks/useFriends'; import Svg, { Path, Circle } from 'react-native-svg'; const taskSchema = z.object({ title: z.string().trim().min(1, 'Task name is required').max(100), description: z.string().max(1000).optional(), categoryId: z.string().optional(), tags: z.array(z.string()).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(), subtasks: z.array(z.object({ title: z.string(), _key: z.string().optional() })).optional(), }); function CollapsibleSection({ title, children, defaultExpanded = false, icon }: { title: string; children: React.ReactNode; defaultExpanded?: boolean; icon: React.ReactNode }) { const { theme } = useSettings(); const [expanded, setExpanded] = React.useState(defaultExpanded); return ( setExpanded(!expanded)} activeOpacity={0.8}> {icon} {title} {expanded && {children}} ); } export default function TaskDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { isReady } = useDatabase(); const router = useRouter(); const { theme } = useSettings(); const { friends } = useFriends(); const { notifications: notificationsEnabled, reminderPreference } = useSettings(); const [loaded, setLoaded] = React.useState(false); const [notFound, setNotFound] = React.useState(false); const scrollViewRef = useRef(null); const methods = useForm({ resolver: zodResolver(taskSchema), defaultValues: { title: '', description: '', categoryId: '', tags: [], priority: 'none', dueDate: null, dueTime: '', endTime: '', allDay: false, repeat: 'none', repeatInterval: 1, repeatDays: [], reminder: 'none', assigneeId: null, subtasks: [], }, }); const { handleSubmit, watch, setValue, control, reset, formState: { errors } } = methods; const tags = watch('tags') ?? []; 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'); const [deleteModalVisible, setDeleteModalVisible] = React.useState(false); React.useEffect(() => { if (!id || !isReady) return; let mounted = true; (async () => { try { const task = await collections.tasks.find(id); const subtasks = await collections.subtasks.query(Q.where('task_id', id)).fetch(); if (!mounted) return; const remindersValue = task.reminders || (task.reminder && task.reminder !== 'none' ? task.reminder : ''); reset({ title: task.title, description: task.description, categoryId: task.categoryId, tags: parseTaskTags(task.tags, task.categoryId), priority: task.priority, dueDate: task.dueDate ? new Date(task.dueDate) : null, dueTime: task.dueTime, endTime: task.endTime || '', allDay: task.allDay ?? false, repeat: task.repeat, repeatInterval: task.repeatInterval || 1, repeatDays: (task.repeatDays || '').split(',').map(Number).filter((d) => !Number.isNaN(d)), reminder: (task.reminder || 'none') as TaskFormData['reminder'], reminders: remindersValue, assigneeId: task.assigneeId ?? null, subtasks: subtasks.map((s) => ({ title: s.title, _key: s.id })), }); setLoaded(true); } catch { if (mounted) setNotFound(true); } })(); return () => { mounted = false; }; }, [id, isReady, reset]); const onSubmit = async (data: TaskFormData) => { if (!id || !isReady) return; Keyboard.dismiss(); scrollViewRef.current?.scrollTo({ y: 0, animated: true }); const now = new Date(); const dueDateTimestamp = data.dueDate ? data.dueDate.getTime() : 0; let savedTask: any = null; await database.write(async () => { const task = await collections.tasks.find(id); savedTask = task; const existingSubtasks = await collections.subtasks.query(Q.where('task_id', id)).fetch(); await task.update((t) => { t.title = data.title.trim(); t.description = data.description || ''; t.categoryId = (data.tags && data.tags[0]) || task.categoryId || ''; t.tags = tagsToString(data.tags || []); t.priority = data.priority; t.dueDate = dueDateTimestamp; t.dueTime = data.dueTime || ''; t.endTime = data.endTime || ''; t.allDay = data.allDay ?? false; t.repeat = data.repeat; t.repeatInterval = data.repeatInterval || 1; t.repeatDays = (data.repeatDays || []).join(','); t.reminder = data.reminder || 'none'; t.reminders = data.reminders || ''; t.assigneeId = data.assigneeId ?? null; if (data.repeat !== 'none' && !t.seriesId) { t.seriesId = `series_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`; } t.updatedAt = now; }); const keptIds = new Set(); let order = 0; for (const formItem of data.subtasks ?? []) { const trimmed = formItem.title.trim(); if (!trimmed) continue; if (formItem._key) { const existing = existingSubtasks.find((s) => s.id === formItem._key && !s.parentSubtaskId); if (existing) { keptIds.add(existing.id); await existing.update((s) => { s.title = trimmed; s.order = order; s.updatedAt = now; }); order++; continue; } } await collections.subtasks.create((s) => { s.taskId = task.id; s.categoryId = task.categoryId || ''; s.tags = task.tags || ''; s.title = trimmed; s.description = ''; s.priority = 'none'; s.completed = false; s.dueDate = 0; s.dueTime = ''; s.endTime = ''; s.allDay = false; s.repeat = 'none'; s.repeatInterval = 1; s.repeatDays = ''; s.seriesId = ''; s.reminder = 'none'; s.reminders = ''; s.assigneeId = null; s.order = order++; s.createdAt = now; s.updatedAt = now; }); } const removedIds: string[] = []; for (const existing of existingSubtasks) { if (existing.parentSubtaskId) continue; if (keptIds.has(existing.id)) continue; removedIds.push(existing.id); const children = await collections.subtasks.query(Q.where('parent_subtask_id', existing.id)).fetch(); for (const child of children) { await child.update((c) => { c.parentSubtaskId = null; c.updatedAt = now; }); } await existing.destroyPermanently(); } if (removedIds.length > 0) { await recordTombstonesInBatch('subtasks', removedIds); } }); await scheduleTaskReminder(savedTask, notificationsEnabled, reminderPreference); router.back(); }; const handleDelete = () => { setDeleteModalVisible(true); }; const doDelete = async (scope: 'this' | 'future' | 'all') => { if (!id) return; setDeleteModalVisible(false); await deleteTaskOccurrences(id, scope); router.back(); }; if (!isReady || !loaded) { return ( {notFound ? 'Task not found' : 'Loading...'} ); } return (
} /> setValue('tags', value)} error={errors.categoryId?.message} /> ( )} /> setValue('priority', value)} /> } defaultExpanded={!!dueDate}> } defaultExpanded={repeat !== 'none'}> { setValue('repeat', nextRepeat); setValue('repeatInterval', nextInterval); setValue('repeatDays', nextDays); }} /> } defaultExpanded={!!reminders && reminders !== ''}> setValue('reminders', value)} /> } defaultExpanded={!!assigneeId}> setValue('assigneeId', value)} friends={friends.map((f) => f.username)} /> } defaultExpanded={!!methods.getValues('description')}> ( )} /> setDeleteModalVisible(false)} onDelete={doDelete} /> ); } const styles = StyleSheet.create({ container: { flex: 1, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, keyboardAvoiding: { flex: 1, }, scrollContent: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 60, gap: 12, }, section: { gap: 6, }, sectionHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 12, borderRadius: 10, borderWidth: 1, }, sectionHeaderLeft: { flexDirection: 'row', alignItems: 'center', gap: 8, }, sectionTitle: { fontSize: 14, fontWeight: '600', }, sectionContent: { paddingHorizontal: 2, gap: 6, }, deleteButton: { width: 32, height: 32, borderRadius: 10, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, });