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:
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ProjectChat } from '@/components/chat/project-chat'
|
||||
import { AgenticDashboard } from '@/components/dashboard/agentic-dashboard'
|
||||
import { MessageSquare, Workflow } from 'lucide-react'
|
||||
import { DesignThinkingDashboard } from '@/components/project/design-thinking-dashboard'
|
||||
import { MessageSquare, Compass } from 'lucide-react'
|
||||
import type { Database } from '@/types/database'
|
||||
|
||||
type Message = Database['public']['Tables']['messages']['Row']
|
||||
@@ -28,11 +28,11 @@ export function ProjectTabs({ projectId, projectName, initialMessages }: Project
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="workflow"
|
||||
value="phases"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-foreground rounded-none px-1 pb-3 pt-2"
|
||||
>
|
||||
<Workflow className="w-4 h-4 mr-2" />
|
||||
Workflow
|
||||
<Compass className="w-4 h-4 mr-2" />
|
||||
Phases
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
@@ -42,12 +42,9 @@ export function ProjectTabs({ projectId, projectName, initialMessages }: Project
|
||||
<ProjectChat projectId={projectId} initialMessages={initialMessages} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="workflow" className="flex-1 overflow-auto m-0 p-6 data-[state=inactive]:hidden">
|
||||
<TabsContent value="phases" className="flex-1 overflow-auto m-0 p-6 data-[state=inactive]:hidden">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<AgenticDashboard
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
/>
|
||||
<DesignThinkingDashboard projectId={projectId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
166
src/components/project/design-thinking-dashboard.tsx
Normal file
166
src/components/project/design-thinking-dashboard.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import { useProject } from '@/hooks/use-project'
|
||||
import { PhaseNavigator } from './phase-navigator'
|
||||
import { HealthWidget } from './health-widget'
|
||||
import { BacklogBoard } from './backlog-board'
|
||||
import { RecommendationsWidget } from './recommendations-widget'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { AlertCircle, RefreshCw, ChevronRight } from 'lucide-react'
|
||||
import type { DesignPhase, BacklogItem, AIRecommendation } from '@/types/design-thinking'
|
||||
|
||||
interface DesignThinkingDashboardProps {
|
||||
projectId: string
|
||||
}
|
||||
|
||||
function DashboardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<Skeleton className="h-48 lg:col-span-2" />
|
||||
<Skeleton className="h-48" />
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
<Skeleton className="h-64" />
|
||||
<Skeleton className="h-64" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorState({ error, onRetry }: { error: Error; onRetry: () => void }) {
|
||||
return (
|
||||
<Card className="border-red-500/20 bg-red-500/5">
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">Failed to load project</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">{error.message}</p>
|
||||
<Button onClick={onRetry} variant="outline" className="gap-2">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Retry
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function DesignThinkingDashboard({ projectId }: DesignThinkingDashboardProps) {
|
||||
const {
|
||||
project,
|
||||
phaseStatuses,
|
||||
backlogItems,
|
||||
health,
|
||||
recommendations,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
advancePhase,
|
||||
loopBack,
|
||||
updateBacklog,
|
||||
dismissRec
|
||||
} = useProject(projectId)
|
||||
|
||||
if (loading) {
|
||||
return <DashboardSkeleton />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorState error={error} onRetry={refresh} />
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handlePhaseClick = (phase: DesignPhase) => {
|
||||
// Could open a modal with phase details
|
||||
console.log('Phase clicked:', phase)
|
||||
}
|
||||
|
||||
const handleLoopBack = (fromPhase: DesignPhase, toPhase: DesignPhase) => {
|
||||
loopBack(fromPhase, toPhase)
|
||||
}
|
||||
|
||||
const handleBacklogItemClick = (item: BacklogItem) => {
|
||||
// Could open item detail modal
|
||||
console.log('Backlog item clicked:', item)
|
||||
}
|
||||
|
||||
const handleStatusChange = (itemId: string, newStatus: BacklogItem['status']) => {
|
||||
updateBacklog(itemId, { status: newStatus })
|
||||
}
|
||||
|
||||
const handlePriorityChange = (itemId: string, direction: 'up' | 'down') => {
|
||||
const item = backlogItems.find(i => i.id === itemId)
|
||||
if (!item) return
|
||||
|
||||
// Adjust user_value to change priority
|
||||
const adjustment = direction === 'up' ? 1 : -1
|
||||
const newValue = Math.max(1, Math.min(10, item.user_value + adjustment))
|
||||
updateBacklog(itemId, { user_value: newValue })
|
||||
}
|
||||
|
||||
const handleRecommendationAction = (rec: AIRecommendation) => {
|
||||
if (rec.action_command) {
|
||||
// Execute the command - this would integrate with the chat
|
||||
console.log('Execute command:', rec.action_command)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDismissRecommendation = (recId: string) => {
|
||||
dismissRec(recId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header with project info */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">{project.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current phase: <span className="capitalize font-medium">{project.current_phase}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => advancePhase(project.current_phase)} className="gap-2">
|
||||
Complete Phase
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Phase Navigator */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<PhaseNavigator
|
||||
currentPhase={project.current_phase}
|
||||
phaseStatuses={phaseStatuses}
|
||||
onPhaseClick={handlePhaseClick}
|
||||
onLoopBack={handleLoopBack}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Health + Recommendations row */}
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<HealthWidget health={health} />
|
||||
</div>
|
||||
<RecommendationsWidget
|
||||
recommendations={recommendations}
|
||||
onAction={handleRecommendationAction}
|
||||
onDismiss={handleDismissRecommendation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Backlog Board - full width */}
|
||||
<BacklogBoard
|
||||
items={backlogItems}
|
||||
onItemClick={handleBacklogItemClick}
|
||||
onStatusChange={handleStatusChange}
|
||||
onPriorityChange={handlePriorityChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export { PhaseNavigator } from './phase-navigator'
|
||||
export { HealthWidget } from './health-widget'
|
||||
export { BacklogBoard } from './backlog-board'
|
||||
export { RecommendationsWidget } from './recommendations-widget'
|
||||
export { DesignThinkingDashboard } from './design-thinking-dashboard'
|
||||
|
||||
15
src/components/ui/skeleton.tsx
Normal file
15
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('animate-pulse rounded-md bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
132
src/hooks/use-project.ts
Normal file
132
src/hooks/use-project.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
getProject,
|
||||
getBacklogItems,
|
||||
getProjectHealth,
|
||||
getRecommendations,
|
||||
getProjectActivities,
|
||||
updateProjectPhase,
|
||||
loopBackToPhase,
|
||||
updateBacklogItem,
|
||||
dismissRecommendation,
|
||||
type Project,
|
||||
type ProjectPhase
|
||||
} from '@/lib/supabase/projects'
|
||||
import type {
|
||||
DesignPhase,
|
||||
PhaseStatus,
|
||||
BacklogItem,
|
||||
ProjectHealth,
|
||||
AIRecommendation
|
||||
} from '@/types/design-thinking'
|
||||
import { DESIGN_PHASES } from '@/types/design-thinking'
|
||||
|
||||
interface UseProjectReturn {
|
||||
project: Project | null
|
||||
phases: ProjectPhase[]
|
||||
phaseStatuses: Record<DesignPhase, PhaseStatus>
|
||||
backlogItems: BacklogItem[]
|
||||
health: ProjectHealth
|
||||
recommendations: AIRecommendation[]
|
||||
activities: Array<{ id: string; activity_type: string; message: string; created_at: string }>
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
refresh: () => Promise<void>
|
||||
advancePhase: (phase: DesignPhase) => Promise<void>
|
||||
loopBack: (fromPhase: DesignPhase, toPhase: DesignPhase) => Promise<void>
|
||||
updateBacklog: (itemId: string, updates: Partial<BacklogItem>) => Promise<void>
|
||||
dismissRec: (recId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function useProject(projectId: string): UseProjectReturn {
|
||||
const [project, setProject] = useState<Project | null>(null)
|
||||
const [phases, setPhases] = useState<ProjectPhase[]>([])
|
||||
const [backlogItems, setBacklogItems] = useState<BacklogItem[]>([])
|
||||
const [health, setHealth] = useState<ProjectHealth>({
|
||||
overall: 100,
|
||||
velocity: 0,
|
||||
blockers: 0,
|
||||
overdue: 0,
|
||||
completion_rate: 0
|
||||
})
|
||||
const [recommendations, setRecommendations] = useState<AIRecommendation[]>([])
|
||||
const [activities, setActivities] = useState<Array<{ id: string; activity_type: string; message: string; created_at: string }>>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
// Derive phase statuses from phases array
|
||||
const phaseStatuses: Record<DesignPhase, PhaseStatus> = DESIGN_PHASES.reduce((acc, phase) => {
|
||||
const latestPhase = phases.find(p => p.phase === phase)
|
||||
acc[phase] = latestPhase?.status || 'not_started'
|
||||
return acc
|
||||
}, {} as Record<DesignPhase, PhaseStatus>)
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const [projectData, backlog, healthData, recs, acts] = await Promise.all([
|
||||
getProject(projectId),
|
||||
getBacklogItems(projectId),
|
||||
getProjectHealth(projectId),
|
||||
getRecommendations(projectId),
|
||||
getProjectActivities(projectId)
|
||||
])
|
||||
|
||||
setProject(projectData.project)
|
||||
setPhases(projectData.phases)
|
||||
setBacklogItems(backlog)
|
||||
setHealth(healthData)
|
||||
setRecommendations(recs)
|
||||
setActivities(acts)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to load project'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [projectId])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAll()
|
||||
}, [fetchAll])
|
||||
|
||||
const advancePhase = useCallback(async (phase: DesignPhase) => {
|
||||
await updateProjectPhase(projectId, phase, 'completed')
|
||||
await fetchAll()
|
||||
}, [projectId, fetchAll])
|
||||
|
||||
const loopBack = useCallback(async (fromPhase: DesignPhase, toPhase: DesignPhase) => {
|
||||
await loopBackToPhase(projectId, fromPhase, toPhase)
|
||||
await fetchAll()
|
||||
}, [projectId, fetchAll])
|
||||
|
||||
const updateBacklog = useCallback(async (itemId: string, updates: Partial<BacklogItem>) => {
|
||||
await updateBacklogItem(itemId, updates)
|
||||
await fetchAll()
|
||||
}, [fetchAll])
|
||||
|
||||
const dismissRec = useCallback(async (recId: string) => {
|
||||
await dismissRecommendation(recId)
|
||||
setRecommendations(prev => prev.filter(r => r.id !== recId))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
project,
|
||||
phases,
|
||||
phaseStatuses,
|
||||
backlogItems,
|
||||
health,
|
||||
recommendations,
|
||||
activities,
|
||||
loading,
|
||||
error,
|
||||
refresh: fetchAll,
|
||||
advancePhase,
|
||||
loopBack,
|
||||
updateBacklog,
|
||||
dismissRec
|
||||
}
|
||||
}
|
||||
319
src/lib/supabase/projects.ts
Normal file
319
src/lib/supabase/projects.ts
Normal 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
|
||||
}>
|
||||
}
|
||||
Reference in New Issue
Block a user