Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 117 additions & 95 deletions app/(dashboard)/plans/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { getCodePlan, getCodePlans, getTeamMembers, getWorkItems, getAssetOptions, getImpactedAssets, getIntegrations } from '@/lib/db/queries'
import { PlanAssetsSection } from './plan-assets-section'
import { PlanDescriptionCard } from './plan-description-card'
import { SpecCard } from '@/components/spec-card'
import { fetchSpecMarkdown } from '@/lib/specs'
import { PlanSyncDialog } from './plan-sync-dialog'
Expand All @@ -15,8 +16,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Calendar, Users, Clock, AlertCircle, CheckCircle2, Circle, Play } from 'lucide-react'
import type { CodePlanStatus, CodePlanType, TaskStatus } from '@/lib/types'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Calendar, Users, AlertCircle } from 'lucide-react'
import type { CodePlanStatus, CodePlanType } from '@/lib/types'
import { cn, formatDate } from '@/lib/utils'
import { PlanStatusButtons, PlanEditSheet, AddTaskDialog } from './plan-actions'

Expand All @@ -41,13 +43,6 @@ const typeStyles: Record<CodePlanType, string> = {
bugfix: 'bg-chart-5/20 text-chart-5',
}

const priorityStyles = {
low: 'bg-muted text-muted-foreground',
medium: 'bg-chart-2/20 text-chart-2',
high: 'bg-warning/20 text-warning',
critical: 'bg-destructive/20 text-destructive',
}

export default async function PlanDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await authAdapter.getUser()
Expand Down Expand Up @@ -87,7 +82,6 @@ export default async function PlanDetailPage({ params }: { params: Promise<{ id:
<Badge variant="secondary" className={cn(typeStyles[plan.type])}>{typeLabels[plan.type]}</Badge>
<Badge variant="secondary" className={cn(statusStyles[plan.status])}>{plan.status}</Badge>
</div>
<p className="text-muted-foreground mb-4">{plan.description}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link
href={`/products/${plan.productSlug}`}
Expand Down Expand Up @@ -194,6 +188,9 @@ export default async function PlanDetailPage({ params }: { params: Promise<{ id:
</div>
)}

{/* Long-form description, clamped with an expander */}
{plan.description && <PlanDescriptionCard description={plan.description} />}

{/* Linked design spec, rendered read-only from its source */}
{plan.specUrl && (
<SpecCard
Expand All @@ -202,95 +199,120 @@ export default async function PlanDetailPage({ params }: { params: Promise<{ id:
/>
)}

{/* Per-asset delivery: branch + PR per targeted asset */}
<PlanAssetsSection planId={plan.id} planAssets={plan.planAssets} assetOptions={productAssetOptions} />
{/* Tasks front and center; delivery/impact/demand context one click away */}
<Tabs defaultValue="tasks">
<div className="flex items-center justify-between flex-wrap gap-2">
<TabsList className="bg-muted">
<TabsTrigger value="tasks">Tasks ({plan.taskCount})</TabsTrigger>
<TabsTrigger value="assets">Assets &amp; PRs ({plan.planAssets.length})</TabsTrigger>
<TabsTrigger value="impact">Impact ({impactedAssets.length})</TabsTrigger>
<TabsTrigger value="work-items">Work Items ({linkedItems.length})</TabsTrigger>
</TabsList>
<AddTaskDialog plan={plan} teamMembers={teamMembers} />
</div>

{/* Impact analysis: assets that depend on this plan's targets */}
{impactedAssets.length > 0 && (
<Card className="bg-card border-border border-l-2 border-l-warning">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Impact Analysis — {impactedAssets.length} dependent asset{impactedAssets.length > 1 ? 's' : ''}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
These assets depend on what this plan changes. Review them for breakage and coordinate their owners.
</p>
<ul className="space-y-2">
{impactedAssets.map((impacted) => (
<li key={`${impacted.id}-${impacted.viaAssetId}`} className="flex items-center gap-2 text-sm flex-wrap">
<span className="font-medium">{impacted.name}</span>
<Badge variant="secondary" className="text-xs capitalize">{impacted.type}</Badge>
<span className="text-muted-foreground text-xs">
{impacted.dependencyType.replace('_', ' ')} → {impacted.viaAssetName}
</span>
{impacted.health !== 'healthy' && (
<Badge
variant="secondary"
className={cn(
'text-xs capitalize',
impacted.health === 'critical' ? 'bg-destructive/20 text-destructive' : 'bg-warning/20 text-warning',
)}
>
{impacted.health}
</Badge>
)}
</li>
))}
</ul>
</CardContent>
</Card>
)}
<TabsContent value="tasks" className="mt-4">
<PlanTasksSection
tasks={plan.tasks}
planId={plan.id}
planTitle={plan.title}
members={teamMembers.map((m) => ({ id: m.userId, name: m.user.name }))}
otherPlans={otherPlans}
/>
</TabsContent>

{/* Linked work items — what this plan delivers or fixes */}
{linkedItems.length > 0 && (
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Linked Work Items ({linkedItems.length})
</CardTitle>
</CardHeader>
<CardContent>
<ul className="divide-y divide-border">
{linkedItems.map((item) => (
<li key={item.id} className="flex items-center justify-between gap-3 py-2.5">
<Link
href={`/work-items?item=${item.id}`}
className="min-w-0 text-sm font-medium truncate hover:text-accent transition-colors"
>
{item.title}
</Link>
<div className="flex items-center gap-2 shrink-0">
<Badge variant="secondary" className="text-xs capitalize">
{item.type.replace('_', ' ')}
</Badge>
<Badge variant="outline" className="text-xs capitalize">
{item.status.replace('_', ' ')}
</Badge>
</div>
</li>
))}
</ul>
</CardContent>
</Card>
)}
<TabsContent value="assets" className="mt-4">
{/* Per-asset delivery: branch + PR per targeted asset */}
<PlanAssetsSection planId={plan.id} planAssets={plan.planAssets} assetOptions={productAssetOptions} />
</TabsContent>

{/* Tasks */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Tasks</h2>
<AddTaskDialog plan={plan} teamMembers={teamMembers} />
</div>
<TabsContent value="impact" className="mt-4">
{/* Impact analysis: assets that depend on this plan's targets */}
{impactedAssets.length === 0 ? (
<Card className="bg-card border-border">
<CardContent className="py-8 text-center text-sm text-muted-foreground">
No dependent assets — nothing downstream depends on what this plan changes.
</CardContent>
</Card>
) : (
<Card className="bg-card border-border border-l-2 border-l-warning">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Impact Analysis — {impactedAssets.length} dependent asset{impactedAssets.length > 1 ? 's' : ''}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
These assets depend on what this plan changes. Review them for breakage and coordinate their owners.
</p>
<ul className="space-y-2">
{impactedAssets.map((impacted) => (
<li key={`${impacted.id}-${impacted.viaAssetId}`} className="flex items-center gap-2 text-sm flex-wrap">
<span className="font-medium">{impacted.name}</span>
<Badge variant="secondary" className="text-xs capitalize">{impacted.type}</Badge>
<span className="text-muted-foreground text-xs">
{impacted.dependencyType.replace('_', ' ')} → {impacted.viaAssetName}
</span>
{impacted.health !== 'healthy' && (
<Badge
variant="secondary"
className={cn(
'text-xs capitalize',
impacted.health === 'critical' ? 'bg-destructive/20 text-destructive' : 'bg-warning/20 text-warning',
)}
>
{impacted.health}
</Badge>
)}
</li>
))}
</ul>
</CardContent>
</Card>
)}
</TabsContent>

<PlanTasksSection
tasks={plan.tasks}
planId={plan.id}
planTitle={plan.title}
members={teamMembers.map((m) => ({ id: m.userId, name: m.user.name }))}
otherPlans={otherPlans}
/>
</div>
<TabsContent value="work-items" className="mt-4">
{/* Linked work items — what this plan delivers or fixes */}
{linkedItems.length === 0 ? (
<Card className="bg-card border-border">
<CardContent className="py-8 text-center text-sm text-muted-foreground">
No linked work items — link features, bugs, or tech debt this plan addresses from the Work Items view.
</CardContent>
</Card>
) : (
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Linked Work Items ({linkedItems.length})
</CardTitle>
</CardHeader>
<CardContent>
<ul className="divide-y divide-border">
{linkedItems.map((item) => (
<li key={item.id} className="flex items-center justify-between gap-3 py-2.5">
<Link
href={`/work-items?item=${item.id}`}
className="min-w-0 text-sm font-medium truncate hover:text-accent transition-colors"
>
{item.title}
</Link>
<div className="flex items-center gap-2 shrink-0">
<Badge variant="secondary" className="text-xs capitalize">
{item.type.replace('_', ' ')}
</Badge>
<Badge variant="outline" className="text-xs capitalize">
{item.status.replace('_', ' ')}
</Badge>
</div>
</li>
))}
</ul>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
)
}
61 changes: 61 additions & 0 deletions app/(dashboard)/plans/[id]/plan-description-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { AlignLeft, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'

/** Long-form plan description in its own card, clamped to ~5 lines with an expander. */
export function PlanDescriptionCard({ description }: { description: string }) {
const [expanded, setExpanded] = useState(false)
const [clamped, setClamped] = useState(false)
const textRef = useRef<HTMLParagraphElement>(null)

useEffect(() => {
const el = textRef.current
if (el) setClamped(el.scrollHeight > el.clientHeight + 1)
}, [description])

return (
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<AlignLeft className="h-4 w-4" />
Description
</CardTitle>
</CardHeader>
<CardContent>
<p
ref={textRef}
className={cn(
'text-sm text-muted-foreground whitespace-pre-line',
!expanded && 'line-clamp-5',
)}
>
{description}
</p>
{(clamped || expanded) && (
<Button
variant="ghost"
size="sm"
className="mt-2 h-7 px-2 text-xs text-muted-foreground"
onClick={() => setExpanded((v) => !v)}
>
{expanded ? (
<>
Show less
<ChevronUp className="ml-1 h-3.5 w-3.5" />
</>
) : (
<>
Show more
<ChevronDown className="ml-1 h-3.5 w-3.5" />
</>
)}
</Button>
)}
</CardContent>
</Card>
)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeplans",
"version": "0.3.20",
"version": "0.3.21",
"description": "Manage and track coordinated changes across your software architecture.",
"author": "Sai Prakash <sylonzero@gmail.com>",
"homepage": "https://codeplans.ai",
Expand Down
Loading