222 lines
6.3 KiB
TypeScript
222 lines
6.3 KiB
TypeScript
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<string, string>;
|
|
}
|
|
|
|
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<string, string>),
|
|
},
|
|
}));
|
|
|
|
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 }
|
|
);
|
|
}
|
|
} |