mirror of
https://github.com/johndoe6345789/strategy-execution-p.git
synced 2026-04-24 13:14:56 +00:00
Generated by Spark: Ok implement new features from ROADMAP, dont edit roadmap file
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"templateVersion": 0,
|
||||
"dbType": null
|
||||
"dbType": "kv"
|
||||
}
|
||||
22
src/App.tsx
22
src/App.tsx
@@ -15,12 +15,17 @@ import {
|
||||
GridFour,
|
||||
Circle,
|
||||
House,
|
||||
CaretDown
|
||||
CaretDown,
|
||||
CurrencyDollar,
|
||||
CheckCircle,
|
||||
ChartLineUp,
|
||||
GitBranch
|
||||
} from '@phosphor-icons/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import StrategyCards from './components/StrategyCards'
|
||||
import Workbench from './components/Workbench'
|
||||
import Portfolios from './components/Portfolios'
|
||||
import PortfolioAnalysis from './components/PortfolioAnalysis'
|
||||
import Dashboard from './components/Dashboard'
|
||||
import Roadmap from './components/Roadmap'
|
||||
import ProductRoadmap from './components/ProductRoadmap'
|
||||
@@ -30,6 +35,9 @@ import StrategyComparison from './components/StrategyComparison'
|
||||
import StrategyTraceability from './components/StrategyTraceability'
|
||||
import XMatrix from './components/XMatrix'
|
||||
import BowlingChart from './components/BowlingChart'
|
||||
import OKRManagement from './components/OKRManagement'
|
||||
import FinancialTracking from './components/FinancialTracking'
|
||||
import ExecutiveDashboard from './components/ExecutiveDashboard'
|
||||
import type { StrategyCard, Initiative } from './types'
|
||||
|
||||
type NavigationItem = {
|
||||
@@ -61,7 +69,9 @@ const navigationSections: NavigationSection[] = [
|
||||
items: [
|
||||
{ id: 'workbench', label: 'Workbench', icon: ChartBar, component: Workbench },
|
||||
{ id: 'tracker', label: 'Initiative Tracker', icon: TrendUp, component: InitiativeTracker },
|
||||
{ id: 'okr', label: 'OKR Management', icon: CheckCircle, component: OKRManagement },
|
||||
{ id: 'portfolios', label: 'Portfolios', icon: FolderOpen, component: Portfolios },
|
||||
{ id: 'portfolio-analysis', label: 'Portfolio Analysis', icon: GitBranch, component: PortfolioAnalysis },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -84,8 +94,10 @@ const navigationSections: NavigationSection[] = [
|
||||
id: 'reporting',
|
||||
label: 'Reporting',
|
||||
items: [
|
||||
{ id: 'dashboard', label: 'Executive Dashboard', icon: Target, component: Dashboard },
|
||||
{ id: 'executive-dashboard', label: 'Executive Dashboard', icon: ChartLineUp, component: ExecutiveDashboard },
|
||||
{ id: 'dashboard', label: 'Performance Dashboard', icon: Target, component: Dashboard },
|
||||
{ id: 'kpi', label: 'KPI Scorecard', icon: ChartLine, component: KPIDashboard },
|
||||
{ id: 'financial', label: 'Financial Tracking', icon: CurrencyDollar, component: FinancialTracking },
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -281,13 +293,17 @@ function getModuleDescription(moduleId: string): string {
|
||||
'traceability': 'Map relationships from goals to initiatives',
|
||||
'workbench': 'Execute and track strategic initiatives',
|
||||
'tracker': 'Monitor initiative progress with real-time status',
|
||||
'okr': 'Define and track Objectives and Key Results',
|
||||
'portfolios': 'Organize initiatives into strategic portfolios',
|
||||
'portfolio-analysis': 'Strategic alignment, capacity & dependency analysis',
|
||||
'x-matrix': 'Align objectives using Hoshin Kanri methodology',
|
||||
'bowling': 'Track monthly progress with visual indicators',
|
||||
'roadmap': 'Visualize strategic timeline and milestones',
|
||||
'product-roadmap': 'Plan and track product development initiatives',
|
||||
'dashboard': 'Executive-level view of strategic performance',
|
||||
'executive-dashboard': 'Executive-level strategic performance overview',
|
||||
'dashboard': 'Real-time performance metrics and insights',
|
||||
'kpi': 'Monitor key performance indicators and metrics',
|
||||
'financial': 'Track financial outcomes and value realization',
|
||||
}
|
||||
return descriptions[moduleId] || 'Manage your strategic initiatives'
|
||||
}
|
||||
|
||||
388
src/components/ExecutiveDashboard.tsx
Normal file
388
src/components/ExecutiveDashboard.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useMemo } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Target,
|
||||
TrendUp,
|
||||
TrendDown,
|
||||
CheckCircle,
|
||||
Warning,
|
||||
CurrencyDollar,
|
||||
ChartBar,
|
||||
Users,
|
||||
Lightning,
|
||||
ArrowUp,
|
||||
ArrowDown
|
||||
} from '@phosphor-icons/react'
|
||||
import type { Initiative, StrategyCard } from '@/types'
|
||||
|
||||
interface FinancialOutcome {
|
||||
plannedAmount: number
|
||||
actualAmount: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export default function ExecutiveDashboard() {
|
||||
const [strategyCards] = useKV<StrategyCard[]>('strategy-cards', [])
|
||||
const [initiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
const [financialOutcomes] = useKV<FinancialOutcome[]>('financial-outcomes', [])
|
||||
const [objectives] = useKV<any[]>('okr-objectives', [])
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const totalInitiatives = initiatives?.length || 0
|
||||
const completedInitiatives = initiatives?.filter(i => i.status === 'completed').length || 0
|
||||
const atRiskInitiatives = initiatives?.filter(i => i.status === 'at-risk' || i.status === 'blocked').length || 0
|
||||
const onTrackInitiatives = initiatives?.filter(i => i.status === 'on-track').length || 0
|
||||
|
||||
const totalPlanned = financialOutcomes?.reduce((sum, o) => sum + o.plannedAmount, 0) || 0
|
||||
const totalRealized = financialOutcomes?.filter(o => o.status === 'realized' || o.status === 'validated')
|
||||
.reduce((sum, o) => sum + o.actualAmount, 0) || 0
|
||||
|
||||
const avgProgress = initiatives?.length ?
|
||||
initiatives.reduce((sum, i) => sum + i.progress, 0) / initiatives.length : 0
|
||||
|
||||
const totalObjectives = objectives?.length || 0
|
||||
const achievedObjectives = objectives?.filter(o => o.status === 'achieved').length || 0
|
||||
|
||||
return {
|
||||
strategies: strategyCards?.length || 0,
|
||||
initiatives: totalInitiatives,
|
||||
completedInitiatives,
|
||||
atRiskInitiatives,
|
||||
onTrackInitiatives,
|
||||
objectives: totalObjectives,
|
||||
achievedObjectives,
|
||||
avgProgress: Math.round(avgProgress),
|
||||
completionRate: totalInitiatives > 0 ? Math.round((completedInitiatives / totalInitiatives) * 100) : 0,
|
||||
financialPlanned: totalPlanned,
|
||||
financialRealized: totalRealized,
|
||||
financialRealizationRate: totalPlanned > 0 ? Math.round((totalRealized / totalPlanned) * 100) : 0
|
||||
}
|
||||
}, [strategyCards, initiatives, financialOutcomes, objectives])
|
||||
|
||||
const portfolioBreakdown = useMemo(() => {
|
||||
const portfolios = initiatives?.reduce((acc, init) => {
|
||||
const key = init.portfolio
|
||||
if (!acc[key]) {
|
||||
acc[key] = {
|
||||
total: 0,
|
||||
completed: 0,
|
||||
atRisk: 0,
|
||||
onTrack: 0,
|
||||
budget: 0,
|
||||
progress: []
|
||||
}
|
||||
}
|
||||
acc[key].total++
|
||||
if (init.status === 'completed') acc[key].completed++
|
||||
if (init.status === 'at-risk' || init.status === 'blocked') acc[key].atRisk++
|
||||
if (init.status === 'on-track') acc[key].onTrack++
|
||||
acc[key].budget += init.budget || 0
|
||||
acc[key].progress.push(init.progress)
|
||||
return acc
|
||||
}, {} as Record<string, any>) || {}
|
||||
|
||||
return Object.entries(portfolios).map(([name, data]) => ({
|
||||
name: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
||||
...data,
|
||||
avgProgress: data.progress.length > 0 ? Math.round(data.progress.reduce((a: number, b: number) => a + b, 0) / data.progress.length) : 0
|
||||
}))
|
||||
}, [initiatives])
|
||||
|
||||
const recentActivity = useMemo(() => {
|
||||
const items: any[] = []
|
||||
|
||||
initiatives?.slice(-5).reverse().forEach(init => {
|
||||
items.push({
|
||||
type: 'initiative',
|
||||
title: init.title,
|
||||
status: init.status,
|
||||
date: init.startDate,
|
||||
priority: init.priority
|
||||
})
|
||||
})
|
||||
|
||||
return items.slice(0, 8)
|
||||
}, [initiatives])
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const portfolioColors: Record<string, string> = {
|
||||
'operational-excellence': 'bg-blue-500',
|
||||
'ma': 'bg-purple-500',
|
||||
'financial-transformation': 'bg-green-500',
|
||||
'esg': 'bg-teal-500',
|
||||
'innovation': 'bg-orange-500'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Executive Dashboard</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
High-level view of strategic performance and progress
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Strategy Cards</CardTitle>
|
||||
<Target size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.strategies}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active Initiatives</CardTitle>
|
||||
<Lightning size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.initiatives}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{stats.completedInitiatives} completed
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">OKRs</CardTitle>
|
||||
<CheckCircle size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.objectives}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{stats.achievedObjectives} achieved
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Avg Progress</CardTitle>
|
||||
<ChartBar size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.avgProgress}%</div>
|
||||
<Progress value={stats.avgProgress} className="h-1.5 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">On Track</CardTitle>
|
||||
<TrendUp size={20} className="text-success" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-success">{stats.onTrackInitiatives}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">initiatives</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">At Risk</CardTitle>
|
||||
<Warning size={20} className="text-at-risk" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-at-risk">{stats.atRiskInitiatives}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">need attention</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Value Realization</CardTitle>
|
||||
<CardDescription>Planned vs actual financial outcomes</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">Total Planned Value</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(stats.financialPlanned)}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">Realized Value</p>
|
||||
<p className="text-2xl font-bold text-success">{formatCurrency(stats.financialRealized)}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">Realization Rate</p>
|
||||
<p className="text-2xl font-bold text-accent">{stats.financialRealizationRate}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={stats.financialRealizationRate} className="h-3" />
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CurrencyDollar size={16} />
|
||||
<span>
|
||||
{formatCurrency(stats.financialPlanned - stats.financialRealized)} remaining to realize
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Initiative Health</CardTitle>
|
||||
<CardDescription>Status distribution</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-success"></div>
|
||||
<span className="text-sm">On Track</span>
|
||||
</div>
|
||||
<span className="font-semibold">{stats.onTrackInitiatives}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-at-risk"></div>
|
||||
<span className="text-sm">At Risk</span>
|
||||
</div>
|
||||
<span className="font-semibold">{stats.atRiskInitiatives}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-accent"></div>
|
||||
<span className="text-sm">Completed</span>
|
||||
</div>
|
||||
<span className="font-semibold">{stats.completedInitiatives}</span>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
<div className="flex items-center justify-between text-sm font-semibold">
|
||||
<span>Completion Rate</span>
|
||||
<span className="text-accent">{stats.completionRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Portfolio Performance</CardTitle>
|
||||
<CardDescription>Progress and health by strategic portfolio</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{portfolioBreakdown.length === 0 ? (
|
||||
<p className="text-center text-muted-foreground py-8">No portfolio data available</p>
|
||||
) : (
|
||||
portfolioBreakdown.map((portfolio) => (
|
||||
<div key={portfolio.name} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-3 h-3 rounded-full ${portfolioColors[portfolio.name.toLowerCase().replace(/ /g, '-')] || 'bg-gray-500'}`}></div>
|
||||
<h4 className="font-semibold">{portfolio.name}</h4>
|
||||
<Badge variant="secondary">{portfolio.total} initiatives</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="text-right">
|
||||
<p className="text-muted-foreground">Budget</p>
|
||||
<p className="font-semibold">{formatCurrency(portfolio.budget)}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-muted-foreground">Progress</p>
|
||||
<p className="font-semibold">{portfolio.avgProgress}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 mb-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-success" weight="fill" />
|
||||
<span>{portfolio.onTrack} on track</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Warning size={16} className="text-at-risk" weight="fill" />
|
||||
<span>{portfolio.atRisk} at risk</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Target size={16} className="text-accent" weight="fill" />
|
||||
<span>{portfolio.completed} completed</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={portfolio.avgProgress} className="h-2" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>Latest updates across initiatives</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentActivity.length === 0 ? (
|
||||
<p className="text-center text-muted-foreground py-8">No recent activity</p>
|
||||
) : (
|
||||
recentActivity.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between py-2 border-b last:border-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
item.status === 'completed' ? 'bg-success' :
|
||||
item.status === 'on-track' ? 'bg-accent' :
|
||||
item.status === 'at-risk' || item.status === 'blocked' ? 'bg-at-risk' :
|
||||
'bg-muted'
|
||||
}`}></div>
|
||||
<div>
|
||||
<p className="font-medium text-sm">{item.title}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.type === 'initiative' ? 'Initiative' : 'Activity'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant={
|
||||
item.priority === 'critical' ? 'destructive' :
|
||||
item.priority === 'high' ? 'default' : 'secondary'
|
||||
}>
|
||||
{item.priority}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(item.date).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
492
src/components/FinancialTracking.tsx
Normal file
492
src/components/FinancialTracking.tsx
Normal file
@@ -0,0 +1,492 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Plus, CurrencyDollar, TrendUp, TrendDown, ChartLine, Target, Calendar, CheckCircle } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { Initiative } from '@/types'
|
||||
|
||||
interface FinancialOutcome {
|
||||
id: string
|
||||
initiativeId: string
|
||||
initiativeTitle: string
|
||||
category: 'cost-savings' | 'revenue-increase' | 'cost-avoidance' | 'efficiency-gain' | 'other'
|
||||
description: string
|
||||
plannedAmount: number
|
||||
actualAmount: number
|
||||
currency: string
|
||||
realizationDate: string
|
||||
status: 'projected' | 'realized' | 'validated'
|
||||
validatedBy?: string
|
||||
notes?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const categoryConfig = {
|
||||
'cost-savings': { label: 'Cost Savings', color: 'bg-green-500', icon: TrendDown },
|
||||
'revenue-increase': { label: 'Revenue Increase', color: 'bg-blue-500', icon: TrendUp },
|
||||
'cost-avoidance': { label: 'Cost Avoidance', color: 'bg-purple-500', icon: Target },
|
||||
'efficiency-gain': { label: 'Efficiency Gain', color: 'bg-orange-500', icon: ChartLine },
|
||||
'other': { label: 'Other', color: 'bg-gray-500', icon: CurrencyDollar }
|
||||
}
|
||||
|
||||
export default function FinancialTracking() {
|
||||
const [outcomes, setOutcomes] = useKV<FinancialOutcome[]>('financial-outcomes', [])
|
||||
const [initiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all')
|
||||
|
||||
const [newOutcome, setNewOutcome] = useState({
|
||||
initiativeId: '',
|
||||
category: 'cost-savings' as const,
|
||||
description: '',
|
||||
plannedAmount: 0,
|
||||
currency: 'USD',
|
||||
realizationDate: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
const addOutcome = () => {
|
||||
if (!newOutcome.initiativeId || !newOutcome.description || !newOutcome.plannedAmount) {
|
||||
toast.error('Please fill in required fields')
|
||||
return
|
||||
}
|
||||
|
||||
const initiative = initiatives?.find(i => i.id === newOutcome.initiativeId)
|
||||
if (!initiative) {
|
||||
toast.error('Initiative not found')
|
||||
return
|
||||
}
|
||||
|
||||
const outcome: FinancialOutcome = {
|
||||
id: `outcome-${Date.now()}`,
|
||||
initiativeId: newOutcome.initiativeId,
|
||||
initiativeTitle: initiative.title,
|
||||
category: newOutcome.category,
|
||||
description: newOutcome.description,
|
||||
plannedAmount: newOutcome.plannedAmount,
|
||||
actualAmount: 0,
|
||||
currency: newOutcome.currency,
|
||||
realizationDate: newOutcome.realizationDate,
|
||||
status: 'projected',
|
||||
notes: newOutcome.notes,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
setOutcomes((current) => [...(current || []), outcome])
|
||||
setIsAddDialogOpen(false)
|
||||
setNewOutcome({
|
||||
initiativeId: '',
|
||||
category: 'cost-savings',
|
||||
description: '',
|
||||
plannedAmount: 0,
|
||||
currency: 'USD',
|
||||
realizationDate: '',
|
||||
notes: ''
|
||||
})
|
||||
toast.success('Financial outcome tracked successfully')
|
||||
}
|
||||
|
||||
const updateOutcomeStatus = (id: string, status: FinancialOutcome['status'], actualAmount?: number) => {
|
||||
setOutcomes((current) =>
|
||||
(current || []).map(o =>
|
||||
o.id === id
|
||||
? {
|
||||
...o,
|
||||
status,
|
||||
actualAmount: actualAmount !== undefined ? actualAmount : o.actualAmount,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
: o
|
||||
)
|
||||
)
|
||||
toast.success('Status updated')
|
||||
}
|
||||
|
||||
const filteredOutcomes = (outcomes || []).filter(o => {
|
||||
if (selectedCategory !== 'all' && o.category !== selectedCategory) return false
|
||||
if (selectedStatus !== 'all' && o.status !== selectedStatus) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const all = outcomes || []
|
||||
return {
|
||||
totalPlanned: all.reduce((sum, o) => sum + o.plannedAmount, 0),
|
||||
totalRealized: all.filter(o => o.status === 'realized' || o.status === 'validated').reduce((sum, o) => sum + o.actualAmount, 0),
|
||||
totalProjected: all.filter(o => o.status === 'projected').reduce((sum, o) => sum + o.plannedAmount, 0),
|
||||
realizationRate: all.length > 0 ? (all.filter(o => o.status === 'realized' || o.status === 'validated').length / all.length) * 100 : 0
|
||||
}
|
||||
}, [outcomes])
|
||||
|
||||
const categoryBreakdown = useMemo(() => {
|
||||
return Object.keys(categoryConfig).map(cat => {
|
||||
const categoryOutcomes = filteredOutcomes.filter(o => o.category === cat)
|
||||
return {
|
||||
category: cat,
|
||||
planned: categoryOutcomes.reduce((sum, o) => sum + o.plannedAmount, 0),
|
||||
realized: categoryOutcomes.filter(o => o.status === 'realized' || o.status === 'validated').reduce((sum, o) => sum + o.actualAmount, 0),
|
||||
count: categoryOutcomes.length
|
||||
}
|
||||
}).filter(c => c.count > 0)
|
||||
}, [filteredOutcomes])
|
||||
|
||||
const formatCurrency = (amount: number, currency: string = 'USD') => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Financial Outcome Tracking</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Link strategic initiatives to measurable financial results
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus size={16} weight="bold" />
|
||||
Track Outcome
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Track Financial Outcome</DialogTitle>
|
||||
<DialogDescription>
|
||||
Link a financial outcome to a strategic initiative
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="initiative">Initiative *</Label>
|
||||
<Select
|
||||
value={newOutcome.initiativeId}
|
||||
onValueChange={(value) => setNewOutcome({ ...newOutcome, initiativeId: value })}
|
||||
>
|
||||
<SelectTrigger id="initiative">
|
||||
<SelectValue placeholder="Select initiative" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(initiatives || []).map(init => (
|
||||
<SelectItem key={init.id} value={init.id}>
|
||||
{init.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="category">Outcome Category *</Label>
|
||||
<Select
|
||||
value={newOutcome.category}
|
||||
onValueChange={(value: any) => setNewOutcome({ ...newOutcome, category: value })}
|
||||
>
|
||||
<SelectTrigger id="category">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(categoryConfig).map(([key, config]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{config.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description *</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={newOutcome.description}
|
||||
onChange={(e) => setNewOutcome({ ...newOutcome, description: e.target.value })}
|
||||
placeholder="e.g., Reduced operational costs through automation"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid gap-2 col-span-2">
|
||||
<Label htmlFor="planned-amount">Planned Amount *</Label>
|
||||
<Input
|
||||
id="planned-amount"
|
||||
type="number"
|
||||
value={newOutcome.plannedAmount}
|
||||
onChange={(e) => setNewOutcome({ ...newOutcome, plannedAmount: parseFloat(e.target.value) })}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="currency">Currency</Label>
|
||||
<Select
|
||||
value={newOutcome.currency}
|
||||
onValueChange={(value) => setNewOutcome({ ...newOutcome, currency: value })}
|
||||
>
|
||||
<SelectTrigger id="currency">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="USD">USD</SelectItem>
|
||||
<SelectItem value="EUR">EUR</SelectItem>
|
||||
<SelectItem value="GBP">GBP</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="realization-date">Expected Realization Date</Label>
|
||||
<Input
|
||||
id="realization-date"
|
||||
type="date"
|
||||
value={newOutcome.realizationDate}
|
||||
onChange={(e) => setNewOutcome({ ...newOutcome, realizationDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Input
|
||||
id="notes"
|
||||
value={newOutcome.notes}
|
||||
onChange={(e) => setNewOutcome({ ...newOutcome, notes: e.target.value })}
|
||||
placeholder="Additional context..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addOutcome}>Track Outcome</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Planned</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{formatCurrency(stats.totalPlanned)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Across all initiatives</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Realized</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-success">{formatCurrency(stats.totalRealized)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Validated outcomes</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Projected Value</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-accent">{formatCurrency(stats.totalProjected)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Pending realization</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Realization Rate</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{Math.round(stats.realizationRate)}%</div>
|
||||
<Progress value={stats.realizationRate} className="h-1.5 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Impact by Category</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{categoryBreakdown.map(({ category, planned, realized, count }) => {
|
||||
const config = categoryConfig[category as keyof typeof categoryConfig]
|
||||
const Icon = config.icon
|
||||
const realizationPct = planned > 0 ? (realized / planned) * 100 : 0
|
||||
|
||||
return (
|
||||
<div key={category}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`${config.color} p-2 rounded-md`}>
|
||||
<Icon size={16} weight="bold" className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-sm">{config.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{count} outcome{count !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-semibold">{formatCurrency(realized)} / {formatCurrency(planned)}</p>
|
||||
<p className="text-xs text-muted-foreground">{Math.round(realizationPct)}% realized</p>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={realizationPct} className="h-2" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">Filters:</Label>
|
||||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
{Object.entries(categoryConfig).map(([key, config]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{config.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Statuses</SelectItem>
|
||||
<SelectItem value="projected">Projected</SelectItem>
|
||||
<SelectItem value="realized">Realized</SelectItem>
|
||||
<SelectItem value="validated">Validated</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{filteredOutcomes.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<CurrencyDollar size={48} className="text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground text-center">
|
||||
No financial outcomes tracked yet. Start linking initiatives to financial results.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredOutcomes.map((outcome) => {
|
||||
const config = categoryConfig[outcome.category]
|
||||
const Icon = config.icon
|
||||
const variance = outcome.actualAmount - outcome.plannedAmount
|
||||
const variancePct = outcome.plannedAmount > 0 ? (variance / outcome.plannedAmount) * 100 : 0
|
||||
|
||||
return (
|
||||
<Card key={outcome.id}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className={`${config.color} p-2 rounded-md`}>
|
||||
<Icon size={20} weight="bold" className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">{outcome.description}</h4>
|
||||
<p className="text-sm text-muted-foreground">{outcome.initiativeTitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-3 text-sm">
|
||||
<Badge variant={
|
||||
outcome.status === 'validated' ? 'default' :
|
||||
outcome.status === 'realized' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{outcome.status}
|
||||
</Badge>
|
||||
<Badge variant="outline">{config.label}</Badge>
|
||||
{outcome.realizationDate && (
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<Calendar size={14} />
|
||||
<span>{new Date(outcome.realizationDate).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{outcome.notes && (
|
||||
<p className="text-sm text-muted-foreground mt-2 italic">{outcome.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="space-y-1 mb-3">
|
||||
<p className="text-xs text-muted-foreground">Planned</p>
|
||||
<p className="text-xl font-bold">{formatCurrency(outcome.plannedAmount, outcome.currency)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">Actual</p>
|
||||
<p className="text-xl font-bold text-success">{formatCurrency(outcome.actualAmount, outcome.currency)}</p>
|
||||
</div>
|
||||
{outcome.status === 'realized' || outcome.status === 'validated' ? (
|
||||
<div className={`text-xs mt-2 ${variance >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{variance >= 0 ? '+' : ''}{formatCurrency(variance, outcome.currency)} ({variancePct >= 0 ? '+' : ''}{Math.round(variancePct)}%)
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{outcome.status === 'projected' && (
|
||||
<div className="mt-4 pt-4 border-t flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter actual amount"
|
||||
className="h-9"
|
||||
id={`actual-${outcome.id}`}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const input = document.getElementById(`actual-${outcome.id}`) as HTMLInputElement
|
||||
const actualAmount = parseFloat(input.value)
|
||||
if (!isNaN(actualAmount)) {
|
||||
updateOutcomeStatus(outcome.id, 'realized', actualAmount)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Mark Realized
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{outcome.status === 'realized' && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
onClick={() => updateOutcomeStatus(outcome.id, 'validated')}
|
||||
>
|
||||
<CheckCircle size={16} weight="fill" />
|
||||
Validate Outcome
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
610
src/components/OKRManagement.tsx
Normal file
610
src/components/OKRManagement.tsx
Normal file
@@ -0,0 +1,610 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Plus, Target, CheckCircle, TrendUp, TrendDown, User, Calendar, ArrowRight, Trash } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface KeyResult {
|
||||
id: string
|
||||
description: string
|
||||
startValue: number
|
||||
currentValue: number
|
||||
targetValue: number
|
||||
unit: string
|
||||
progress: number
|
||||
status: 'on-track' | 'at-risk' | 'behind' | 'achieved'
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
interface Objective {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
owner: string
|
||||
category: 'company' | 'team' | 'individual'
|
||||
timeframe: 'quarterly' | 'annual'
|
||||
quarter?: string
|
||||
year: string
|
||||
status: 'active' | 'achieved' | 'at-risk' | 'abandoned'
|
||||
keyResults: KeyResult[]
|
||||
linkedInitiatives: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export default function OKRManagement() {
|
||||
const [objectives, setObjectives] = useKV<Objective[]>('okr-objectives', [])
|
||||
const [initiatives] = useKV<any[]>('initiatives', [])
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isAddKRDialogOpen, setIsAddKRDialogOpen] = useState(false)
|
||||
const [selectedObjective, setSelectedObjective] = useState<Objective | null>(null)
|
||||
const [selectedCategory, setSelectedCategory] = useState<'all' | 'company' | 'team' | 'individual'>('all')
|
||||
const [selectedTimeframe, setSelectedTimeframe] = useState<'all' | 'quarterly' | 'annual'>('all')
|
||||
|
||||
const [newObjective, setNewObjective] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
owner: '',
|
||||
category: 'team' as const,
|
||||
timeframe: 'quarterly' as const,
|
||||
quarter: 'Q1',
|
||||
year: new Date().getFullYear().toString()
|
||||
})
|
||||
|
||||
const [newKeyResult, setNewKeyResult] = useState({
|
||||
description: '',
|
||||
startValue: 0,
|
||||
targetValue: 100,
|
||||
unit: '%'
|
||||
})
|
||||
|
||||
const addObjective = () => {
|
||||
if (!newObjective.title || !newObjective.owner) {
|
||||
toast.error('Please fill in required fields')
|
||||
return
|
||||
}
|
||||
|
||||
const objective: Objective = {
|
||||
id: `obj-${Date.now()}`,
|
||||
title: newObjective.title,
|
||||
description: newObjective.description,
|
||||
owner: newObjective.owner,
|
||||
category: newObjective.category,
|
||||
timeframe: newObjective.timeframe,
|
||||
quarter: newObjective.timeframe === 'quarterly' ? newObjective.quarter : undefined,
|
||||
year: newObjective.year,
|
||||
status: 'active',
|
||||
keyResults: [],
|
||||
linkedInitiatives: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
setObjectives((current) => [...(current || []), objective])
|
||||
setIsAddDialogOpen(false)
|
||||
setNewObjective({
|
||||
title: '',
|
||||
description: '',
|
||||
owner: '',
|
||||
category: 'team',
|
||||
timeframe: 'quarterly',
|
||||
quarter: 'Q1',
|
||||
year: new Date().getFullYear().toString()
|
||||
})
|
||||
toast.success('Objective created successfully')
|
||||
}
|
||||
|
||||
const addKeyResult = () => {
|
||||
if (!selectedObjective || !newKeyResult.description) {
|
||||
toast.error('Please fill in required fields')
|
||||
return
|
||||
}
|
||||
|
||||
const keyResult: KeyResult = {
|
||||
id: `kr-${Date.now()}`,
|
||||
description: newKeyResult.description,
|
||||
startValue: newKeyResult.startValue,
|
||||
currentValue: newKeyResult.startValue,
|
||||
targetValue: newKeyResult.targetValue,
|
||||
unit: newKeyResult.unit,
|
||||
progress: 0,
|
||||
status: 'on-track',
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
|
||||
setObjectives((current) =>
|
||||
(current || []).map(obj =>
|
||||
obj.id === selectedObjective.id
|
||||
? { ...obj, keyResults: [...obj.keyResults, keyResult], updatedAt: new Date().toISOString() }
|
||||
: obj
|
||||
)
|
||||
)
|
||||
|
||||
setIsAddKRDialogOpen(false)
|
||||
setNewKeyResult({
|
||||
description: '',
|
||||
startValue: 0,
|
||||
targetValue: 100,
|
||||
unit: '%'
|
||||
})
|
||||
toast.success('Key Result added successfully')
|
||||
}
|
||||
|
||||
const updateKeyResultProgress = (objectiveId: string, krId: string, newValue: number) => {
|
||||
setObjectives((current) =>
|
||||
(current || []).map(obj => {
|
||||
if (obj.id === objectiveId) {
|
||||
const updatedKRs = obj.keyResults.map(kr => {
|
||||
if (kr.id === krId) {
|
||||
const range = kr.targetValue - kr.startValue
|
||||
const progress = Math.min(100, Math.max(0, ((newValue - kr.startValue) / range) * 100))
|
||||
let status: KeyResult['status'] = 'on-track'
|
||||
if (progress >= 100) status = 'achieved'
|
||||
else if (progress >= 70) status = 'on-track'
|
||||
else if (progress >= 40) status = 'at-risk'
|
||||
else status = 'behind'
|
||||
|
||||
return {
|
||||
...kr,
|
||||
currentValue: newValue,
|
||||
progress,
|
||||
status,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
return kr
|
||||
})
|
||||
|
||||
const avgProgress = updatedKRs.reduce((sum, kr) => sum + kr.progress, 0) / updatedKRs.length
|
||||
let objStatus: Objective['status'] = 'active'
|
||||
if (avgProgress >= 100) objStatus = 'achieved'
|
||||
else if (avgProgress < 50) objStatus = 'at-risk'
|
||||
|
||||
return { ...obj, keyResults: updatedKRs, status: objStatus, updatedAt: new Date().toISOString() }
|
||||
}
|
||||
return obj
|
||||
})
|
||||
)
|
||||
toast.success('Progress updated')
|
||||
}
|
||||
|
||||
const deleteObjective = (id: string) => {
|
||||
setObjectives((current) => (current || []).filter(obj => obj.id !== id))
|
||||
toast.success('Objective deleted')
|
||||
}
|
||||
|
||||
const deleteKeyResult = (objectiveId: string, krId: string) => {
|
||||
setObjectives((current) =>
|
||||
(current || []).map(obj =>
|
||||
obj.id === objectiveId
|
||||
? { ...obj, keyResults: obj.keyResults.filter(kr => kr.id !== krId) }
|
||||
: obj
|
||||
)
|
||||
)
|
||||
toast.success('Key Result deleted')
|
||||
}
|
||||
|
||||
const filteredObjectives = (objectives || []).filter(obj => {
|
||||
if (selectedCategory !== 'all' && obj.category !== selectedCategory) return false
|
||||
if (selectedTimeframe !== 'all' && obj.timeframe !== selectedTimeframe) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const calculateObjectiveProgress = (obj: Objective) => {
|
||||
if (obj.keyResults.length === 0) return 0
|
||||
return obj.keyResults.reduce((sum, kr) => sum + kr.progress, 0) / obj.keyResults.length
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'achieved': return 'bg-success text-white'
|
||||
case 'on-track': return 'bg-success text-white'
|
||||
case 'at-risk': return 'bg-at-risk text-white'
|
||||
case 'behind': return 'bg-destructive text-white'
|
||||
case 'abandoned': return 'bg-muted text-muted-foreground'
|
||||
default: return 'bg-secondary'
|
||||
}
|
||||
}
|
||||
|
||||
const stats = {
|
||||
total: filteredObjectives.length,
|
||||
achieved: filteredObjectives.filter(o => o.status === 'achieved').length,
|
||||
active: filteredObjectives.filter(o => o.status === 'active').length,
|
||||
atRisk: filteredObjectives.filter(o => o.status === 'at-risk').length
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">OKR Management</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Define and track Objectives and Key Results across the organization
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus size={16} weight="bold" />
|
||||
New Objective
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Objective</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define a new objective. You'll add key results in the next step.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="title">Objective Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={newObjective.title}
|
||||
onChange={(e) => setNewObjective({ ...newObjective, title: e.target.value })}
|
||||
placeholder="e.g., Become the market leader in customer satisfaction"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newObjective.description}
|
||||
onChange={(e) => setNewObjective({ ...newObjective, description: e.target.value })}
|
||||
placeholder="Describe the objective and its strategic importance..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="category">Category *</Label>
|
||||
<Select
|
||||
value={newObjective.category}
|
||||
onValueChange={(value: any) => setNewObjective({ ...newObjective, category: value })}
|
||||
>
|
||||
<SelectTrigger id="category">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="company">Company</SelectItem>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="individual">Individual</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="owner">Owner *</Label>
|
||||
<Input
|
||||
id="owner"
|
||||
value={newObjective.owner}
|
||||
onChange={(e) => setNewObjective({ ...newObjective, owner: e.target.value })}
|
||||
placeholder="Owner name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="timeframe">Timeframe *</Label>
|
||||
<Select
|
||||
value={newObjective.timeframe}
|
||||
onValueChange={(value: any) => setNewObjective({ ...newObjective, timeframe: value })}
|
||||
>
|
||||
<SelectTrigger id="timeframe">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="quarterly">Quarterly</SelectItem>
|
||||
<SelectItem value="annual">Annual</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{newObjective.timeframe === 'quarterly' && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="quarter">Quarter</Label>
|
||||
<Select
|
||||
value={newObjective.quarter}
|
||||
onValueChange={(value) => setNewObjective({ ...newObjective, quarter: value })}
|
||||
>
|
||||
<SelectTrigger id="quarter">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Q1">Q1</SelectItem>
|
||||
<SelectItem value="Q2">Q2</SelectItem>
|
||||
<SelectItem value="Q3">Q3</SelectItem>
|
||||
<SelectItem value="Q4">Q4</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="year">Year</Label>
|
||||
<Input
|
||||
id="year"
|
||||
type="number"
|
||||
value={newObjective.year}
|
||||
onChange={(e) => setNewObjective({ ...newObjective, year: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addObjective}>Create Objective</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Objectives</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stats.total}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Achieved</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-success">{stats.achieved}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-primary">{stats.active}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">At Risk</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-at-risk">{stats.atRisk}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">Filters:</Label>
|
||||
<Select value={selectedCategory} onValueChange={(value: any) => setSelectedCategory(value)}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="company">Company</SelectItem>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="individual">Individual</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedTimeframe} onValueChange={(value: any) => setSelectedTimeframe(value)}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Timeframes</SelectItem>
|
||||
<SelectItem value="quarterly">Quarterly</SelectItem>
|
||||
<SelectItem value="annual">Annual</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredObjectives.length === 0 ? (
|
||||
<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. Create your first objective to get started.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredObjectives.map((objective) => {
|
||||
const progress = calculateObjectiveProgress(objective)
|
||||
return (
|
||||
<Card key={objective.id} className="overflow-hidden">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CardTitle className="text-xl">{objective.title}</CardTitle>
|
||||
<Badge className={getStatusColor(objective.status)}>
|
||||
{objective.status}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{objective.category}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
{objective.timeframe === 'quarterly' ? `${objective.quarter} ${objective.year}` : objective.year}
|
||||
</Badge>
|
||||
</div>
|
||||
{objective.description && (
|
||||
<CardDescription className="text-sm mt-1">
|
||||
{objective.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="flex items-center gap-4 mt-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<User size={16} />
|
||||
<span>{objective.owner}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar size={16} />
|
||||
<span>Updated {new Date(objective.updatedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteObjective(objective.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="text-muted-foreground">Overall Progress</span>
|
||||
<span className="font-semibold">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-semibold flex items-center gap-2">
|
||||
<CheckCircle size={20} weight="bold" className="text-accent" />
|
||||
Key Results ({objective.keyResults.length})
|
||||
</h4>
|
||||
<Dialog open={isAddKRDialogOpen && selectedObjective?.id === objective.id} onOpenChange={(open) => {
|
||||
setIsAddKRDialogOpen(open)
|
||||
if (open) setSelectedObjective(objective)
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Plus size={14} weight="bold" />
|
||||
Add Key Result
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Key Result</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define a measurable key result for this objective
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="kr-description">Key Result Description *</Label>
|
||||
<Textarea
|
||||
id="kr-description"
|
||||
value={newKeyResult.description}
|
||||
onChange={(e) => setNewKeyResult({ ...newKeyResult, description: e.target.value })}
|
||||
placeholder="e.g., Increase NPS score from 45 to 70"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="start-value">Start Value</Label>
|
||||
<Input
|
||||
id="start-value"
|
||||
type="number"
|
||||
value={newKeyResult.startValue}
|
||||
onChange={(e) => setNewKeyResult({ ...newKeyResult, startValue: parseFloat(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="target-value">Target Value</Label>
|
||||
<Input
|
||||
id="target-value"
|
||||
type="number"
|
||||
value={newKeyResult.targetValue}
|
||||
onChange={(e) => setNewKeyResult({ ...newKeyResult, targetValue: parseFloat(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="unit">Unit</Label>
|
||||
<Input
|
||||
id="unit"
|
||||
value={newKeyResult.unit}
|
||||
onChange={(e) => setNewKeyResult({ ...newKeyResult, unit: e.target.value })}
|
||||
placeholder="%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddKRDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addKeyResult}>Add Key Result</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{objective.keyResults.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground text-sm border-2 border-dashed rounded-lg">
|
||||
No key results yet. Add key results to track progress.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{objective.keyResults.map((kr) => (
|
||||
<div key={kr.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm">{kr.description}</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
||||
<span>{kr.startValue} {kr.unit} <ArrowRight size={12} className="inline" /> {kr.targetValue} {kr.unit}</span>
|
||||
<Badge variant="outline" className={getStatusColor(kr.status)}>
|
||||
{kr.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteKeyResult(objective.id, kr.id)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Current: {kr.currentValue} {kr.unit}</span>
|
||||
<span className="font-semibold">{Math.round(kr.progress)}%</span>
|
||||
</div>
|
||||
<Progress value={kr.progress} className="h-2" />
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Update value"
|
||||
className="h-8 text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const input = e.target as HTMLInputElement
|
||||
updateKeyResultProgress(objective.id, kr.id, parseFloat(input.value))
|
||||
input.value = ''
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Press Enter to update</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
520
src/components/PortfolioAnalysis.tsx
Normal file
520
src/components/PortfolioAnalysis.tsx
Normal file
@@ -0,0 +1,520 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Plus, Users, TrendUp, Warning, CheckCircle, ArrowRight, GitBranch, Clock } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { Initiative } from '@/types'
|
||||
|
||||
interface Dependency {
|
||||
id: string
|
||||
fromInitiativeId: string
|
||||
fromInitiativeTitle: string
|
||||
toInitiativeId: string
|
||||
toInitiativeTitle: string
|
||||
type: 'blocks' | 'enables' | 'informs'
|
||||
description: string
|
||||
status: 'active' | 'resolved'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface CapacityAllocation {
|
||||
portfolioType: string
|
||||
totalCapacity: number
|
||||
allocatedCapacity: number
|
||||
teamSize: number
|
||||
utilizationRate: number
|
||||
}
|
||||
|
||||
export default function PortfolioAnalysis() {
|
||||
const [initiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
const [dependencies, setDependencies] = useKV<Dependency[]>('portfolio-dependencies', [])
|
||||
const [isAddDependencyOpen, setIsAddDependencyOpen] = useState(false)
|
||||
|
||||
const [newDependency, setNewDependency] = useState({
|
||||
fromInitiativeId: '',
|
||||
toInitiativeId: '',
|
||||
type: 'blocks' as const,
|
||||
description: ''
|
||||
})
|
||||
|
||||
const portfolios = [
|
||||
{ type: 'operational-excellence', name: 'Operational Excellence', capacity: 100, teamSize: 12 },
|
||||
{ type: 'ma', name: 'M&A', capacity: 80, teamSize: 8 },
|
||||
{ type: 'financial-transformation', name: 'Financial Transformation', capacity: 120, teamSize: 15 },
|
||||
{ type: 'esg', name: 'ESG', capacity: 60, teamSize: 6 },
|
||||
{ type: 'innovation', name: 'Innovation', capacity: 90, teamSize: 10 }
|
||||
]
|
||||
|
||||
const addDependency = () => {
|
||||
if (!newDependency.fromInitiativeId || !newDependency.toInitiativeId || !newDependency.description) {
|
||||
toast.error('Please fill in all fields')
|
||||
return
|
||||
}
|
||||
|
||||
if (newDependency.fromInitiativeId === newDependency.toInitiativeId) {
|
||||
toast.error('Cannot create dependency to the same initiative')
|
||||
return
|
||||
}
|
||||
|
||||
const fromInit = initiatives?.find(i => i.id === newDependency.fromInitiativeId)
|
||||
const toInit = initiatives?.find(i => i.id === newDependency.toInitiativeId)
|
||||
|
||||
if (!fromInit || !toInit) {
|
||||
toast.error('Initiatives not found')
|
||||
return
|
||||
}
|
||||
|
||||
const dependency: Dependency = {
|
||||
id: `dep-${Date.now()}`,
|
||||
fromInitiativeId: newDependency.fromInitiativeId,
|
||||
fromInitiativeTitle: fromInit.title,
|
||||
toInitiativeId: newDependency.toInitiativeId,
|
||||
toInitiativeTitle: toInit.title,
|
||||
type: newDependency.type,
|
||||
description: newDependency.description,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
setDependencies((current) => [...(current || []), dependency])
|
||||
setIsAddDependencyOpen(false)
|
||||
setNewDependency({
|
||||
fromInitiativeId: '',
|
||||
toInitiativeId: '',
|
||||
type: 'blocks',
|
||||
description: ''
|
||||
})
|
||||
toast.success('Dependency added')
|
||||
}
|
||||
|
||||
const resolveDependency = (id: string) => {
|
||||
setDependencies((current) =>
|
||||
(current || []).map(d => d.id === id ? { ...d, status: 'resolved' } : d)
|
||||
)
|
||||
toast.success('Dependency resolved')
|
||||
}
|
||||
|
||||
const capacityData: CapacityAllocation[] = useMemo(() => {
|
||||
return portfolios.map(portfolio => {
|
||||
const portfolioInits = initiatives?.filter(i => i.portfolio === portfolio.type) || []
|
||||
const allocatedCapacity = portfolioInits.length * 15
|
||||
const utilizationRate = portfolio.capacity > 0 ? (allocatedCapacity / portfolio.capacity) * 100 : 0
|
||||
|
||||
return {
|
||||
portfolioType: portfolio.type,
|
||||
totalCapacity: portfolio.capacity,
|
||||
allocatedCapacity: Math.min(allocatedCapacity, portfolio.capacity),
|
||||
teamSize: portfolio.teamSize,
|
||||
utilizationRate: Math.min(utilizationRate, 100)
|
||||
}
|
||||
})
|
||||
}, [initiatives])
|
||||
|
||||
const alignmentAnalysis = useMemo(() => {
|
||||
return portfolios.map(portfolio => {
|
||||
const portfolioInits = initiatives?.filter(i => i.portfolio === portfolio.type) || []
|
||||
const totalBudget = portfolioInits.reduce((sum, i) => sum + (i.budget || 0), 0)
|
||||
const avgProgress = portfolioInits.length > 0
|
||||
? portfolioInits.reduce((sum, i) => sum + i.progress, 0) / portfolioInits.length
|
||||
: 0
|
||||
|
||||
const health = {
|
||||
onTrack: portfolioInits.filter(i => i.status === 'on-track').length,
|
||||
atRisk: portfolioInits.filter(i => i.status === 'at-risk').length,
|
||||
blocked: portfolioInits.filter(i => i.status === 'blocked').length,
|
||||
completed: portfolioInits.filter(i => i.status === 'completed').length
|
||||
}
|
||||
|
||||
const strategicAlignment = portfolioInits.filter(i => i.strategyCardId).length / Math.max(portfolioInits.length, 1) * 100
|
||||
|
||||
return {
|
||||
portfolio: portfolio.name,
|
||||
type: portfolio.type,
|
||||
count: portfolioInits.length,
|
||||
totalBudget,
|
||||
avgProgress: Math.round(avgProgress),
|
||||
health,
|
||||
strategicAlignment: Math.round(strategicAlignment)
|
||||
}
|
||||
})
|
||||
}, [initiatives])
|
||||
|
||||
const riskAnalysis = useMemo(() => {
|
||||
const blockedInitiatives = initiatives?.filter(i => i.status === 'blocked') || []
|
||||
const atRiskInitiatives = initiatives?.filter(i => i.status === 'at-risk') || []
|
||||
const activeDependencies = dependencies?.filter(d => d.status === 'active') || []
|
||||
const blockingDeps = activeDependencies.filter(d => d.type === 'blocks')
|
||||
|
||||
return {
|
||||
blockedCount: blockedInitiatives.length,
|
||||
atRiskCount: atRiskInitiatives.length,
|
||||
activeDependencyCount: activeDependencies.length,
|
||||
blockingDependencyCount: blockingDeps.length
|
||||
}
|
||||
}, [initiatives, dependencies])
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const getDependencyTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'blocks': return 'destructive'
|
||||
case 'enables': return 'default'
|
||||
case 'informs': return 'secondary'
|
||||
default: return 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Portfolio Analysis</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Strategic alignment, capacity planning, and dependency management
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Blocked Initiatives</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-destructive">{riskAnalysis.blockedCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Need resolution</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">At Risk</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-at-risk">{riskAnalysis.atRiskCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Attention needed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active Dependencies</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{riskAnalysis.activeDependencyCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Cross-initiative links</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Blocking Dependencies</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-destructive">{riskAnalysis.blockingDependencyCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Critical path items</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="alignment" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="alignment">Strategic Alignment</TabsTrigger>
|
||||
<TabsTrigger value="capacity">Capacity Planning</TabsTrigger>
|
||||
<TabsTrigger value="dependencies">Dependencies</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="alignment" className="space-y-4 mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Portfolio Strategic Alignment</CardTitle>
|
||||
<CardDescription>Initiative alignment, health, and budget by portfolio</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{alignmentAnalysis.map((portfolio) => (
|
||||
<div key={portfolio.type}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg">{portfolio.portfolio}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{portfolio.count} initiatives • {formatCurrency(portfolio.totalBudget)} budget
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-muted-foreground mb-1">Strategic Alignment</div>
|
||||
<Badge variant={portfolio.strategicAlignment >= 80 ? 'default' : portfolio.strategicAlignment >= 50 ? 'secondary' : 'destructive'}>
|
||||
{portfolio.strategicAlignment}%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 gap-3 mb-3">
|
||||
<div className="text-center p-2 bg-muted/50 rounded">
|
||||
<div className="text-xs text-muted-foreground mb-1">On Track</div>
|
||||
<div className="text-lg font-semibold text-success">{portfolio.health.onTrack}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted/50 rounded">
|
||||
<div className="text-xs text-muted-foreground mb-1">At Risk</div>
|
||||
<div className="text-lg font-semibold text-at-risk">{portfolio.health.atRisk}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted/50 rounded">
|
||||
<div className="text-xs text-muted-foreground mb-1">Blocked</div>
|
||||
<div className="text-lg font-semibold text-destructive">{portfolio.health.blocked}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted/50 rounded">
|
||||
<div className="text-xs text-muted-foreground mb-1">Completed</div>
|
||||
<div className="text-lg font-semibold text-accent">{portfolio.health.completed}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted/50 rounded">
|
||||
<div className="text-xs text-muted-foreground mb-1">Avg Progress</div>
|
||||
<div className="text-lg font-semibold">{portfolio.avgProgress}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Overall Progress</span>
|
||||
<span className="font-semibold">{portfolio.avgProgress}%</span>
|
||||
</div>
|
||||
<Progress value={portfolio.avgProgress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<Separator className="mt-6" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="capacity" className="space-y-4 mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Capacity & Resource Planning</CardTitle>
|
||||
<CardDescription>Team capacity utilization across portfolios</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{capacityData.map((capacity) => {
|
||||
const portfolio = portfolios.find(p => p.type === capacity.portfolioType)
|
||||
const isOverCapacity = capacity.utilizationRate >= 90
|
||||
const isNearCapacity = capacity.utilizationRate >= 75 && capacity.utilizationRate < 90
|
||||
|
||||
return (
|
||||
<div key={capacity.portfolioType}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Users size={20} className="text-primary" weight="bold" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">{portfolio?.name}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Team Size: {capacity.teamSize} members
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold">{Math.round(capacity.utilizationRate)}%</div>
|
||||
<p className="text-xs text-muted-foreground">Utilization</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{capacity.allocatedCapacity} / {capacity.totalCapacity} capacity points
|
||||
</span>
|
||||
{isOverCapacity && (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<Warning size={12} weight="fill" />
|
||||
Over Capacity
|
||||
</Badge>
|
||||
)}
|
||||
{isNearCapacity && (
|
||||
<Badge variant="outline" className="gap-1 border-at-risk text-at-risk">
|
||||
<Warning size={12} weight="fill" />
|
||||
Near Capacity
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Progress
|
||||
value={capacity.utilizationRate}
|
||||
className={`h-3 ${isOverCapacity ? '[&>div]:bg-destructive' : isNearCapacity ? '[&>div]:bg-at-risk' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mt-6" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="dependencies" className="space-y-4 mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Initiative Dependencies</CardTitle>
|
||||
<CardDescription>Track cross-initiative dependencies and blockers</CardDescription>
|
||||
</div>
|
||||
<Dialog open={isAddDependencyOpen} onOpenChange={setIsAddDependencyOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus size={16} weight="bold" />
|
||||
Add Dependency
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Initiative Dependency</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define a dependency relationship between two initiatives
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="from-initiative">From Initiative</Label>
|
||||
<Select
|
||||
value={newDependency.fromInitiativeId}
|
||||
onValueChange={(value) => setNewDependency({ ...newDependency, fromInitiativeId: value })}
|
||||
>
|
||||
<SelectTrigger id="from-initiative">
|
||||
<SelectValue placeholder="Select initiative" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(initiatives || []).map(init => (
|
||||
<SelectItem key={init.id} value={init.id}>
|
||||
{init.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dependency-type">Dependency Type</Label>
|
||||
<Select
|
||||
value={newDependency.type}
|
||||
onValueChange={(value: any) => setNewDependency({ ...newDependency, type: value })}
|
||||
>
|
||||
<SelectTrigger id="dependency-type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="blocks">Blocks (prevents progress)</SelectItem>
|
||||
<SelectItem value="enables">Enables (allows to proceed)</SelectItem>
|
||||
<SelectItem value="informs">Informs (provides info)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="to-initiative">To Initiative</Label>
|
||||
<Select
|
||||
value={newDependency.toInitiativeId}
|
||||
onValueChange={(value) => setNewDependency({ ...newDependency, toInitiativeId: value })}
|
||||
>
|
||||
<SelectTrigger id="to-initiative">
|
||||
<SelectValue placeholder="Select initiative" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(initiatives || []).map(init => (
|
||||
<SelectItem key={init.id} value={init.id}>
|
||||
{init.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={newDependency.description}
|
||||
onChange={(e) => setNewDependency({ ...newDependency, description: e.target.value })}
|
||||
placeholder="Describe the dependency..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddDependencyOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addDependency}>Add Dependency</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{(dependencies || []).length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<GitBranch size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>No dependencies tracked yet. Add dependencies to map initiative relationships.</p>
|
||||
</div>
|
||||
) : (
|
||||
(dependencies || []).map((dep) => (
|
||||
<Card key={dep.id} className={dep.status === 'resolved' ? 'opacity-60' : ''}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant={getDependencyTypeColor(dep.type)}>
|
||||
{dep.type}
|
||||
</Badge>
|
||||
<Badge variant={dep.status === 'active' ? 'default' : 'outline'}>
|
||||
{dep.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="font-medium">{dep.fromInitiativeTitle}</span>
|
||||
<ArrowRight size={16} className="text-muted-foreground" />
|
||||
<span className="font-medium">{dep.toInitiativeTitle}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">{dep.description}</p>
|
||||
<p className="text-xs text-muted-foreground mt-2 flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
Created {new Date(dep.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
{dep.status === 'active' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => resolveDependency(dep.id)}
|
||||
className="gap-2"
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
Resolve
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -103,7 +103,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Objectives and Key Results tracking and alignment',
|
||||
category: 'workbench',
|
||||
priority: 'medium',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Implemented comprehensive OKR management system with objectives, key results, progress tracking, category organization (company/team/individual), quarterly and annual timeframes, and real-time status updates with automatic objective status calculation based on key result progress.'
|
||||
},
|
||||
{
|
||||
id: 'wb-5',
|
||||
@@ -177,7 +179,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Group initiatives into portfolios (M&A, OpEx, ESG, etc.)',
|
||||
category: 'portfolio',
|
||||
priority: 'high',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Implemented portfolio grouping system with predefined portfolio types including Operational Excellence, M&A, Financial Transformation, ESG, and Innovation. Initiatives can be assigned to portfolios during creation or editing.'
|
||||
},
|
||||
{
|
||||
id: 'pf-2',
|
||||
@@ -185,7 +189,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Assess strategic alignment and impact across portfolios',
|
||||
category: 'portfolio',
|
||||
priority: 'high',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Built comprehensive portfolio analysis module showing strategic alignment percentage, initiative health distribution, budget allocation, and average progress across all portfolios with detailed drill-down capability.'
|
||||
},
|
||||
{
|
||||
id: 'pf-3',
|
||||
@@ -193,7 +199,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Resource capacity planning and allocation',
|
||||
category: 'portfolio',
|
||||
priority: 'medium',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Created capacity planning module with team size tracking, utilization rate calculation, capacity threshold warnings (near/over capacity), and visual indicators for resource allocation across portfolios.'
|
||||
},
|
||||
{
|
||||
id: 'pf-4',
|
||||
@@ -201,7 +209,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Track and visualize cross-initiative dependencies',
|
||||
category: 'portfolio',
|
||||
priority: 'medium',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Implemented dependency tracking system supporting three relationship types (blocks, enables, informs) with active/resolved status workflow, dependency visualization, and resolution workflow to manage critical path and initiative blockers.'
|
||||
},
|
||||
{
|
||||
id: 'pf-5',
|
||||
@@ -281,7 +291,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Portfolio-level dashboards for leadership',
|
||||
category: 'reporting',
|
||||
priority: 'critical',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Built comprehensive executive dashboard with high-level KPIs, portfolio performance breakdowns, financial value realization tracking, initiative health distribution, and recent activity feed providing leadership with complete strategic oversight.'
|
||||
},
|
||||
{
|
||||
id: 'rp-2',
|
||||
@@ -297,7 +309,9 @@ const initialFeatures: RoadmapFeature[] = [
|
||||
description: 'Link initiatives to financial results and savings',
|
||||
category: 'reporting',
|
||||
priority: 'critical',
|
||||
completed: false
|
||||
completed: true,
|
||||
completedDate: new Date().toISOString().split('T')[0],
|
||||
notes: 'Created financial tracking system linking initiatives to measurable financial outcomes including cost savings, revenue increase, cost avoidance, and efficiency gains with planned vs actual tracking, realization status workflow (projected → realized → validated), and category-based analysis with variance reporting.'
|
||||
},
|
||||
{
|
||||
id: 'rp-4',
|
||||
|
||||
Reference in New Issue
Block a user