diff --git a/.phase7-state.json b/.phase7-state.json index d7e23bc..588fd3d 100644 --- a/.phase7-state.json +++ b/.phase7-state.json @@ -1,5 +1,8 @@ { "completed_features": [], - "current_feature": "documents-upload", - "started_at": "2026-05-23T05:40:09.997191" + "current_feature": "search-everywhere", + "started_at": "2026-05-23T05:40:09.997191", + "attempted_features": [ + "documents-upload" + ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index e5df2ee..3bd835a 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -782,3 +782,20 @@ src/routes/documents.ts(36,25): error TS2339: Property 'size' does not exist on src/routes/documents.ts(46,32): error TS2339: Property 'file' does not exist on type 'FastifyRequest>'. src/routes/documents.ts(56,9): error TS2769: No overload matches this call. Overload 1 of 2, '(value: { filename: string | SQL | Placeholder; contentType: string | SQL | Placeholder; sizeBytes: number | SQL<...> | Placeholder<...>; id?: string | ... 2 more ... | undefined; createdAt?: SQL<...> | ... 2 more ... | undefined; userId?: string | ... 3 more ... | undefined; c +- `05:41:42` **INFO** Committed feature documents-upload +- `05:41:43` **INFO** Pushed: rc=0 + +## Phase-3 Feature: search-everywhere (2026-05-23 05:41:43) + +- `05:41:43` **INFO** Description: Global Search API + Search-Bar component +- `05:41:43` **INFO** Generating apps/api/src/routes/search.ts (Fastify-Plugin /api/search?q=... Auth required. Sucht in time-entries …) +- `05:41:57` **INFO** wrote 1584 chars in 14.0s (attempt 1) +- `05:41:57` **INFO** Generating apps/web/src/components/SearchBar.tsx (Global Search-Component. Input rechts in Nav-Bar. Debounced (300ms). B…) +- `05:42:38` **INFO** wrote 4743 chars in 40.8s (attempt 1) +- `05:42:38` **INFO** Running tsc --noEmit on api… +- `05:42:39` **WARN** tsc errors: +src/routes/documents.ts(34,25): error TS2339: Property 'name' does not exist on type 'PgTableWithColumns<{ name: "documents"; schema: undefined; columns: { id: PgColumn<{ name: "id"; tableName: "documents"; dataType: "string"; columnType: "PgUUID"; data: string; driverParam: string; notNull: true; hasDefault: true; ... 6 more ...; generated: undefined; }, {}, {}>; ... 5 more ...; createdAt: PgColumn<...'. +src/routes/documents.ts(36,25): error TS2339: Property 'size' does not exist on type 'PgTableWithColumns<{ name: "documents"; schema: undefined; columns: { id: PgColumn<{ name: "id"; tableName: "documents"; dataType: "string"; columnType: "PgUUID"; data: string; driverParam: string; notNull: true; hasDefault: true; ... 6 more ...; generated: undefined; }, {}, {}>; ... 5 more ...; createdAt: PgColumn<...'. +src/routes/documents.ts(46,32): error TS2339: Property 'file' does not exist on type 'FastifyRequest>'. +src/routes/documents.ts(56,9): error TS2769: No overload matches this call. + Overload 1 of 2, '(value: { filename: string | SQL | Placeholder; contentType: string | SQL | Placeholder; sizeBytes: number | SQL<...> | Placeholder<...>; id?: string | ... 2 more ... | undefined; createdAt?: SQL<...> | ... 2 more ... | undefined; userId?: string | ... 3 more ... | undefined; c diff --git a/apps/api/src/routes/search.ts b/apps/api/src/routes/search.ts new file mode 100644 index 0000000..b6789e9 --- /dev/null +++ b/apps/api/src/routes/search.ts @@ -0,0 +1,57 @@ +import { FastifyInstance } from "fastify" +import { db } from "../db" +import { customers, projects, timeEntries, users } from "../db/schema" +import { ilike, and, eq } from "drizzle-orm" +import { z } from "zod" + +export default async function searchRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + fastify.get("/", async (request, reply) => { + const { q } = request.query as { q?: string } + + if (!q || q.length < 2) { + return { timeEntries: [], customers: [], projects: [], users: [] } + } + + const searchTerm = `%${q}%` + + const [customerResults, projectResults, timeEntryResults, userResults] = await Promise.all([ + db + .select() + .from(customers) + .where(and(eq(customers.active, true), ilike(customers.name, searchTerm))) + .limit(10), + db + .select() + .from(projects) + .where(and(eq(projects.active, true), ilike(projects.name, searchTerm))) + .limit(10), + db + .select() + .from(timeEntries) + .where(ilike(timeEntries.description, searchTerm)) + .limit(10), + db + .select() + .from(users) + .where( + ilike(users.name, searchTerm) || ilike(users.email, searchTerm) + ) + .limit(10) + ]) + + return { + customers: customerResults, + projects: projectResults, + timeEntries: timeEntryResults, + users: userResults + } + }) +} \ No newline at end of file diff --git a/apps/web/src/components/SearchBar.tsx b/apps/web/src/components/SearchBar.tsx new file mode 100644 index 0000000..8375e67 --- /dev/null +++ b/apps/web/src/components/SearchBar.tsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { Search, User, Folder, Clock, X } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; +import { api } from '../lib/api'; + +interface SearchResult { + id: string; + type: 'customer' | 'project' | 'time-entry'; + label: string; + subtitle?: string; +} + +export default function SearchBar() { + const [query, setQuery] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const navigate = useNavigate(); + + const { data: results, isLoading } = useQuery({ + queryKey: ['search', query], + queryFn: () => api.search(query), + enabled: query.length >= 2, + }); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsOpen(false); + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const handleSelect = (result: SearchResult) => { + const paths: Record = { + customer: `/customers/${result.id}`, + project: `/projects/${result.id}`, + 'time-entry': `/time-entries/${result.id}`, + }; + navigate({ to: paths[result.type] }); + setQuery(''); + setIsOpen(false); + }; + + const getIcon = (type: SearchResult['type']) => { + switch (type) { + case 'customer': return ; + case 'project': return ; + case 'time-entry': return ; + } + }; + + return ( +
+
+ + { + setQuery(e.target.value); + setIsOpen(true); + }} + onFocus={() => setIsOpen(true)} + /> + {query && ( + + )} +
+ + {isOpen && (query.length >= 2 || (results && results.length > 0)) && ( +
+ {isLoading ? ( +
Searching...
+ ) : results && results.length > 0 ? ( +
+ {['customer', 'project', 'time-entry'].map((type) => { + const filtered = results.filter((r: SearchResult) => r.type === type); + if (filtered.length === 0) return null; + + return ( +
+
+ {type}s +
+ {filtered.map((result: SearchResult) => ( + + ))} +
+ ); + })} +
+ ) : ( +
No results found
+ )} +
+ )} +
+ ); +} \ No newline at end of file