added private journal

This commit is contained in:
beo3000 2026-02-22 17:16:55 +01:00
parent 4d00be63e7
commit 63d36a1611
8 changed files with 142 additions and 35 deletions

View File

@ -1 +1 @@
1.0.52 1.0.54

View File

@ -6,10 +6,13 @@
context: AgendaContext; context: AgendaContext;
mode?: 'prep' | 'meeting'; mode?: 'prep' | 'meeting';
onmodechange?: (mode: 'prep' | 'meeting') => void; onmodechange?: (mode: 'prep' | 'meeting') => void;
journalScope?: 'business' | 'private';
onjournalscopechange?: (scope: 'business' | 'private') => void;
} }
let { context, mode, onmodechange }: Props = $props(); let { context, mode, onmodechange, journalScope, onjournalscopechange }: Props = $props();
const showModeSwitch = $derived(!!onmodechange && context.type === 'meeting' && context.id !== 'daily-log'); const showModeSwitch = $derived(!!onmodechange && context.type === 'meeting' && context.id !== 'daily-log');
const showScopeSwitch = $derived(!!onjournalscopechange && context.id === 'daily-log');
const canRename = $derived(context.id !== 'daily-log'); const canRename = $derived(context.id !== 'daily-log');
let editing = $state(false); let editing = $state(false);
let nameInput = $state(''); let nameInput = $state('');
@ -83,4 +86,16 @@
</button> </button>
</div> </div>
{/if} {/if}
{#if showScopeSwitch}
<div class="flex gap-1 rounded-full bg-[#333] p-1">
<button
class="rounded-full border-none px-4 py-2 font-bold transition-all {journalScope === 'business' ? 'bg-accent text-white shadow-md' : 'bg-transparent text-[#aaa] cursor-pointer'}"
onclick={() => onjournalscopechange?.('business')}
>Firma</button>
<button
class="rounded-full border-none px-4 py-2 font-bold transition-all {journalScope === 'private' ? 'bg-accent text-white shadow-md' : 'bg-transparent text-[#aaa] cursor-pointer'}"
onclick={() => onjournalscopechange?.('private')}
>Privat</button>
</div>
{/if}
</h1> </h1>

View File

@ -24,6 +24,15 @@
let mode = $state<'prep' | 'meeting'>(contextId === 'daily-log' ? 'meeting' : 'prep'); let mode = $state<'prep' | 'meeting'>(contextId === 'daily-log' ? 'meeting' : 'prep');
let activeView = $state('journal'); 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);
}
let compact = $state(false); let compact = $state(false);
// Rating modal state // Rating modal state
@ -84,7 +93,7 @@
<div bind:this={containerEl}> <div bind:this={containerEl}>
{#if $context} {#if $context}
{#if isDailyLog} {#if isDailyLog}
<ContextHeader context={$context} /> <ContextHeader context={$context} {journalScope} onjournalscopechange={handleScopeChange} />
<ViewTabs context={$context} {activeView} onviewchange={handleViewChange} /> <ViewTabs context={$context} {activeView} onviewchange={handleViewChange} />
{:else} {:else}
<ContextHeader context={$context} {mode} onmodechange={handleModeChange} /> <ContextHeader context={$context} {mode} onmodechange={handleModeChange} />
@ -94,7 +103,7 @@
{#if activeView === 'agenda'} {#if activeView === 'agenda'}
<AgendaView {contextId} {mode} /> <AgendaView {contextId} {mode} />
{:else if activeView === 'journal'} {:else if activeView === 'journal'}
<JournalView {contextId} /> <JournalView {contextId} {journalScope} />
{:else if activeView === 'persons'} {:else if activeView === 'persons'}
<PersonsView {contextId} /> <PersonsView {contextId} />
{:else if activeView === 'snoozed'} {:else if activeView === 'snoozed'}

View File

@ -221,14 +221,36 @@
upsertContext({ id: context.id, meta: meta as any }); upsertContext({ id: context.id, meta: meta as any });
} }
function calcAge(birthday: string | undefined): string {
if (!birthday) return '';
const b = new Date(birthday);
if (isNaN(b.getTime())) return '';
const now = new Date();
let age = now.getFullYear() - b.getFullYear();
const md = now.getMonth() - b.getMonth();
if (md < 0 || (md === 0 && now.getDate() < b.getDate())) age--;
return age >= 0 ? `${age} J.` : '';
}
function calcTenure(joinDate: string | undefined): string {
if (!joinDate) return '';
const j = new Date(joinDate);
if (isNaN(j.getTime())) return '';
const now = new Date();
let years = now.getFullYear() - j.getFullYear();
const md = now.getMonth() - j.getMonth();
if (md < 0 || (md === 0 && now.getDate() < j.getDate())) years--;
return years >= 0 ? `${years} Jahr${years !== 1 ? 'e' : ''}` : '';
}
</script> </script>
<!-- Person sub-type selector (always visible for persons) --> <!-- Person sub-type selector (always visible for persons) -->
{#if isPerson} {#if isPerson}
{@const subTypeColors = { contact: 'border-[#555] text-[#ccc]', employee: 'border-accent text-accent', colleague: 'border-[#00b894] text-[#00b894]' } as Record<PersonSubType, string>} {@const subTypeColors = { contact: 'border-[#555] text-[#ccc]', employee: 'border-accent text-accent', colleague: 'border-[#00b894] text-[#00b894]', family: 'border-[#e84393] text-[#e84393]', acquaintance: 'border-[#a29bfe] text-[#a29bfe]' } as Record<PersonSubType, string>}
<div class="mb-5 flex items-center gap-3"> <div class="mb-5 flex flex-wrap items-center gap-3">
<span class="text-sm text-[#aaa]">Typ:</span> <span class="text-sm text-[#aaa]">Typ:</span>
{#each [['contact', 'Kontakt'], ['employee', 'Mitarbeiter'], ['colleague', 'Kollege']] as [value, label]} {#each [['contact', 'Kontakt'], ['employee', 'Mitarbeiter'], ['colleague', 'Kollege'], ['family', 'Familie'], ['acquaintance', 'Bekannte']] as [value, label]}
<button <button
class="rounded-full border px-3 py-1 text-sm transition-colors {personSubType === value ? subTypeColors[value as PersonSubType] + ' bg-white/10 font-bold' : 'border-[#333] text-[#666] hover:border-[#555] hover:text-[#aaa]'}" class="rounded-full border px-3 py-1 text-sm transition-colors {personSubType === value ? subTypeColors[value as PersonSubType] + ' bg-white/10 font-bold' : 'border-[#333] text-[#666] hover:border-[#555] hover:text-[#aaa]'}"
onclick={() => handleSubTypeChange(value as PersonSubType)} onclick={() => handleSubTypeChange(value as PersonSubType)}
@ -347,25 +369,46 @@
</div> </div>
{:else} {:else}
{@const meta = (context.meta ?? { fullName: '', email: '', phone: '', duSince: '' }) as PersonMeta} {@const meta = (context.meta ?? { fullName: '', email: '', phone: '', duSince: '' }) as PersonMeta}
{@const age = calcAge(meta.birthday)}
{@const tenure = calcTenure(meta.joinDate)}
{@const showJoinDate = personSubType === 'employee' || personSubType === 'colleague'}
<div class="mb-2.5 flex flex-col"> <div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Voller Name:</label> <label class="mb-1 text-sm text-[#aaa]">Voller Name:</label>
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]" <input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.fullName} onchange={(e) => updateMeta('fullName', e.currentTarget.value)} /> value={meta.fullName} onchange={(e) => updateMeta('fullName', e.currentTarget.value)} />
</div> </div>
<div class="mb-2.5 flex flex-col"> <div class="mb-2.5 grid grid-cols-2 gap-3">
<label class="mb-1 text-sm text-[#aaa]">Email:</label> <div class="flex flex-col">
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]" <label class="mb-1 text-sm text-[#aaa]">Email:</label>
value={meta.email} onchange={(e) => updateMeta('email', e.currentTarget.value)} /> <input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
</div> value={meta.email} onchange={(e) => updateMeta('email', e.currentTarget.value)} />
<div class="mb-2.5 flex flex-col"> </div>
<label class="mb-1 text-sm text-[#aaa]">Telefon:</label> <div class="flex flex-col">
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]" <label class="mb-1 text-sm text-[#aaa]">Telefon:</label>
value={meta.phone} onchange={(e) => updateMeta('phone', e.currentTarget.value)} /> <input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
</div> value={meta.phone} onchange={(e) => updateMeta('phone', e.currentTarget.value)} />
<div class="mb-2.5 flex flex-col"> </div>
<label class="mb-1 text-sm text-[#aaa]">Per Du seit:</label> <div class="flex flex-col">
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]" <label class="mb-1 text-sm text-[#aaa]">
value={meta.duSince} onchange={(e) => updateMeta('duSince', e.currentTarget.value)} /> Geburtstag:{#if age} <span class="text-info font-normal">{age}</span>{/if}
</label>
<input type="date" class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.birthday ?? ''} onchange={(e) => updateMeta('birthday', e.currentTarget.value)} />
</div>
<div class="flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Per Du seit:</label>
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.duSince} onchange={(e) => updateMeta('duSince', e.currentTarget.value)} />
</div>
{#if showJoinDate}
<div class="col-span-2 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">
Eintrittsdatum:{#if tenure} <span class="text-[#00b894] font-normal">{tenure}</span>{/if}
</label>
<input type="date" class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.joinDate ?? ''} onchange={(e) => updateMeta('joinDate', e.currentTarget.value)} />
</div>
{/if}
</div> </div>
<div class="mb-2.5 flex flex-col"> <div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Notizen:</label> <label class="mb-1 text-sm text-[#aaa]">Notizen:</label>

View File

@ -13,8 +13,9 @@
interface Props { interface Props {
contextId: string; contextId: string;
journalScope?: 'business' | 'private';
} }
let { contextId }: Props = $props(); let { contextId, journalScope = 'business' }: Props = $props();
const isDailyLog = $derived(contextId === 'daily-log'); const isDailyLog = $derived(contextId === 'daily-log');
@ -45,10 +46,10 @@
.toArray(); .toArray();
}); });
// Filter journal entries by selected date // Filter journal entries by selected date and scope
const filteredEntries = $derived( const filteredEntries = $derived(
($journalEntries ?? []) ($journalEntries ?? [])
.filter(e => e.date === selectedDate) .filter(e => e.date === selectedDate && (journalScope === 'private' ? !!e.isPrivate : !e.isPrivate))
.sort((a, b) => b.sortOrder - a.sortOrder) .sort((a, b) => b.sortOrder - a.sortOrder)
); );
@ -86,15 +87,16 @@
} }
} }
const isPrivate = journalScope === 'private';
if (selectedLinkedContextId) { if (selectedLinkedContextId) {
const topic = await createTopic(selectedLinkedContextId, title); const topic = await createTopic(selectedLinkedContextId, title);
if (body) { if (body) {
await createHistoryEntry(topic.id, selectedDate, body); await createHistoryEntry(topic.id, selectedDate, body, null, false, isPrivate);
} }
} else { } else {
const text = body ? `${title}\n${body}` : title; const text = body ? `${title}\n${body}` : title;
await getOrCreateJournalTopic(); await getOrCreateJournalTopic();
await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked); await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked, isPrivate);
} }
entryTitle = ''; entryTitle = '';
@ -186,12 +188,40 @@
} }
return [...groups.entries()].sort(([a], [b]) => b.localeCompare(a)); return [...groups.entries()].sort(([a], [b]) => b.localeCompare(a));
}); });
// Birthday banner — filtered by scope
const allPersons = liveQuery(() =>
db.contexts.filter(c => !c.deletedAt && c.type === 'person').toArray()
);
const BUSINESS_SUBTYPES = new Set(['employee', 'colleague']);
const PRIVATE_SUBTYPES = new Set(['family', 'acquaintance']);
const birthdayPersons = $derived(
($allPersons ?? []).filter(p => {
const meta = p.meta as { birthday?: string; personSubType?: string } | null;
const bd = meta?.birthday;
if (!bd || bd.slice(5) !== selectedDate.slice(5)) return false;
const sub = meta?.personSubType;
if (journalScope === 'private') return !!sub && PRIVATE_SUBTYPES.has(sub);
// business: employee/colleague/contact/undefined
return !sub || !PRIVATE_SUBTYPES.has(sub);
})
);
</script> </script>
{#if isDailyLog} {#if isDailyLog}
<!-- Daily-log: chronological entry log with date navigation --> <!-- Daily-log: chronological entry log with date navigation -->
<DateNavigator {selectedDate} onchange={(d) => selectedDate = d} /> <DateNavigator {selectedDate} onchange={(d) => selectedDate = d} />
{#if birthdayPersons.length > 0}
<div class="mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-[#d4a017] bg-[#2a1f00] px-4 py-3">
<span class="text-lg">🎂</span>
<span class="text-sm font-bold text-[#d4a017]">Geburtstag heute:</span>
{#each birthdayPersons as p}
<span class="rounded-full bg-[#d4a017]/20 px-2 py-0.5 text-sm text-[#f0c040]">{p.name}</span>
{/each}
</div>
{/if}
<div class="mb-8 flex flex-col gap-2.5 rounded-lg border border-border bg-sidebar p-4"> <div class="mb-8 flex flex-col gap-2.5 rounded-lg border border-border bg-sidebar p-4">
<div class="relative flex items-center gap-2"> <div class="relative flex items-center gap-2">
<input <input
@ -257,7 +287,7 @@
</div> </div>
</div> </div>
<WiedervorlageSection date={selectedDate} /> <WiedervorlageSection date={selectedDate} {journalScope} />
{#if filteredEntries.length > 0} {#if filteredEntries.length > 0}
<div class="mb-8 border-l-2 border-[#555] pl-5"> <div class="mb-8 border-l-2 border-[#555] pl-5">

View File

@ -4,18 +4,24 @@
interface Props { interface Props {
date: string; date: string;
journalScope?: 'business' | 'private';
} }
let { date }: Props = $props(); let { date, journalScope = 'business' }: Props = $props();
const pending = $derived(pendingWiedervorlage(date)); const allPending = $derived(pendingWiedervorlage(date));
const pending = $derived(
($allPending ?? []).filter(e =>
journalScope === 'private' ? !!e.isPrivate : !e.isPrivate
)
);
</script> </script>
{#if ($pending ?? []).length > 0} {#if pending.length > 0}
<div class="mb-6 rounded-lg border border-amber-500/40 bg-amber-950/20 p-4"> <div class="mb-6 rounded-lg border border-amber-500/40 bg-amber-950/20 p-4">
<div class="mb-3 text-sm font-semibold uppercase tracking-wider text-amber-400"> <div class="mb-3 text-sm font-semibold uppercase tracking-wider text-amber-400">
Wiedervorlage ({($pending ?? []).length}) Wiedervorlage ({pending.length})
</div> </div>
{#each $pending ?? [] as entry (entry.id)} {#each pending as entry (entry.id)}
<WiedervorlageCard {entry} /> <WiedervorlageCard {entry} />
{/each} {/each}
</div> </div>

View File

@ -211,7 +211,7 @@ export async function getAllHistoryByContext(contextId: string): Promise<(Histor
return allHistory; return allHistory;
} }
export async function createHistoryEntry(topicId: string, date: string, text: string, linkedContextId: string | null = null, wiedervorlage = false): Promise<HistoryEntry> { export async function createHistoryEntry(topicId: string, date: string, text: string, linkedContextId: string | null = null, wiedervorlage = false, isPrivate = false): Promise<HistoryEntry> {
const existing = await getHistoryByTopic(topicId); const existing = await getHistoryByTopic(topicId);
const autoWiedervorlage = date > today() || wiedervorlage; const autoWiedervorlage = date > today() || wiedervorlage;
const entry: HistoryEntry = { const entry: HistoryEntry = {
@ -226,7 +226,8 @@ export async function createHistoryEntry(topicId: string, date: string, text: st
wiedervorlageResolvedAt: null, wiedervorlageResolvedAt: null,
updatedAt: now(), updatedAt: now(),
deletedAt: null, deletedAt: null,
version: 1 version: 1,
...(isPrivate ? { isPrivate: true } : {})
}; };
await db.historyEntries.put(entry); await db.historyEntries.put(entry);
return entry; return entry;

View File

@ -8,7 +8,7 @@ export interface SyncEntity {
export type ContextType = 'meeting' | 'project' | 'person' | 'company'; export type ContextType = 'meeting' | 'project' | 'person' | 'company';
export type PersonSubType = 'contact' | 'employee' | 'colleague'; export type PersonSubType = 'contact' | 'employee' | 'colleague' | 'family' | 'acquaintance';
export interface AgendaContext extends SyncEntity { export interface AgendaContext extends SyncEntity {
name: string; name: string;
@ -30,6 +30,8 @@ export interface PersonMeta {
email: string; email: string;
phone: string; phone: string;
duSince: string; duSince: string;
birthday?: string; // YYYY-MM-DD
joinDate?: string; // YYYY-MM-DD (employee/colleague)
personSubType?: PersonSubType; personSubType?: PersonSubType;
notes?: string; notes?: string;
} }
@ -60,6 +62,7 @@ export interface HistoryEntry extends SyncEntity {
doneAt: string | null; doneAt: string | null;
wiedervorlageDate: string | null; // YYYY-MM-DD wiedervorlageDate: string | null; // YYYY-MM-DD
wiedervorlageResolvedAt: string | null; // ISO timestamp wiedervorlageResolvedAt: string | null; // ISO timestamp
isPrivate?: boolean; // undefined/false = business (default)
} }
export interface Rating extends SyncEntity { export interface Rating extends SyncEntity {