import { NextResponse } from "next/server"; import { createClient } from "@supabase/supabase-js"; export const runtime = "nodejs"; // Search result types - extendable for future search types export type SearchResultType = | "task" | "project" | "document" | "sprint" | "activity"; export interface SearchResult { id: string; type: SearchResultType; title: string; subtitle?: string; description?: string; status?: string; priority?: string; color?: string; url?: string; icon?: string; metadata?: Record; } export interface SearchResponse { results: SearchResult[]; total: number; query: string; } // Search configuration - easily extensible interface SearchConfig { table: string; type: SearchResultType; fields: string[]; titleField: string; subtitleField?: string; descriptionField?: string; statusField?: string; priorityField?: string; colorField?: string; urlGenerator: (item: any) => string; icon: string; enabled: boolean; searchFields: string[]; // Fields to search in } // Define searchable entities - add new ones here const searchConfigs: SearchConfig[] = [ { table: "tasks", type: "task", fields: ["id", "title", "description", "status", "priority", "project_id", "type"], titleField: "title", subtitleField: "type", descriptionField: "description", statusField: "status", priorityField: "priority", urlGenerator: (item) => `https://gantt-board.vercel.app/tasks/${item.id}`, icon: "kanban", enabled: true, searchFields: ["title", "description"], }, { table: "projects", type: "project", fields: ["id", "name", "description", "color", "status"], titleField: "name", descriptionField: "description", colorField: "color", statusField: "status", urlGenerator: (item) => `/projects`, icon: "folder-kanban", enabled: true, searchFields: ["name", "description"], }, { table: "sprints", type: "sprint", fields: ["id", "name", "goal", "status", "start_date", "end_date", "project_id"], titleField: "name", subtitleField: "goal", statusField: "status", urlGenerator: (item) => `https://gantt-board.vercel.app/sprints/${item.id}`, icon: "timer", enabled: true, searchFields: ["name", "goal"], }, { table: "mission_control_documents", type: "document", fields: ["id", "title", "content", "folder", "tags"], titleField: "title", subtitleField: "folder", descriptionField: "content", urlGenerator: (item) => `/documents`, icon: "file-text", enabled: true, searchFields: ["title", "content"], }, // Future: Add more search types here // { // table: "meetings", // type: "meeting", // ... // } ]; export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const query = searchParams.get("q")?.trim().toLowerCase(); if (!query || query.length < 2) { return NextResponse.json({ results: [], total: 0, query: query || "" }); } // Create Supabase client with service role for full access const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseKey) { return NextResponse.json( { error: "Database not configured" }, { status: 500 } ); } const supabase = createClient(supabaseUrl, supabaseKey); const results: SearchResult[] = []; // Search each configured entity for (const config of searchConfigs) { if (!config.enabled) continue; try { // Build OR filter dynamically based on searchFields const orConditions = config.searchFields .map(field => `${field}.ilike.%${query}%`) .join(","); const { data, error } = await supabase .from(config.table) .select(config.fields.join(", ")) .or(orConditions) .limit(10); if (error) { console.error(`Search error in ${config.table}:`, error); continue; } if (data) { const mappedResults: SearchResult[] = data.map((item: any) => ({ id: item.id, type: config.type, title: item[config.titleField] || "Untitled", subtitle: config.subtitleField ? item[config.subtitleField] : undefined, description: config.descriptionField ? item[config.descriptionField] : undefined, status: config.statusField ? item[config.statusField] : undefined, priority: config.priorityField ? item[config.priorityField] : undefined, color: config.colorField ? item[config.colorField] : undefined, url: config.urlGenerator(item), icon: config.icon, metadata: { table: config.table, ...Object.entries(item).reduce((acc, [key, value]) => { if (typeof value === "string" || typeof value === "number") { acc[key] = String(value); } return acc; }, {} as Record), }, })); results.push(...mappedResults); } } catch (err) { console.error(`Error searching ${config.table}:`, err); } } // Sort results by relevance (exact matches first, then partial) results.sort((a, b) => { const aTitle = a.title.toLowerCase(); const bTitle = b.title.toLowerCase(); // Exact match gets highest priority if (aTitle === query && bTitle !== query) return -1; if (bTitle === query && aTitle !== query) return 1; // Starts with query gets second priority if (aTitle.startsWith(query) && !bTitle.startsWith(query)) return -1; if (bTitle.startsWith(query) && !aTitle.startsWith(query)) return 1; // Otherwise alphabetical return aTitle.localeCompare(bTitle); }); // Limit total results const limitedResults = results.slice(0, 50); return NextResponse.json({ results: limitedResults, total: limitedResults.length, query, }); } catch (error) { console.error("Search API error:", error); return NextResponse.json( { error: "Search failed" }, { status: 500 } ); } }