diff --git a/src/app/api/tasks/natural/route.ts b/src/app/api/tasks/natural/route.ts new file mode 100644 index 0000000..ace988d --- /dev/null +++ b/src/app/api/tasks/natural/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { addTaskToGantt, parseTaskInput } from "@/lib/server/taskParser"; + +export async function POST(req: NextRequest) { + try { + const { text } = await req.json(); + + if (!text || typeof text !== "string") { + return NextResponse.json({ error: "Missing text" }, { status: 400 }); + } + + const result = await addTaskToGantt(text); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 500 }); + } + + const parsed = parseTaskInput(text); + + return NextResponse.json({ + success: true, + task: result.task, + parsed: { + title: parsed.title, + assignee: parsed.assigneeId === "0a3e400c-3932-48ae-9b65-f3f9c6f26fe9" ? "Matt" : "Max", + priority: parsed.priority, + dueDate: parsed.dueDate, + }, + }); + } catch (err: any) { + console.error("Error parsing task:", err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 1d7d070..eb9fc45 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -24,8 +24,8 @@ interface UserRow { } function getPublicSupabase() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { throw new Error("Missing Supabase public environment variables"); } diff --git a/src/lib/server/taskParser.ts b/src/lib/server/taskParser.ts new file mode 100644 index 0000000..6318868 --- /dev/null +++ b/src/lib/server/taskParser.ts @@ -0,0 +1,129 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; +const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; +const supabase = createClient(supabaseUrl, supabaseKey); + +// Matt's user ID +const MATT_USER_ID = "0a3e400c-3932-48ae-9b65-f3f9c6f26fe9"; +const MAX_USER_ID = "9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa"; + +interface ParsedTask { + title: string; + description?: string; + assigneeId: string; + priority: "low" | "medium" | "high"; + dueDate?: string; + projectId?: string; +} + +export function parseTaskInput(input: string): ParsedTask { + const lower = input.toLowerCase(); + + // Extract assignee + let assigneeId = MATT_USER_ID; + if (lower.includes("assign to max") || lower.includes("for max")) { + assigneeId = MAX_USER_ID; + } + + // Extract priority + let priority: "low" | "medium" | "high" = "medium"; + if (lower.includes("urgent") || lower.includes("asap") || lower.includes("high priority")) { + priority = "high"; + } else if (lower.includes("low priority") || lower.includes("whenever") || lower.includes("eventually")) { + priority = "low"; + } + + // Extract due date phrases + let dueDate: string | undefined; + const today = new Date(); + + if (lower.includes("tomorrow")) { + const d = new Date(today); + d.setDate(d.getDate() + 1); + dueDate = d.toISOString().split("T")[0]; + } else if (lower.includes("next week")) { + const d = new Date(today); + d.setDate(d.getDate() + 7); + dueDate = d.toISOString().split("T")[0]; + } else if (lower.includes("by friday") || lower.includes("this friday")) { + const d = new Date(today); + const daysUntilFriday = (5 - d.getDay() + 7) % 7 || 7; + d.setDate(d.getDate() + daysUntilFriday); + dueDate = d.toISOString().split("T")[0]; + } else if (lower.includes("by monday") || lower.includes("this monday")) { + const d = new Date(today); + const daysUntilMonday = (1 - d.getDay() + 7) % 7 || 7; + d.setDate(d.getDate() + daysUntilMonday); + dueDate = d.toISOString().split("T")[0]; + } + + // Clean up the title - remove assignee/priority/due date phrases + let title = input + .replace(/assign to (max|matt)/gi, "") + .replace(/for (max|matt)/gi, "") + .replace(/urgent|asap|high priority|low priority/gi, "") + .replace(/tomorrow|next week|by friday|this friday|by monday|this monday/gi, "") + .replace(/\s+/g, " ") + .trim(); + + // If it starts with "add task" or similar, remove that + title = title.replace(/^(add|create|new)\s+(task|todo|item)\s*[:\-]?\s*/i, ""); + + // Capitalize first letter + title = title.charAt(0).toUpperCase() + title.slice(1); + + return { + title, + assigneeId, + priority, + dueDate, + }; +} + +export async function addTaskToGantt(input: string): Promise<{ success: boolean; task?: any; error?: string }> { + try { + const parsed = parseTaskInput(input); + + // Get default project (first one) or use a specific project + const { data: projects } = await supabase + .from("projects") + .select("id") + .order("created_at", { ascending: true }) + .limit(1); + + const projectId = projects?.[0]?.id; + + if (!projectId) { + return { success: false, error: "No projects found in gantt board" }; + } + + // Create the task + const { data: task, error } = await supabase + .from("tasks") + .insert({ + title: parsed.title, + description: parsed.description || null, + status: "todo", + priority: parsed.priority, + assignee_id: parsed.assigneeId, + created_by: MATT_USER_ID, + project_id: projectId, + due_date: parsed.dueDate || null, + start_date: new Date().toISOString().split("T")[0], + end_date: parsed.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0], + }) + .select() + .single(); + + if (error) { + console.error("Error adding task:", error); + return { success: false, error: error.message }; + } + + return { success: true, task }; + } catch (err: any) { + console.error("Failed to add task:", err); + return { success: false, error: err.message }; + } +} diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts index 0def225..b2e2265 100644 --- a/src/lib/supabase/client.ts +++ b/src/lib/supabase/client.ts @@ -1,8 +1,8 @@ import { createClient } from '@supabase/supabase-js'; -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; -const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY; if (!supabaseUrl || !supabaseAnonKey) { throw new Error(