upd meniu

This commit is contained in:
beo3000 2026-02-20 17:49:04 +01:00
parent fa4d3046d0
commit 81a5a90b20
12 changed files with 89 additions and 23 deletions

View File

@ -21,10 +21,16 @@
const contexts = liveQuery(() => db.contexts.filter(c => !c.deletedAt && !c.archivedAt).sortBy('sortOrder')); const contexts = liveQuery(() => db.contexts.filter(c => !c.deletedAt && !c.archivedAt).sortBy('sortOrder'));
const dailyLog = $derived(($contexts ?? []).find((c: AgendaContext) => c.id === 'daily-log')); const dailyLog = $derived(($contexts ?? []).find((c: AgendaContext) => c.id === 'daily-log'));
const meetings = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'meeting' && c.id !== 'daily-log')); const meetings = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'meeting' && c.id !== 'daily-log' && c.isFavorite));
const projects = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'project')); const projects = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'project' && c.isFavorite));
const companies = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'company')); const companies = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'company' && c.isFavorite));
const people = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'person')); const people = $derived(($contexts ?? []).filter((c: AgendaContext) => c.type === 'person' && c.isFavorite));
let collapsed: Record<string, boolean> = $state({});
function toggleSection(key: string) {
collapsed[key] = !collapsed[key];
}
let creatingType: ContextType | null = $state(null); let creatingType: ContextType | null = $state(null);
let newName = $state(''); let newName = $state('');
@ -103,9 +109,13 @@
<!-- Jour Fixes --> <!-- Jour Fixes -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1"> <div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<span class="text-xs uppercase text-muted">Jour Fixes</span> <button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('meetings')}>
<span class="inline-block text-[10px] transition-transform {collapsed['meetings'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Jour Fixes
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('meeting')}>+</button> <button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('meeting')}>+</button>
</div> </div>
{#if !collapsed['meetings']}
<ul class="m-0 list-none p-0"> <ul class="m-0 list-none p-0">
{#each meetings as ctx} {#each meetings as ctx}
<li> <li>
@ -138,12 +148,17 @@
</button> </button>
</li> </li>
</ul> </ul>
{/if}
<!-- Projects --> <!-- Projects -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1"> <div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<span class="text-xs uppercase text-muted">Projects</span> <button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('projects')}>
<span class="inline-block text-[10px] transition-transform {collapsed['projects'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Projects
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('project')}>+</button> <button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('project')}>+</button>
</div> </div>
{#if !collapsed['projects']}
<ul class="m-0 list-none p-0"> <ul class="m-0 list-none p-0">
{#each projects as ctx} {#each projects as ctx}
<li> <li>
@ -176,12 +191,17 @@
</button> </button>
</li> </li>
</ul> </ul>
{/if}
<!-- Companies --> <!-- Companies -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1"> <div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<span class="text-xs uppercase text-muted">Firmen</span> <button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('companies')}>
<span class="inline-block text-[10px] transition-transform {collapsed['companies'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Firmen
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('company')}>+</button> <button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('company')}>+</button>
</div> </div>
{#if !collapsed['companies']}
<ul class="m-0 list-none p-0"> <ul class="m-0 list-none p-0">
{#each companies as ctx} {#each companies as ctx}
<li> <li>
@ -214,12 +234,17 @@
</button> </button>
</li> </li>
</ul> </ul>
{/if}
<!-- People --> <!-- People -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1"> <div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<span class="text-xs uppercase text-muted">People</span> <button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('people')}>
<span class="inline-block text-[10px] transition-transform {collapsed['people'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
People
</button>
<button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('person')}>+</button> <button class="text-muted hover:text-white text-sm leading-none" onclick={() => startCreating('person')}>+</button>
</div> </div>
{#if !collapsed['people']}
<ul class="m-0 list-none p-0"> <ul class="m-0 list-none p-0">
{#each people as ctx} {#each people as ctx}
<li> <li>
@ -252,6 +277,7 @@
</button> </button>
</li> </li>
</ul> </ul>
{/if}
{#if isDev} {#if isDev}
<div class="mt-auto border-t border-border pt-3"> <div class="mt-auto border-t border-border pt-3">

View File

@ -28,6 +28,7 @@ export async function upsertContext(ctx: Partial<AgendaContext> & { id: string }
sortOrder: 0, sortOrder: 0,
meta: null, meta: null,
archivedAt: null, archivedAt: null,
isFavorite: false,
deletedAt: null, deletedAt: null,
updatedAt: now(), updatedAt: now(),
version: 1, version: 1,
@ -57,6 +58,13 @@ export async function unarchiveContext(id: string): Promise<void> {
} }
} }
export async function toggleFavorite(id: string): Promise<void> {
const ctx = await db.contexts.get(id);
if (ctx) {
await db.contexts.update(id, { isFavorite: !ctx.isFavorite, updatedAt: now(), version: ctx.version + 1 });
}
}
export async function findContextByMentionName(name: string, type: 'person' | 'project' | 'company'): Promise<AgendaContext | undefined> { export async function findContextByMentionName(name: string, type: 'person' | 'project' | 'company'): Promise<AgendaContext | undefined> {
const q = name.toLowerCase(); const q = name.toLowerCase();
return db.contexts return db.contexts

View File

@ -60,6 +60,14 @@ export class KaNoteDB extends Dexie {
if (rating.comment === undefined) rating.comment = null; if (rating.comment === undefined) rating.comment = null;
}); });
}); });
this.version(6).stores({
contexts: 'id, type, sortOrder, deletedAt, archivedAt, isFavorite'
}).upgrade(tx => {
return tx.table('contexts').toCollection().modify(ctx => {
if (ctx.isFavorite === undefined) ctx.isFavorite = false;
});
});
} }
} }

View File

@ -13,16 +13,16 @@ export async function seedIfEmpty(): Promise<void> {
const ts = now(); const ts = now();
const contexts: AgendaContext[] = [ const contexts: AgendaContext[] = [
{ id: 'daily-log', name: 'Daily Log / Inbox', type: 'meeting', sortOrder: 0, meta: null, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'daily-log', name: 'Daily Log / Inbox', type: 'meeting', sortOrder: 0, meta: null, archivedAt: null, isFavorite: true, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'jf-sysadmins', name: 'JF Team Sysadmins', type: 'meeting', sortOrder: 1, meta: null, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'jf-sysadmins', name: 'JF Team Sysadmins', type: 'meeting', sortOrder: 1, meta: null, archivedAt: null, isFavorite: true, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'jf-devs', name: 'JF Developer', type: 'meeting', sortOrder: 2, meta: null, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'jf-devs', name: 'JF Developer', type: 'meeting', sortOrder: 2, meta: null, archivedAt: null, isFavorite: true, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'p-tisax', name: 'Project TISAX', type: 'project', sortOrder: 0, meta: { status: '#stInArbeit', owner: 'STEFE', links: 'FileServer/Projects/TISAX' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'p-tisax', name: 'Project TISAX', type: 'project', sortOrder: 0, meta: { status: '#stInArbeit', owner: 'STEFE', links: 'FileServer/Projects/TISAX' }, archivedAt: null, isFavorite: true, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'p-cloud-migration', name: 'Project CLOUD-MIGRATION', type: 'project', sortOrder: 1, meta: { status: '#stBlocked', owner: 'CHFI', links: 'Jira-1234' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'p-cloud-migration', name: 'Project CLOUD-MIGRATION', type: 'project', sortOrder: 1, meta: { status: '#stBlocked', owner: 'CHFI', links: 'Jira-1234' }, archivedAt: null, isFavorite: false, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'u-stefe', name: 'Person STEFE', type: 'person', sortOrder: 0, meta: { fullName: 'Stefan E.', email: 'stefe@example.com', phone: '+49 123 456', duSince: '2020', personSubType: 'employee' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'u-stefe', name: 'Person STEFE', type: 'person', sortOrder: 0, meta: { fullName: 'Stefan E.', email: 'stefe@example.com', phone: '+49 123 456', duSince: '2020', personSubType: 'employee' }, archivedAt: null, isFavorite: true, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'u-chfi', name: 'Person CHFI', type: 'person', sortOrder: 1, meta: { fullName: 'Christoph F.', email: 'chfi@example.com', phone: '98765', duSince: '2024', personSubType: 'employee' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'u-chfi', name: 'Person CHFI', type: 'person', sortOrder: 1, meta: { fullName: 'Christoph F.', email: 'chfi@example.com', phone: '98765', duSince: '2024', personSubType: 'employee' }, archivedAt: null, isFavorite: false, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'u-vendor-x', name: 'Person VENDOR-X', type: 'person', sortOrder: 2, meta: { fullName: 'Hr. Müller (Vendor X)', email: 'sales@vendor-x.com', phone: '', duSince: '', personSubType: 'contact' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'u-vendor-x', name: 'Person VENDOR-X', type: 'person', sortOrder: 2, meta: { fullName: 'Hr. Müller (Vendor X)', email: 'sales@vendor-x.com', phone: '', duSince: '', personSubType: 'contact' }, archivedAt: null, isFavorite: false, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'u-chrkl', name: 'Person ChrKl', type: 'person', sortOrder: 3, meta: { fullName: 'Christian Kl.', email: '', phone: '', duSince: '', personSubType: 'colleague' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 }, { id: 'u-chrkl', name: 'Person ChrKl', type: 'person', sortOrder: 3, meta: { fullName: 'Christian Kl.', email: '', phone: '', duSince: '', personSubType: 'colleague' }, archivedAt: null, isFavorite: false, updatedAt: ts, deletedAt: null, version: 1 },
{ id: 'f-vendor-x', name: 'Firma VENDOR-X', type: 'company', sortOrder: 0, meta: { website: 'https://vendor-x.com', address: 'Musterstr. 1, 12345 Berlin' }, archivedAt: null, updatedAt: ts, deletedAt: null, version: 1 } { id: 'f-vendor-x', name: 'Firma VENDOR-X', type: 'company', sortOrder: 0, meta: { website: 'https://vendor-x.com', address: 'Musterstr. 1, 12345 Berlin' }, archivedAt: null, isFavorite: true, updatedAt: ts, deletedAt: null, version: 1 }
]; ];
// Topic IDs // Topic IDs

View File

@ -47,7 +47,7 @@
<!-- Sidebar --> <!-- Sidebar -->
<aside <aside
class="fixed z-40 flex h-full w-[250px] flex-col border-r border-border bg-sidebar p-5 transition-transform md:static md:translate-x-0 {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}" class="fixed z-40 flex h-full w-[250px] flex-col border-r border-border bg-sidebar p-5 overflow-y-auto transition-transform md:static md:translate-x-0 {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}"
> >
<Sidebar onnavigate={closeSidebar} /> <Sidebar onnavigate={closeSidebar} />
</aside> </aside>

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema'; import { db } from '$lib/db/schema';
import { softDeleteContext } from '$lib/db/repositories'; import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
const allCompanies = liveQuery(() => const allCompanies = liveQuery(() =>
@ -36,6 +36,11 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each companies as ctx (ctx.id)} {#each companies as ctx (ctx.id)}
<div class="flex items-center gap-2"> <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 <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]" 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}`)} onclick={() => goto(`/context/${ctx.id}`)}

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema'; import { db } from '$lib/db/schema';
import { softDeleteContext } from '$lib/db/repositories'; import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
const allMeetings = liveQuery(() => const allMeetings = liveQuery(() =>
@ -32,6 +32,11 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each meetings as ctx (ctx.id)} {#each meetings as ctx (ctx.id)}
<div class="flex items-center gap-2"> <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 <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]" 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}`)} onclick={() => goto(`/context/${ctx.id}`)}

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema'; import { db } from '$lib/db/schema';
import { softDeleteContext } from '$lib/db/repositories'; import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import type { PersonMeta, PersonSubType } from '@ka-note/shared'; import type { PersonMeta, PersonSubType } from '@ka-note/shared';
@ -50,6 +50,11 @@
{#each persons as ctx (ctx.id)} {#each persons as ctx (ctx.id)}
{@const subType = ((ctx.meta as PersonMeta | null)?.personSubType ?? 'contact') as PersonSubType} {@const subType = ((ctx.meta as PersonMeta | null)?.personSubType ?? 'contact') as PersonSubType}
<div class="flex items-center gap-2"> <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 <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]" 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}`)} onclick={() => goto(`/context/${ctx.id}`)}

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema'; import { db } from '$lib/db/schema';
import { softDeleteContext } from '$lib/db/repositories'; import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import type { AgendaContext, ProjectMeta } from '@ka-note/shared'; import type { AgendaContext, ProjectMeta } from '@ka-note/shared';
@ -43,6 +43,11 @@
<div class="mb-8 flex flex-col gap-2"> <div class="mb-8 flex flex-col gap-2">
{#each activeProjects as ctx (ctx.id)} {#each activeProjects as ctx (ctx.id)}
<div class="flex items-center gap-2"> <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 <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]" 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}`)} onclick={() => goto(`/context/${ctx.id}`)}

View File

@ -7,6 +7,7 @@ export const contexts = sqliteTable('contexts', {
sortOrder: integer('sort_order').notNull().default(0), sortOrder: integer('sort_order').notNull().default(0),
meta: text('meta'), // JSON string meta: text('meta'), // JSON string
archivedAt: text('archived_at'), archivedAt: text('archived_at'),
isFavorite: integer('is_favorite', { mode: 'boolean' }).notNull().default(false),
updatedAt: text('updated_at').notNull(), updatedAt: text('updated_at').notNull(),
deletedAt: text('deleted_at'), deletedAt: text('deleted_at'),
version: integer('version').notNull().default(1), version: integer('version').notNull().default(1),

View File

@ -21,6 +21,7 @@ function mapContext(row: typeof contexts.$inferSelect): AgendaContext {
sortOrder: row.sortOrder, sortOrder: row.sortOrder,
meta: row.meta ? JSON.parse(row.meta) : null, meta: row.meta ? JSON.parse(row.meta) : null,
archivedAt: row.archivedAt, archivedAt: row.archivedAt,
isFavorite: row.isFavorite,
updatedAt: row.updatedAt, updatedAt: row.updatedAt,
deletedAt: row.deletedAt, deletedAt: row.deletedAt,
version: row.version, version: row.version,
@ -219,6 +220,7 @@ export async function seedDailyLog(): Promise<void> {
sortOrder: 0, sortOrder: 0,
meta: null, meta: null,
archivedAt: null, archivedAt: null,
isFavorite: true,
updatedAt: now(), updatedAt: now(),
deletedAt: null, deletedAt: null,
version: 1, version: 1,

View File

@ -15,6 +15,7 @@ export interface AgendaContext extends SyncEntity {
sortOrder: number; sortOrder: number;
meta: ProjectMeta | PersonMeta | CompanyMeta | null; meta: ProjectMeta | PersonMeta | CompanyMeta | null;
archivedAt: string | null; archivedAt: string | null;
isFavorite: boolean;
} }
export interface ProjectMeta { export interface ProjectMeta {