2025-07-30 16:59:58 +02:00

308 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 { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
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(t("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',
}}>
{t('app_name')}
</ThemedText>
< Pressable onPress={() => {
router.back();
}
}>
<Ionicons name='close-circle-outline' size={32} color="#fff" />
</Pressable>
</ThemedView>
< ScrollView style={styles.scrollView} >
<ThemedText type="subtitle">{t('add_new_task')}</ThemedText>
<ThemedView style={styles.stepContainer}>
<TextInput
placeholder={t('task_title_placeholder')}
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={`${t(timeUnits.find(unit => unit.value === selectedTimeUnit)?.label!)} ${t('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.map((tu) => {
return {
label: t(tu.label),
value: tu.value
}
})}
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={t('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(t('task_updated_successfully'));
router.back();
});
} else {
// Create new task
taskRepository.create(new Task(newTask.title, newTask.daysToRedo, newTask.category)).then(() => {
toast.success(t('task_added_successfully'));
router.back();
});
}
} else {
toast.error(t("please_fill_in_all_fields_correctly"));
}
}}>
<Ionicons name='checkmark-circle-outline' size={24} color="#fff" />
<ThemedText style={styles.addButtonText}>{task?.id ? t('update_task') : t('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',
},
});