import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import { StyleSheet, Alert, ScrollView, TouchableOpacity, TextInput, Platform } from 'react-native';
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text';
import { Task, Priority, TaskStatus, Project, Recurrence } from '@/types/gtd';
import { taskService, projectService } from '@/services';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { parseTaskTitle } from '@/utils/date-parser';
import { isValidRecurrence, WEEKDAY_LABELS } from '@/utils/recurrence';
import DateTimePicker from '@react-native-community/datetimepicker';
type RepeatPreset = 'none' | 'weekly' | 'monthly' | 'custom';
const repeatPresetOf = (recurrence?: Recurrence): RepeatPreset => {
if (!recurrence) return 'none';
if (recurrence.kind === 'weekly') return 'weekly';
if (recurrence.kind === 'monthly') return 'monthly';
return 'custom';
};
interface TaskFormProps {
id?: string;
projectId?: string;
onSave?: (taskId: string) => void;
onClose?: () => void;
}
export const TaskForm = forwardRef(function TaskForm({ id, projectId, onSave, onClose }: TaskFormProps, ref) {
const isNew = id === 'new';
const colorScheme = useColorScheme() ?? 'light';
const colors = Colors[colorScheme];
const [task, setTask] = useState<Partial<Task>>({
title: '',
description: '',
status: undefined,
priority: undefined,
dueDate: undefined,
projectId: projectId,
tagIds: [],
completed: false,
});
const [projects, setProjects] = useState<Project[]>([]);
const [showDatePicker, setShowDatePicker] = useState(false);
useImperativeHandle(ref, () => ({
handleSave,
}));
useEffect(() => {
loadData();
}, [id]);
const loadData = async () => {
try {
const allProjects = await projectService.getAllProjects();
setProjects(allProjects);
if (!isNew && id) {
const tasks = await taskService.getAllTasks();
const foundTask = tasks.find(t => t.id === id);
if (foundTask) {
setTask(foundTask);
} else {
Alert.alert('Error', 'Task not found');
onClose?.();
}
}
} catch {
Alert.alert('Error', 'Failed to load data');
}
};
const handleSave = async () => {
if (!task.title?.trim()) {
return false;
}
// Drop an incomplete/invalid recurrence (e.g. weekdays with nothing selected).
const payload: Partial<Task> = { ...task };
if (payload.recurrence && !isValidRecurrence(payload.recurrence)) {
payload.recurrence = undefined;
}
try {
let savedTask: Task | undefined;
if (isNew) {
savedTask = await taskService.createTask(payload);
} else if (id) {
savedTask = await taskService.updateTask(id, payload);
}
return !!savedTask;
} catch {
Alert.alert('Error', 'Failed to save task');
return false;
}
};
const selectRepeatPreset = (preset: RepeatPreset) => {
if (preset === 'none') {
setTask({ ...task, recurrence: undefined });
} else if (preset === 'weekly') {
setTask({ ...task, recurrence: { kind: 'weekly' } });
} else if (preset === 'monthly') {
setTask({ ...task, recurrence: { kind: 'monthly' } });
} else if (repeatPresetOf(task.recurrence) !== 'custom') {
// Switching into custom: default to "every 2 days".
setTask({ ...task, recurrence: { kind: 'everyNDays', intervalDays: 2 } });
}
};
const setCustomEveryNDays = () => {
setTask({
...task,
recurrence: { kind: 'everyNDays', intervalDays: task.recurrence?.intervalDays ?? 2 },
});
};
const setCustomWeekdays = () => {
setTask({
...task,
recurrence: {
kind: 'weekdays',
weekdays: task.recurrence?.weekdays ?? [new Date().getDay()],
},
});
};
const handleIntervalChange = (text: string) => {
const parsed = parseInt(text, 10);
const intervalDays = Number.isNaN(parsed) ? 1 : Math.max(1, parsed);
setTask({ ...task, recurrence: { kind: 'everyNDays', intervalDays } });
};
const toggleWeekday = (day: number) => {
const current = new Set(task.recurrence?.weekdays ?? []);
if (current.has(day)) {
current.delete(day);
} else {
current.add(day);
}
const weekdays = [...current].sort((a, b) => a - b);
setTask({ ...task, recurrence: { kind: 'weekdays', weekdays } });
};
const handleDelete = async () => {
Alert.alert(
'Delete Task',
'Are you sure you want to delete this task?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
try {
if (id && id !== 'new') {
await taskService.deleteTask(id);
onClose?.();
}
} catch {
Alert.alert('Error', 'Failed to delete task');
}
},
},
]
);
};
const formatDate = (timestamp?: number) => {
if (!timestamp) return 'No date';
return new Date(timestamp).toLocaleDateString('de-DE', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
});
};
const isDueDateToday = (timestamp: number, daysOffset: number) => {
const taskDate = new Date(timestamp);
taskDate.setHours(0, 0, 0, 0);
const compareDate = new Date();
compareDate.setDate(compareDate.getDate() + daysOffset);
compareDate.setHours(0, 0, 0, 0);
return taskDate.getTime() === compareDate.getTime();
};
const setDueDate = (days: number) => {
const date = new Date();
date.setDate(date.getDate() + days);
date.setHours(23, 59, 59, 999);
const newDate = date.getTime();
if (task.dueDate) {
const existingDate = new Date(task.dueDate);
existingDate.setHours(0, 0, 0, 0);
const compareDate = new Date();
compareDate.setDate(compareDate.getDate() + days);
compareDate.setHours(0, 0, 0, 0);
if (existingDate.getTime() === compareDate.getTime()) {
setTask({ ...task, dueDate: undefined });
return;
}
}
setTask({ ...task, dueDate: newDate });
};
const handleDateChange = (event: any, selectedDate?: Date) => {
setShowDatePicker(Platform.OS === 'ios');
if (selectedDate) {
selectedDate.setHours(23, 59, 59, 999);
setTask({ ...task, dueDate: selectedDate.getTime() });
}
};
const openDatePicker = () => {
setShowDatePicker(true);
};
const handleTitleChange = (text: string) => {
const shouldParse = text.endsWith(' ');
if (shouldParse && text.trim()) {
const parsed = parseTaskTitle(text.trim());
if (parsed.detectedDate) {
setTask({
...task,
title: '',
dueDate: parsed.detectedDate,
});
return;
}
}
setTask({ ...task, title: text });
};
return (
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContent}
showsVerticalScrollIndicator={true}
scrollEnabled={true}
nestedScrollEnabled={true}
keyboardShouldPersistTaps="handled"
>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Title *</ThemedText>
<TextInput
style={[styles.input, {
backgroundColor: colors.inputBackground,
borderColor: colors.inputBorder,
color: colors.text
}]}
value={task.title}
onChangeText={handleTitleChange}
placeholder="Enter task title"
placeholderTextColor={colors.placeholder}
autoFocus={isNew}
/>
</ThemedView>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Description</ThemedText>
<TextInput
style={[styles.input, styles.textArea, {
backgroundColor: colors.inputBackground,
borderColor: colors.inputBorder,
color: colors.text
}]}
value={task.description}
onChangeText={(text) => setTask({ ...task, description: text })}
placeholder="Add description"
placeholderTextColor={colors.placeholder}
multiline
numberOfLines={4}
textAlignVertical="top"
/>
</ThemedView>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Priority</ThemedText>
<ThemedView style={styles.priorityContainer}>
{(['high', 'medium', 'low'] as Priority[]).map((priority) => (
<TouchableOpacity
key={priority}
style={[
styles.priorityButton,
{ borderColor: colors.border },
task.priority === priority && [styles.priorityButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
]}
onPress={() => setTask({ ...task, priority: task.priority === priority ? undefined : priority })}
>
<ThemedText
style={[
styles.priorityText,
task.priority === priority && styles.priorityTextActive,
]}
>
{priority === 'high' ? '🔴 High' : priority === 'medium' ? '🟡 Medium' : '🟢 Low'}
</ThemedText>
</TouchableOpacity>
))}
</ThemedView>
</ThemedView>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Due Date</ThemedText>
<ThemedView style={styles.dateContainer}>
<TouchableOpacity
style={[
styles.dateButton,
{ borderColor: colors.border },
task.dueDate != null && isDueDateToday(task.dueDate, 0) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
]}
onPress={() => setDueDate(0)}
>
<ThemedText style={[
styles.dateButtonText,
task.dueDate != null && isDueDateToday(task.dueDate, 0) && styles.dateButtonTextActive
]}>Today</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.dateButton,
{ borderColor: colors.border },
task.dueDate != null && isDueDateToday(task.dueDate, 1) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
]}
onPress={() => setDueDate(1)}
>
<ThemedText style={[
styles.dateButtonText,
task.dueDate != null && isDueDateToday(task.dueDate, 1) && styles.dateButtonTextActive
]}>Tomorrow</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.dateButton,
{ borderColor: colors.border },
task.dueDate != null && isDueDateToday(task.dueDate, 7) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
]}
onPress={() => setDueDate(7)}
>
<ThemedText style={[
styles.dateButtonText,
task.dueDate != null && isDueDateToday(task.dueDate, 7) && styles.dateButtonTextActive
]}>Next Week</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.dateButton,
{ borderColor: colors.border },
]}
onPress={openDatePicker}
>
<ThemedText style={styles.dateButtonText}>📅 Pick Date</ThemedText>
</TouchableOpacity>
</ThemedView>
{task.dueDate && (
<ThemedView style={styles.selectedDateContainer}>
<ThemedText style={styles.selectedDate}>
📅 {formatDate(task.dueDate)}
</ThemedText>
<TouchableOpacity onPress={() => setTask({ ...task, dueDate: undefined })}>
<ThemedText style={[styles.clearDateButton, { color: colors.danger }]}>Clear</ThemedText>
</TouchableOpacity>
</ThemedView>
)}
{showDatePicker && (
<DateTimePicker
value={task.dueDate ? new Date(task.dueDate) : new Date()}
mode="date"
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
onChange={handleDateChange}
minimumDate={new Date()}
/>
)}
</ThemedView>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Repeat</ThemedText>
<ThemedView style={styles.repeatContainer}>
{(['none', 'weekly', 'monthly', 'custom'] as RepeatPreset[]).map((preset) => {
const active = repeatPresetOf(task.recurrence) === preset;
return (
<TouchableOpacity
key={preset}
style={[
styles.repeatButton,
{ borderColor: colors.border },
active && [styles.repeatButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
]}
onPress={() => selectRepeatPreset(preset)}
>
<ThemedText style={[styles.repeatText, active && styles.repeatTextActive]}>
{preset === 'none' ? 'None' : preset === 'weekly' ? 'Weekly' : preset === 'monthly' ? 'Monthly' : 'Custom'}
</ThemedText>
</TouchableOpacity>
);
})}
</ThemedView>
{repeatPresetOf(task.recurrence) === 'custom' && (
<ThemedView style={styles.customRepeat}>
<ThemedView style={styles.repeatContainer}>
<TouchableOpacity
style={[
styles.repeatButton,
{ borderColor: colors.border },
task.recurrence?.kind === 'everyNDays' && [styles.repeatButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
]}
onPress={setCustomEveryNDays}
>
<ThemedText style={[styles.repeatText, task.recurrence?.kind === 'everyNDays' && styles.repeatTextActive]}>
Every N days
</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.repeatButton,
{ borderColor: colors.border },
task.recurrence?.kind === 'weekdays' && [styles.repeatButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
]}
onPress={setCustomWeekdays}
>
<ThemedText style={[styles.repeatText, task.recurrence?.kind === 'weekdays' && styles.repeatTextActive]}>
Weekdays
</ThemedText>
</TouchableOpacity>
</ThemedView>
{task.recurrence?.kind === 'everyNDays' && (
<ThemedView style={styles.intervalRow}>
<ThemedText style={styles.intervalLabel}>Every</ThemedText>
<TextInput
style={[styles.intervalInput, {
backgroundColor: colors.inputBackground,
borderColor: colors.inputBorder,
color: colors.text,
}]}
value={String(task.recurrence.intervalDays ?? 1)}
onChangeText={handleIntervalChange}
keyboardType="number-pad"
maxLength={3}
/>
<ThemedText style={styles.intervalLabel}>day(s)</ThemedText>
</ThemedView>
)}
{task.recurrence?.kind === 'weekdays' && (
<ThemedView style={styles.weekdayRow}>
{WEEKDAY_LABELS.map((label, day) => {
const selected = task.recurrence?.weekdays?.includes(day) ?? false;
return (
<TouchableOpacity
key={label}
style={[
styles.weekdayChip,
{ borderColor: colors.border },
selected && { backgroundColor: colors.tint, borderColor: colors.tint },
]}
onPress={() => toggleWeekday(day)}
>
<ThemedText style={[styles.weekdayChipText, selected && styles.weekdayChipTextActive]}>
{label[0]}
</ThemedText>
</TouchableOpacity>
);
})}
</ThemedView>
)}
</ThemedView>
)}
</ThemedView>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Status</ThemedText>
<ThemedView style={styles.statusContainer}>
{(['waiting', 'someday'] as TaskStatus[]).map((status) => (
<TouchableOpacity
key={status}
style={[
styles.statusButton,
{ borderColor: colors.border },
task.status === status && [styles.statusButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
]}
onPress={() => setTask({ ...task, status: task.status === status ? undefined : status })}
>
<ThemedText
style={[
styles.statusText,
task.status === status && styles.statusTextActive,
]}
>
{status === 'waiting' ? '⏳ Waiting' : '💭 Someday'}
</ThemedText>
</TouchableOpacity>
))}
</ThemedView>
</ThemedView>
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<ThemedText style={styles.label}>Project</ThemedText>
<ThemedView style={styles.projectContainer}>
{projects.map((project) => (
<TouchableOpacity
key={project.id}
style={[
styles.projectButton,
{ borderColor: colors.border },
task.projectId === project.id && [styles.projectButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
]}
onPress={() => setTask({ ...task, projectId: task.projectId === project.id ? undefined : project.id })}
>
<ThemedText style={[styles.projectText, task.projectId === project.id && styles.projectTextActive]}>{project.name}</ThemedText>
</TouchableOpacity>
))}
</ThemedView>
</ThemedView>
{!isNew && (
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
<TouchableOpacity style={[styles.deleteButton, { backgroundColor: colors.dangerBackground }]} onPress={handleDelete}>
<ThemedText style={[styles.deleteButtonText, { color: colors.danger }]}>Delete Task</ThemedText>
</TouchableOpacity>
</ThemedView>
)}
</ScrollView>
);
});
const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
scrollViewContent: {
paddingBottom: 120,
},
section: {
paddingHorizontal: 16,
paddingVertical: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
opacity: 0.7,
},
input: {
fontSize: 16,
paddingVertical: 8,
paddingHorizontal: 12,
borderWidth: 1,
borderRadius: 8,
minHeight: 44,
},
textArea: {
minHeight: 100,
paddingTop: 12,
},
priorityContainer: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
priorityButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
},
priorityButtonActive: {
},
priorityText: {
fontSize: 14,
},
priorityTextActive: {
color: '#fff',
fontWeight: '600',
},
dateContainer: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
dateButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
},
dateButtonActive: {
},
dateButtonText: {
fontSize: 14,
},
dateButtonTextActive: {
color: '#fff',
fontWeight: '600',
},
selectedDate: {
fontSize: 14,
opacity: 0.6,
flex: 1,
},
selectedDateContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 8,
},
clearDateButton: {
fontSize: 14,
fontWeight: '600',
},
repeatContainer: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
repeatButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
},
repeatButtonActive: {
},
repeatText: {
fontSize: 14,
},
repeatTextActive: {
color: '#fff',
fontWeight: '600',
},
customRepeat: {
marginTop: 12,
gap: 12,
},
intervalRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
intervalLabel: {
fontSize: 14,
},
intervalInput: {
fontSize: 16,
paddingVertical: 8,
paddingHorizontal: 12,
borderWidth: 1,
borderRadius: 8,
minWidth: 64,
textAlign: 'center',
},
weekdayRow: {
flexDirection: 'row',
gap: 6,
flexWrap: 'wrap',
},
weekdayChip: {
width: 40,
height: 40,
borderRadius: 20,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
weekdayChipText: {
fontSize: 14,
fontWeight: '600',
},
weekdayChipTextActive: {
color: '#fff',
},
statusContainer: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
statusButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
},
statusButtonActive: {
},
statusText: {
fontSize: 14,
},
statusTextActive: {
color: '#fff',
fontWeight: '600',
},
projectContainer: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
projectButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
},
projectButtonActive: {
},
projectText: {
fontSize: 14,
},
projectTextActive: {
color: '#fff',
fontWeight: '600',
},
deleteButton: {
paddingVertical: 16,
borderRadius: 8,
alignItems: 'center',
},
deleteButtonText: {
fontSize: 16,
fontWeight: '600',
},
});