upd person <-> meeting ref
This commit is contained in:
parent
6e35822708
commit
d14669bdcb
|
|
@ -1 +1 @@
|
||||||
1.1.105
|
1.1.106
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import CompanyPersonsView from './CompanyPersonsView.svelte';
|
import CompanyPersonsView from './CompanyPersonsView.svelte';
|
||||||
import RatingModal from './RatingModal.svelte';
|
import RatingModal from './RatingModal.svelte';
|
||||||
import RatingsView from './RatingsView.svelte';
|
import RatingsView from './RatingsView.svelte';
|
||||||
|
import PersonMeetingsView from './PersonMeetingsView.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contextId: string;
|
contextId: string;
|
||||||
|
|
@ -118,6 +119,8 @@
|
||||||
<CompanyPersonsView context={$context} />
|
<CompanyPersonsView context={$context} />
|
||||||
{:else if activeView === 'ratings'}
|
{:else if activeView === 'ratings'}
|
||||||
<RatingsView personName={$context.name.replace(/^Person /, '')} />
|
<RatingsView personName={$context.name.replace(/^Person /, '')} />
|
||||||
|
{:else if activeView === 'person-meetings'}
|
||||||
|
<PersonMeetingsView context={$context} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-muted">Context not found.</div>
|
<div class="text-muted">Context not found.</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { db } from '$lib/db/schema';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { AgendaContext, EventMeta } from '@ka-note/shared';
|
||||||
|
import { extractMentions, extractAssignments } from '$lib/utils/extractors';
|
||||||
|
import { JOURNAL_TOPIC_ID } from '$lib/db/repositories';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
context: AgendaContext;
|
||||||
|
}
|
||||||
|
let { context }: Props = $props();
|
||||||
|
|
||||||
|
const RANGES = [
|
||||||
|
{ label: '30 Tage', days: 30 },
|
||||||
|
{ label: '90 Tage', days: 90 },
|
||||||
|
{ label: '1 Jahr', days: 365 },
|
||||||
|
{ label: 'Alles', days: 0 },
|
||||||
|
];
|
||||||
|
let rangeIdx = $state(1);
|
||||||
|
|
||||||
|
function cutoffDate(days: number): string {
|
||||||
|
if (!days) return '';
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() - days);
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesName(name: string, personName: string): boolean {
|
||||||
|
return name.trim().toLowerCase() === personName.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityItem {
|
||||||
|
date: string;
|
||||||
|
time?: string;
|
||||||
|
title: string;
|
||||||
|
snippet?: string;
|
||||||
|
/** navigate to this context */
|
||||||
|
contextId: string;
|
||||||
|
/** if set, open journal tab at this date */
|
||||||
|
journalDate?: string;
|
||||||
|
type: 'event' | 'mention';
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = liveQuery(async () => {
|
||||||
|
const personName = context.name.replace(/^Person /, '');
|
||||||
|
const items: ActivityItem[] = [];
|
||||||
|
|
||||||
|
// 1. Events where person is participant
|
||||||
|
const events = await db.contexts
|
||||||
|
.filter(c => !c.deletedAt && c.type === 'event')
|
||||||
|
.toArray();
|
||||||
|
for (const ev of events) {
|
||||||
|
const meta = ev.meta as EventMeta | null;
|
||||||
|
if (!meta?.participants?.length) continue;
|
||||||
|
if (!meta.participants.some(p => matchesName(p, personName))) continue;
|
||||||
|
const targetContextId = meta.linkedContextId ?? 'daily-log';
|
||||||
|
items.push({
|
||||||
|
date: meta.date,
|
||||||
|
time: meta.time,
|
||||||
|
title: ev.name,
|
||||||
|
contextId: targetContextId,
|
||||||
|
journalDate: meta.date,
|
||||||
|
type: 'event',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Journal history entries mentioning person (@mention or -> assignment)
|
||||||
|
const journalTopic = await db.topics.get(JOURNAL_TOPIC_ID);
|
||||||
|
if (journalTopic) {
|
||||||
|
const entries = await db.historyEntries
|
||||||
|
.where('topicId').equals(JOURNAL_TOPIC_ID)
|
||||||
|
.filter(h => !h.deletedAt)
|
||||||
|
.toArray();
|
||||||
|
for (const h of entries) {
|
||||||
|
const names = new Set([...extractMentions(h.text), ...extractAssignments(h.text)]);
|
||||||
|
if (!names.has(personName)) continue;
|
||||||
|
// Show first non-empty line as snippet
|
||||||
|
const snippet = h.text.split('\n').find(l => l.trim()) ?? '';
|
||||||
|
items.push({
|
||||||
|
date: h.date,
|
||||||
|
title: 'Journal',
|
||||||
|
snippet,
|
||||||
|
contextId: 'daily-log',
|
||||||
|
journalDate: h.date,
|
||||||
|
type: 'mention',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.sort((a, b) => b.date.localeCompare(a.date) || (b.time ?? '').localeCompare(a.time ?? ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
const filtered = $derived(() => {
|
||||||
|
const cutoff = cutoffDate(RANGES[rangeIdx].days);
|
||||||
|
return ($activity ?? []).filter(item => !cutoff || item.date >= cutoff);
|
||||||
|
});
|
||||||
|
|
||||||
|
function navigate(item: ActivityItem) {
|
||||||
|
if (item.journalDate) {
|
||||||
|
goto(`/context/${item.contextId}?date=${item.journalDate}`);
|
||||||
|
} else {
|
||||||
|
goto(`/context/${item.contextId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string): string {
|
||||||
|
const [y, m, d] = iso.split('-');
|
||||||
|
return `${d}.${m}.${y}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4 flex gap-2">
|
||||||
|
{#each RANGES as r, i}
|
||||||
|
<button
|
||||||
|
class="rounded px-3 py-1 text-sm transition-colors {rangeIdx === i
|
||||||
|
? 'bg-accent text-white'
|
||||||
|
: 'bg-[#2a2a2a] text-[#888] hover:text-[#bbb]'}"
|
||||||
|
onclick={() => rangeIdx = i}
|
||||||
|
>{r.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each filtered() as item (item.contextId + item.date + (item.time ?? ''))}
|
||||||
|
<button
|
||||||
|
class="mb-2 w-full rounded-lg border-l-[5px] bg-card-bg p-3 text-left transition-colors hover:bg-[#2a2a2a] {item.type === 'event' ? 'border-l-accent' : 'border-l-info'}"
|
||||||
|
onclick={() => navigate(item)}
|
||||||
|
>
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<span class="text-xs text-[#666]">{formatDate(item.date)}</span>
|
||||||
|
{#if item.time}
|
||||||
|
<span class="text-xs text-[#555]">{item.time}</span>
|
||||||
|
{/if}
|
||||||
|
<span class="text-sm font-medium text-[#ddd]">{item.title}</span>
|
||||||
|
</div>
|
||||||
|
{#if item.snippet}
|
||||||
|
<div class="mt-0.5 truncate text-xs text-[#777]">{item.snippet}</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center text-muted">Keine Aktivität im gewählten Zeitraum.</div>
|
||||||
|
{/each}
|
||||||
|
|
@ -54,6 +54,9 @@
|
||||||
...(isEmployee
|
...(isEmployee
|
||||||
? [{ id: "ratings", label: "Bewertungen" }]
|
? [{ id: "ratings", label: "Bewertungen" }]
|
||||||
: []),
|
: []),
|
||||||
|
...(isPerson
|
||||||
|
? [{ id: "person-meetings", label: "Meetings" }]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue