comp person list

This commit is contained in:
beo3000 2026-02-20 18:19:20 +01:00
parent a352518569
commit 9886b602f0
7 changed files with 228 additions and 133 deletions

View File

@ -0,0 +1,46 @@
<script lang="ts">
import type { AgendaContext } from '@ka-note/shared';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { extractMentions, extractAssignments, extractCompanies } from '$lib/utils/extractors';
import PersonList from './PersonList.svelte';
interface Props {
context: AgendaContext;
}
let { context }: Props = $props();
const entityName = $derived(context.name.replace(/^Firma /, ''));
const linkedPersons = liveQuery(async () => {
const allHistory = await db.historyEntries.filter(h => !h.deletedAt).toArray();
const personContexts = await db.contexts.filter(c => !c.deletedAt && c.type === 'person').toArray();
const matchingPersonNames = new Set<string>();
for (const h of allHistory) {
if (extractCompanies(h.text).includes(entityName)) {
for (const m of extractMentions(h.text)) {
matchingPersonNames.add(m);
}
for (const a of extractAssignments(h.text)) {
matchingPersonNames.add(a);
}
}
}
return personContexts.filter(p => {
const pName = p.name.replace(/^Person\s+/, '');
return matchingPersonNames.has(pName);
});
});
const persons = $derived($linkedPersons ?? []);
</script>
<div class="mx-auto max-w-3xl p-4">
{#if persons.length > 0}
<PersonList {persons} />
{:else}
<div class="text-muted">Keine zugeordneten Personen.</div>
{/if}
</div>

View File

@ -9,6 +9,7 @@
import PersonsView from './PersonsView.svelte';
import SnoozedView from './SnoozedView.svelte';
import DashboardView from './DashboardView.svelte';
import CompanyPersonsView from './CompanyPersonsView.svelte';
import RatingModal from './RatingModal.svelte';
import RatingsView from './RatingsView.svelte';
@ -93,6 +94,8 @@
<SnoozedView {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 /, '')} />
{/if}

View File

@ -9,6 +9,7 @@
import { mention } from '$lib/actions/mention';
import RenderedMarkdown from './RenderedMarkdown.svelte';
import MarkdownEditor from './MarkdownEditor.svelte';
import EditableMarkdown from './EditableMarkdown.svelte';
import ConfirmDialog from './ConfirmDialog.svelte';
interface Props {
@ -202,30 +203,6 @@
return results.sort((a, b) => b.date.localeCompare(a.date));
});
// Linked persons (company only): find person contexts mentioned alongside this company
const linkedPersons = liveQuery(async () => {
if (!isCompany) return [];
const allHistory = await db.historyEntries.filter(h => !h.deletedAt).toArray();
const personContexts = await db.contexts.filter(c => !c.deletedAt && c.type === 'person').toArray();
const matchingPersonNames = new Set<string>();
for (const h of allHistory) {
if (extractCompanies(h.text).includes(entityName)) {
for (const mention of extractMentions(h.text)) {
matchingPersonNames.add(mention);
}
for (const assignment of extractAssignments(h.text)) {
matchingPersonNames.add(assignment);
}
}
}
return personContexts.filter(p => {
const pName = p.name.replace(/^Person\s+/, '');
return matchingPersonNames.has(pName);
});
});
// Archive / unarchive
async function handleArchive() {
await archiveContext(context.id);
@ -243,21 +220,6 @@
upsertContext({ id: context.id, meta: meta as any });
}
// Local state for meta notes — only sync from DB on context switch
let metaNotes = $state('');
let metaNotesContextId = '';
$effect(() => {
if (context.id !== metaNotesContextId) {
metaNotesContextId = context.id;
metaNotes = (context.meta as any)?.notes ?? '';
}
});
let metaNotesTimer: ReturnType<typeof setTimeout>;
function handleMetaNotesChange(md: string) {
metaNotes = md;
clearTimeout(metaNotesTimer);
metaNotesTimer = setTimeout(() => updateMeta('notes', md), 500);
}
</script>
<!-- Person sub-type selector (always visible for persons) -->
@ -375,11 +337,11 @@
</div>
<div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Notizen:</label>
<MarkdownEditor
content={metaNotes}
<EditableMarkdown
content={(context.meta as CompanyMeta)?.notes ?? ''}
placeholder="Notizen..."
minHeight="60px"
onchange={handleMetaNotesChange}
onchange={(md) => updateMeta('notes', md)}
/>
</div>
{:else}
@ -406,11 +368,11 @@
</div>
<div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Notizen:</label>
<MarkdownEditor
content={metaNotes}
<EditableMarkdown
content={(context.meta as PersonMeta)?.notes ?? ''}
placeholder="Notizen..."
minHeight="60px"
onchange={handleMetaNotesChange}
onchange={(md) => updateMeta('notes', md)}
/>
</div>
{/if}
@ -509,24 +471,6 @@
</div>
{/if}
<!-- Linked persons (company only) -->
{#if isCompany}
{@const persons = $linkedPersons ?? []}
{#if persons.length > 0}
<div class="mb-5 rounded-lg border border-[#444] bg-sidebar p-4">
<div class="mb-3 text-sm font-bold text-[#00b894]">Verknüpfte Personen</div>
{#each persons as p (p.id)}
<button
class="mb-1.5 block text-sm text-[#ccc] hover:text-white"
onclick={() => goto(`/context/${p.id}`)}
>
{p.name.replace(/^Person\s+/, '')}
</button>
{/each}
</div>
{/if}
{/if}
<!-- Activity log -->
<div class="rounded-lg border-l-[5px] {isProject ? 'border-l-project-tag' : isCompany ? 'border-l-company-tag' : 'border-l-info'} bg-card-bg p-4">
<div class="mb-2.5 border-b border-[#444] pb-1 text-xl font-bold {isProject ? 'text-[#a29bfe]' : isCompany ? 'text-[#00b894]' : 'text-info'}">

View File

@ -0,0 +1,91 @@
<script lang="ts">
import RenderedMarkdown from './RenderedMarkdown.svelte';
import MarkdownEditor from './MarkdownEditor.svelte';
interface Props {
content: string;
placeholder?: string;
minHeight?: string;
onchange?: (markdown: string) => void;
class?: string;
}
let { content, placeholder = 'Click to edit...', minHeight = '60px', onchange, class: className = '' }: Props = $props();
let editing = $state(false);
let editorContent = $state('');
let wrapper: HTMLDivElement;
let editorRef: MarkdownEditor;
function enterEdit() {
editorContent = content;
editing = true;
}
function save() {
const md = editorRef?.getMarkdown() ?? editorContent;
editing = false;
if (md !== content) {
onchange?.(md);
}
}
function cancel() {
editing = false;
}
function handleFocusOut(e: FocusEvent) {
if (!editing) return;
const related = e.relatedTarget as Node | null;
// Stay in edit mode if focus moved within the wrapper (Tiptap nested elements)
if (related && wrapper?.contains(related)) return;
save();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault();
cancel();
}
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={wrapper}
class="editable-markdown {className}"
onfocusout={handleFocusOut}
onkeydown={handleKeydown}
>
{#if editing}
<MarkdownEditor
bind:this={editorRef}
content={editorContent}
{placeholder}
{minHeight}
onchange={(md) => editorContent = md}
/>
{:else}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="editable-markdown-read" onclick={enterEdit}>
{#if content}
<RenderedMarkdown text={content} />
{:else}
<span class="text-sm text-[#666] italic">{placeholder}</span>
{/if}
</div>
{/if}
</div>
<style>
.editable-markdown-read {
cursor: text;
min-height: 2em;
padding: 0.25rem;
border-radius: 0.25rem;
border: 1px solid transparent;
}
.editable-markdown-read:hover {
border-color: #444;
background: rgba(255, 255, 255, 0.02);
}
</style>

View File

@ -0,0 +1,77 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import type { AgendaContext, PersonMeta, PersonSubType } from '@ka-note/shared';
interface Props {
persons: AgendaContext[];
}
let { persons }: Props = $props();
function displayName(name: string): string {
return name.replace('Person ', '');
}
const subTypeLabels: Record<PersonSubType, string> = {
contact: 'Kontakt',
employee: 'Mitarbeiter',
colleague: 'Kollege'
};
const subTypeColors: Record<PersonSubType, string> = {
contact: 'bg-[#555] text-[#ccc]',
employee: 'bg-accent/30 text-accent',
colleague: 'bg-[#00b894]/30 text-[#00b894]'
};
let confirmDeleteId = $state<string | null>(null);
let confirmDeleteName = $state('');
function requestDelete(id: string, name: string) {
confirmDeleteId = id;
confirmDeleteName = name;
}
async function handleDelete() {
if (confirmDeleteId) await softDeleteContext(confirmDeleteId);
confirmDeleteId = null;
}
</script>
{#if persons.length > 0}
<div class="flex flex-col gap-2">
{#each persons as ctx (ctx.id)}
{@const subType = ((ctx.meta as PersonMeta | null)?.personSubType ?? 'contact') as PersonSubType}
<div class="flex items-center gap-2">
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 transition-colors hover:border-[#666] {ctx.isFavorite ? 'text-yellow-400' : 'text-[#555] hover:text-yellow-400/60'}"
onclick={() => toggleFavorite(ctx.id)}
title={ctx.isFavorite ? 'Aus Sidebar entfernen' : 'In Sidebar anzeigen'}
>&#9733;</button>
<button
class="flex flex-1 items-center justify-between rounded-lg border border-[#444] bg-sidebar px-4 py-3 text-left transition-colors hover:border-[#666] hover:bg-[#2a2a2a]"
onclick={() => goto(`/context/${ctx.id}`)}
>
<span class="font-bold text-white">{displayName(ctx.name)}</span>
<span class="rounded px-2 py-0.5 text-xs {subTypeColors[subType]}">{subTypeLabels[subType]}</span>
</button>
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 text-[#888] transition-colors hover:border-red-900 hover:text-red-400"
onclick={() => requestDelete(ctx.id, displayName(ctx.name))}
title="Löschen"
>🗑</button>
</div>
{/each}
</div>
{:else}
<div class="text-muted">Keine Personen vorhanden.</div>
{/if}
{#if confirmDeleteId}
<ConfirmDialog
message={`Person \u201E${confirmDeleteName}\u201C wirklich löschen?`}
onconfirm={handleDelete}
oncancel={() => confirmDeleteId = null}
/>
{/if}

View File

@ -17,6 +17,7 @@
const isDailyLog = $derived(context.id === 'daily-log');
const isPerson = $derived(context.type === 'person');
const isCompany = $derived(context.type === 'company');
const isEmployee = $derived(isPerson && ((context.meta as PersonMeta | null)?.personSubType ?? 'contact') === 'employee');
const tabs = $derived<Tab[]>(
@ -29,6 +30,7 @@
]
: [
{ id: 'dashboard', label: `Dashboard: ${context.name.replace(/^(Project |Person |Firma )/, '')}` },
...(isCompany ? [{ id: 'company-persons', label: 'Personen' }] : []),
...(isEmployee ? [{ id: 'ratings', label: 'Bewertungen' }] : [])
]
);

View File

@ -1,84 +1,16 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import type { PersonMeta, PersonSubType } from '@ka-note/shared';
import PersonList from '$lib/components/PersonList.svelte';
const allPersons = liveQuery(() =>
db.contexts.filter(c => !c.deletedAt && c.type === 'person').sortBy('sortOrder')
);
const persons = $derived($allPersons ?? []);
function displayName(name: string): string {
return name.replace('Person ', '');
}
const subTypeLabels: Record<PersonSubType, string> = {
contact: 'Kontakt',
employee: 'Mitarbeiter',
colleague: 'Kollege'
};
const subTypeColors: Record<PersonSubType, string> = {
contact: 'bg-[#555] text-[#ccc]',
employee: 'bg-accent/30 text-accent',
colleague: 'bg-[#00b894]/30 text-[#00b894]'
};
let confirmDeleteId = $state<string | null>(null);
let confirmDeleteName = $state('');
function requestDelete(id: string, name: string) {
confirmDeleteId = id;
confirmDeleteName = name;
}
async function handleDelete() {
if (confirmDeleteId) await softDeleteContext(confirmDeleteId);
confirmDeleteId = null;
}
</script>
<div class="mx-auto max-w-3xl p-6">
<h1 class="mb-6 text-2xl font-bold text-white">Alle Personen</h1>
{#if persons.length > 0}
<div class="flex flex-col gap-2">
{#each persons as ctx (ctx.id)}
{@const subType = ((ctx.meta as PersonMeta | null)?.personSubType ?? 'contact') as PersonSubType}
<div class="flex items-center gap-2">
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 transition-colors hover:border-[#666] {ctx.isFavorite ? 'text-yellow-400' : 'text-[#555] hover:text-yellow-400/60'}"
onclick={() => toggleFavorite(ctx.id)}
title={ctx.isFavorite ? 'Aus Sidebar entfernen' : 'In Sidebar anzeigen'}
>&#9733;</button>
<button
class="flex flex-1 items-center justify-between rounded-lg border border-[#444] bg-sidebar px-4 py-3 text-left transition-colors hover:border-[#666] hover:bg-[#2a2a2a]"
onclick={() => goto(`/context/${ctx.id}`)}
>
<span class="font-bold text-white">{displayName(ctx.name)}</span>
<span class="rounded px-2 py-0.5 text-xs {subTypeColors[subType]}">{subTypeLabels[subType]}</span>
</button>
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 text-[#888] transition-colors hover:border-red-900 hover:text-red-400"
onclick={() => requestDelete(ctx.id, displayName(ctx.name))}
title="Löschen"
>🗑</button>
</div>
{/each}
</div>
{:else}
<div class="text-muted">Keine Personen vorhanden.</div>
{/if}
<PersonList {persons} />
</div>
{#if confirmDeleteId}
<ConfirmDialog
message={`Person \u201E${confirmDeleteName}\u201C wirklich löschen?`}
onconfirm={handleDelete}
oncancel={() => confirmDeleteId = null}
/>
{/if}