185 lines
6.5 KiB
Vue
185 lines
6.5 KiB
Vue
|
|
<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>
|