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
+7 -8
View File
@@ -1,21 +1,20 @@
<script setup lang="ts">
import Heading from '@/components/Heading.vue';
import { onMounted, ref, computed } from "vue"
import { onMounted, ref } from "vue"
import AppLayout from '@/layouts/AppLayout.vue';
import { Trophy, ArrowRight, UserCheck2, Repeat, ClipboardCheck, X, ChevronRight } from 'lucide-vue-next';
import { Trophy, UserCheck2, X, ChevronRight } from 'lucide-vue-next';
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
import Button from '@/components/ui/crm-button/Button.vue';
import { invoices } from '@/routes';
import { toLocalDate, toRoundedCurrency } from '@/lib/utils'
import { toRoundedCurrency } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/crm-badge'
import { Link, usePage } from '@inertiajs/vue3';
import axios, { AxiosError } from "axios";
import { toast } from "vue-sonner";
import { Todo } from "@/types";
import { AppPageProps, Todo } from "@/types";
import Todos from '@/components/Todos.vue';
const salesStatistics = ref({
@@ -83,7 +82,7 @@ onMounted(async () => {
</AlertDescription>
<Button variant="ghost" class="rounded-full w-6 h-6 absolute top-0.5 right-0.5">
<X />
<X />
</Button>
</Alert>
@@ -97,7 +96,7 @@ onMounted(async () => {
</AlertDescription>
<Button variant="ghost" class="rounded-full w-6 h-6 absolute top-0.5 right-0.5">
<X />
<X />
</Button>
</Alert>
@@ -122,7 +121,7 @@ onMounted(async () => {
</div>
</CardHeader>
<CardContent>
<Todos :modelValue="todos" :show-completed="showCompleted" />
<Todos :modelValue="todos" :show-completed="showCompleted" :show-todoable="true" />
</CardContent>
</Card>
+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>
+4 -1
View File
@@ -159,11 +159,14 @@ const createInvoice = async () => {
let billedEntries: TimesheetEntry[] = []
let invoice: Invoice = newInvoice()
invoice.customer = null
invoice.customerId = null
invoice.billingData = null
timesheets.value[selectedTimesheet.value].entries?.forEach((entry => {
timesheets.value[selectedTimesheet.value].entries?.forEach(((entry, i) => {
if (entry.billed) return
let lineItem: LineItem = newLineItem(false)
lineItem.position = i + 1
lineItem.title = toLocalDate(entry.date)
lineItem.description = entry.description || ''
lineItem.quantity = entry.hours