219 lines
7.3 KiB
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>
|