diff --git a/app/Console/Commands/CaldavSyncCommand.php b/app/Console/Commands/CaldavSyncCommand.php index 8a66849..22dd99e 100644 --- a/app/Console/Commands/CaldavSyncCommand.php +++ b/app/Console/Commands/CaldavSyncCommand.php @@ -16,7 +16,8 @@ class CaldavSyncCommand extends Command public function handle(CaldavService $service) { - // only run every 5 minutes although the task is called every minute + // Throttle execution + // only run every 5 minutes although the scheduler is run every minute $cacheKey = 'caldav_sync_last_run'; if (\Illuminate\Support\Facades\Cache::has($cacheKey)) { Log::info('CalDAV sync Throttled'); @@ -29,6 +30,7 @@ public function handle(CaldavService $service) $todos = $service->getTodos(); + // Update $count = 0; foreach ($todos as $todo) { // Only update the fields that are present in CalDAV @@ -74,12 +76,27 @@ public function handle(CaldavService $service) } } - Todo::upsert($data, 'id'); + // Get the existing todo from the database + $existingTodo = Todo::find($data['id']); + + if ($existingTodo) { + // Compare the etag or modification date to determine if the todo needs to be updated + if ($existingTodo->etag !== $data['etag'] || $existingTodo->last_modified < $data['last_modified']) { + // Update the existing todo with the new data + $existingTodo->update($data); + } + } else { + // Create a new todo if it doesn't exist + Todo::create($data); + } + $count++; } - // Collect hrefs/URLs returned by the CalDAV server so we can remove local - // todos that belong to this calendar but were deleted on the server. + + // Collect hrefs/URLs returned by the CalDAV server so we can compare + // them to local todos and find those that belong to this calendar but + // were deleted on the server. $hrefs = array_values(array_filter(array_map(function ($t) { return $t->url ?? null; }, $todos))); @@ -98,12 +115,14 @@ public function handle(CaldavService $service) ->delete(); } + // Remove old todos Todo::where('status', 'COMPLETED') ->where('last_modified', '<', now()->subDays(30)) // TODO: get from settings ->where('due_date', '<', now()->subDays(30)) ->delete(); + Log::info("Synced " . count($todos) . " todos."); $this->info("Synced " . count($todos) . " todos."); diff --git a/app/Http/Controllers/TodoController.php b/app/Http/Controllers/TodoController.php index 6563e76..937662d 100644 --- a/app/Http/Controllers/TodoController.php +++ b/app/Http/Controllers/TodoController.php @@ -19,7 +19,7 @@ public function index() * Display a listing of the resource. * @param Request $request * @param string $modelType The type of the model (e.g., 'Customer', 'Invoice') - * @param string $modelId The ID of the model + * @param int $modelId The ID of the model * @return \Illuminate\Http\JsonResponse */ public function todosForModel(Request $request, string $modelType, int $modelId) @@ -27,9 +27,12 @@ public function todosForModel(Request $request, string $modelType, int $modelId) $model = app("App\\Models\\" . $modelType)::findOrFail($modelId); // Load all todos of the model with the user relationship - $todos = $model->todos()->with('type')->orderBy('created_at', 'desc')->get(); + $todos = $model->todos() + ->with('type') + ->orderBy('created_at', 'desc') + ->get(); - // Transformiere die Daten in camelCase + // Transform data to camelCase $notesArray = $todos->map(function ($todo) { return ApiDataTransformer::snakeToCamel($todo->toArray()); }); diff --git a/app/Services/CaldavService.php b/app/Services/CaldavService.php index ad72d2c..40d3b96 100644 --- a/app/Services/CaldavService.php +++ b/app/Services/CaldavService.php @@ -163,8 +163,8 @@ public function getTodos() if (isset($vcalendar->VTODO->PRIORITY)) $todo->priority = $vcalendar->VTODO->PRIORITY->getValue(); if (isset($vcalendar->VTODO->STATUS)) $todo->status = $vcalendar->VTODO->STATUS->getValue(); if (isset($vcalendar->VTODO->{'RELATED-TO'})) $todo->parent = $vcalendar->VTODO->{'RELATED-TO'}->getValue(); - $todo->created_at = $vcalendar->VTODO->CREATED->getDateTime(); - $todo->last_modified = $vcalendar->VTODO->{'LAST-MODIFIED'}->getDateTime(); + $todo->created_at = $vcalendar->VTODO->CREATED?->getDateTime(); + $todo->last_modified = $vcalendar->VTODO->{'LAST-MODIFIED'}?->getDateTime(); $todos[] = $todo; } diff --git a/resources/js/components/Todos.vue b/resources/js/components/Todos.vue index 75e991c..34c22fe 100644 --- a/resources/js/components/Todos.vue +++ b/resources/js/components/Todos.vue @@ -23,46 +23,69 @@ onMounted(() => { } }) +// Define a type for the key-value pairs +type GroupedTodos = Array<{ key: string; todos: Todo[] }>; + const groupedTodos = computed(() => { const groups: Record = {}; if (todos.value) { for (let todo of todos.value) { - if (!todo.dueDate) continue + if (todo.dueDate) { + let dueDate = new Date(todo.dueDate) - let dueDate = new Date(todo.dueDate) - - // today - if (isToday(dueDate)) { - if (!groups['today']) groups['today'] = [] - groups['today'].push(todo) - } - // tomorrow - else if (daysFromNow(dueDate) === 1) { - if (!groups['tomorrow']) groups['tomorrow'] = [] - groups['tomorrow'].push(todo) - } - // overdue - else if (daysFromNow(dueDate) < 0) { - if (!groups['overdue']) groups['overdue'] = [] - groups['overdue'].push(todo) - } - // by month - else { - let month = dueDate.toLocaleDateString('de-DE', { month: 'long' }) - if (!groups[month]) groups[month] = [] - groups[month].push(todo) + // today + if (isToday(dueDate)) { + if (!groups['today']) groups['today'] = [] + groups['today'].push(todo) + } + // tomorrow + else if (daysFromNow(dueDate) === 1) { + if (!groups['tomorrow']) groups['tomorrow'] = [] + groups['tomorrow'].push(todo) + } + // overdue + else if (daysFromNow(dueDate) < 0) { + if (!groups['overdue']) groups['overdue'] = [] + groups['overdue'].push(todo) + } + // by month + else { + let month = dueDate.toLocaleDateString('de-DE', { month: 'long' }) + if (!groups[month]) groups[month] = [] + groups[month].push(todo) + } + } else { + if (!groups['noDueDate']) groups['noDueDate'] = [] + groups['noDueDate'].push(todo) } } } - return groups + // Convert the groupedTodos object to an array of key-value pairs + const entries = Object.entries(groups); + + // Sort the entries so that 'noDueDate' is always last + entries.sort((a, b) => { + if (a[0] === 'noDueDate') return 1; + if (b[0] === 'noDueDate') return -1; + return 0; + }); + + // Convert the sorted array to an array of key-value pairs + const sortedGroupedTodos: GroupedTodos = entries.map(([key, todos]) => ({ + key, + todos, + })); + + return sortedGroupedTodos; }) const groupNameForKey = (key: string) => { if (key === 'today') return 'Heute' if (key === 'tomorrow') return 'Morgen' if (key === 'overdue') return 'Überfällig' + if (key === 'noDueDate') return 'Ohne Fälligkeitsdatum' return key } @@ -105,20 +128,20 @@ const shouldDisplay = (todo: Todo) => {