Feature: Globale Schnellsuche (Cmd/Ctrl+K oder „/")

Keine Sprung-zu-Suche bei wachsender Projekt-/Lead-Zahl. Neuer search-Router
(ILIKE ueber Projekte, Leads, offene Blocker) + Command-Palette ueber die
Modal-Basis, getriggert per Cmd/Ctrl+K oder „/". Klick springt zum Ziel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 12:34:25 +02:00
1 parent aad247dd4a
commit a0a2392e3d
4 files changed
+160

No files matched your search

+2
View File
@@ -14,6 +14,7 @@ import { campaignRouter } from './campaign.js';
import { areaRouter } from './area.js';
import { blockerRouter } from './blocker.js';
import { timeRouter } from './time.js';
import { searchRouter } from './search.js';
export const appRouter = router({
health: publicProcedure.query(() => ({
@@ -34,6 +35,7 @@ export const appRouter = router({
dayEntry: dayEntryRouter,
time: timeRouter,
dashboard: dashboardRouter,
search: searchRouter,
});
export type AppRouter = typeof appRouter;
+38
View File
@@ -0,0 +1,38 @@
import { z } from 'zod';
import { and, ilike, isNull } from 'drizzle-orm';
import { project, lead, blocker } from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
/**
* Globale Schnellsuche über die wichtigsten Entitäten (Projekte, Leads, offene
* Blocker). Postgres ILIKE — für die Teamgröße völlig ausreichend, kein FTS nötig.
*/
export const searchRouter = router({
all: protectedProcedure
.input(z.object({ q: z.string().min(1).max(100) }))
.query(async ({ ctx, input }) => {
const term = `%${input.q.replace(/[%_]/g, '\\$&')}%`;
const [projects, leads, blockers] = await Promise.all([
ctx.db
.select({ id: project.id, name: project.name, archiviert: project.archiviert })
.from(project)
.where(ilike(project.name, term))
.limit(8),
ctx.db
.select({ id: lead.id, firma: lead.firma })
.from(lead)
.where(ilike(lead.firma, term))
.limit(8),
ctx.db
.select({ id: blocker.id, title: blocker.title })
.from(blocker)
.where(and(ilike(blocker.title, term), isNull(blocker.resolvedAt)))
.limit(8),
]);
return {
projects: projects.map((p) => ({ id: p.id, label: p.name, archived: p.archiviert })),
leads: leads.map((l) => ({ id: l.id, label: l.firma })),
blockers: blockers.map((b) => ({ id: b.id, label: b.title })),
};
}),
});