Two month of work

This commit is contained in:
2026-02-17 10:35:03 +01:00
parent 0ffbeeedff
commit d9fd3d1ccb
158 changed files with 5637 additions and 1512 deletions
+185
View File
@@ -0,0 +1,185 @@
<script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue';
import { Button } from '@/components/ui/crm-button'
import TextEditor from '@/components/TextEditor.vue';
import { Trash2, CornerDownLeft, Pencil, X } from "lucide-vue-next"
import { Note, NoteableType } from '@/types';
import { bgColorForString, toDatetimeLocal, toDuration, toShortISOString } from '@/lib/utils';
import { getInitials } from '@/composables/useInitials';
import { alertStore } from '@/stores/alertStore';
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Input } from './ui/crm-input';
import axios, { AxiosError } from 'axios';
import { toast } from 'vue-sonner';
import { usePage } from '@inertiajs/vue3';
import NotesService from '@/services/NotesService';
const props = defineProps<{
notableId: number
notableType: NoteableType
title?: string
modelValue?: Note[]
}>()
defineEmits(['update:modelValue'])
const notes = ref(props.modelValue)
const isTakingNote = ref(false)
const noteEditor = useTemplateRef('note-editor')
const noteDate = ref<string>(toDatetimeLocal(new Date())) //
const alert = alertStore()
const page = usePage();
const auth = computed(() => page.props.auth);
watch(() => props.modelValue, (newValue) => {
notes.value = newValue
})
const toggleNoteEditor = () => {
isTakingNote.value = !isTakingNote.value
if (isTakingNote.value) {
noteDate.value = toDatetimeLocal(new Date())
noteEditor.value?.editor?.commands.clearContent()
noteEditor.value?.editor?.commands.focus()
}
}
const saveNote = async () => {
if (!noteEditor.value?.getContent().trim()) {
isTakingNote.value = false
return
}
try {
const response = await axios.post(`/api/notes`, {
userId: auth.value.user.id,
text: noteEditor.value?.getContent(),
noteableId: props.notableId,
noteableType: props.notableType,
createdAt: new Date(noteDate.value).toISOString() || new Date().toISOString()
});
// Add to notes array and sort by creation date
notes.value?.unshift(response.data)
notes.value?.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
// Clear editor and hide the note editor
toggleNoteEditor()
} catch (error) {
console.error("Fehler beim Speichern der Notiz:", error);
toast.error("Fehler beim Speichern der Notiz", {
description: (error as AxiosError).message || String(error)
});
}
}
const deleteNote = async (id: number) => {
alert.show(
"Möchtest Du diese Notiz wirklich löschen?", null,
{
actionText: "Löschen",
actionVariant: "destructive",
onAction: async () => {
const deleted = await NotesService.deleteNote(id)
if (!deleted) return
// Remove from notes array
const index = notes.value?.findIndex(note => note.id === id)
if (index !== -1 && index !== undefined) {
notes.value?.splice(index, 1)
}
}
}
)
}
</script>
<template>
<div class="notes overflow-y-auto flex flex-col">
<!-- Header -->
<div class="flex justify-between items-center gap-6 sticky top-0 bg-background z-1 mb-6">
<h2 class="font-bold text-md">{{ title || 'Notizen' }}</h2>
<Button variant="ghost" size="icon" @click="toggleNoteEditor">
<X v-if="isTakingNote" />
<Pencil v-else />
</Button>
</div>
<!-- Note editor -->
<div class="flex flex-col overflow-hidden transition-all min-h-40 bg-accent mb-8 rounded-lg p-4"
:class="{ 'h-0! min-h-0! mb-0 py-0': !isTakingNote }">
<Input type="datetime-local" ref="note-date" class="mb-4" :model-value="noteDate"
@update:model-value="value => noteDate = value as string" />
<TextEditor ref="note-editor" class="grow" />
<div class="flex gap-3 items-center justify-end">
<KbdGroup class="ml-2">
<Kbd class="visible-mac">⌘</Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>
<CornerDownLeft class="h-3 w-3" />
</Kbd>
</KbdGroup>
<Button variant="action" size="sm" @click="saveNote">
Speichern
</Button>
</div>
</div>
<!-- Notes -->
<article v-for="note in notes" class="group">
<div class="text-muted-foreground text-sm font-medium flex gap-3 items-center">
<Avatar class="size-7">
<AvatarImage :src="'storage/uploads/users/' + (note.user.avatar || '')" loading="lazy" />
<AvatarFallback :class="bgColorForString(getInitials(note.user.name))">
{{ getInitials(note.user.name) }}
</AvatarFallback>
</Avatar>
<span class="text-sm">{{ toDuration(note.createdAt) }}</span>
<div class="grow"></div>
<div class="transition-opacity opacity-0 group-hover:opacity-100">
<Button variant="ghost" size="sm" @click="deleteNote(note.id)" class="text-muted-foreground">
<Trash2 />
</Button>
<Button variant="ghost" size="sm" @click="" class="text-muted-foreground">
<Pencil />
</Button>
</div>
</div>
<div v-html="note.text" class="note-content ml-3.5 mt-3 pl-6.5 border-l" />
</article>
</div>
</template>
<style lang="css">
.notes {
p:not(:last-child) {
margin-bottom: calc(var(--spacing) * 1.333);
}
article:not(:last-child) {
margin-bottom: calc(var(--spacing) * 3);
}
article:not(:last-child) .note-content {
padding-bottom: calc(var(--spacing) * 3);
}
blockquote {
margin: calc(var(--spacing) * 4) 0;
padding: calc(var(--spacing) * 4) calc(var(--spacing) * 6);
color: var(--color-muted-foreground);
background-color: var(--color-muted);
border-radius: var(--radius-lg);
border-left: 4px solid var(--color-border);
box-shadow: var(--shadow-md);
}
}
</style>