2025-07-30 14:10:23 +02:00

299 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { toast } from '@backpackapp-io/react-native-toast';
import Ionicons from '@expo/vector-icons/Ionicons';
import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
import React, { useState } from 'react';
import { Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native';
import Dropdown from 'react-native-input-select';
import { ThemedText } from '../../components/ThemedText';
import { ThemedView } from '../../components/ThemedView';
import { Category, CategoryQuery } from '../../models/category';
import { Task, TaskQuery } from '../../models/task';
import { CategoryRepository } from '../../repositories/CategoryRepository';
import { TaskRepository } from '../../repositories/TaskRepository';
import { SQLiteDataService } from '../../services/data/sqliteDataService';
const taskRepository = new TaskRepository(new SQLiteDataService<Task>(TaskQuery));
const categoryRepository = new CategoryRepository(new SQLiteDataService<Category>(CategoryQuery));
const timeUnits = [
{ label: 'Minutes', value: 'minutes' },
{ label: 'Hours', value: 'hours' },
{ label: 'Days', value: 'days' },
{ label: 'Weeks', value: 'weeks' },
{ label: 'Months', value: 'months' }
];
const timeUnitToDays = (num: number, unit: string): number => {
let result = 0;
switch (unit) { // convert to days
case 'minutes':
result = num / 1440; // Convert minutes to days
break;
case 'hours':
result = num / 24; // Convert hours to days
break;
case 'days':
result = num; // Convert days to seconds
break;
case 'weeks':
result = num * 7; // Convert weeks to days
break;
case 'months':
result = num * 30; // Convert months to days (approximation)
break;
}
return result;
}
export default function TasksForm() {
const { task: taskJson } = useLocalSearchParams();
const task: Task = taskJson ? JSON.parse(taskJson as string) : null;
const [categories, setCategories] = useState<Category[]>([]);
const [selectedTimeUnit, setSelectedTimeUnit] = useState<string>("days");
const router = useRouter();
const [newTask, setNewTask] = useState({
title: task?.title || '',
daysToRedo: task?.daysToRedo || NaN,
category: task?.category || -1,
});
const adaptDaysToGoToUnit = () => {
if (!task) return;
if (task.daysToRedo! <= 1000 * 3600) {
setSelectedTimeUnit("minutes");
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! * 60 * 24 });
}
else if (task.daysToRedo! <= 1000 * 3600 * 24) {
setSelectedTimeUnit("hours");
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! * 24 });
} else if (task.daysToRedo! <= 1000 * 3600 * 24 * 7) {
setSelectedTimeUnit("days");
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! });
} else if (task.daysToRedo! <= 1000 * 3600 * 24 * 30) {
setSelectedTimeUnit("weeks");
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! / 7 });
} else {
setSelectedTimeUnit("months");
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! / 30 });
}
}
useFocusEffect(React.useCallback(() => {
categoryRepository.findAll().then((categories) => {
categories.push(new Category("Create new category", "", 0));
setCategories(categories);
if (newTask.category === 0) {
setNewTask({ ...newTask, category: -1 });
}
adaptDaysToGoToUnit();
}).catch((error) => {
console.error("Error fetching categories:", error);
});
}, []));
return (
<ThemedView style={styles.mainContainer} >
<Stack.Screen options={{ header: () => null }} />
<ThemedView style={styles.headerContainer}>
<ThemedText type="title" style={{
color: '#fff',
}}> Taskeep! </ThemedText>
< Pressable onPress={() => {
router.back();
}
}>
<Ionicons name='close-circle-outline' size={32} color="#fff" />
</Pressable>
</ThemedView>
< ScrollView style={styles.scrollView} >
<ThemedText type="subtitle">Add a new task</ThemedText>
<ThemedView style={styles.stepContainer}>
<TextInput
placeholder="Task title *"
style={styles.inputText}
placeholderTextColor="#333"
value={newTask.title}
onChange={(e) => setNewTask({ ...newTask, title: e.nativeEvent.text })}
/>
<View style={{ flexDirection: 'row', gap: 8, height: 60 }}>
<TextInput
placeholder={`${timeUnits.find(unit => unit.value === selectedTimeUnit)?.label} after which task is due *`}
style={styles.inputText}
placeholderTextColor="#333"
keyboardType='numeric'
value={isNaN(newTask.daysToRedo ?? NaN) ? undefined : newTask.daysToRedo?.toString()}
onChange={(e) => setNewTask({ ...newTask, daysToRedo: e.nativeEvent.text.length > 0 ? parseInt(e.nativeEvent.text) : NaN })}
/>
<Dropdown
options={timeUnits}
onValueChange={(value) => setSelectedTimeUnit(value?.toString() ?? "")}
selectedValue={selectedTimeUnit}
maxSelectableItems={1}
checkboxControls={{
checkboxLabelStyle: { fontSize: 20, paddingVertical: 10 },
checkboxStyle: {
backgroundColor: '#e744a9ff',
borderColor: 'transparent',
}
}}
dropdownIconStyle={{
alignSelf: 'flex-start',
margin: 0,
padding: 0,
marginTop: -30
}}
dropdownContainerStyle={
{
flex: 1,
marginTop: -2
}
}
dropdownStyle={{
borderColor: '#ccc',
borderWidth: 2,
backgroundColor: '#fff',
}}
/>
</View>
<Dropdown
placeholder="Select category *"
options={
categories.map(category => ({
label: `${category.icon} ${category.title}`,
value: category.id
}))
}
onValueChange={(value) => {
if (value === 0) {
router.push({ pathname: "./categoryForm" });
}
setNewTask({ ...newTask, category: (value ?? -1) as number }
)
}}
selectedValue={newTask.category > -1 ? newTask.category : undefined}
maxSelectableItems={1}
checkboxControls={{
checkboxLabelStyle: { fontSize: 20, paddingVertical: 10 },
checkboxStyle: {
backgroundColor: '#e744a9ff',
borderColor: 'transparent',
}
}}
dropdownIconStyle={{
alignSelf: 'flex-start',
margin: 0,
padding: 0,
marginTop: -30
}}
dropdownStyle={{
borderColor: '#ccc',
borderWidth: 2
}}
/>
</ThemedView>
<Pressable style={styles.addButton} onPress={() => {
newTask.daysToRedo = timeUnitToDays(newTask.daysToRedo, selectedTimeUnit);
if (newTask.title.length > 0 && newTask.daysToRedo > 0 && newTask.category > -1) {
if (task && task.id) {
// Update existing task
taskRepository.update(task.id, {
...task,
...newTask
}).then(() => {
toast.success("Task updated successfully!");
router.back();
});
} else {
// Create new task
taskRepository.create(new Task(newTask.title, newTask.daysToRedo, newTask.category)).then(() => {
toast.success("Task added successfully!");
router.back();
});
}
} else {
toast.error("Please fill in all fields correctly.");
}
}}>
<Ionicons name='checkmark-circle-outline' size={24} color="#fff" />
<ThemedText style={styles.addButtonText}>{task?.id ? "Update" : "Add"} Task</ThemedText>
</Pressable>
< View style={{ height: 50 }} />
</ScrollView >
</ThemedView >
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
},
headerContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 8,
paddingHorizontal: 24,
paddingTop: 50,
paddingBottom: 18,
backgroundColor: '#6a254fff',
},
scrollView: {
padding: 24,
flex: 1,
},
stepContainer: {
gap: 24,
marginBottom: 8,
marginTop: 16,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
},
inputText: {
flex: 1,
backgroundColor: '#fff',
height: 60,
color: '#333333',
borderRadius: 8,
padding: 10,
lineHeight: 30,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
borderWidth: 2,
borderColor: '#ccc',
},
textArea: {
backgroundColor: '#fff',
color: '#333333',
borderRadius: 8,
padding: 10,
paddingHorizontal: 32,
minHeight: 100,
textAlignVertical: 'top'
},
addButton: {
flexDirection: 'row',
gap: 8,
backgroundColor: '#b91f7eff',
color: '#fff',
borderRadius: 8,
padding: 20,
alignItems: 'center',
justifyContent: 'center',
},
addButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});