Add Supabase data layer and integrate design thinking dashboard

- Add projects.ts with full CRUD operations for design thinking entities
- Add use-project hook for reactive data fetching
- Add DesignThinkingDashboard that integrates all components
- Update project tabs to use new Phases view
- Add Skeleton component for loading states
- All operations use explicit type casting for schema compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 20:00:49 +01:00
parent ffb4dc28c5
commit 44cfd4d5f1
6 changed files with 640 additions and 10 deletions

View File

@@ -0,0 +1,319 @@
import { createClient } from '@/lib/supabase/client'
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<Project[]> {
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<Project> {
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<void> {
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<void> {
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<BacklogItem[]> {
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<BacklogItem, 'id' | 'priority_score' | 'created_at' | 'updated_at'>
): Promise<BacklogItem> {
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<BacklogItem>): Promise<BacklogItem> {
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<ProjectHealth> {
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<AIRecommendation[]> {
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<void> {
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
}>
}