diff --git a/src/hooks/useProjectPhase.ts b/src/hooks/useProjectPhase.ts new file mode 100644 index 0000000..166e9c9 --- /dev/null +++ b/src/hooks/useProjectPhase.ts @@ -0,0 +1,187 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { createClient } from '@/lib/supabase/client' +import type { Phase } from '@/components/ui/progress-timeline' +import type { Activity } from '@/components/ui/activity-feed' + +export interface ProjectPhaseState { + projectId: string + currentPhase: Phase + completedPhases: Phase[] + phaseStatus: 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'failed' + activities: Activity[] + metrics: { + totalDuration: number + phaseDurations: Record + iterationCount: number + } +} + +export function useProjectPhase(projectId: string) { + const [state, setState] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchProjectState = useCallback(async () => { + const supabase = createClient() + + try { + const { data: project, error: projectError } = await supabase + .from('projects') + .select('*') + .eq('id', projectId) + .single() + + if (projectError) throw projectError + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const metadata = (project as any)?.phase_metadata as ProjectPhaseState | undefined + + setState({ + projectId, + currentPhase: metadata?.currentPhase || 'think', + completedPhases: metadata?.completedPhases || [], + phaseStatus: metadata?.phaseStatus || 'not_started', + activities: metadata?.activities || [], + metrics: metadata?.metrics || { + totalDuration: 0, + phaseDurations: {} as Record, + iterationCount: 0 + } + }) + } catch (err) { + setError(err instanceof Error ? err : new Error('Failed to fetch project')) + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { + fetchProjectState() + + const supabase = createClient() + const channel = supabase + .channel(`project:${projectId}`) + .on('postgres_changes', { + event: 'UPDATE', + schema: 'public', + table: 'projects', + filter: `id=eq.${projectId}` + }, (payload) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const metadata = (payload.new as any)?.phase_metadata as ProjectPhaseState | undefined + if (metadata) { + setState(prev => prev ? { ...prev, ...metadata, projectId } : null) + } + }) + .subscribe() + + return () => { + supabase.removeChannel(channel) + } + }, [projectId, fetchProjectState]) + + const transitionPhase = useCallback(async (newPhase: Phase) => { + if (!state) return + + const supabase = createClient() + + const newActivity: Activity = { + id: crypto.randomUUID(), + type: 'phase_change', + message: `Transitioned to ${newPhase} phase`, + timestamp: new Date() + } + + const updatedState: ProjectPhaseState = { + ...state, + currentPhase: newPhase, + completedPhases: state.currentPhase !== newPhase + ? [...state.completedPhases, state.currentPhase] + : state.completedPhases, + phaseStatus: 'in_progress', + activities: [newActivity, ...state.activities].slice(0, 50) + } + + // Optimistic update + setState(updatedState) + + // Persist to database + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error: updateError } = await (supabase as any) + .from('projects') + .update({ phase_metadata: updatedState }) + .eq('id', projectId) + + if (updateError) { + console.error('Failed to persist phase:', updateError) + // Revert on error + setState(state) + } + }, [state, projectId]) + + const setPhaseStatus = useCallback(async (status: ProjectPhaseState['phaseStatus']) => { + if (!state) return + + const supabase = createClient() + + const updatedState: ProjectPhaseState = { + ...state, + phaseStatus: status + } + + setState(updatedState) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error: updateError } = await (supabase as any) + .from('projects') + .update({ phase_metadata: updatedState }) + .eq('id', projectId) + + if (updateError) { + console.error('Failed to update phase status:', updateError) + setState(state) + } + }, [state, projectId]) + + const addActivity = useCallback(async (activity: Omit) => { + if (!state) return + + const supabase = createClient() + + const newActivity: Activity = { + ...activity, + id: crypto.randomUUID(), + timestamp: new Date() + } + + const updatedState: ProjectPhaseState = { + ...state, + activities: [newActivity, ...state.activities].slice(0, 50) + } + + setState(updatedState) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error: updateError } = await (supabase as any) + .from('projects') + .update({ phase_metadata: updatedState }) + .eq('id', projectId) + + if (updateError) { + console.error('Failed to add activity:', updateError) + setState(state) + } + }, [state, projectId]) + + return { + state, + loading, + error, + transitionPhase, + setPhaseStatus, + addActivity, + refresh: fetchProjectState + } +} diff --git a/src/types/database.ts b/src/types/database.ts index 672c139..e51714d 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -98,6 +98,7 @@ export interface Database { tech_stack: string[] platform: string | null status: 'active' | 'archived' | 'paused' + phase_metadata: Json | null created_at: string updated_at: string } @@ -111,6 +112,7 @@ export interface Database { tech_stack?: string[] platform?: string | null status?: 'active' | 'archived' | 'paused' + phase_metadata?: Json | null created_at?: string updated_at?: string } @@ -124,6 +126,7 @@ export interface Database { tech_stack?: string[] platform?: string | null status?: 'active' | 'archived' | 'paused' + phase_metadata?: Json | null created_at?: string updated_at?: string }