Generated by Spark: Roadmap should have projects, i guess projects can have metrics and objectives

This commit is contained in:
2026-01-18 20:58:48 +00:00
committed by GitHub
parent 0c536d1cb2
commit bd1a0aa8b0
2 changed files with 388 additions and 149 deletions

View File

@@ -1,8 +1,15 @@
import { useKV } from '@github/spark/hooks'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Target,
ChartBar,
@@ -10,48 +17,18 @@ import {
TrendUp,
Gauge,
ListChecks,
ArrowsOut,
CheckCircle,
WarningCircle,
XCircle,
Circle
Circle,
Plus,
FolderOpen,
Trash,
PencilSimple
} from '@phosphor-icons/react'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Progress } from '@/components/ui/progress'
interface Objective {
id: string
category: 'breakthrough' | 'annual' | 'improvement'
description: string
owner: string
targetDate: string
status: 'on-track' | 'at-risk' | 'completed' | 'not-started'
metrics: Metric[]
}
interface Metric {
id: string
name: string
baseline: number
current: number
target: number
unit: string
frequency: 'monthly' | 'quarterly' | 'annual'
lastUpdated: string
trend: 'improving' | 'stable' | 'declining'
}
interface BowlingChartData {
objective: string
months: MonthStatus[]
}
interface MonthStatus {
month: string
status: 'green' | 'yellow' | 'red' | 'not-started'
actual: number
target: number
}
import { toast } from 'sonner'
import type { RoadmapProject, RoadmapObjective, RoadmapMetric, BowlingChartData, StatusType, PriorityType } from '@/types'
interface XMatrixItem {
id: string
@@ -60,93 +37,264 @@ interface XMatrixItem {
relationships: string[]
}
const mockObjectives: Objective[] = [
{
id: 'obj-1',
category: 'breakthrough',
description: 'Achieve 25% revenue growth through new market expansion',
owner: 'Chief Growth Officer',
targetDate: '2025-12-31',
status: 'on-track',
metrics: [
{
id: 'm1',
name: 'Revenue Growth',
baseline: 100,
current: 115,
target: 125,
unit: '%',
frequency: 'quarterly',
lastUpdated: '2025-01-15',
trend: 'improving'
},
{
id: 'm2',
name: 'New Market Share',
baseline: 0,
current: 12,
target: 20,
unit: '%',
frequency: 'monthly',
lastUpdated: '2025-01-15',
trend: 'improving'
}
]
},
{
id: 'obj-2',
category: 'annual',
description: 'Reduce operational costs by 15% while maintaining quality',
owner: 'Chief Operating Officer',
targetDate: '2025-12-31',
status: 'at-risk',
metrics: [
{
id: 'm3',
name: 'Cost Reduction',
baseline: 100,
current: 92,
target: 85,
unit: '%',
frequency: 'monthly',
lastUpdated: '2025-01-15',
trend: 'stable'
},
{
id: 'm4',
name: 'Quality Score',
baseline: 85,
current: 87,
target: 90,
unit: 'pts',
frequency: 'monthly',
lastUpdated: '2025-01-15',
trend: 'improving'
}
]
},
{
id: 'obj-3',
category: 'improvement',
description: 'Improve customer satisfaction score to 95%',
owner: 'Chief Customer Officer',
targetDate: '2025-06-30',
status: 'on-track',
metrics: [
{
id: 'm5',
name: 'NPS Score',
baseline: 72,
current: 84,
target: 95,
unit: 'pts',
frequency: 'monthly',
lastUpdated: '2025-01-15',
trend: 'improving'
}
]
function ProjectsView({ projects, setProjects }: { projects: RoadmapProject[], setProjects: (updater: (prev: RoadmapProject[]) => RoadmapProject[]) => void }) {
const [isAddingProject, setIsAddingProject] = useState(false)
const [editingProject, setEditingProject] = useState<RoadmapProject | null>(null)
const [newProject, setNewProject] = useState<Partial<RoadmapProject>>({
name: '',
description: '',
owner: '',
status: 'not-started',
priority: 'medium',
startDate: '',
endDate: '',
progress: 0,
objectives: [],
metrics: []
})
const handleAddProject = () => {
if (!newProject.name || !newProject.owner || !newProject.startDate || !newProject.endDate) {
toast.error('Please fill in all required fields')
return
}
const project: RoadmapProject = {
id: `proj-${Date.now()}`,
name: newProject.name,
description: newProject.description || '',
owner: newProject.owner,
status: newProject.status as StatusType,
priority: newProject.priority as PriorityType,
startDate: newProject.startDate,
endDate: newProject.endDate,
progress: 0,
objectives: [],
metrics: []
}
setProjects((prev) => [...prev, project])
setIsAddingProject(false)
setNewProject({
name: '',
description: '',
owner: '',
status: 'not-started',
priority: 'medium',
startDate: '',
endDate: '',
progress: 0,
objectives: [],
metrics: []
})
toast.success('Project created successfully')
}
]
const handleDeleteProject = (projectId: string) => {
setProjects((prev) => prev.filter(p => p.id !== projectId))
toast.success('Project deleted')
}
const statusColors = {
'not-started': 'bg-muted text-muted-foreground',
'on-track': 'bg-success/10 text-success border-success/30',
'at-risk': 'bg-warning/10 text-warning border-warning/30',
'blocked': 'bg-destructive/10 text-destructive border-destructive/30',
'completed': 'bg-primary/10 text-primary border-primary/30'
}
const priorityColors = {
'critical': 'bg-destructive/10 text-destructive border-destructive/30',
'high': 'bg-warning/10 text-warning border-warning/30',
'medium': 'bg-primary/10 text-primary border-primary/30',
'low': 'bg-muted text-muted-foreground border-muted'
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Strategic Projects</h3>
<p className="text-sm text-muted-foreground">Manage projects with objectives and metrics</p>
</div>
<Dialog open={isAddingProject} onOpenChange={setIsAddingProject}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus size={20} weight="bold" />
Add Project
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription>Add a strategic project to track on the roadmap</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Project Name *</Label>
<Input
id="name"
value={newProject.name}
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
placeholder="Enter project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={newProject.description}
onChange={(e) => setNewProject({ ...newProject, description: e.target.value })}
placeholder="Enter project description"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="owner">Owner *</Label>
<Input
id="owner"
value={newProject.owner}
onChange={(e) => setNewProject({ ...newProject, owner: e.target.value })}
placeholder="Project owner"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="priority">Priority</Label>
<Select
value={newProject.priority}
onValueChange={(value) => setNewProject({ ...newProject, priority: value as PriorityType })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="critical">Critical</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="startDate">Start Date *</Label>
<Input
id="startDate"
type="date"
value={newProject.startDate}
onChange={(e) => setNewProject({ ...newProject, startDate: e.target.value })}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="endDate">End Date *</Label>
<Input
id="endDate"
type="date"
value={newProject.endDate}
onChange={(e) => setNewProject({ ...newProject, endDate: e.target.value })}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="status">Status</Label>
<Select
value={newProject.status}
onValueChange={(value) => setNewProject({ ...newProject, status: value as StatusType })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="not-started">Not Started</SelectItem>
<SelectItem value="on-track">On Track</SelectItem>
<SelectItem value="at-risk">At Risk</SelectItem>
<SelectItem value="blocked">Blocked</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingProject(false)}>Cancel</Button>
<Button onClick={handleAddProject}>Create Project</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<FolderOpen size={48} className="text-muted-foreground mb-4" />
<p className="text-muted-foreground text-center">No projects yet. Create your first strategic project to get started.</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{projects.map((project) => (
<Card key={project.id}>
<CardHeader>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Badge variant="outline" className={`${statusColors[project.status]} capitalize font-semibold`}>
{project.status.replace('-', ' ')}
</Badge>
<Badge variant="outline" className={`${priorityColors[project.priority]} capitalize font-semibold`}>
{project.priority}
</Badge>
</div>
<CardTitle className="text-lg">{project.name}</CardTitle>
{project.description && (
<CardDescription className="mt-2">{project.description}</CardDescription>
)}
<CardDescription className="mt-2">
<div className="flex items-center gap-4 text-sm">
<span><strong>Owner:</strong> {project.owner}</span>
<span><strong>Timeline:</strong> {new Date(project.startDate).toLocaleDateString()} - {new Date(project.endDate).toLocaleDateString()}</span>
</div>
</CardDescription>
</div>
<div className="flex gap-2">
<Button size="sm" variant="ghost" onClick={() => handleDeleteProject(project.id)}>
<Trash size={16} />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold">Overall Progress</span>
<span className="text-sm font-mono">{project.progress}%</span>
</div>
<Progress value={project.progress} className="h-2" />
</div>
<Separator />
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Objectives:</span>
<span className="ml-2 font-semibold">{project.objectives.length}</span>
</div>
<div>
<span className="text-muted-foreground">Metrics:</span>
<span className="ml-2 font-semibold">{project.metrics.length}</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
)
}
const mockBowlingChart: BowlingChartData[] = [
{
@@ -202,7 +350,9 @@ const mockBowlingChart: BowlingChartData[] = [
}
]
function ObjectivesView() {
function ObjectivesView({ projects }: { projects: RoadmapProject[] }) {
const allObjectives = projects.flatMap(p => p.objectives)
const categoryColors = {
breakthrough: 'bg-accent/10 text-accent border-accent/30',
annual: 'bg-primary/10 text-primary border-primary/30',
@@ -212,13 +362,25 @@ function ObjectivesView() {
const statusIcons = {
'on-track': <CheckCircle size={20} weight="fill" className="text-success" />,
'at-risk': <WarningCircle size={20} weight="fill" className="text-warning" />,
'blocked': <XCircle size={20} weight="fill" className="text-destructive" />,
'completed': <CheckCircle size={20} weight="fill" className="text-success" />,
'not-started': <Circle size={20} className="text-muted-foreground" />
}
if (allObjectives.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Target size={48} className="text-muted-foreground mb-4" />
<p className="text-muted-foreground text-center">No objectives yet. Add projects with objectives to track them here.</p>
</CardContent>
</Card>
)
}
return (
<div className="space-y-4">
{mockObjectives.map((objective) => (
{allObjectives.map((objective) => (
<Card key={objective.id}>
<CardHeader>
<div className="flex items-start justify-between gap-4">
@@ -275,9 +437,9 @@ function ObjectivesView() {
)
}
function MetricsView() {
const allMetrics = mockObjectives.flatMap(obj =>
obj.metrics.map(m => ({ ...m, objective: obj.description }))
function MetricsView({ projects }: { projects: RoadmapProject[] }) {
const allMetrics = projects.flatMap(project =>
project.metrics.map(m => ({ ...m, projectName: project.name }))
)
const trendIcons = {
@@ -286,6 +448,17 @@ function MetricsView() {
declining: <TrendUp size={16} className="text-destructive rotate-180" weight="bold" />
}
if (allMetrics.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<ChartBar size={48} className="text-muted-foreground mb-4" />
<p className="text-muted-foreground text-center">No metrics yet. Add projects with metrics to track them here.</p>
</CardContent>
</Card>
)
}
return (
<div className="space-y-6">
<Card>
@@ -308,7 +481,7 @@ function MetricsView() {
<h4 className="font-semibold">{metric.name}</h4>
{trendIcons[metric.trend]}
</div>
<p className="text-xs text-muted-foreground">{metric.objective}</p>
<p className="text-xs text-muted-foreground">{metric.projectName}</p>
</div>
<Badge variant="outline" className="font-mono shrink-0">
{metric.frequency}
@@ -520,16 +693,18 @@ function XMatrixView() {
)
}
function DashboardView() {
function DashboardView({ projects }: { projects: RoadmapProject[] }) {
const allObjectives = projects.flatMap(p => p.objectives)
const overallHealth = {
onTrack: 2,
atRisk: 1,
offTrack: 0,
notStarted: 0
onTrack: allObjectives.filter(o => o.status === 'on-track').length,
atRisk: allObjectives.filter(o => o.status === 'at-risk').length,
offTrack: allObjectives.filter(o => o.status === 'blocked').length,
notStarted: allObjectives.filter(o => o.status === 'not-started').length
}
const total = Object.values(overallHealth).reduce((a, b) => a + b, 0)
const healthPercent = Math.round((overallHealth.onTrack / total) * 100)
const healthPercent = total > 0 ? Math.round((overallHealth.onTrack / total) * 100) : 0
return (
<div className="space-y-6">
@@ -590,7 +765,10 @@ function DashboardView() {
<CardDescription>Progress across all strategic goals</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{mockObjectives.map((obj) => {
{allObjectives.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">No objectives to display</p>
) : (
allObjectives.map((obj) => {
const avgProgress = obj.metrics.reduce((sum, m) => {
const progress = ((m.current - m.baseline) / (m.target - m.baseline)) * 100
return sum + progress
@@ -607,7 +785,7 @@ function DashboardView() {
<Progress value={avgProgress} className="h-2" />
</div>
)
})}
}))}
</CardContent>
</Card>
@@ -618,19 +796,21 @@ function DashboardView() {
</CardHeader>
<CardContent className="space-y-4">
{['breakthrough', 'annual', 'improvement'].map((category) => {
const objectives = mockObjectives.filter(obj => obj.category === category)
const allMetrics = objectives.flatMap(obj => obj.metrics)
const avgProgress = allMetrics.reduce((sum, m) => {
const progress = ((m.current - m.baseline) / (m.target - m.baseline)) * 100
return sum + progress
}, 0) / allMetrics.length
const objectives = allObjectives.filter(obj => obj.category === category)
const categoryMetrics = objectives.flatMap(obj => obj.metrics)
const avgProgress = categoryMetrics.length > 0
? categoryMetrics.reduce((sum, m) => {
const progress = ((m.current - m.baseline) / (m.target - m.baseline)) * 100
return sum + progress
}, 0) / categoryMetrics.length
: 0
return (
<div key={category} className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium capitalize">{category} Goals</span>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{allMetrics.length} metrics</span>
<span className="text-xs text-muted-foreground">{categoryMetrics.length} metrics</span>
<Badge variant="outline" className="font-mono">
{Math.round(avgProgress)}%
</Badge>
@@ -685,6 +865,8 @@ function DashboardView() {
}
export default function Roadmap() {
const [projects, setProjects] = useKV<RoadmapProject[]>('roadmap-projects', [])
return (
<div className="space-y-6">
<div>
@@ -692,8 +874,12 @@ export default function Roadmap() {
<p className="text-muted-foreground mt-1">Hoshin Kanri planning and execution tracking</p>
</div>
<Tabs defaultValue="dashboard" className="w-full">
<TabsList className="grid w-full grid-cols-5 h-14 bg-muted/50">
<Tabs defaultValue="projects" className="w-full">
<TabsList className="grid w-full grid-cols-6 h-14 bg-muted/50">
<TabsTrigger value="projects" className="gap-2 text-base font-semibold data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<FolderOpen size={20} weight="bold" />
Projects
</TabsTrigger>
<TabsTrigger value="dashboard" className="gap-2 text-base font-semibold data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<Gauge size={20} weight="bold" />
Dashboard
@@ -716,16 +902,20 @@ export default function Roadmap() {
</TabsTrigger>
</TabsList>
<TabsContent value="projects" className="mt-6">
<ProjectsView projects={projects || []} setProjects={setProjects} />
</TabsContent>
<TabsContent value="dashboard" className="mt-6">
<DashboardView />
<DashboardView projects={projects || []} />
</TabsContent>
<TabsContent value="objectives" className="mt-6">
<ObjectivesView />
<ObjectivesView projects={projects || []} />
</TabsContent>
<TabsContent value="metrics" className="mt-6">
<MetricsView />
<MetricsView projects={projects || []} />
</TabsContent>
<TabsContent value="bowling" className="mt-6">

View File

@@ -46,3 +46,52 @@ export interface Portfolio {
capacity: number
utilized: number
}
export interface RoadmapProject {
id: string
name: string
description: string
owner: string
status: StatusType
priority: PriorityType
startDate: string
endDate: string
progress: number
objectives: RoadmapObjective[]
metrics: RoadmapMetric[]
}
export interface RoadmapObjective {
id: string
projectId: string
category: 'breakthrough' | 'annual' | 'improvement'
description: string
owner: string
targetDate: string
status: StatusType
metrics: RoadmapMetric[]
}
export interface RoadmapMetric {
id: string
name: string
baseline: number
current: number
target: number
unit: string
frequency: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'annual'
lastUpdated: string
trend: 'improving' | 'stable' | 'declining'
}
export interface BowlingChartData {
objective: string
months: MonthStatus[]
}
export interface MonthStatus {
month: string
status: 'green' | 'yellow' | 'red' | 'not-started'
actual: number
target: number
}