340 lines
9.1 KiB
Svelte
340 lines
9.1 KiB
Svelte
<script lang="ts">
|
|
import type { Topic, HistoryEntry } from "@ka-note/shared";
|
|
import { get } from "svelte/store";
|
|
import { liveQuery } from "dexie";
|
|
import { db } from "$lib/db/schema";
|
|
import {
|
|
createHistoryEntry,
|
|
updateHistoryEntry,
|
|
updateTopic,
|
|
archiveTopic,
|
|
softDeleteTopic,
|
|
} from "$lib/db/repositories";
|
|
import { today } from "$lib/db/helpers";
|
|
import { processedTopicIds, collapsedTopicIds } from "$lib/stores/agenda";
|
|
import { scopeSettings } from "$lib/stores/scopeContext";
|
|
import { normalizeTitleAndBody } from "$lib/utils/titleUtils";
|
|
import HistoryItem from "./HistoryItem.svelte";
|
|
import LinkTitle from "./LinkTitle.svelte";
|
|
import MeetingControls from "./MeetingControls.svelte";
|
|
import MarkdownEditor from "./MarkdownEditor.svelte";
|
|
import ConfirmDialog from "./ConfirmDialog.svelte";
|
|
|
|
interface Props {
|
|
topic: Topic;
|
|
contextId: string;
|
|
isDailyLog: boolean;
|
|
isMeetingMode: boolean;
|
|
isProcessed: boolean;
|
|
}
|
|
let { topic, contextId, isDailyLog, isMeetingMode, isProcessed }: Props =
|
|
$props();
|
|
|
|
const history = liveQuery(() =>
|
|
db.historyEntries
|
|
.where("topicId")
|
|
.equals(topic.id)
|
|
.filter((h) => !h.deletedAt)
|
|
.toArray()
|
|
.then((entries) => entries.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0))),
|
|
);
|
|
|
|
let noteText = $state("");
|
|
let noteEditor: MarkdownEditor;
|
|
let collapsed = $state(false);
|
|
let confirmDeleteTopic = $state(false);
|
|
let editingTitle = $state(false);
|
|
let titleInput = $state("");
|
|
let showSnoozeDialog = $state(false);
|
|
let snoozeDate = $state("");
|
|
let snoozeDateText = $state("");
|
|
|
|
// Sync collapsed state with global store
|
|
$effect(() => {
|
|
const unsub = collapsedTopicIds.subscribe((s) => {
|
|
collapsed = s.has(topic.id);
|
|
});
|
|
return unsub;
|
|
});
|
|
|
|
function toggleCollapse() {
|
|
collapsedTopicIds.update((s) => {
|
|
const next = new Set(s);
|
|
if (next.has(topic.id)) next.delete(topic.id);
|
|
else next.add(topic.id);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
function saveNote(): boolean {
|
|
const text = noteText.trim();
|
|
if (!text) return false;
|
|
createHistoryEntry(topic.id, today(), text);
|
|
noteText = "";
|
|
noteEditor?.clear();
|
|
return true;
|
|
}
|
|
|
|
function handleNote() {
|
|
saveNote();
|
|
updateTopic(topic.id, { isNew: false });
|
|
processedTopicIds.add(topic.id);
|
|
}
|
|
|
|
function handleSkip() {
|
|
updateTopic(topic.id, { isNew: false });
|
|
processedTopicIds.add(topic.id);
|
|
}
|
|
|
|
function handleSnooze() {
|
|
snoozeDate = "";
|
|
snoozeDateText = "";
|
|
showSnoozeDialog = true;
|
|
}
|
|
|
|
function commitSnooze() {
|
|
const date = snoozeDate || snoozeDateText.trim();
|
|
if (!date) return;
|
|
saveNote();
|
|
updateTopic(topic.id, { snoozeUntil: date, status: "snoozed", isNew: false });
|
|
// Do NOT add to processedTopicIds — snoozed topics are filtered out of openTopics
|
|
// by the status check; adding here would wrongly place them in "Bereits abgelegt heute"
|
|
showSnoozeDialog = false;
|
|
}
|
|
|
|
function handleDone() {
|
|
saveNote();
|
|
updateTopic(topic.id, { isNew: false });
|
|
archiveTopic(topic.id);
|
|
}
|
|
|
|
function handleSaveOnly() {
|
|
if (saveNote()) {
|
|
// Saved
|
|
}
|
|
}
|
|
|
|
function handleEditHistory(id: string, text: string, date: string) {
|
|
updateHistoryEntry(id, text, undefined, date);
|
|
}
|
|
|
|
function startTitleEdit() {
|
|
titleInput = topic.title;
|
|
editingTitle = true;
|
|
}
|
|
|
|
async function saveTitleEdit() {
|
|
const raw = titleInput.trim();
|
|
if (raw) {
|
|
const { maxTitleLength } = get(scopeSettings);
|
|
const { title, body } = normalizeTitleAndBody(raw, '', maxTitleLength);
|
|
await updateTopic(topic.id, { title });
|
|
if (body) {
|
|
await createHistoryEntry(topic.id, today(), body);
|
|
}
|
|
}
|
|
editingTitle = false;
|
|
}
|
|
|
|
function handleTitleKeydown(e: KeyboardEvent) {
|
|
if (e.key === "Enter") saveTitleEdit();
|
|
if (e.key === "Escape") editingTitle = false;
|
|
}
|
|
|
|
// Border color based on status
|
|
const borderColor = $derived(
|
|
isProcessed
|
|
? "border-l-muted"
|
|
: topic.isNew
|
|
? "border-l-success"
|
|
: "border-l-accent",
|
|
);
|
|
|
|
const cardBg = $derived(isProcessed ? "bg-[#222]" : "bg-card-bg");
|
|
const cardOpacity = $derived(isProcessed ? "opacity-60" : "");
|
|
|
|
const createdDate = $derived(
|
|
$history?.find((h) => h.text === 'Thema angelegt.')?.date ?? null,
|
|
);
|
|
|
|
// Determine which controls to show
|
|
const showDailyControls = $derived(isDailyLog);
|
|
const showMeetingControls = $derived(
|
|
!isDailyLog && isMeetingMode && !isProcessed,
|
|
);
|
|
const showPrepControls = $derived(
|
|
!isDailyLog && (!isMeetingMode || isProcessed),
|
|
);
|
|
</script>
|
|
|
|
<div
|
|
class="rounded-2xl mb-5 shadow-lg border-l-[4px] {borderColor} {cardBg} {cardOpacity} transition-all"
|
|
>
|
|
<!-- Header -->
|
|
<div
|
|
class="flex items-center justify-between px-5 py-4 cursor-pointer select-none hover:bg-white/5"
|
|
onclick={toggleCollapse}
|
|
role="button"
|
|
tabindex="0"
|
|
onkeydown={(e) => e.key === "Enter" && toggleCollapse()}
|
|
>
|
|
<div class="flex items-center flex-1 text-lg font-bold">
|
|
<span
|
|
class="mr-2.5 text-xs text-[#aaa] transition-transform inline-block {collapsed
|
|
? '-rotate-90'
|
|
: ''}">▼</span
|
|
>
|
|
|
|
{#if editingTitle}
|
|
<!-- svelte-ignore a11y_autofocus -->
|
|
<input
|
|
class="bg-[#111] text-white border border-accent rounded px-2 py-1 text-lg font-bold flex-1"
|
|
bind:value={titleInput}
|
|
onkeydown={handleTitleKeydown}
|
|
onblur={saveTitleEdit}
|
|
onclick={(e) => e.stopPropagation()}
|
|
autofocus
|
|
/>
|
|
{:else}
|
|
<LinkTitle text={topic.title} />
|
|
<button
|
|
class="opacity-40 group-hover:opacity-100 ml-2.5 bg-transparent border-none text-[#aaa] cursor-pointer text-xs hover:text-white"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
startTitleEdit();
|
|
}}>✎</button
|
|
>
|
|
{/if}
|
|
|
|
{#if createdDate}
|
|
<span class="ml-2 text-[0.7em] text-[#555]">{createdDate}</span>
|
|
{/if}
|
|
{#if topic.isNew}
|
|
<span
|
|
class="ml-2.5 rounded bg-success px-1.5 py-0.5 text-[0.7em] font-bold uppercase text-white"
|
|
>NEU</span
|
|
>
|
|
{/if}
|
|
{#if isProcessed}
|
|
<button
|
|
class="ml-2.5 rounded bg-[#444] px-1.5 py-0.5 text-[0.7em] text-[#aaa] hover:bg-[#555] hover:text-white"
|
|
title="Als aktiv markieren"
|
|
onclick={(e) => { e.stopPropagation(); processedTopicIds.remove(topic.id); }}
|
|
>BESPROCHEN</button
|
|
>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
class="ml-2 text-[#666] hover:text-red-400 text-lg leading-none"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
confirmDeleteTopic = true;
|
|
}}
|
|
title="Thema löschen">×</button
|
|
>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
{#if !collapsed}
|
|
<div class="px-5 pb-5 border-t border-[#444]">
|
|
<!-- Note input -->
|
|
<div class="mt-2.5 flex flex-col gap-1">
|
|
<MarkdownEditor
|
|
bind:this={noteEditor}
|
|
placeholder="- Notiz... (z.B. '-> NAME' or '@P:PROJECT')"
|
|
wikiScope={null}
|
|
onchange={(md) => (noteText = md)}
|
|
onsave={handleSaveOnly}
|
|
onsavearchive={handleDone}
|
|
/>
|
|
|
|
{#if showDailyControls}
|
|
<MeetingControls
|
|
{contextId}
|
|
{isDailyLog}
|
|
{isMeetingMode}
|
|
{isProcessed}
|
|
onnote={handleNote}
|
|
onsnooze={handleSnooze}
|
|
ondone={handleDone}
|
|
/>
|
|
{:else if showMeetingControls}
|
|
<MeetingControls
|
|
{contextId}
|
|
{isDailyLog}
|
|
{isMeetingMode}
|
|
{isProcessed}
|
|
onnote={handleNote}
|
|
onskip={handleSkip}
|
|
onsnooze={handleSnooze}
|
|
ondone={handleDone}
|
|
/>
|
|
{:else if showPrepControls}
|
|
<MeetingControls
|
|
{contextId}
|
|
{isDailyLog}
|
|
{isMeetingMode}
|
|
{isProcessed}
|
|
onnote={handleSaveOnly}
|
|
onsnooze={handleSnooze}
|
|
ondone={handleDone}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if showSnoozeDialog}
|
|
<div class="mt-2 rounded border border-[#555] bg-[#2a2a2a] p-3">
|
|
<div class="mb-2 text-sm font-semibold text-[#ccc]">Verschieben auf</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<input
|
|
type="date"
|
|
class="rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white"
|
|
bind:value={snoozeDate}
|
|
oninput={() => (snoozeDateText = "")}
|
|
/>
|
|
<span class="text-xs text-[#888]">oder</span>
|
|
<input
|
|
type="text"
|
|
placeholder="YYYY-MM-DD"
|
|
class="w-32 rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white placeholder-[#666]"
|
|
bind:value={snoozeDateText}
|
|
oninput={() => (snoozeDate = "")}
|
|
onkeydown={(e) => e.key === 'Enter' && commitSnooze()}
|
|
/>
|
|
<button
|
|
class="rounded bg-warning px-3 py-1 text-sm font-bold text-[#222] hover:brightness-110"
|
|
onclick={commitSnooze}
|
|
>Verschieben</button>
|
|
<button
|
|
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
|
|
onclick={() => (showSnoozeDialog = false)}
|
|
>Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- History -->
|
|
{#if $history?.some((h) => h.text !== 'Thema angelegt.')}
|
|
<div
|
|
class="mt-4 max-h-[300px] overflow-y-auto rounded border border-[#333] bg-[#222] p-2.5"
|
|
>
|
|
{#each $history as entry (entry.id)}
|
|
<HistoryItem {entry} onedit={handleEditHistory} />
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if confirmDeleteTopic}
|
|
<ConfirmDialog
|
|
message="Thema wirklich löschen?"
|
|
onconfirm={() => {
|
|
softDeleteTopic(topic.id);
|
|
confirmDeleteTopic = false;
|
|
}}
|
|
oncancel={() => (confirmDeleteTopic = false)}
|
|
/>
|
|
{/if}
|