comp person list
This commit is contained in:
parent
a352518569
commit
9886b602f0
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'}">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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'}
|
||||
>★</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}
|
||||
|
|
@ -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' }] : [])
|
||||
]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
>★</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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue