Files
carry-your-live/carry-your-live/app/add-task.tsx
T

270 lines
9.1 KiB
TypeScript

import React, { useMemo } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform } from 'react-native';
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 { useRouter, useLocalSearchParams } from 'expo-router';
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 { database, collections } from '@/database';
import { TaskFormData, tagsToString } from '@/types';
import { useSettings } from '@/theme';
import { scheduleTaskReminder } from '@/services/notifications';
import { useFriends } from '@/hooks/useFriends';
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(),
});
export default function AddTaskScreen() {
const { isReady } = useDatabase();
const { defaultCategoryId } = useSettings();
const router = useRouter();
const { theme } = useSettings();
const { date: dateParam } = useLocalSearchParams<{ date?: string }>();
const { friends } = useFriends();
const initialCategory = defaultCategoryId || '';
const initialDate = useMemo(() => {
if (!dateParam) return null;
const parsed = new Date(Array.isArray(dateParam) ? dateParam[0] : dateParam);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}, [dateParam]);
const methods = useForm<TaskFormData>({
resolver: zodResolver(taskSchema),
defaultValues: {
title: '',
description: '',
categoryId: initialCategory,
tags: initialCategory ? [initialCategory] : [],
priority: 'none',
dueDate: initialDate,
dueTime: '',
endTime: '',
allDay: false,
repeat: 'none',
repeatInterval: 1,
repeatDays: [],
reminder: 'none',
reminders: '',
assigneeId: null,
subtasks: [],
},
});
const {
handleSubmit,
watch,
setValue,
control,
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');
React.useEffect(() => {
if (tags.length === 0 && initialCategory) {
setValue('tags', [initialCategory]);
}
}, [initialCategory, tags, setValue]);
const onSubmit = async (data: TaskFormData) => {
if (!isReady) return;
const now = new Date();
const dueDateTimestamp = data.dueDate ? data.dueDate.getTime() : 0;
const seriesId = data.repeat !== 'none'
? `series_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`
: '';
const resolvedCategoryId = (data.tags && data.tags[0]) || '';
let createdTask: any = null;
await database.write(async () => {
const task = await collections.tasks.create((t) => {
t.title = data.title.trim();
t.description = data.description || '';
t.categoryId = resolvedCategoryId;
t.tags = tagsToString(data.tags || []);
t.priority = data.priority;
t.completed = false;
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.seriesId = seriesId;
t.reminder = data.reminder || 'none';
t.reminders = data.reminders || '';
t.assigneeId = data.assigneeId ?? null;
t.createdAt = now;
t.updatedAt = now;
});
createdTask = task;
if (data.subtasks && data.subtasks.length > 0) {
for (let i = 0; i < data.subtasks.length; i++) {
const subtask = data.subtasks[i];
if (subtask.title.trim()) {
await collections.subtasks.create((s) => {
s.taskId = task.id;
s.title = subtask.title.trim();
s.description = '';
s.priority = 'none';
s.completed = false;
s.dueDate = 0;
s.dueTime = '';
s.endTime = '';
s.reminder = 'none';
s.assigneeId = null;
s.order = i;
s.createdAt = now;
s.updatedAt = now;
});
}
}
}
});
await scheduleTaskReminder(createdTask);
router.back();
};
if (!isReady) {
return (
<View style={[styles.loadingContainer, { backgroundColor: theme.background }]}>
<Text style={{ color: theme.textFaint }}>Loading...</Text>
</View>
);
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<Header title="Add New Task" showLogo={true} />
<FormProvider {...methods}>
<KeyboardAvoidingView
style={styles.keyboardAvoiding}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<CategorySelector
value={tags}
onChange={(value) => setValue('tags', value)}
error={errors.categoryId?.message}
/>
<Controller
control={control}
name="title"
render={({ field }) => (
<TaskNameInput
value={field.value}
onChangeText={field.onChange}
onBlur={field.onBlur}
error={errors.title?.message}
/>
)}
/>
<SubtasksSection control={control} />
<PrioritySelector
value={priority}
onChange={(value) => setValue('priority', value)}
/>
<DateTimePickerComponent control={control} />
<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: 12,
paddingBottom: 120,
gap: 20,
},
});