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;
mode?: 'prep' | 'meeting';
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 showScopeSwitch = $derived(!!onjournalscopechange && context.id === 'daily-log');
const canRename = $derived(context.id !== 'daily-log');
let editing = $state(false);
let nameInput = $state('');
@ -83,4 +86,16 @@
</button>
</div>
{/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>

View File

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

View File

@ -221,14 +221,36 @@
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>
<!-- Person sub-type selector (always visible for persons) -->
{#if isPerson}
{@const subTypeColors = { contact: 'border-[#555] text-[#ccc]', employee: 'border-accent text-accent', colleague: 'border-[#00b894] text-[#00b894]' } as Record<PersonSubType, string>}
<div class="mb-5 flex items-center gap-3">
{@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 flex-wrap items-center gap-3">
<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
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)}
@ -347,25 +369,46 @@
</div>
{:else}
{@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">
<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]"
value={meta.fullName} onchange={(e) => updateMeta('fullName', e.currentTarget.value)} />
</div>
<div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Email:</label>
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.email} onchange={(e) => updateMeta('email', e.currentTarget.value)} />
</div>
<div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Telefon:</label>
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.phone} onchange={(e) => updateMeta('phone', e.currentTarget.value)} />
</div>
<div class="mb-2.5 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 class="mb-2.5 grid grid-cols-2 gap-3">
<div class="flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Email:</label>
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.email} onchange={(e) => updateMeta('email', e.currentTarget.value)} />
</div>
<div class="flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Telefon:</label>
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.phone} onchange={(e) => updateMeta('phone', e.currentTarget.value)} />
</div>
<div class="flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">
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 class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Notizen:</label>

View File

@ -13,8 +13,9 @@
interface Props {
contextId: string;
journalScope?: 'business' | 'private';
}
let { contextId }: Props = $props();
let { contextId, journalScope = 'business' }: Props = $props();
const isDailyLog = $derived(contextId === 'daily-log');
@ -45,10 +46,10 @@
.toArray();
});
// Filter journal entries by selected date
// Filter journal entries by selected date and scope
const filteredEntries = $derived(
($journalEntries ?? [])
.filter(e => e.date === selectedDate)
.filter(e => e.date === selectedDate && (journalScope === 'private' ? !!e.isPrivate : !e.isPrivate))
.sort((a, b) => b.sortOrder - a.sortOrder)
);
@ -86,15 +87,16 @@
}
}
const isPrivate = journalScope === 'private';
if (selectedLinkedContextId) {
const topic = await createTopic(selectedLinkedContextId, title);
if (body) {
await createHistoryEntry(topic.id, selectedDate, body);
await createHistoryEntry(topic.id, selectedDate, body, null, false, isPrivate);
}
} else {
const text = body ? `${title}\n${body}` : title;
await getOrCreateJournalTopic();
await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked);
await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked, isPrivate);
}
entryTitle = '';
@ -186,12 +188,40 @@
}
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>
{#if isDailyLog}
<!-- Daily-log: chronological entry log with date navigation -->
<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="relative flex items-center gap-2">
<input
@ -257,7 +287,7 @@
</div>
</div>
<WiedervorlageSection date={selectedDate} />
<WiedervorlageSection date={selectedDate} {journalScope} />
{#if filteredEntries.length > 0}
<div class="mb-8 border-l-2 border-[#555] pl-5">

View File

@ -4,18 +4,24 @@
interface Props {
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>
{#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-3 text-sm font-semibold uppercase tracking-wider text-amber-400">
Wiedervorlage ({($pending ?? []).length})
Wiedervorlage ({pending.length})
</div>
{#each $pending ?? [] as entry (entry.id)}
{#each pending as entry (entry.id)}
<WiedervorlageCard {entry} />
{/each}
</div>

View File

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

View File

@ -8,7 +8,7 @@ export interface SyncEntity {
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 {
name: string;
@ -30,6 +30,8 @@ export interface PersonMeta {
email: string;
phone: string;
duSince: string;
birthday?: string; // YYYY-MM-DD
joinDate?: string; // YYYY-MM-DD (employee/colleague)
personSubType?: PersonSubType;
notes?: string;
}
@ -60,6 +62,7 @@ export interface HistoryEntry extends SyncEntity {
doneAt: string | null;
wiedervorlageDate: string | null; // YYYY-MM-DD
wiedervorlageResolvedAt: string | null; // ISO timestamp
isPrivate?: boolean; // undefined/false = business (default)
}
export interface Rating extends SyncEntity {