import { createClient } from '@/lib/supabase/client' import { deleteGiteaRepo } from '@/app/(dashboard)/projects/new/actions' import type { DesignPhase, PhaseStatus, BacklogItem, ProjectHealth, AIRecommendation } from '@/types/design-thinking' export interface Project { id: string user_id: string name: string description: string | null current_phase: DesignPhase health_score: number created_at: string updated_at: string } export interface ProjectPhase { id: string project_id: string phase: DesignPhase status: PhaseStatus started_at: string | null completed_at: string | null notes: string | null iteration: number } // Fetch all projects for current user export async function getProjects(): Promise { const supabase = createClient() const { data, error } = await supabase .from('projects') .select('*') .order('updated_at', { ascending: false }) if (error) throw error return (data || []) as unknown as Project[] } // Fetch single project with phases export async function getProject(projectId: string): Promise<{ project: Project; phases: ProjectPhase[] }> { const supabase = createClient() const [projectResult, phasesResult] = await Promise.all([ supabase.from('projects').select('*').eq('id', projectId).single(), supabase.from('project_phases').select('*').eq('project_id', projectId).order('iteration', { ascending: false }) ]) if (projectResult.error) throw projectResult.error if (phasesResult.error) throw phasesResult.error return { project: projectResult.data as unknown as Project, phases: (phasesResult.data || []) as unknown as ProjectPhase[] } } // Create new project export async function createProject(name: string, description?: string): Promise { const supabase = createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error('Not authenticated') // Use raw SQL-like insert with explicit types const { data, error } = await supabase .from('projects') .insert([{ user_id: user.id, name, description: description || null, current_phase: 'empathize' as DesignPhase, health_score: 100 }] as never) .select() .single() if (error) throw error const project = data as unknown as Project // Initialize phase tracking const phases: DesignPhase[] = ['empathize', 'define', 'ideate', 'prototype', 'test'] await supabase.from('project_phases').insert( phases.map((phase, index) => ({ project_id: project.id, phase, status: index === 0 ? 'in_progress' : 'not_started', iteration: 1, started_at: index === 0 ? new Date().toISOString() : null })) as never ) return project } // Update project phase export async function updateProjectPhase(projectId: string, phase: DesignPhase, status: PhaseStatus): Promise { const supabase = createClient() // Update the phase record const { error: phaseError } = await supabase .from('project_phases') .update({ status, started_at: status === 'in_progress' ? new Date().toISOString() : undefined, completed_at: status === 'completed' ? new Date().toISOString() : undefined } as never) .eq('project_id', projectId) .eq('phase', phase) if (phaseError) throw phaseError // Update project's current phase if advancing if (status === 'completed') { const phases: DesignPhase[] = ['empathize', 'define', 'ideate', 'prototype', 'test'] const currentIndex = phases.indexOf(phase) if (currentIndex < phases.length - 1) { const nextPhase = phases[currentIndex + 1] await supabase .from('projects') .update({ current_phase: nextPhase } as never) .eq('id', projectId) // Start the next phase await supabase .from('project_phases') .update({ status: 'in_progress', started_at: new Date().toISOString() } as never) .eq('project_id', projectId) .eq('phase', nextPhase) } } } // Loop back to a previous phase (start new iteration) export async function loopBackToPhase(projectId: string, fromPhase: DesignPhase, toPhase: DesignPhase): Promise { const supabase = createClient() // Get current max iteration const { data: currentPhases } = await supabase .from('project_phases') .select('iteration') .eq('project_id', projectId) .order('iteration', { ascending: false }) .limit(1) const phasesData = currentPhases as unknown as Array<{ iteration: number }> | null const newIteration = (phasesData?.[0]?.iteration || 1) + 1 // Create new iteration phases from toPhase onwards const phases: DesignPhase[] = ['empathize', 'define', 'ideate', 'prototype', 'test'] const startIndex = phases.indexOf(toPhase) await supabase.from('project_phases').insert( phases.slice(startIndex).map((phase, index) => ({ project_id: projectId, phase, status: index === 0 ? 'in_progress' : 'not_started', iteration: newIteration, started_at: index === 0 ? new Date().toISOString() : null })) as never ) // Update project's current phase await supabase .from('projects') .update({ current_phase: toPhase } as never) .eq('id', projectId) // Log activity await supabase.from('project_activities').insert([{ project_id: projectId, activity_type: 'loop_back', message: `Looped back from ${fromPhase} to ${toPhase} (iteration ${newIteration})`, metadata: { from_phase: fromPhase, to_phase: toPhase, iteration: newIteration } }] as never) } // Fetch backlog items for a project export async function getBacklogItems(projectId: string): Promise { const supabase = createClient() const { data, error } = await supabase .from('backlog_items') .select('*') .eq('project_id', projectId) .order('priority_score', { ascending: false }) if (error) throw error return (data || []) as unknown as BacklogItem[] } // Create backlog item export async function createBacklogItem( projectId: string, item: Omit ): Promise { const supabase = createClient() const { data, error } = await supabase .from('backlog_items') .insert([{ ...item, project_id: projectId }] as never) .select() .single() if (error) throw error return data as unknown as BacklogItem } // Update backlog item export async function updateBacklogItem(itemId: string, updates: Partial): Promise { const supabase = createClient() const { data, error } = await supabase .from('backlog_items') .update(updates as never) .eq('id', itemId) .select() .single() if (error) throw error return data as unknown as BacklogItem } // Fetch project health export async function getProjectHealth(projectId: string): Promise { const supabase = createClient() // Get latest health snapshot const { data: snapshot } = await supabase .from('project_health_snapshots') .select('*') .eq('project_id', projectId) .order('snapshot_date', { ascending: false }) .limit(1) .single() const snapshotData = snapshot as unknown as { overall_score: number velocity: number blockers: number overdue: number completion_rate: number } | null if (snapshotData) { return { overall: snapshotData.overall_score, velocity: snapshotData.velocity, blockers: snapshotData.blockers, overdue: snapshotData.overdue, completion_rate: snapshotData.completion_rate } } // Calculate on the fly if no snapshot const { data: items } = await supabase .from('backlog_items') .select('status, due_date') .eq('project_id', projectId) const itemsData = items as unknown as Array<{ status: string; due_date: string | null }> | null const total = itemsData?.length || 0 const done = itemsData?.filter(i => i.status === 'done').length || 0 const blocked = itemsData?.filter(i => i.status === 'blocked').length || 0 const overdue = itemsData?.filter(i => i.due_date && new Date(i.due_date) < new Date() && i.status !== 'done' ).length || 0 return { overall: Math.max(0, 100 - (blocked * 10) - (overdue * 5)), velocity: done, blockers: blocked, overdue, completion_rate: total > 0 ? Math.round((done / total) * 100) : 0 } } // Fetch AI recommendations export async function getRecommendations(projectId: string): Promise { const supabase = createClient() const { data, error } = await supabase .from('ai_recommendations') .select('*') .eq('project_id', projectId) .eq('dismissed', false) .order('created_at', { ascending: false }) if (error) throw error return (data || []) as unknown as AIRecommendation[] } // Dismiss recommendation export async function dismissRecommendation(recommendationId: string): Promise { const supabase = createClient() await supabase .from('ai_recommendations') .update({ dismissed: true, dismissed_at: new Date().toISOString() } as never) .eq('id', recommendationId) } // Fetch project activities export async function getProjectActivities(projectId: string, limit = 20) { const supabase = createClient() const { data, error } = await supabase .from('project_activities') .select('*') .eq('project_id', projectId) .order('created_at', { ascending: false }) .limit(limit) if (error) throw error return (data || []) as unknown as Array<{ id: string activity_type: string message: string created_at: string }> } // Delete project with cascade (includes Gitea repo deletion) export async function deleteProject(projectId: string): Promise<{ success: boolean; error?: string }> { const supabase = createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) { return { success: false, error: 'Not authenticated' } } try { // First, get the project to check ownership and get gitea_repo const { data: projectData, error: fetchError } = await supabase .from('projects') .select('id, user_id, gitea_repo, name') .eq('id', projectId) .single() if (fetchError || !projectData) { return { success: false, error: 'Project not found' } } const project = projectData as unknown as { id: string user_id: string gitea_repo: string | null name: string } // Verify ownership if (project.user_id !== user.id) { return { success: false, error: 'Not authorized to delete this project' } } // Extract repo name from gitea_repo URL if it exists // Format: https://gitea.mylder.io/admin/repo-name if (project.gitea_repo) { const repoMatch = project.gitea_repo.match(/\/([^/]+)$/) const repoName = repoMatch ? repoMatch[1] : null if (repoName) { const giteaResult = await deleteGiteaRepo(repoName) if (!giteaResult.success) { console.error('Failed to delete Gitea repo:', giteaResult.error) // Continue with database deletion even if Gitea fails } } } // Delete related data in order (due to foreign key constraints) // Delete agent_runs await supabase .from('agent_runs') .delete() .eq('project_id', projectId) // Delete messages await supabase .from('messages') .delete() .eq('project_id', projectId) // Delete backlog items await supabase .from('backlog_items') .delete() .eq('project_id', projectId) // Delete project activities await supabase .from('project_activities') .delete() .eq('project_id', projectId) // Delete AI recommendations await supabase .from('ai_recommendations') .delete() .eq('project_id', projectId) // Delete project phases await supabase .from('project_phases') .delete() .eq('project_id', projectId) // Delete health snapshots await supabase .from('project_health_snapshots') .delete() .eq('project_id', projectId) // Finally, delete the project itself const { error: deleteError } = await supabase .from('projects') .delete() .eq('id', projectId) if (deleteError) { return { success: false, error: deleteError.message } } return { success: true } } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to delete project' } } }