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

311 lines
10 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { resetAndReseed } from '$lib/db/seed';
import { upsertContext } from '$lib/db/repositories';
import { newId } from '$lib/db/helpers';
import type { AgendaContext, ContextType } from '@ka-note/shared';
import { account, logout } from '$lib/auth/authStore.js';
const isDev = import.meta.env.DEV;
interface Props {
onnavigate?: () => void;
}
let { onnavigate }: Props = $props();
const currentId = $derived($page.params.id ?? 'daily-log');
const contexts = liveQuery(() => db.contexts.filter(c => !c.deletedAt && !c.archivedAt).sortBy('sortOrder'));
const dailyLog = $derived(($contexts ?? []).find((c: AgendaContext) => c.id === 'daily-log'));
const meetings = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'meeting' && c.id !== 'daily-log' && c.isFavorite));
const projects = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'project' && c.isFavorite));
const companies = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'company' && c.isFavorite));
const people = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'person' && c.isFavorite));
let collapsed: Record<string, boolean> = $state({});
function toggleSection(key: string) {
collapsed[key] = !collapsed[key];
}
let creatingType: ContextType | null = $state(null);
let newName = $state('');
function navigate(id: string) {
goto(`/context/${id}`);
onnavigate?.();
}
function displayName(ctx: AgendaContext): string {
if (ctx.type === 'project') return ctx.name.replace('Project ', '');
if (ctx.type === 'company') return ctx.name.replace('Firma ', '');
if (ctx.type === 'person') return ctx.name.replace('Person ', '');
return ctx.name;
}
function startCreating(type: ContextType) {
creatingType = type;
newName = '';
}
function cancelCreating() {
creatingType = null;
newName = '';
}
async function createNewContext() {
const name = newName.trim();
if (!name || !creatingType) return;
const type = creatingType;
const slug = name.toLowerCase().replace(/\s+/g, '-');
const id = type === 'company' ? `f-${slug}` : type === 'person' ? `u-${slug}` : type === 'project' ? `p-${slug}` : newId();
const fullName = type === 'company' ? `Firma ${name}` : type === 'project' ? `Project ${name}` : type === 'person' ? `Person ${name}` : name;
const existing = ($contexts ?? []).filter((c: AgendaContext) => c.type === type);
const sortOrder = existing.length;
const meta = type === 'person' ? { fullName: '', email: '', phone: '', duSince: '', personSubType: 'contact' as const } : undefined;
await upsertContext({ id, name: fullName, type, sortOrder, ...(meta ? { meta } : {}) });
cancelCreating();
navigate(id);
}
function handleInputKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
createNewContext();
} else if (e.key === 'Escape') {
cancelCreating();
}
}
function autofocus(node: HTMLElement) {
node.focus();
}
</script>
<div class="mb-4 border-b-2 border-accent pb-1 text-lg font-bold uppercase tracking-wider text-accent">
Ka-Note
</div>
<!-- General -->
<div class="mb-1 mt-4 pl-1 text-xs uppercase text-muted">General</div>
<ul class="m-0 list-none p-0">
{#if dailyLog}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === 'daily-log' ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate('daily-log')}
>
{dailyLog.name}
</button>
</li>
{/if}
</ul>
<!-- Jour Fixes -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('meetings')}>
<span class="inline-block text-[10px] transition-transform {collapsed['meetings'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Jour Fixes
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('meeting')}>+</button>
</div>
{#if !collapsed['meetings']}
<ul class="m-0 list-none p-0">
{#each meetings as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{ctx.name}
</button>
</li>
{/each}
{#if creatingType === 'meeting'}
<li>
<input
class="mb-1 w-full rounded bg-white/10 px-2.5 py-2 text-sm text-white outline-none placeholder:text-muted"
placeholder="Name..."
bind:value={newName}
onkeydown={handleInputKeydown}
onblur={cancelCreating}
use:autofocus
/>
</li>
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/meetings'); onnavigate?.(); }}
>
Alle Jour Fixes &rarr;
</button>
</li>
</ul>
{/if}
<!-- Projects -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('projects')}>
<span class="inline-block text-[10px] transition-transform {collapsed['projects'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Projects
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('project')}>+</button>
</div>
{#if !collapsed['projects']}
<ul class="m-0 list-none p-0">
{#each projects as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{displayName(ctx)}
</button>
</li>
{/each}
{#if creatingType === 'project'}
<li>
<input
class="mb-1 w-full rounded bg-white/10 px-2.5 py-2 text-sm text-white outline-none placeholder:text-muted"
placeholder="Name..."
bind:value={newName}
onkeydown={handleInputKeydown}
onblur={cancelCreating}
use:autofocus
/>
</li>
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/projects'); onnavigate?.(); }}
>
Alle Projekte &rarr;
</button>
</li>
</ul>
{/if}
<!-- Companies -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('companies')}>
<span class="inline-block text-[10px] transition-transform {collapsed['companies'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Firmen
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('company')}>+</button>
</div>
{#if !collapsed['companies']}
<ul class="m-0 list-none p-0">
{#each companies as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{displayName(ctx)}
</button>
</li>
{/each}
{#if creatingType === 'company'}
<li>
<input
class="mb-1 w-full rounded bg-white/10 px-2.5 py-2 text-sm text-white outline-none placeholder:text-muted"
placeholder="Name..."
bind:value={newName}
onkeydown={handleInputKeydown}
onblur={cancelCreating}
use:autofocus
/>
</li>
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/companies'); onnavigate?.(); }}
>
Alle Firmen &rarr;
</button>
</li>
</ul>
{/if}
<!-- People -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('people')}>
<span class="inline-block text-[10px] transition-transform {collapsed['people'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
People
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('person')}>+</button>
</div>
{#if !collapsed['people']}
<ul class="m-0 list-none p-0">
{#each people as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{displayName(ctx)}
</button>
</li>
{/each}
{#if creatingType === 'person'}
<li>
<input
class="mb-1 w-full rounded bg-white/10 px-2.5 py-2 text-sm text-white outline-none placeholder:text-muted"
placeholder="Name..."
bind:value={newName}
onkeydown={handleInputKeydown}
onblur={cancelCreating}
use:autofocus
/>
</li>
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/persons'); onnavigate?.(); }}
>
Alle Personen &rarr;
</button>
</li>
</ul>
{/if}
<div class="mt-auto border-t border-border pt-3">
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-muted transition-colors hover:bg-white/5 hover:text-white"
onclick={() => { goto('/settings'); onnavigate?.(); }}
>
Einstellungen
</button>
{#if $account}
<div class="mb-2 truncate px-1 text-xs text-muted" title={$account.username}>
{$account.name ?? $account.username}
</div>
{/if}
<button
class="w-full rounded px-2.5 py-2 text-left text-xs text-muted transition-colors hover:bg-white/5 hover:text-white"
onclick={() => logout()}
>
Sign out
</button>
{#if isDev}
<button
class="mt-1 w-full rounded bg-danger/20 px-2.5 py-2 text-left text-xs text-danger hover:bg-danger/30 transition-colors"
onclick={async () => { await resetAndReseed(); location.reload(); }}
>
Reset Seed Data
</button>
{/if}
</div>