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

233 lines
7.8 KiB
Svelte

<script lang="ts">
import { page } from '$app/state';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { collapsedTopicIds } from '$lib/stores/agenda';
import ContextHeader from './ContextHeader.svelte';
import ViewTabs from './ViewTabs.svelte';
import AgendaView from './AgendaView.svelte';
import JournalView from './JournalView.svelte';
import PersonsView from './PersonsView.svelte';
import SnoozedView from './SnoozedView.svelte';
import ArchivedView from './ArchivedView.svelte';
import DashboardView from './DashboardView.svelte';
import CompanyPersonsView from './CompanyPersonsView.svelte';
import RatingModal from './RatingModal.svelte';
import RatingsView from './RatingsView.svelte';
import PersonMeetingsView from './PersonMeetingsView.svelte';
import PersonTasksView from './PersonTasksView.svelte';
import TaskCreateModal from './TaskCreateModal.svelte';
import type { MeetingMeta } from '@ka-note/shared';
import { getEventsForDate } from '$lib/stores/calendarStore';
import { today } from '$lib/db/helpers';
interface Props {
contextId: string;
}
let { contextId }: Props = $props();
const context = liveQuery(() => db.contexts.get(contextId));
const isDailyLog = $derived(contextId === 'daily-log');
let mode = $state<'prep' | 'meeting'>(contextId === 'daily-log' ? 'meeting' : 'prep');
let activeView = $state('journal');
const SCOPE_KEY = 'journal-scope';
let journalScope = $state<'business' | 'private'>(
(typeof localStorage !== 'undefined' ? localStorage.getItem(SCOPE_KEY) : null) === 'private'
? 'private' : 'business'
);
function handleScopeChange(s: 'business' | 'private') {
journalScope = s;
localStorage.setItem(SCOPE_KEY, s);
}
const initialDate = $derived(page.url.searchParams.get('date') ?? undefined);
let compact = $state(false);
let showAutoMeetingHint = $state(false);
function timeToMinutes(hhmm: string): number {
const [h, m] = hhmm.split(':').map(Number);
return h * 60 + m;
}
function currentTimeHHMM(): string {
const now = new Date();
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
}
function timeIsInRange(now: string, start: string, end: string, bufferMin: number): boolean {
const n = timeToMinutes(now);
const s = timeToMinutes(start) - bufferMin;
const e = timeToMinutes(end) + bufferMin;
return n >= s && n <= e;
}
$effect(() => {
const ctx = $context;
if (ctx?.type === 'meeting' && !isDailyLog) {
const calendarTitle = (ctx.meta as MeetingMeta | null)?.calendarTitle;
if (calendarTitle) {
const events = getEventsForDate(today());
const match = events.find(e => e.subject.toLowerCase() === calendarTitle.toLowerCase());
if (match) {
const now = currentTimeHHMM();
const isActive = timeIsInRange(now, match.start, match.end, 10);
if (isActive && mode !== 'meeting') {
mode = 'meeting';
showAutoMeetingHint = true;
setTimeout(() => { showAutoMeetingHint = false; }, 4000);
}
}
}
}
});
// Rating modal state
let ratingModal = $state<{ personName: string; topicId: string; historyEntryId: string } | null>(null);
// Task create modal state
let taskModal = $state<{ title: string; assignee: string; historyEntryId: string | null; contextId: string } | null>(null);
// Default view based on context type
$effect(() => {
if ($context) {
if (isDailyLog) {
activeView = 'journal';
} else if ($context.type === 'meeting') {
activeView = 'agenda';
} else {
activeView = 'dashboard';
}
}
});
function handleModeChange(m: 'prep' | 'meeting') {
mode = m;
}
function handleViewChange(v: string) {
activeView = v;
}
function toggleCompact() {
compact = !compact;
if (compact) {
db.topics
.where('contextId').equals(contextId)
.filter(t => !t.deletedAt)
.toArray()
.then(topics => {
collapsedTopicIds.update(() => new Set(topics.map(t => t.id)));
});
} else {
collapsedTopicIds.set(new Set());
}
}
let containerEl = $state<HTMLDivElement>();
function handleRatingEvent(e: Event) {
const detail = (e as CustomEvent).detail;
if (detail?.personName && detail?.topicId && detail?.historyEntryId) {
ratingModal = detail;
}
}
async function handleTaskCreateEvent(e: Event) {
const detail = (e as CustomEvent<{ title: string; assignee: string; historyEntryId: string | null; topicId: string | null }>).detail;
if (!detail) return;
// Resolve contextId: historyEntryId → entry.topicId → topic.contextId
// Fallback: topicId directly → topic.contextId
let resolvedContextId = contextId;
let resolvedHistoryEntryId = detail.historyEntryId;
if (detail.historyEntryId) {
const entry = await db.historyEntries.get(detail.historyEntryId);
if (entry) {
const topic = await db.topics.get(entry.topicId);
if (topic) resolvedContextId = topic.contextId;
}
} else if (detail.topicId) {
const topic = await db.topics.get(detail.topicId);
if (topic) resolvedContextId = topic.contextId;
// Find the most recent history entry for this topic to update text
const entries = await db.historyEntries
.where('topicId').equals(detail.topicId)
.filter(h => !h.deletedAt)
.sortBy('updatedAt');
if (entries.length > 0) resolvedHistoryEntryId = entries[entries.length - 1].id;
}
taskModal = { title: detail.title, assignee: detail.assignee, historyEntryId: resolvedHistoryEntryId, contextId: resolvedContextId };
}
$effect(() => {
if (!containerEl) return;
containerEl.addEventListener('ka-open-rating', handleRatingEvent);
containerEl.addEventListener('ka-open-task-create', handleTaskCreateEvent);
return () => {
containerEl?.removeEventListener('ka-open-rating', handleRatingEvent);
containerEl?.removeEventListener('ka-open-task-create', handleTaskCreateEvent);
};
});
</script>
<div bind:this={containerEl}>
{#if $context}
{#if showAutoMeetingHint}
<div class="mb-3 flex items-center justify-between bg-warning/20 border border-warning text-sm px-3 py-1 rounded">
<span>Meeting läuft - automatisch in Meeting-Modus gewechselt</span>
<button class="ml-2 text-muted hover:text-white" onclick={() => showAutoMeetingHint = false}>&times;</button>
</div>
{/if}
{#if isDailyLog}
<ContextHeader context={$context} {journalScope} onjournalscopechange={handleScopeChange} />
<ViewTabs context={$context} {activeView} onviewchange={handleViewChange} />
{:else}
<ContextHeader context={$context} {mode} onmodechange={handleModeChange} />
<ViewTabs context={$context} {activeView} {compact} onviewchange={handleViewChange} ontogglecompact={toggleCompact} />
{/if}
{#if activeView === 'agenda'}
<AgendaView {contextId} {mode} />
{:else if activeView === 'journal'}
<JournalView {contextId} {journalScope} {initialDate} />
{:else if activeView === 'persons'}
<PersonsView {contextId} />
{:else if activeView === 'snoozed'}
<SnoozedView {contextId} />
{:else if activeView === 'archived'}
<ArchivedView {contextId} />
{:else if activeView === 'dashboard'}
<DashboardView context={$context} />
{:else if activeView === 'company-persons'}
<CompanyPersonsView context={$context} />
{:else if activeView === 'ratings'}
<RatingsView personName={$context.name.replace(/^Person /, '')} />
{:else if activeView === 'person-meetings'}
<PersonMeetingsView context={$context} />
{:else if activeView === 'tasks'}
<PersonTasksView context={$context} />
{/if}
{:else}
<div class="text-muted">Context not found.</div>
{/if}
</div>
{#if ratingModal}
<RatingModal
personName={ratingModal.personName}
topicId={ratingModal.topicId}
historyEntryId={ratingModal.historyEntryId}
onclose={() => ratingModal = null}
/>
{/if}
{#if taskModal}
<TaskCreateModal
title={taskModal.title}
assignee={taskModal.assignee}
historyEntryId={taskModal.historyEntryId}
contextId={taskModal.contextId}
onclose={() => taskModal = null}
/>
{/if}