From c6241732efcfc0d63108f50bc38308e95103cb2f Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 22 Jan 2026 14:28:10 +0000 Subject: [PATCH] Generated by Spark: Ok implement new features from ROADMAP, dont edit roadmap file --- spark.meta.json | 2 +- src/App.tsx | 22 +- src/components/ExecutiveDashboard.tsx | 388 ++++++++++++++++ src/components/FinancialTracking.tsx | 492 +++++++++++++++++++++ src/components/OKRManagement.tsx | 610 ++++++++++++++++++++++++++ src/components/PortfolioAnalysis.tsx | 520 ++++++++++++++++++++++ src/components/ProductRoadmap.tsx | 28 +- 7 files changed, 2051 insertions(+), 11 deletions(-) create mode 100644 src/components/ExecutiveDashboard.tsx create mode 100644 src/components/FinancialTracking.tsx create mode 100644 src/components/OKRManagement.tsx create mode 100644 src/components/PortfolioAnalysis.tsx diff --git a/spark.meta.json b/spark.meta.json index fd74d91..81f8456 100644 --- a/spark.meta.json +++ b/spark.meta.json @@ -1,4 +1,4 @@ { "templateVersion": 0, - "dbType": null + "dbType": "kv" } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 544faae..a29ea85 100644 --- a/src/App.tsx +++ b/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' } diff --git a/src/components/ExecutiveDashboard.tsx b/src/components/ExecutiveDashboard.tsx new file mode 100644 index 0000000..1b03daf --- /dev/null +++ b/src/components/ExecutiveDashboard.tsx @@ -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('strategy-cards', []) + const [initiatives] = useKV('initiatives', []) + const [financialOutcomes] = useKV('financial-outcomes', []) + const [objectives] = useKV('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) || {} + + 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 = { + 'operational-excellence': 'bg-blue-500', + 'ma': 'bg-purple-500', + 'financial-transformation': 'bg-green-500', + 'esg': 'bg-teal-500', + 'innovation': 'bg-orange-500' + } + + return ( +
+
+

Executive Dashboard

+

+ High-level view of strategic performance and progress +

+
+ +
+ + +
+ Strategy Cards + +
+
+ +
{stats.strategies}
+
+
+ + + +
+ Active Initiatives + +
+
+ +
{stats.initiatives}
+

+ {stats.completedInitiatives} completed +

+
+
+ + + +
+ OKRs + +
+
+ +
{stats.objectives}
+

+ {stats.achievedObjectives} achieved +

+
+
+ + + +
+ Avg Progress + +
+
+ +
{stats.avgProgress}%
+ +
+
+ + + +
+ On Track + +
+
+ +
{stats.onTrackInitiatives}
+

initiatives

+
+
+ + + +
+ At Risk + +
+
+ +
{stats.atRiskInitiatives}
+

need attention

+
+
+
+ +
+ + + Financial Value Realization + Planned vs actual financial outcomes + + +
+
+
+

Total Planned Value

+

{formatCurrency(stats.financialPlanned)}

+
+
+

Realized Value

+

{formatCurrency(stats.financialRealized)}

+
+
+

Realization Rate

+

{stats.financialRealizationRate}%

+
+
+ +
+ + + {formatCurrency(stats.financialPlanned - stats.financialRealized)} remaining to realize + +
+
+
+
+ + + + Initiative Health + Status distribution + + +
+
+
+
+ On Track +
+ {stats.onTrackInitiatives} +
+
+
+
+ At Risk +
+ {stats.atRiskInitiatives} +
+
+
+
+ Completed +
+ {stats.completedInitiatives} +
+ +
+ Completion Rate + {stats.completionRate}% +
+
+
+
+
+ + + + Portfolio Performance + Progress and health by strategic portfolio + + +
+ {portfolioBreakdown.length === 0 ? ( +

No portfolio data available

+ ) : ( + portfolioBreakdown.map((portfolio) => ( +
+
+
+
+

{portfolio.name}

+ {portfolio.total} initiatives +
+
+
+

Budget

+

{formatCurrency(portfolio.budget)}

+
+
+

Progress

+

{portfolio.avgProgress}%

+
+
+
+
+
+ + {portfolio.onTrack} on track +
+
+ + {portfolio.atRisk} at risk +
+
+ + {portfolio.completed} completed +
+
+ +
+ )) + )} +
+
+
+ + + + Recent Activity + Latest updates across initiatives + + +
+ {recentActivity.length === 0 ? ( +

No recent activity

+ ) : ( + recentActivity.map((item, idx) => ( +
+
+
+
+

{item.title}

+

+ {item.type === 'initiative' ? 'Initiative' : 'Activity'} +

+
+
+
+ + {item.priority} + + + {new Date(item.date).toLocaleDateString()} + +
+
+ )) + )} +
+
+
+
+ ) +} diff --git a/src/components/FinancialTracking.tsx b/src/components/FinancialTracking.tsx new file mode 100644 index 0000000..2405783 --- /dev/null +++ b/src/components/FinancialTracking.tsx @@ -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('financial-outcomes', []) + const [initiatives] = useKV('initiatives', []) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [selectedCategory, setSelectedCategory] = useState('all') + const [selectedStatus, setSelectedStatus] = useState('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 ( +
+
+
+

Financial Outcome Tracking

+

+ Link strategic initiatives to measurable financial results +

+
+ + + + + + + Track Financial Outcome + + Link a financial outcome to a strategic initiative + + +
+
+ + +
+
+ + +
+
+ + setNewOutcome({ ...newOutcome, description: e.target.value })} + placeholder="e.g., Reduced operational costs through automation" + /> +
+
+
+ + setNewOutcome({ ...newOutcome, plannedAmount: parseFloat(e.target.value) })} + placeholder="0" + /> +
+
+ + +
+
+
+ + setNewOutcome({ ...newOutcome, realizationDate: e.target.value })} + /> +
+
+ + setNewOutcome({ ...newOutcome, notes: e.target.value })} + placeholder="Additional context..." + /> +
+
+ + + + +
+
+
+ +
+ + + Total Planned + + +
{formatCurrency(stats.totalPlanned)}
+

Across all initiatives

+
+
+ + + Total Realized + + +
{formatCurrency(stats.totalRealized)}
+

Validated outcomes

+
+
+ + + Projected Value + + +
{formatCurrency(stats.totalProjected)}
+

Pending realization

+
+
+ + + Realization Rate + + +
{Math.round(stats.realizationRate)}%
+ +
+
+
+ + + + Financial Impact by Category + + +
+ {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 ( +
+
+
+
+ +
+
+

{config.label}

+

{count} outcome{count !== 1 ? 's' : ''}

+
+
+
+

{formatCurrency(realized)} / {formatCurrency(planned)}

+

{Math.round(realizationPct)}% realized

+
+
+ +
+ ) + })} +
+
+
+ +
+ + + +
+ +
+ {filteredOutcomes.length === 0 ? ( + + + +

+ No financial outcomes tracked yet. Start linking initiatives to financial results. +

+
+
+ ) : ( + 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 ( + + +
+
+
+
+ +
+
+

{outcome.description}

+

{outcome.initiativeTitle}

+
+
+
+ + {outcome.status} + + {config.label} + {outcome.realizationDate && ( +
+ + {new Date(outcome.realizationDate).toLocaleDateString()} +
+ )} +
+ {outcome.notes && ( +

{outcome.notes}

+ )} +
+
+
+

Planned

+

{formatCurrency(outcome.plannedAmount, outcome.currency)}

+
+
+

Actual

+

{formatCurrency(outcome.actualAmount, outcome.currency)}

+
+ {outcome.status === 'realized' || outcome.status === 'validated' ? ( +
= 0 ? 'text-success' : 'text-destructive'}`}> + {variance >= 0 ? '+' : ''}{formatCurrency(variance, outcome.currency)} ({variancePct >= 0 ? '+' : ''}{Math.round(variancePct)}%) +
+ ) : null} +
+
+ {outcome.status === 'projected' && ( +
+ + +
+ )} + {outcome.status === 'realized' && ( +
+ +
+ )} +
+
+ ) + }) + )} +
+
+ ) +} diff --git a/src/components/OKRManagement.tsx b/src/components/OKRManagement.tsx new file mode 100644 index 0000000..07c47ac --- /dev/null +++ b/src/components/OKRManagement.tsx @@ -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('okr-objectives', []) + const [initiatives] = useKV('initiatives', []) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [isAddKRDialogOpen, setIsAddKRDialogOpen] = useState(false) + const [selectedObjective, setSelectedObjective] = useState(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 ( +
+
+
+

OKR Management

+

+ Define and track Objectives and Key Results across the organization +

+
+ + + + + + + Create New Objective + + Define a new objective. You'll add key results in the next step. + + +
+
+ + setNewObjective({ ...newObjective, title: e.target.value })} + placeholder="e.g., Become the market leader in customer satisfaction" + /> +
+
+ +