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',
},
});