Fix: CardDAV sync broke when todo items didn't have a creation date. Also fixed todos without a due date not being displays and wrong calculation of warning badge states.
This commit is contained in:
@@ -16,7 +16,8 @@ class CaldavSyncCommand extends Command
|
|||||||
|
|
||||||
public function handle(CaldavService $service)
|
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';
|
$cacheKey = 'caldav_sync_last_run';
|
||||||
if (\Illuminate\Support\Facades\Cache::has($cacheKey)) {
|
if (\Illuminate\Support\Facades\Cache::has($cacheKey)) {
|
||||||
Log::info('CalDAV sync Throttled');
|
Log::info('CalDAV sync Throttled');
|
||||||
@@ -29,6 +30,7 @@ public function handle(CaldavService $service)
|
|||||||
|
|
||||||
$todos = $service->getTodos();
|
$todos = $service->getTodos();
|
||||||
|
|
||||||
|
// Update
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($todos as $todo) {
|
foreach ($todos as $todo) {
|
||||||
// Only update the fields that are present in CalDAV
|
// 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++;
|
$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) {
|
$hrefs = array_values(array_filter(array_map(function ($t) {
|
||||||
return $t->url ?? null;
|
return $t->url ?? null;
|
||||||
}, $todos)));
|
}, $todos)));
|
||||||
@@ -98,12 +115,14 @@ public function handle(CaldavService $service)
|
|||||||
->delete();
|
->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Remove old todos
|
// Remove old todos
|
||||||
Todo::where('status', 'COMPLETED')
|
Todo::where('status', 'COMPLETED')
|
||||||
->where('last_modified', '<', now()->subDays(30)) // TODO: get from settings
|
->where('last_modified', '<', now()->subDays(30)) // TODO: get from settings
|
||||||
->where('due_date', '<', now()->subDays(30))
|
->where('due_date', '<', now()->subDays(30))
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
|
|
||||||
Log::info("Synced " . count($todos) . " todos.");
|
Log::info("Synced " . count($todos) . " todos.");
|
||||||
$this->info("Synced " . count($todos) . " todos.");
|
$this->info("Synced " . count($todos) . " todos.");
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public function index()
|
|||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param string $modelType The type of the model (e.g., 'Customer', 'Invoice')
|
* @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
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
public function todosForModel(Request $request, string $modelType, int $modelId)
|
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);
|
$model = app("App\\Models\\" . $modelType)::findOrFail($modelId);
|
||||||
|
|
||||||
// Load all todos of the model with the user relationship
|
// 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) {
|
$notesArray = $todos->map(function ($todo) {
|
||||||
return ApiDataTransformer::snakeToCamel($todo->toArray());
|
return ApiDataTransformer::snakeToCamel($todo->toArray());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,8 +163,8 @@ public function getTodos()
|
|||||||
if (isset($vcalendar->VTODO->PRIORITY)) $todo->priority = $vcalendar->VTODO->PRIORITY->getValue();
|
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->STATUS)) $todo->status = $vcalendar->VTODO->STATUS->getValue();
|
||||||
if (isset($vcalendar->VTODO->{'RELATED-TO'})) $todo->parent = $vcalendar->VTODO->{'RELATED-TO'}->getValue();
|
if (isset($vcalendar->VTODO->{'RELATED-TO'})) $todo->parent = $vcalendar->VTODO->{'RELATED-TO'}->getValue();
|
||||||
$todo->created_at = $vcalendar->VTODO->CREATED->getDateTime();
|
$todo->created_at = $vcalendar->VTODO->CREATED?->getDateTime();
|
||||||
$todo->last_modified = $vcalendar->VTODO->{'LAST-MODIFIED'}->getDateTime();
|
$todo->last_modified = $vcalendar->VTODO->{'LAST-MODIFIED'}?->getDateTime();
|
||||||
|
|
||||||
$todos[] = $todo;
|
$todos[] = $todo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,46 +23,69 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Define a type for the key-value pairs
|
||||||
|
type GroupedTodos = Array<{ key: string; todos: Todo[] }>;
|
||||||
|
|
||||||
const groupedTodos = computed(() => {
|
const groupedTodos = computed(() => {
|
||||||
const groups: Record<string, Todo[]> = {};
|
const groups: Record<string, Todo[]> = {};
|
||||||
|
|
||||||
if (todos.value) {
|
if (todos.value) {
|
||||||
for (let todo of 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)) {
|
||||||
// today
|
if (!groups['today']) groups['today'] = []
|
||||||
if (isToday(dueDate)) {
|
groups['today'].push(todo)
|
||||||
if (!groups['today']) groups['today'] = []
|
}
|
||||||
groups['today'].push(todo)
|
// tomorrow
|
||||||
}
|
else if (daysFromNow(dueDate) === 1) {
|
||||||
// tomorrow
|
if (!groups['tomorrow']) groups['tomorrow'] = []
|
||||||
else if (daysFromNow(dueDate) === 1) {
|
groups['tomorrow'].push(todo)
|
||||||
if (!groups['tomorrow']) groups['tomorrow'] = []
|
}
|
||||||
groups['tomorrow'].push(todo)
|
// overdue
|
||||||
}
|
else if (daysFromNow(dueDate) < 0) {
|
||||||
// overdue
|
if (!groups['overdue']) groups['overdue'] = []
|
||||||
else if (daysFromNow(dueDate) < 0) {
|
groups['overdue'].push(todo)
|
||||||
if (!groups['overdue']) groups['overdue'] = []
|
}
|
||||||
groups['overdue'].push(todo)
|
// by month
|
||||||
}
|
else {
|
||||||
// by month
|
let month = dueDate.toLocaleDateString('de-DE', { month: 'long' })
|
||||||
else {
|
if (!groups[month]) groups[month] = []
|
||||||
let month = dueDate.toLocaleDateString('de-DE', { month: 'long' })
|
groups[month].push(todo)
|
||||||
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) => {
|
const groupNameForKey = (key: string) => {
|
||||||
if (key === 'today') return 'Heute'
|
if (key === 'today') return 'Heute'
|
||||||
if (key === 'tomorrow') return 'Morgen'
|
if (key === 'tomorrow') return 'Morgen'
|
||||||
if (key === 'overdue') return 'Überfällig'
|
if (key === 'overdue') return 'Überfällig'
|
||||||
|
if (key === 'noDueDate') return 'Ohne Fälligkeitsdatum'
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,20 +128,20 @@ const shouldDisplay = (todo: Todo) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="todos" v-for="(todos, groupKey) in groupedTodos" :key="groupKey">
|
<div v-if="todos" v-for="group in groupedTodos">
|
||||||
<div v-if="!todosEmpty(todos)">
|
<div v-if="!todosEmpty(group.todos)">
|
||||||
<!-- Group header -->
|
<!-- Group header -->
|
||||||
<h3 class="mt-4 mb-2 text-sm text-muted-foreground" :class="{
|
<h3 class="mt-4 mb-2 text-sm text-muted-foreground" :class="{
|
||||||
'text-destructive! font-bold': groupKey === 'overdue',
|
'text-destructive! font-bold': group.key === 'overdue',
|
||||||
'text-warning! font-bold': groupKey === 'today'
|
'text-warning! font-bold': group.key === 'today'
|
||||||
}">
|
}">
|
||||||
{{ groupNameForKey(groupKey) }}
|
{{ groupNameForKey(group.key) }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="todo in todos" class="flex gap-3 items-baseline py-2.5 pr-1 transition-all"
|
<li v-for="todo in group.todos" class="flex gap-3 items-baseline py-2.5 pr-1 transition-all"
|
||||||
:class="{ 'scale-y-0 h-0 py-0! my-0 origin-top': !shouldDisplay(todo) }">
|
:class="{ 'scale-y-0 h-0 py-0! my-0 origin-top': !shouldDisplay(todo) }">
|
||||||
|
|
||||||
<!-- Check mark -->
|
<!-- Check mark -->
|
||||||
@@ -148,11 +171,11 @@ const shouldDisplay = (todo: Todo) => {
|
|||||||
<div class="text-xs text-muted-foreground flex gap-3 items-center mt-1">
|
<div class="text-xs text-muted-foreground flex gap-3 items-center mt-1">
|
||||||
<Badge v-if="props.showTodoable && todoBadge(todo.title)" variant="outline">{{
|
<Badge v-if="props.showTodoable && todoBadge(todo.title)" variant="outline">{{
|
||||||
todoBadge(todo.title)
|
todoBadge(todo.title)
|
||||||
}}
|
}}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span v-if="todo.dueDate" :class="{
|
<span v-if="todo.dueDate" :class="{
|
||||||
'text-destructive! font-bold': groupKey === 'overdue',
|
'text-destructive! font-bold': group.key === 'overdue',
|
||||||
'text-warning! font-bold': groupKey === 'today'
|
'text-warning! font-bold': group.key === 'today'
|
||||||
}">
|
}">
|
||||||
{{ toDuration(todo.dueDate) }}</span>
|
{{ toDuration(todo.dueDate) }}</span>
|
||||||
<Repeat v-if="todo.recurring" stroke-width="2" :size="14" />
|
<Repeat v-if="todo.recurring" stroke-width="2" :size="14" />
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import TodoService from '@/services/TodoService'
|
|||||||
import NumberInput from '@/components/ui/crm-number-input/NumberInput.vue';
|
import NumberInput from '@/components/ui/crm-number-input/NumberInput.vue';
|
||||||
import { alertStore } from '@/stores/alertStore'
|
import { alertStore } from '@/stores/alertStore'
|
||||||
import PipelineService from '@/services/PipelineService'
|
import PipelineService from '@/services/PipelineService'
|
||||||
import { cva } from "class-variance-authority"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pipeline: PipelineLane[]
|
pipeline: PipelineLane[]
|
||||||
@@ -144,6 +143,12 @@ const badgeVariant = (date: string | null): "default" | "secondary" | "destructi
|
|||||||
return "secondary"
|
return "secondary"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemsNextTodoDueDate = (item: PipelineItem): string | null => {
|
||||||
|
if (!item.todos || item.todos.length === 0) return null
|
||||||
|
const next = item.todos.find(t => t.status.toLowerCase() !== 'completed')
|
||||||
|
return next?.dueDate || null
|
||||||
|
}
|
||||||
|
|
||||||
const editItem = async (item: PipelineItem) => {
|
const editItem = async (item: PipelineItem) => {
|
||||||
// Load todos lazily
|
// Load todos lazily
|
||||||
if (item.id !== 0 && (item.todos === undefined || item.todos.length === 0)) {
|
if (item.id !== 0 && (item.todos === undefined || item.todos.length === 0)) {
|
||||||
@@ -287,7 +292,7 @@ const saveItem = (item: PipelineItem | undefined) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Badge v-if="item.todos && item.todos.length > 0"
|
<Badge v-if="item.todos && item.todos.length > 0"
|
||||||
:variant="badgeVariant(item.todos[item.todos.length - 1]?.dueDate || null)">
|
:variant="badgeVariant(itemsNextTodoDueDate(item))">
|
||||||
<SquareCheckBig /> {{item.todos.filter(todo => todo.status.toLowerCase() !==
|
<SquareCheckBig /> {{item.todos.filter(todo => todo.status.toLowerCase() !==
|
||||||
'completed').length}}
|
'completed').length}}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -351,7 +356,7 @@ const saveItem = (item: PipelineItem | undefined) => {
|
|||||||
<template v-slot:sidebar>
|
<template v-slot:sidebar>
|
||||||
<NumberInput label="Erwarteter Umsatz" :modelValue="selectedItem?.expectedRevenue as number" suffix=" €"
|
<NumberInput label="Erwarteter Umsatz" :modelValue="selectedItem?.expectedRevenue as number" suffix=" €"
|
||||||
@update:model-value="console.log" />
|
@update:model-value="console.log" />
|
||||||
<Todos v-if="selectedItem" title="Aufgaben" :modelValue="selectedItem.todos" :show-completed="false" />
|
<Todos v-if="selectedItem" :modelValue="selectedItem.todos" :show-completed="false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</EditorDialog>
|
</EditorDialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user