Connect CalDAV todos to DB-Items, finish todo component and add it to pipeline items

This commit is contained in:
2026-02-24 16:15:21 +01:00
parent 7e2094847f
commit 823cd6391d
27 changed files with 605 additions and 205 deletions
+48 -25
View File
@@ -4,7 +4,7 @@ import { Badge } from '@/components/ui/crm-badge/'
import AppLayout from '@/layouts/AppLayout.vue'
import { daysFromNow, isSoon, isToday, toCurrency, toDuration } from '@/lib/utils'
import { Head } from '@inertiajs/vue3'
import { Calendar, ClipboardCheck, MessageCircle, CircleHelp, Plus, Trash2 } from 'lucide-vue-next'
import { Calendar, ClipboardCheck, MessageCircle, CircleHelp, Plus, Trash2, Check, SquareCheckBig, MessageSquare } from 'lucide-vue-next'
import { ref, computed } from 'vue'
import axios from 'axios'
import draggable from 'vuedraggable'
@@ -18,9 +18,11 @@ import TextEditor from '@/components/TextEditor.vue'
import Todos from '@/components/Todos.vue';
import Notes from '@/components/Notes.vue'
import NotesService from '@/services/NotesService'
import TodoService from '@/services/TodoService'
import NumberInput from '@/components/ui/crm-number-input/NumberInput.vue';
import { alertStore } from '@/stores/alertStore'
import PipelineService from '@/services/PipelineService'
import { cva } from "class-variance-authority"
interface Props {
pipeline: PipelineLane[]
@@ -42,7 +44,6 @@ const dragOptions = ref({
dragClass: "drag", // Class name for the dragging item
})
const editorDialogOpen = ref(false)
const todos = ref<Todo[]>([])
const alert = alertStore()
const laneSums = computed(() => {
@@ -123,31 +124,35 @@ const getLaneIdFromEl = (el: any): number | null => {
}
const cardClasses = (item: PipelineItem): string => {
// Due date
if (item.dueDate && daysFromNow(item.dueDate) < 0) return "border-l-4 border-destructive"
else if (item.dueDate && isSoon(item.dueDate)) return "border-l-4 border-warning"
return ""
// Destructive
if (item.dueDate && daysFromNow(item.dueDate) < 0 ||
item.nextTodoDueDate && daysFromNow(item.nextTodoDueDate) < 0
) return "border-l-4 border-destructive"
// Warning
if (item.dueDate && isSoon(item.dueDate) ||
item.nextTodoDueDate && isSoon(item.nextTodoDueDate)
) return "border-l-4 border-warning"
}
const badgeVariant = (item: PipelineItem): "default" | "secondary" | "destructive" | "warning" | "outline" | null | undefined => {
const badgeVariant = (date: string | null): "default" | "secondary" | "destructive" | "warning" | "outline" | null | undefined => {
// Due date
if (item.dueDate && daysFromNow(item.dueDate) < 0) return "destructive"
else if (item.dueDate && isSoon(item.dueDate)) return "warning"
if (date && daysFromNow(date) < 0) return "destructive"
else if (date && isSoon(date)) return "warning"
return "secondary"
}
const editItem = async (item: PipelineItem) => {
// Load Todos
// try {
// let response = await axios.get('/api/todos')
// todos.value = response.data
// } catch (error) {
// toast.error('Fehler beim Laden der Daten', { description: (error as AxiosError).message })
// }
// Load todos lazily
if (item.id !== 0 && (item.todos === undefined || item.todos.length === 0)) {
TodoService.getTodosForModel('PipelineItem', item.id).then(todos => {
if (todos) item.todos = todos
})
}
// Load notes lazily
if (item.id !== 0 && (item.notes === undefined || item.notes.length === 0)) {
NotesService.getAllNotes('PipelineItem', item.id).then(notes => {
NotesService.getNotesForModel('PipelineItem', item.id).then(notes => {
if (notes) item.notes = notes
})
}
@@ -177,6 +182,14 @@ const deleteItem = (item: PipelineItem | undefined) => {
}
)
}
const saveItem = (item: PipelineItem | undefined) => {
if (!item) return
if (item.id === 0) {
PipelineService.createPipelineItem(item);
} else {
PipelineService.updatePipelineItem(item);
}
}
</script>
@@ -185,6 +198,7 @@ const deleteItem = (item: PipelineItem | undefined) => {
<Head title="Vertriebspipeline" />
<AppLayout title="Vertriebspipeline">
<div class="flex flex-col h-full">
<!-- Header -->
@@ -263,22 +277,30 @@ const deleteItem = (item: PipelineItem | undefined) => {
toCurrency(item.expectedRevenue) }}</p>
<div class="flex items-center gap-2 flex-wrap">
<Badge variant="secondary" v-if="item.actions">
<ClipboardCheck /> {{ item.actions }}
</Badge>
<Badge v-if="item.dueDate" :variant="badgeVariant(item)">
<Badge v-if="item.dueDate" :variant="badgeVariant(item.dueDate)">
<Calendar />
<span v-if="isToday(item.dueDate)">Heute</span>
<span v-else>{{ toDuration(item.dueDate) }}</span>
</Badge>
<Badge v-if="item.todos && item.todos.length > 0"
:variant="badgeVariant(item.todos[item.todos.length - 1]?.dueDate || null)">
<SquareCheckBig /> {{item.todos.filter(todo => todo.status.toLowerCase() !==
'completed').length}}
</Badge>
<Badge v-else-if="item.todosCount > 0"
:variant="badgeVariant(item.nextTodoDueDate)">
<SquareCheckBig /> {{ item.todosCount }}
</Badge>
<Badge variant="secondary" v-if="item.notes && item.notes.length > 0">
<MessageCircle /> {{ item.notes.length }}
<MessageSquare /> {{ item.notes.length }}
</Badge>
<Badge variant="secondary" v-else-if="item.notesCount > 0">
<MessageCircle /> {{ item.notesCount }}
<MessageSquare /> {{ item.notesCount }}
</Badge>
</div>
</div>
@@ -317,7 +339,8 @@ const deleteItem = (item: PipelineItem | undefined) => {
</template>
<template v-slot:content>
<TextEditor :model-value="selectedItem?.description" @change:model-value="console.log"
<TextEditor :model-value="selectedItem?.description"
@change:model-value="value => { selectedItem!.description = value; saveItem(selectedItem) }"
ref="description-editor" />
<Notes v-if="selectedItem" title="Protokoll" :notableId="selectedItem.id" notableType="PipelineItem"
:modelValue="selectedItem.notes" />
@@ -326,7 +349,7 @@ const deleteItem = (item: PipelineItem | undefined) => {
<template v-slot:sidebar>
<NumberInput label="Erwarteter Umsatz" :modelValue="selectedItem?.expectedRevenue as number" suffix=" "
@update:model-value="console.log" />
<Todos :modelValue="todos" :show-completed="false" />
<Todos v-if="selectedItem" title="Aufgaben" :modelValue="selectedItem.todos" :show-completed="false" />
</template>
</EditorDialog>