Ka-Note/ka-note/client/src/lib/components/EventCard.svelte

219 lines
7.3 KiB
Svelte

<script lang="ts">
import type { AgendaContext } from '@ka-note/shared';
import type { EventMeta } from '@ka-note/shared';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import MarkdownEditor from './MarkdownEditor.svelte';
import RenderedMarkdown from './RenderedMarkdown.svelte';
import { updateEvent, updateEventNotes, softDeleteContext } from '$lib/db/repositories';
import { notesTopicId } from '$lib/db/repositories';
import { mention } from '$lib/actions/mention';
import { handlePersonClick } from '$lib/actions/refClick';
import { goto } from '$app/navigation';
interface Props {
event: AgendaContext;
}
let { event }: Props = $props();
let meta = $derived(event.meta as EventMeta);
const notesEntries = liveQuery(() =>
db.historyEntries
.where('topicId').equals(notesTopicId(event.id))
.filter(h => !h.deletedAt)
.toArray()
);
const meetingContexts = liveQuery(() =>
db.contexts
.filter(c => !c.deletedAt && c.type === 'meeting' && c.id !== 'daily-log')
.sortBy('sortOrder')
);
let notesText = $derived(($notesEntries ?? [])[0]?.text ?? '');
let editingNotes = $state(false);
let editingMeta = $state(false);
let collapsed = $state(false);
let editTitle = $state('');
let editTime = $state('');
let editParticipants = $state('');
let editLinkedContextId = $state<string>('');
let notesEditor: MarkdownEditor;
const linkedContext = $derived(
meta.linkedContextId
? ($meetingContexts ?? []).find(c => c.id === meta.linkedContextId) ?? null
: null
);
function startEditMeta() {
editTitle = event.name;
editTime = meta.time;
editParticipants = meta.participants
.map(p => p.includes(' ') ? `@"${p}"` : `@${p}`)
.join(' ');
editLinkedContextId = meta.linkedContextId ?? '';
editingMeta = true;
}
function parseParticipants(raw: string): string[] {
const results: string[] = [];
const re = /@"([^"]+)"|@([\w\-\u00C0-\u024F]+)/g;
let m: RegExpExecArray | null;
while ((m = re.exec(raw)) !== null) {
results.push((m[1] ?? m[2]).trim());
}
return results.filter(Boolean);
}
async function saveMeta() {
await updateEvent(event.id, {
title: editTitle.trim() || event.name,
time: editTime,
participants: parseParticipants(editParticipants),
linkedContextId: editLinkedContextId || null,
});
editingMeta = false;
}
async function saveNotes() {
if (!notesEditor) return;
const md = notesEditor.getMarkdown();
await updateEventNotes(event.id, md);
editingNotes = false;
}
async function handleDelete() {
if (confirm(`Meeting "${event.name}" löschen?`)) {
await softDeleteContext(event.id);
}
}
</script>
<div class="group rounded border border-[#333] bg-card-bg">
<!-- Header -->
{#if editingMeta}
<div class="flex flex-col gap-2 p-3">
<input
class="w-full rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white placeholder-[#666]"
type="text"
bind:value={editTitle}
placeholder="Titel"
/>
<div class="flex gap-2">
<input
class="w-28 rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white"
type="time"
bind:value={editTime}
/>
<div class="relative flex-1">
<input
class="w-full rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white placeholder-[#666]"
type="text"
bind:value={editParticipants}
placeholder="Teilnehmer (@Name, ...)"
use:mention
/>
</div>
</div>
<div class="relative w-full">
<select
class="w-full appearance-none rounded px-2 py-1 pr-7 text-sm text-white"
style="background-color: #1a1a22; border: 1px solid #666; min-height: 2rem;"
bind:value={editLinkedContextId}
>
<option value="" style="background-color: #252530">— kein Jour-Fix —</option>
{#each $meetingContexts ?? [] as ctx}
<option value={ctx.id} style="background-color: #252530">{ctx.name}</option>
{/each}
</select>
<span class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-xs text-[#aaa]"></span>
</div>
<div class="flex gap-2">
<button class="rounded bg-accent px-3 py-1 text-sm font-bold text-white hover:brightness-110" onclick={saveMeta}>Speichern</button>
<button class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]" onclick={() => (editingMeta = false)}>Abbrechen</button>
</div>
</div>
{:else}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex cursor-pointer select-none items-start justify-between gap-2 px-3 py-3 hover:bg-white/5"
onclick={() => collapsed = !collapsed}
role="button"
tabindex="0"
onkeydown={(e) => e.key === 'Enter' && (collapsed = !collapsed)}
>
<div class="flex flex-1 items-start gap-2">
<span class="mt-0.5 text-xs text-[#aaa] transition-transform inline-block {collapsed ? '-rotate-90' : ''}"></span>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-2">
{#if meta.time && meta.time !== '00:00'}
<span class="font-mono text-sm text-muted">{meta.time}</span>
{/if}
<span class="font-semibold text-white">{event.name}</span>
{#if linkedContext}
<button
class="rounded bg-accent/15 px-1.5 py-0.5 text-xs text-accent hover:bg-accent/30 transition-colors"
onclick={(e) => { e.stopPropagation(); goto(`/context/${linkedContext.id}`); }}
title="Zum Jour-Fix navigieren"
>{linkedContext.name}</button>
{/if}
</div>
{#if meta.participants.length > 0}
<div class="mt-1 flex flex-wrap gap-1">
{#each meta.participants as p}
<button
class="rounded-full bg-[#2a2a2a] px-2 py-0.5 text-xs text-[#aaa] hover:bg-accent/20 hover:text-accent transition-colors"
onclick={(e) => { e.stopPropagation(); handlePersonClick(p, e, e.currentTarget as HTMLElement); }}
title="Zur Person navigieren"
>{p}</button>
{/each}
</div>
{/if}
</div>
</div>
<div class="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button class="rounded p-1 text-muted hover:text-white" onclick={(e) => { e.stopPropagation(); startEditMeta(); }} title="Bearbeiten">✏️</button>
<button class="rounded p-1 text-muted hover:text-red-400" onclick={(e) => { e.stopPropagation(); handleDelete(); }} title="Löschen">🗑️</button>
</div>
</div>
{/if}
<!-- Notes -->
{#if !collapsed}
<div class="border-t border-[#2a2a2a] px-3 pb-3 pt-2">
{#if editingNotes}
<MarkdownEditor
bind:this={notesEditor}
content={notesText}
placeholder="Notizen..."
minHeight="80px"
onsave={saveNotes}
/>
<div class="mt-1 flex gap-2">
<button class="rounded bg-accent px-3 py-1 text-sm font-bold text-white hover:brightness-110" onclick={saveNotes}>Speichern</button>
<button class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]" onclick={() => (editingNotes = false)}>Abbrechen</button>
</div>
{:else}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="min-h-8 cursor-text rounded px-1 py-1 text-sm text-white hover:bg-[#2f2f3a]"
onclick={() => (editingNotes = true)}
onkeydown={(e) => e.key === 'Enter' && (editingNotes = true)}
>
{#if notesText}
<RenderedMarkdown text={notesText} class="text-[#ccc]" />
{:else}
<span class="italic text-muted">Notizen hinzufügen...</span>
{/if}
</div>
{/if}
</div>
{/if}
</div>