mirror of
https://github.com/johndoe6345789/strategy-execution-p.git
synced 2026-04-24 13:14:56 +00:00
Generated by Spark: 1. Product Scope & Positioning
1.1 Product Portfolio The product suite SHALL consist of two tightly integrated products: Strategy Cards — strategy creation and alignment Workbench — strategy execution, tracking, and governance Each product MUST be independently usable but deliver maximum value when combined. 2. Core Product Outcomes 2.1 Primary Outcomes The system MUST enable organizations to: Create clear, shared strategic intent Translate strategy into executable initiatives Track progress against goals in real time Maintain a single authoritative view of strategy execution 2.2 Target Users The product MUST support: Executives and senior leaders Strategy offices / PMOs Operational excellence teams Portfolio and program managers Project owners and contributors 3. Strategy Cards — Design Criteria 3.1 Strategy Creation Strategy Cards MUST: Support structured strategy creation using proven frameworks Enable exploration, comparison, and refinement of strategic options Reduce reliance on slides, spreadsheets, and ad-hoc documents 3.2 Alignment & Collaboration Strategy Cards MUST: Enable collaborative workshops and facilitated discussions Promote shared understanding across teams Capture rationale, assumptions, and decisions 3.3 Usability & Speed Strategy Cards MUST: Minimize time spent on blank or unstructured work Guide users step-by-step through strategy formulation Enable rapid iteration and refinement 4. Workbench — Design Criteria 4.1 Strategy Execution Workbench MUST: Translate strategic objectives into initiatives, projects, and actions Link strategy directly to delivery mechanisms Support execution methodologies including: Strategic Portfolio Management Hoshin Kanri Operational Excellence OKRs 4.2 Tracking & Measurement Workbench MUST: Provide real-time tracking of initiatives and projects Support KPI, metric, and scorecard management Enable visibility from enterprise level down to individual ownership 4.3 Governance & Accountability Workbench MUST: Clearly define ownership and accountability Support portfolio-level governance and prioritization Enable decision-making based on impact, capacity, and alignment 5. Cross-Product System Capabilities 5.1 Single Source of Truth The system MUST: Centralize all strategy, execution, metrics, and reporting Eliminate fragmented tools and duplicate data Provide one undisputed, authoritative view of progress 5.2 Traceability The system MUST provide end-to-end traceability between: Strategic goals Initiatives and projects KPIs and financial outcomes Individual and team objectives 6. Portfolio Management Criteria 6.1 Portfolio Structuring The system MUST: Support grouping of initiatives into strategic portfolios Allow portfolios to reflect business-specific structures Support domains such as: M&A Operational Excellence Financial Transformation ESG Private Equity portfolios 6.2 Portfolio Analysis The system MUST enable: Alignment and impact assessment Capacity and demand balancing Dependency and delivery risk analysis Funding and resource evaluation 7. Integration & Extensibility 7.1 External Systems The platform MUST integrate with: Project management tools ERP systems CRM systems Other enterprise data sources 7.2 Configurability The system MUST: Adapt to organizational language, processes, and structures Support multiple regions, languages, and operating models Avoid forcing rigid or prescriptive workflows 8. Operational Excellence & Continuous Improvement 8.1 Process Enablement The system MUST: Support Lean, Hoshin Kanri, and continuous improvement methods Enable consistent reporting across units and regions Encourage countermeasure-driven improvement, not just KPI reporting 8.2 Automation The system MUST: Automate manual reporting and reconciliation Reduce time spent maintaining spreadsheets Free users to focus on execution and improvement 9. Reporting & Visibility 9.1 Reporting Capabilities The system MUST provide: Portfolio-level dashboards Scorecards with consistent definitions Drill-down from enterprise to project level Real-time status visibility 9.2 Financial Visibility The system MUST: Link improvement activity to financial outcomes Provide shared visibility between Operations and Finance Support savings tracking and value realization 10. Non-Functional Design Criteria 10.1 Usability Intuitive for non-technical users Minimal training required for core workflows Clear visual models of strategy and execution 10.2 Scalability Support global organizations Scale across portfolios, regions, and users Maintain performance with large datasets 10.3 Reliability High availability for executive-critical reporting Data integrity across integrated systems 11. Success Criteria The product SHALL be considered successful if it: Replaces spreadsheet-based strategy execution Improves alignment across teams and functions Increases execution confidence for leadership Demonstrably improves delivery of strategic initiatives
This commit is contained in:
1
.spark-initial-sha
Normal file
1
.spark-initial-sha
Normal file
@@ -0,0 +1 @@
|
||||
HEAD
|
||||
145
PRD.md
Normal file
145
PRD.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Planning Guide
|
||||
|
||||
An integrated strategy management platform that enables organizations to create, align, execute, and track strategic initiatives from conception to delivery, replacing fragmented spreadsheets with a unified source of truth.
|
||||
|
||||
**Experience Qualities**:
|
||||
1. **Authoritative** - Every view, metric, and decision point radiates confidence and clarity, positioning this as the definitive system of record for strategic execution.
|
||||
2. **Structured yet Flexible** - Guided frameworks accelerate strategy creation while adapting to diverse methodologies (Hoshin Kanri, OKRs, Portfolio Management) without rigid constraints.
|
||||
3. **Traceable** - Crystal-clear visibility from enterprise goals down to individual initiatives, with instant drill-down and relationship mapping that eliminates ambiguity.
|
||||
|
||||
**Complexity Level**: Complex Application (advanced functionality, likely with multiple views)
|
||||
This is an enterprise-grade platform requiring multiple interconnected views (Strategy Cards creation, Workbench execution tracking, Portfolio management, KPI dashboards), role-based workflows, real-time data relationships, and sophisticated visualization of strategic hierarchies and dependencies.
|
||||
|
||||
## Essential Features
|
||||
|
||||
### Strategy Card Creation
|
||||
- **Functionality**: Structured strategy formulation using proven frameworks (SWOT, Value Disciplines, Strategic Pillars)
|
||||
- **Purpose**: Eliminate blank-page paralysis and ensure comprehensive strategic thinking
|
||||
- **Trigger**: User clicks "Create Strategy Card" or "New Strategic Initiative"
|
||||
- **Progression**: Select framework → Fill guided sections (Vision, Goals, Assumptions) → Add metrics → Review & Publish → Link to Workbench initiatives
|
||||
- **Success criteria**: User can create a complete strategy card in under 10 minutes; all critical strategic elements are captured with clear rationale
|
||||
|
||||
### Initiative Execution Tracking
|
||||
- **Functionality**: Translate strategic objectives into trackable initiatives with owners, timelines, and KPIs
|
||||
- **Purpose**: Bridge the gap between strategy and execution with clear accountability
|
||||
- **Trigger**: User creates initiative from Strategy Card or Workbench portfolio view
|
||||
- **Progression**: Define initiative → Assign owner → Set timeline & budget → Link strategic goals → Define KPIs → Track status updates
|
||||
- **Success criteria**: Every initiative clearly traces to strategic goals; real-time status is always current; owners receive clear accountability signals
|
||||
|
||||
### Portfolio Management & Governance
|
||||
- **Functionality**: Group initiatives into portfolios (M&A, OpEx, ESG) with capacity planning and impact analysis
|
||||
- **Purpose**: Enable enterprise-level prioritization and resource allocation decisions
|
||||
- **Trigger**: User navigates to "Portfolios" and creates/manages portfolio groups
|
||||
- **Progression**: Create portfolio → Add initiatives → Analyze capacity vs demand → Assess alignment score → Prioritize → Review governance dashboard
|
||||
- **Success criteria**: Leadership can assess entire strategic portfolio at a glance; capacity conflicts are immediately visible; prioritization decisions are data-informed
|
||||
|
||||
### Real-Time KPI Dashboard
|
||||
- **Functionality**: Multi-level scorecards from enterprise to initiative with drill-down capability
|
||||
- **Purpose**: Provide unified visibility into strategic progress and financial outcomes
|
||||
- **Trigger**: User accesses Dashboard or clicks metric in any view
|
||||
- **Progression**: View enterprise scorecard → Select portfolio or initiative → Drill into specific KPI → View trend & targets → Navigate to related initiatives
|
||||
- **Success criteria**: No ambiguity about performance; financial and operational metrics connected; accessible to all stakeholders
|
||||
|
||||
### Strategy-to-Execution Traceability
|
||||
- **Functionality**: Visual relationship mapping showing how initiatives link to strategic goals and outcomes
|
||||
- **Purpose**: Ensure every activity directly supports strategic intent; identify gaps
|
||||
- **Trigger**: User clicks "View Connections" or "Trace to Strategy" on any initiative
|
||||
- **Progression**: Select initiative or goal → View relationship map → Identify gaps or misalignments → Create new links or initiatives
|
||||
- **Success criteria**: Complete line of sight from daily work to enterprise strategy; no orphaned initiatives
|
||||
|
||||
## Edge Case Handling
|
||||
|
||||
- **Orphaned Initiatives** - Display warnings when initiatives lack strategic linkage; suggest potential connections
|
||||
- **Capacity Overload** - Flag portfolios exceeding resource capacity with visual indicators; recommend rebalancing
|
||||
- **Stale Data** - Highlight KPIs and initiatives not updated within defined thresholds; send gentle reminders to owners
|
||||
- **Conflicting Priorities** - Surface initiatives competing for same resources with conflict resolution workflow
|
||||
- **Incomplete Strategy Cards** - Auto-save drafts; provide completion checklist; allow progressive elaboration
|
||||
- **Access Control** - Role-based visibility ensuring executives see enterprise view while contributors focus on their initiatives
|
||||
|
||||
## Design Direction
|
||||
|
||||
The design should evoke **executive confidence, operational precision, and strategic clarity**. This is a tool for serious decisions—it must feel authoritative without being bureaucratic, data-rich without overwhelming, and sophisticated yet accessible. Visual language should suggest enterprise-grade reliability (think Bloomberg Terminal meets modern SaaS), with clear hierarchies, purposeful data density, and confident use of space to signal importance.
|
||||
|
||||
## Color Selection
|
||||
|
||||
**Deep Navy & Gold Executive Palette** - Conveys authority, strategic thinking, and high-stakes decision-making appropriate for C-suite and strategy offices.
|
||||
|
||||
- **Primary Color**: Deep Navy `oklch(0.28 0.05 250)` - Strategic authority and executive presence; used for primary navigation, key actions, and strategic elements
|
||||
- **Secondary Colors**:
|
||||
- Slate `oklch(0.42 0.02 250)` - Supporting structure for secondary actions and containers
|
||||
- Cool Gray `oklch(0.92 0.01 250)` - Subtle backgrounds and dividers maintaining visual hierarchy
|
||||
- **Accent Color**: Rich Gold `oklch(0.72 0.14 85)` - Strategic focus and achievement; highlights critical metrics, success states, and high-priority initiatives
|
||||
- **Foreground/Background Pairings**:
|
||||
- Primary Navy (oklch(0.28 0.05 250)): White text (oklch(0.99 0 0)) - Ratio 11.2:1 ✓
|
||||
- Slate (oklch(0.42 0.02 250)): White text (oklch(0.99 0 0)) - Ratio 6.8:1 ✓
|
||||
- Gold Accent (oklch(0.72 0.14 85)): Deep Navy text (oklch(0.28 0.05 250)) - Ratio 4.9:1 ✓
|
||||
- Background (oklch(0.98 0.005 250)): Foreground text (oklch(0.25 0.02 250)) - Ratio 13.4:1 ✓
|
||||
|
||||
## Font Selection
|
||||
|
||||
Typography should communicate **executive gravitas, analytical precision, and structured thinking**—appropriate for strategy documents and financial reporting while remaining highly scannable for dashboard views.
|
||||
|
||||
- **Typographic Hierarchy**:
|
||||
- H1 (Strategic Section Headers): Outfit Bold / 32px / tight (-0.02em) - Commands attention for major sections
|
||||
- H2 (Portfolio/Card Titles): Outfit SemiBold / 24px / tight (-0.01em) - Clear hierarchical signaling
|
||||
- H3 (Initiative Titles): Outfit Medium / 18px / normal - Balanced weight for scannability
|
||||
- Body (Descriptions/Content): Inter Regular / 15px / relaxed (1.6) - Exceptional readability for detailed content
|
||||
- Data/Metrics: JetBrains Mono Medium / 14px / normal - Precision and clarity for numbers, KPIs, dates
|
||||
- Captions/Labels: Inter Medium / 13px / wide (0.01em) / uppercase - Clear visual separation for metadata
|
||||
|
||||
## Animations
|
||||
|
||||
Animations should reinforce the sense of **authoritative transitions and data relationships**, never frivolous. Use purposeful motion to guide attention during navigation between strategy levels (enterprise → portfolio → initiative), smooth drill-downs that maintain spatial context, and subtle highlights when metrics update or thresholds are crossed. Strategic elements should feel weighty—cards and modals transition with slight deceleration suggesting substance and importance.
|
||||
|
||||
## Component Selection
|
||||
|
||||
- **Components**:
|
||||
- `Card` - Primary container for Strategy Cards, initiative summaries, and portfolio groups; add subtle shadow and border treatment for depth
|
||||
- `Tabs` - Navigation between Strategy Cards and Workbench; styled with underline indicator and bold active state
|
||||
- `Dialog` - Full-screen modals for creating/editing Strategy Cards and initiatives with structured forms
|
||||
- `Table` - Initiative lists, KPI scorecards, and portfolio views with sortable columns and row hover states
|
||||
- `Badge` - Status indicators (On Track, At Risk, Blocked), priority levels, and portfolio tags with semantic colors
|
||||
- `Progress` - Visual representation of initiative completion, capacity utilization, and KPI achievement
|
||||
- `Select` - Framework selection, owner assignment, portfolio categorization with clear dropdown styling
|
||||
- `Button` - Primary actions use solid navy with gold hover accent; secondary actions use outline style
|
||||
- `Separator` - Clear visual breaks between strategy sections and dashboard panels
|
||||
- `Avatar` - Owner identification with fallback initials
|
||||
- `ScrollArea` - Smooth scrolling for long strategy content and initiative lists
|
||||
|
||||
- **Customizations**:
|
||||
- Strategy Card component with collapsible framework sections (Vision, Goals, Metrics, Assumptions)
|
||||
- Traceability visualization using connected cards or tree view showing goal → initiative relationships
|
||||
- Portfolio capacity gauge combining Progress with numeric indicators
|
||||
- KPI scorecard component with trend indicators (up/down arrows) and target comparison
|
||||
- Initiative timeline component showing milestones and current progress
|
||||
|
||||
- **States**:
|
||||
- Buttons: Navy default → Gold hover → Pressed with slight scale → Disabled with reduced opacity
|
||||
- Cards: Subtle hover elevation; active state with gold left border; selected state with gold outline
|
||||
- Table rows: Hover with light slate background; selected with stronger slate background
|
||||
- Status badges: Green (On Track), Amber (At Risk), Red (Blocked), Gray (Not Started)
|
||||
|
||||
- **Icon Selection**:
|
||||
- `Strategy` - Use structured grid or layers icon for Strategy Cards
|
||||
- `ChartBar` - Workbench and execution tracking
|
||||
- `FolderOpen` - Portfolio management
|
||||
- `Target` - Goals and KPIs
|
||||
- `Link` - Traceability and connections
|
||||
- `Plus` - Create new cards/initiatives
|
||||
- `ArrowRight` - Drill-down and navigation
|
||||
- `Warning` - Risk flags and capacity alerts
|
||||
- `TrendUp/TrendDown` - KPI performance indicators
|
||||
|
||||
- **Spacing**:
|
||||
- Page margins: `p-8` for generous breathing room around main content
|
||||
- Card padding: `p-6` for substantial internal space
|
||||
- Section gaps: `gap-6` between major sections; `gap-4` between related elements
|
||||
- List items: `py-3` for comfortable touch targets and scannability
|
||||
|
||||
- **Mobile**:
|
||||
- Tab navigation converts to bottom sheet selector
|
||||
- Strategy Cards stack vertically with collapsible sections expanded one at a time
|
||||
- Tables transform to card-based lists with key data prioritized
|
||||
- Portfolio dashboard shows one metric card at a time with horizontal swipe
|
||||
- Create/Edit dialogs use full viewport with simplified forms
|
||||
- Reduce typographic scale by 10-15% for mobile readability
|
||||
@@ -4,9 +4,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title></title>
|
||||
<title>StrategyOS - Strategy Management Platform</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
|
||||
<link href="/src/main.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
|
||||
82
src/App.tsx
82
src/App.tsx
@@ -1,5 +1,85 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState } from 'react'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Strategy, ChartBar, FolderOpen, Target } from '@phosphor-icons/react'
|
||||
import StrategyCards from './components/StrategyCards'
|
||||
import Workbench from './components/Workbench'
|
||||
import Portfolios from './components/Portfolios'
|
||||
import Dashboard from './components/Dashboard'
|
||||
import type { StrategyCard, Initiative } from './types'
|
||||
|
||||
function App() {
|
||||
return <div></div>
|
||||
const [strategyCards] = useKV<StrategyCard[]>('strategy-cards', [])
|
||||
const [initiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
const [activeTab, setActiveTab] = useState('strategy')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
|
||||
<div className="container mx-auto px-8 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
StrategyOS
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1 font-medium tracking-wide uppercase">
|
||||
Strategy Management Platform
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 font-mono text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs uppercase tracking-wider">Cards</span>
|
||||
<span className="text-accent font-semibold text-base">{strategyCards?.length || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs uppercase tracking-wider">Initiatives</span>
|
||||
<span className="text-accent font-semibold text-base">{initiatives?.length || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-8 py-8">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4 mb-8 h-14 bg-muted/50">
|
||||
<TabsTrigger value="strategy" className="gap-2 text-base font-semibold data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
|
||||
<Strategy size={20} weight="bold" />
|
||||
Strategy Cards
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="workbench" className="gap-2 text-base font-semibold data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
|
||||
<ChartBar size={20} weight="bold" />
|
||||
Workbench
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="portfolios" className="gap-2 text-base font-semibold data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
|
||||
<FolderOpen size={20} weight="bold" />
|
||||
Portfolios
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="dashboard" className="gap-2 text-base font-semibold data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
|
||||
<Target size={20} weight="bold" />
|
||||
Dashboard
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="strategy" className="mt-0">
|
||||
<StrategyCards />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="workbench" className="mt-0">
|
||||
<Workbench />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="portfolios" className="mt-0">
|
||||
<Portfolios />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="dashboard" className="mt-0">
|
||||
<Dashboard />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
279
src/components/Dashboard.tsx
Normal file
279
src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Target, TrendUp, TrendDown, Minus, CheckCircle, Warning, Link as LinkIcon } from '@phosphor-icons/react'
|
||||
import type { Initiative, StrategyCard } from '@/types'
|
||||
|
||||
export default function Dashboard() {
|
||||
const [initiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
const [strategyCards] = useKV<StrategyCard[]>('strategy-cards', [])
|
||||
|
||||
const totalInitiatives = initiatives?.length || 0
|
||||
const totalBudget = initiatives?.reduce((sum, i) => sum + i.budget, 0) || 0
|
||||
const avgProgress = totalInitiatives > 0
|
||||
? Math.round((initiatives?.reduce((sum, i) => sum + i.progress, 0) || 0) / totalInitiatives)
|
||||
: 0
|
||||
|
||||
const statusBreakdown = {
|
||||
onTrack: initiatives?.filter(i => i.status === 'on-track').length || 0,
|
||||
atRisk: initiatives?.filter(i => i.status === 'at-risk').length || 0,
|
||||
blocked: initiatives?.filter(i => i.status === 'blocked').length || 0,
|
||||
notStarted: initiatives?.filter(i => i.status === 'not-started').length || 0,
|
||||
completed: initiatives?.filter(i => i.status === 'completed').length || 0,
|
||||
}
|
||||
|
||||
const strategicAlignment = strategyCards?.map(card => {
|
||||
const linkedInitiatives = initiatives?.filter(i => i.strategyCardId === card.id) || []
|
||||
const totalLinkedBudget = linkedInitiatives.reduce((sum, i) => sum + i.budget, 0)
|
||||
const avgLinkedProgress = linkedInitiatives.length > 0
|
||||
? Math.round(linkedInitiatives.reduce((sum, i) => sum + i.progress, 0) / linkedInitiatives.length)
|
||||
: 0
|
||||
|
||||
return {
|
||||
card,
|
||||
initiativeCount: linkedInitiatives.length,
|
||||
totalBudget: totalLinkedBudget,
|
||||
avgProgress: avgLinkedProgress,
|
||||
}
|
||||
}) || []
|
||||
|
||||
const kpiMockData = [
|
||||
{ name: 'Cost Reduction', current: 8.2, target: 10, unit: 'M USD', trend: 'up' as const },
|
||||
{ name: 'Cycle Time Improvement', current: 18, target: 25, unit: '%', trend: 'up' as const },
|
||||
{ name: 'Customer Satisfaction', current: 87, target: 90, unit: 'NPS', trend: 'up' as const },
|
||||
{ name: 'Revenue Growth', current: 12.5, target: 15, unit: '%', trend: 'up' as const },
|
||||
]
|
||||
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return TrendUp
|
||||
case 'down': return TrendDown
|
||||
default: return Minus
|
||||
}
|
||||
}
|
||||
|
||||
const getKPIProgress = (current: number, target: number) => {
|
||||
return Math.min((current / target) * 100, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Executive Dashboard</h2>
|
||||
<p className="text-muted-foreground mt-1">Real-time view of strategic execution</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription className="text-xs uppercase tracking-wider">Total Initiatives</CardDescription>
|
||||
<CardTitle className="text-4xl font-bold font-mono text-accent">{totalInitiatives}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Badge className="bg-success text-white text-xs">{statusBreakdown.onTrack} On Track</Badge>
|
||||
{statusBreakdown.atRisk > 0 && (
|
||||
<Badge className="bg-at-risk text-white text-xs">{statusBreakdown.atRisk} At Risk</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription className="text-xs uppercase tracking-wider">Total Investment</CardDescription>
|
||||
<CardTitle className="text-4xl font-bold font-mono text-accent">
|
||||
${(totalBudget / 1000000).toFixed(1)}M
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Across {strategyCards?.length || 0} strategic themes
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription className="text-xs uppercase tracking-wider">Average Progress</CardDescription>
|
||||
<CardTitle className="text-4xl font-bold font-mono text-accent">{avgProgress}%</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={avgProgress} className="h-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription className="text-xs uppercase tracking-wider">Completion Rate</CardDescription>
|
||||
<CardTitle className="text-4xl font-bold font-mono text-accent">
|
||||
{totalInitiatives > 0 ? Math.round((statusBreakdown.completed / totalInitiatives) * 100) : 0}%
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{statusBreakdown.completed} of {totalInitiatives} completed
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CheckCircle size={20} weight="fill" className="text-accent" />
|
||||
Initiative Status Breakdown
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">On Track</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Progress value={totalInitiatives > 0 ? (statusBreakdown.onTrack / totalInitiatives) * 100 : 0} className="w-32 h-2" />
|
||||
<span className="font-mono font-semibold text-success w-8">{statusBreakdown.onTrack}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">At Risk</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Progress value={totalInitiatives > 0 ? (statusBreakdown.atRisk / totalInitiatives) * 100 : 0} className="w-32 h-2" />
|
||||
<span className="font-mono font-semibold text-at-risk w-8">{statusBreakdown.atRisk}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Blocked</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Progress value={totalInitiatives > 0 ? (statusBreakdown.blocked / totalInitiatives) * 100 : 0} className="w-32 h-2" />
|
||||
<span className="font-mono font-semibold text-destructive w-8">{statusBreakdown.blocked}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Not Started</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Progress value={totalInitiatives > 0 ? (statusBreakdown.notStarted / totalInitiatives) * 100 : 0} className="w-32 h-2" />
|
||||
<span className="font-mono font-semibold text-muted-foreground w-8">{statusBreakdown.notStarted}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Completed</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Progress value={totalInitiatives > 0 ? (statusBreakdown.completed / totalInitiatives) * 100 : 0} className="w-32 h-2" />
|
||||
<span className="font-mono font-semibold text-primary w-8">{statusBreakdown.completed}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{statusBreakdown.atRisk + statusBreakdown.blocked > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex items-center gap-2 text-sm text-warning">
|
||||
<Warning size={16} weight="fill" />
|
||||
<span className="font-medium">
|
||||
{statusBreakdown.atRisk + statusBreakdown.blocked} {statusBreakdown.atRisk + statusBreakdown.blocked === 1 ? 'initiative requires' : 'initiatives require'} attention
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Target size={20} weight="fill" className="text-accent" />
|
||||
Key Performance Indicators
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{kpiMockData.map((kpi) => {
|
||||
const TrendIcon = getTrendIcon(kpi.trend)
|
||||
const progress = getKPIProgress(kpi.current, kpi.target)
|
||||
const isOnTarget = progress >= 80
|
||||
|
||||
return (
|
||||
<div key={kpi.name} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">{kpi.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendIcon size={16} weight="bold" className={isOnTarget ? 'text-success' : 'text-muted-foreground'} />
|
||||
<span className="font-mono text-sm font-semibold">
|
||||
{kpi.current} / {kpi.target} {kpi.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<LinkIcon size={20} weight="fill" className="text-accent" />
|
||||
Strategic Alignment & Traceability
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
View how initiatives map to strategic goals
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{strategicAlignment.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>Create strategy cards to view strategic alignment</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{strategicAlignment.map(({ card, initiativeCount, totalBudget, avgProgress }) => (
|
||||
<div key={card.id} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-lg">{card.title}</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{card.framework.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{initiativeCount} {initiativeCount === 1 ? 'Initiative' : 'Initiatives'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{initiativeCount > 0 ? (
|
||||
<div className="grid grid-cols-3 gap-4 pt-3 border-t">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Investment</p>
|
||||
<p className="font-mono font-semibold text-accent">
|
||||
${(totalBudget / 1000000).toFixed(2)}M
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Avg Progress</p>
|
||||
<p className="font-mono font-semibold text-accent">{avgProgress}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Goals Defined</p>
|
||||
<p className="font-mono font-semibold">{card.goals.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-3 border-t">
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No initiatives linked yet - create initiatives in the Workbench
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
211
src/components/Portfolios.tsx
Normal file
211
src/components/Portfolios.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { FolderOpen, ChartBar, Warning, TrendUp } from '@phosphor-icons/react'
|
||||
import type { Initiative, Portfolio as PortfolioType } from '@/types'
|
||||
|
||||
const portfolioDefinitions: PortfolioType[] = [
|
||||
{
|
||||
type: 'operational-excellence',
|
||||
name: 'Operational Excellence',
|
||||
description: 'Lean, Hoshin Kanri, and continuous improvement initiatives',
|
||||
capacity: 100,
|
||||
utilized: 0,
|
||||
},
|
||||
{
|
||||
type: 'ma',
|
||||
name: 'M&A',
|
||||
description: 'Merger and acquisition integration programs',
|
||||
capacity: 100,
|
||||
utilized: 0,
|
||||
},
|
||||
{
|
||||
type: 'financial-transformation',
|
||||
name: 'Financial Transformation',
|
||||
description: 'Finance operations and systems modernization',
|
||||
capacity: 100,
|
||||
utilized: 0,
|
||||
},
|
||||
{
|
||||
type: 'esg',
|
||||
name: 'ESG',
|
||||
description: 'Environmental, Social, and Governance programs',
|
||||
capacity: 100,
|
||||
utilized: 0,
|
||||
},
|
||||
{
|
||||
type: 'innovation',
|
||||
name: 'Innovation',
|
||||
description: 'New products, services, and business models',
|
||||
capacity: 100,
|
||||
utilized: 0,
|
||||
},
|
||||
]
|
||||
|
||||
export default function Portfolios() {
|
||||
const [initiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
|
||||
const getPortfolioMetrics = (portfolioType: string) => {
|
||||
const portfolioInitiatives = initiatives?.filter(i => i.portfolio === portfolioType) || []
|
||||
const totalBudget = portfolioInitiatives.reduce((sum, i) => sum + i.budget, 0)
|
||||
const avgProgress = portfolioInitiatives.length > 0
|
||||
? portfolioInitiatives.reduce((sum, i) => sum + i.progress, 0) / portfolioInitiatives.length
|
||||
: 0
|
||||
|
||||
const statusCounts = {
|
||||
onTrack: portfolioInitiatives.filter(i => i.status === 'on-track').length,
|
||||
atRisk: portfolioInitiatives.filter(i => i.status === 'at-risk').length,
|
||||
blocked: portfolioInitiatives.filter(i => i.status === 'blocked').length,
|
||||
}
|
||||
|
||||
const capacityUsed = portfolioInitiatives.length * 20
|
||||
|
||||
return {
|
||||
count: portfolioInitiatives.length,
|
||||
totalBudget,
|
||||
avgProgress: Math.round(avgProgress),
|
||||
statusCounts,
|
||||
capacityUsed: Math.min(capacityUsed, 100),
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Portfolio Management</h2>
|
||||
<p className="text-muted-foreground mt-1">Strategic portfolio analysis and governance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{portfolioDefinitions.map((portfolio) => {
|
||||
const metrics = getPortfolioMetrics(portfolio.type)
|
||||
const hasRisks = metrics.statusCounts.atRisk > 0 || metrics.statusCounts.blocked > 0
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={portfolio.type}
|
||||
className={`hover:shadow-lg transition-all ${hasRisks ? 'border-l-4 border-l-warning' : ''}`}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="rounded-lg bg-primary/10 p-3">
|
||||
<FolderOpen size={24} weight="fill" className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">{portfolio.name}</CardTitle>
|
||||
<CardDescription className="mt-1">{portfolio.description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<Badge variant="outline" className="font-mono text-base px-3 py-1">
|
||||
{metrics.count} {metrics.count === 1 ? 'Initiative' : 'Initiatives'}
|
||||
</Badge>
|
||||
{hasRisks && (
|
||||
<Badge className="bg-warning text-white gap-1.5">
|
||||
<Warning size={14} weight="fill" />
|
||||
Attention Required
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground uppercase tracking-wide">
|
||||
<ChartBar size={14} />
|
||||
<span>Total Budget</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold font-mono text-accent">
|
||||
${(metrics.totalBudget / 1000000).toFixed(1)}M
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground uppercase tracking-wide">
|
||||
<TrendUp size={14} />
|
||||
<span>Avg Progress</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold font-mono text-accent">
|
||||
{metrics.avgProgress}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">Status Summary</p>
|
||||
<div className="flex gap-2">
|
||||
{metrics.statusCounts.onTrack > 0 && (
|
||||
<Badge className="bg-success text-white text-xs">
|
||||
{metrics.statusCounts.onTrack} On Track
|
||||
</Badge>
|
||||
)}
|
||||
{metrics.statusCounts.atRisk > 0 && (
|
||||
<Badge className="bg-at-risk text-white text-xs">
|
||||
{metrics.statusCounts.atRisk} At Risk
|
||||
</Badge>
|
||||
)}
|
||||
{metrics.statusCounts.blocked > 0 && (
|
||||
<Badge className="bg-destructive text-white text-xs">
|
||||
{metrics.statusCounts.blocked} Blocked
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">Capacity</p>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-mono font-semibold">
|
||||
{metrics.capacityUsed}%
|
||||
</span>
|
||||
<span className={`text-xs font-medium ${metrics.capacityUsed > 80 ? 'text-warning' : 'text-muted-foreground'}`}>
|
||||
{metrics.capacityUsed > 80 ? 'High utilization' : 'Available'}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={metrics.capacityUsed} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{metrics.count === 0 && (
|
||||
<div className="pt-4 border-t">
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No initiatives in this portfolio yet
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card className="bg-muted/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Portfolio Governance Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-foreground">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-accent font-bold">•</span>
|
||||
<p><span className="font-semibold">Capacity Planning:</span> Each portfolio shows utilization based on active initiatives (estimated 20% capacity per initiative)</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-accent font-bold">•</span>
|
||||
<p><span className="font-semibold">Risk Flags:</span> Portfolios with at-risk or blocked initiatives receive attention markers</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-accent font-bold">•</span>
|
||||
<p><span className="font-semibold">Resource Balancing:</span> Monitor capacity utilization to avoid portfolio overload and ensure delivery success</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
358
src/components/StrategyCards.tsx
Normal file
358
src/components/StrategyCards.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogDescription, 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 { Separator } from '@/components/ui/separator'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Plus, Target, Lightbulb, ChartLineUp } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { StrategyCard } from '@/types'
|
||||
|
||||
const frameworks = [
|
||||
{ value: 'swot', label: 'SWOT Analysis', description: 'Strengths, Weaknesses, Opportunities, Threats' },
|
||||
{ value: 'value-disciplines', label: 'Value Disciplines', description: 'Operational Excellence, Product Leadership, Customer Intimacy' },
|
||||
{ value: 'strategic-pillars', label: 'Strategic Pillars', description: 'Core strategic themes and priorities' },
|
||||
{ value: 'hoshin-kanri', label: 'Hoshin Kanri', description: 'Policy deployment and strategic alignment' },
|
||||
{ value: 'okr', label: 'OKR Framework', description: 'Objectives and Key Results' },
|
||||
]
|
||||
|
||||
export default function StrategyCards() {
|
||||
const [strategyCards, setStrategyCards] = useKV<StrategyCard[]>('strategy-cards', [])
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [selectedCard, setSelectedCard] = useState<StrategyCard | null>(null)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
framework: '',
|
||||
vision: '',
|
||||
goals: '',
|
||||
metrics: '',
|
||||
assumptions: '',
|
||||
})
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!formData.title || !formData.framework || !formData.vision) {
|
||||
toast.error('Please fill in required fields')
|
||||
return
|
||||
}
|
||||
|
||||
const newCard: StrategyCard = {
|
||||
id: `card-${Date.now()}`,
|
||||
title: formData.title,
|
||||
framework: formData.framework,
|
||||
vision: formData.vision,
|
||||
goals: formData.goals.split('\n').filter(g => g.trim()),
|
||||
metrics: formData.metrics.split('\n').filter(m => m.trim()),
|
||||
assumptions: formData.assumptions.split('\n').filter(a => a.trim()),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
setStrategyCards((current) => [...(current || []), newCard])
|
||||
toast.success('Strategy Card created successfully')
|
||||
setIsDialogOpen(false)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
framework: '',
|
||||
vision: '',
|
||||
goals: '',
|
||||
metrics: '',
|
||||
assumptions: '',
|
||||
})
|
||||
}
|
||||
|
||||
const handleCardClick = (card: StrategyCard) => {
|
||||
setSelectedCard(card)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Strategy Cards</h2>
|
||||
<p className="text-muted-foreground mt-1">Create and manage strategic frameworks</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2 bg-primary hover:bg-accent hover:text-accent-foreground transition-colors">
|
||||
<Plus size={18} weight="bold" />
|
||||
Create Strategy Card
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">Create New Strategy Card</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use a proven framework to structure your strategic thinking
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[70vh] pr-4">
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Strategy Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g., Digital Transformation 2025"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="framework">Framework *</Label>
|
||||
<Select value={formData.framework} onValueChange={(value) => setFormData({ ...formData, framework: value })}>
|
||||
<SelectTrigger id="framework">
|
||||
<SelectValue placeholder="Select a strategic framework" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{frameworks.map((fw) => (
|
||||
<SelectItem key={fw.value} value={fw.value}>
|
||||
<div>
|
||||
<div className="font-semibold">{fw.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{fw.description}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vision">Vision Statement *</Label>
|
||||
<Textarea
|
||||
id="vision"
|
||||
placeholder="Describe the future state you want to achieve..."
|
||||
rows={3}
|
||||
value={formData.vision}
|
||||
onChange={(e) => setFormData({ ...formData, vision: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="goals">Strategic Goals (one per line)</Label>
|
||||
<Textarea
|
||||
id="goals"
|
||||
placeholder="Enter strategic goals, one per line..."
|
||||
rows={4}
|
||||
value={formData.goals}
|
||||
onChange={(e) => setFormData({ ...formData, goals: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="metrics">Key Metrics (one per line)</Label>
|
||||
<Textarea
|
||||
id="metrics"
|
||||
placeholder="Enter metrics to track, one per line..."
|
||||
rows={4}
|
||||
value={formData.metrics}
|
||||
onChange={(e) => setFormData({ ...formData, metrics: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="assumptions">Key Assumptions (one per line)</Label>
|
||||
<Textarea
|
||||
id="assumptions"
|
||||
placeholder="Enter assumptions, one per line..."
|
||||
rows={4}
|
||||
value={formData.assumptions}
|
||||
onChange={(e) => setFormData({ ...formData, assumptions: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleCreate} className="bg-primary hover:bg-accent hover:text-accent-foreground">Create Card</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{(!strategyCards || strategyCards.length === 0) ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="rounded-full bg-muted p-6 mb-4">
|
||||
<Target size={48} weight="duotone" className="text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">No Strategy Cards Yet</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
||||
Start by creating your first strategy card using a proven framework
|
||||
</p>
|
||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||
<Plus size={18} weight="bold" />
|
||||
Create Your First Card
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{strategyCards.map((card) => (
|
||||
<Card
|
||||
key={card.id}
|
||||
className="cursor-pointer hover:shadow-lg hover:border-accent/50 transition-all group"
|
||||
onClick={() => handleCardClick(card)}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-xl group-hover:text-accent transition-colors">{card.title}</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{frameworks.find(f => f.value === card.framework)?.label || card.framework}
|
||||
</Badge>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Lightbulb size={16} weight="fill" className="text-accent" />
|
||||
<span className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Vision</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground line-clamp-2">{card.vision}</p>
|
||||
</div>
|
||||
|
||||
{card.goals.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Target size={16} weight="fill" className="text-accent" />
|
||||
<span className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Goals</span>
|
||||
</div>
|
||||
<ul className="text-sm space-y-1">
|
||||
{card.goals.slice(0, 2).map((goal, idx) => (
|
||||
<li key={idx} className="text-foreground line-clamp-1">• {goal}</li>
|
||||
))}
|
||||
{card.goals.length > 2 && (
|
||||
<li className="text-muted-foreground italic">+{card.goals.length - 2} more</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{card.metrics.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<ChartLineUp size={16} weight="fill" className="text-accent" />
|
||||
<span className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Metrics</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{card.metrics.slice(0, 3).map((metric, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">
|
||||
{metric}
|
||||
</Badge>
|
||||
))}
|
||||
{card.metrics.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs">+{card.metrics.length - 3}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t text-xs text-muted-foreground font-mono">
|
||||
Created {new Date(card.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedCard && (
|
||||
<Dialog open={!!selectedCard} onOpenChange={() => setSelectedCard(null)}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">{selectedCard.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Badge variant="outline" className="mt-2">
|
||||
{frameworks.find(f => f.value === selectedCard.framework)?.label}
|
||||
</Badge>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[70vh] pr-4">
|
||||
<div className="space-y-6 py-4">
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||
<Lightbulb size={16} weight="fill" className="text-accent" />
|
||||
Vision Statement
|
||||
</h4>
|
||||
<p className="text-foreground">{selectedCard.vision}</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{selectedCard.goals.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide mb-3 flex items-center gap-2">
|
||||
<Target size={16} weight="fill" className="text-accent" />
|
||||
Strategic Goals
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{selectedCard.goals.map((goal, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3">
|
||||
<span className="text-accent font-bold font-mono text-sm mt-0.5">{idx + 1}.</span>
|
||||
<span className="text-foreground">{goal}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedCard.metrics.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide mb-3 flex items-center gap-2">
|
||||
<ChartLineUp size={16} weight="fill" className="text-accent" />
|
||||
Key Metrics
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{selectedCard.metrics.map((metric, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3">
|
||||
<span className="text-accent font-bold font-mono text-sm mt-0.5">•</span>
|
||||
<span className="text-foreground">{metric}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedCard.assumptions.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide mb-3">Key Assumptions</h4>
|
||||
<ul className="space-y-2">
|
||||
{selectedCard.assumptions.map((assumption, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="text-foreground">{assumption}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
475
src/components/Workbench.tsx
Normal file
475
src/components/Workbench.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogDescription, 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 { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Plus, User, Calendar, CurrencyDollar, CheckCircle, Warning, XCircle, Clock } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { Initiative, StrategyCard, StatusType, PriorityType, PortfolioType } from '@/types'
|
||||
|
||||
const statusConfig = {
|
||||
'not-started': { label: 'Not Started', color: 'bg-muted text-muted-foreground', icon: Clock },
|
||||
'on-track': { label: 'On Track', color: 'bg-success text-white', icon: CheckCircle },
|
||||
'at-risk': { label: 'At Risk', color: 'bg-at-risk text-white', icon: Warning },
|
||||
'blocked': { label: 'Blocked', color: 'bg-destructive text-white', icon: XCircle },
|
||||
'completed': { label: 'Completed', color: 'bg-primary text-primary-foreground', icon: CheckCircle },
|
||||
}
|
||||
|
||||
const priorityConfig = {
|
||||
'critical': { label: 'Critical', color: 'bg-destructive text-destructive-foreground' },
|
||||
'high': { label: 'High', color: 'bg-at-risk text-white' },
|
||||
'medium': { label: 'Medium', color: 'bg-secondary text-secondary-foreground' },
|
||||
'low': { label: 'Low', color: 'bg-muted text-muted-foreground' },
|
||||
}
|
||||
|
||||
const portfolioOptions = [
|
||||
{ value: 'operational-excellence', label: 'Operational Excellence' },
|
||||
{ value: 'ma', label: 'M&A' },
|
||||
{ value: 'financial-transformation', label: 'Financial Transformation' },
|
||||
{ value: 'esg', label: 'ESG' },
|
||||
{ value: 'innovation', label: 'Innovation' },
|
||||
]
|
||||
|
||||
export default function Workbench() {
|
||||
const [initiatives, setInitiatives] = useKV<Initiative[]>('initiatives', [])
|
||||
const [strategyCards] = useKV<StrategyCard[]>('strategy-cards', [])
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [selectedInitiative, setSelectedInitiative] = useState<Initiative | null>(null)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
strategyCardId: '',
|
||||
owner: '',
|
||||
status: 'not-started' as StatusType,
|
||||
priority: 'medium' as PriorityType,
|
||||
portfolio: 'operational-excellence' as PortfolioType,
|
||||
progress: 0,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
budget: 0,
|
||||
})
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!formData.title || !formData.strategyCardId || !formData.owner) {
|
||||
toast.error('Please fill in required fields')
|
||||
return
|
||||
}
|
||||
|
||||
const newInitiative: Initiative = {
|
||||
id: `init-${Date.now()}`,
|
||||
...formData,
|
||||
kpis: [],
|
||||
}
|
||||
|
||||
setInitiatives((current) => [...(current || []), newInitiative])
|
||||
toast.success('Initiative created successfully')
|
||||
setIsDialogOpen(false)
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
strategyCardId: '',
|
||||
owner: '',
|
||||
status: 'not-started',
|
||||
priority: 'medium',
|
||||
portfolio: 'operational-excellence',
|
||||
progress: 0,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
budget: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const getOwnerInitials = (name: string) => {
|
||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Workbench</h2>
|
||||
<p className="text-muted-foreground mt-1">Execute and track strategic initiatives</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2 bg-primary hover:bg-accent hover:text-accent-foreground transition-colors" disabled={!strategyCards || strategyCards.length === 0}>
|
||||
<Plus size={18} weight="bold" />
|
||||
Create Initiative
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">Create New Initiative</DialogTitle>
|
||||
<DialogDescription>
|
||||
Link this initiative to a strategy card and define execution details
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[70vh] pr-4">
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Initiative Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g., Implement Cloud Infrastructure"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Describe the initiative..."
|
||||
rows={3}
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="strategyCard">Link to Strategy Card *</Label>
|
||||
<Select value={formData.strategyCardId} onValueChange={(value) => setFormData({ ...formData, strategyCardId: value })}>
|
||||
<SelectTrigger id="strategyCard">
|
||||
<SelectValue placeholder="Select a strategy card" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{strategyCards?.map((card) => (
|
||||
<SelectItem key={card.id} value={card.id}>
|
||||
{card.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="portfolio">Portfolio</Label>
|
||||
<Select value={formData.portfolio} onValueChange={(value) => setFormData({ ...formData, portfolio: value as PortfolioType })}>
|
||||
<SelectTrigger id="portfolio">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{portfolioOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="priority">Priority</Label>
|
||||
<Select value={formData.priority} onValueChange={(value) => setFormData({ ...formData, priority: value as PriorityType })}>
|
||||
<SelectTrigger id="priority">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="owner">Initiative Owner *</Label>
|
||||
<Input
|
||||
id="owner"
|
||||
placeholder="e.g., Sarah Johnson"
|
||||
value={formData.owner}
|
||||
onChange={(e) => setFormData({ ...formData, owner: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="startDate">Start Date</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
type="date"
|
||||
value={formData.startDate}
|
||||
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Input
|
||||
id="endDate"
|
||||
type="date"
|
||||
value={formData.endDate}
|
||||
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="budget">Budget (USD)</Label>
|
||||
<Input
|
||||
id="budget"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={formData.budget || ''}
|
||||
onChange={(e) => setFormData({ ...formData, budget: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleCreate} className="bg-primary hover:bg-accent hover:text-accent-foreground">Create Initiative</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{!strategyCards || strategyCards.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="rounded-full bg-muted p-6 mb-4">
|
||||
<Warning size={48} weight="duotone" className="text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">Create a Strategy Card First</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md">
|
||||
Initiatives must be linked to a Strategy Card. Go to the Strategy Cards tab to create one.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (!initiatives || initiatives.length === 0) ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="rounded-full bg-muted p-6 mb-4">
|
||||
<CheckCircle size={48} weight="duotone" className="text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">No Initiatives Yet</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md mb-6">
|
||||
Start executing your strategy by creating your first initiative
|
||||
</p>
|
||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||
<Plus size={18} weight="bold" />
|
||||
Create Your First Initiative
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{initiatives.map((initiative) => {
|
||||
const status = statusConfig[initiative.status]
|
||||
const priority = priorityConfig[initiative.priority]
|
||||
const StatusIcon = status.icon
|
||||
const linkedCard = strategyCards?.find(c => c.id === initiative.strategyCardId)
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={initiative.id}
|
||||
className="hover:shadow-lg hover:border-accent/50 transition-all cursor-pointer group"
|
||||
onClick={() => setSelectedInitiative(initiative)}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CardTitle className="text-xl group-hover:text-accent transition-colors">
|
||||
{initiative.title}
|
||||
</CardTitle>
|
||||
<Badge className={priority.color}>{priority.label}</Badge>
|
||||
</div>
|
||||
{linkedCard && (
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<span className="text-xs uppercase tracking-wider font-medium">Linked to:</span>
|
||||
<Badge variant="outline">{linkedCard.title}</Badge>
|
||||
</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className={`${status.color} gap-1.5`}>
|
||||
<StatusIcon size={14} weight="fill" />
|
||||
{status.label}
|
||||
</Badge>
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarFallback className="bg-secondary text-secondary-foreground font-semibold">
|
||||
{getOwnerInitials(initiative.owner)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{initiative.description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">{initiative.description}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground font-medium">Progress</span>
|
||||
<span className="font-mono font-semibold text-accent">{initiative.progress}%</span>
|
||||
</div>
|
||||
<Progress value={initiative.progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 pt-2 border-t">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground uppercase tracking-wide">
|
||||
<User size={14} />
|
||||
<span>Owner</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium">{initiative.owner}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground uppercase tracking-wide">
|
||||
<Calendar size={14} />
|
||||
<span>Timeline</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium font-mono">
|
||||
{initiative.startDate && initiative.endDate
|
||||
? `${new Date(initiative.startDate).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })} - ${new Date(initiative.endDate).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })}`
|
||||
: 'Not set'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground uppercase tracking-wide">
|
||||
<CurrencyDollar size={14} />
|
||||
<span>Budget</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium font-mono">
|
||||
${initiative.budget.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{portfolioOptions.find(p => p.value === initiative.portfolio)?.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedInitiative && (
|
||||
<Dialog open={!!selectedInitiative} onOpenChange={() => setSelectedInitiative(null)}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<DialogTitle className="text-2xl">{selectedInitiative.title}</DialogTitle>
|
||||
<DialogDescription className="mt-2 flex items-center gap-2">
|
||||
{strategyCards?.find(c => c.id === selectedInitiative.strategyCardId) && (
|
||||
<>
|
||||
<span className="text-xs uppercase tracking-wider font-medium">Linked to:</span>
|
||||
<Badge variant="outline">
|
||||
{strategyCards.find(c => c.id === selectedInitiative.strategyCardId)?.title}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={priorityConfig[selectedInitiative.priority].color}>
|
||||
{priorityConfig[selectedInitiative.priority].label}
|
||||
</Badge>
|
||||
<Badge className={statusConfig[selectedInitiative.status].color}>
|
||||
{statusConfig[selectedInitiative.status].label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[70vh] pr-4">
|
||||
<div className="space-y-6 py-4">
|
||||
{selectedInitiative.description && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide mb-2">Description</h4>
|
||||
<p className="text-foreground">{selectedInitiative.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide">Progress</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Overall Completion</span>
|
||||
<span className="font-mono font-semibold text-lg text-accent">{selectedInitiative.progress}%</span>
|
||||
</div>
|
||||
<Progress value={selectedInitiative.progress} className="h-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide">Owner</h4>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarFallback className="bg-secondary text-secondary-foreground font-semibold text-lg">
|
||||
{getOwnerInitials(selectedInitiative.owner)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{selectedInitiative.owner}</p>
|
||||
<p className="text-xs text-muted-foreground">Initiative Owner</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide">Portfolio</h4>
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{portfolioOptions.find(p => p.value === selectedInitiative.portfolio)?.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide">Timeline</h4>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Start Date</span>
|
||||
<span className="font-mono font-medium">
|
||||
{selectedInitiative.startDate ? new Date(selectedInitiative.startDate).toLocaleDateString() : 'Not set'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">End Date</span>
|
||||
<span className="font-mono font-medium">
|
||||
{selectedInitiative.endDate ? new Date(selectedInitiative.endDate).toLocaleDateString() : 'Not set'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm uppercase tracking-wide">Budget</h4>
|
||||
<p className="text-2xl font-bold font-mono text-accent">
|
||||
${selectedInitiative.budget.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,83 @@
|
||||
/* This is where custom CSS goes */
|
||||
@import 'tailwindcss';
|
||||
@import "tw-animate-css";
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0.98 0.005 250);
|
||||
--foreground: oklch(0.25 0.02 250);
|
||||
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.25 0.02 250);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.25 0.02 250);
|
||||
|
||||
--primary: oklch(0.28 0.05 250);
|
||||
--primary-foreground: oklch(0.99 0 0);
|
||||
--secondary: oklch(0.42 0.02 250);
|
||||
--secondary-foreground: oklch(0.99 0 0);
|
||||
--muted: oklch(0.92 0.01 250);
|
||||
--muted-foreground: oklch(0.50 0.02 250);
|
||||
--accent: oklch(0.72 0.14 85);
|
||||
--accent-foreground: oklch(0.28 0.05 250);
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.99 0 0);
|
||||
|
||||
--border: oklch(0.88 0.01 250);
|
||||
--input: oklch(0.88 0.01 250);
|
||||
--ring: oklch(0.72 0.14 85);
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--success: oklch(0.65 0.15 145);
|
||||
--warning: oklch(0.75 0.15 75);
|
||||
--at-risk: oklch(0.70 0.18 65);
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-success: var(--success);
|
||||
--color-warning: var(--warning);
|
||||
--color-at-risk: var(--at-risk);
|
||||
|
||||
--radius-sm: calc(var(--radius) * 0.5);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) * 1.5);
|
||||
--radius-xl: calc(var(--radius) * 2);
|
||||
--radius-2xl: calc(var(--radius) * 3);
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
48
src/types/index.ts
Normal file
48
src/types/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export type StatusType = 'not-started' | 'on-track' | 'at-risk' | 'blocked' | 'completed'
|
||||
export type PriorityType = 'critical' | 'high' | 'medium' | 'low'
|
||||
export type PortfolioType = 'operational-excellence' | 'ma' | 'financial-transformation' | 'esg' | 'innovation'
|
||||
|
||||
export interface StrategyCard {
|
||||
id: string
|
||||
title: string
|
||||
framework: string
|
||||
vision: string
|
||||
goals: string[]
|
||||
metrics: string[]
|
||||
assumptions: string[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface Initiative {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
strategyCardId: string
|
||||
owner: string
|
||||
status: StatusType
|
||||
priority: PriorityType
|
||||
portfolio: PortfolioType
|
||||
progress: number
|
||||
startDate: string
|
||||
endDate: string
|
||||
budget: number
|
||||
kpis: KPI[]
|
||||
}
|
||||
|
||||
export interface KPI {
|
||||
id: string
|
||||
name: string
|
||||
current: number
|
||||
target: number
|
||||
unit: string
|
||||
trend: 'up' | 'down' | 'flat'
|
||||
}
|
||||
|
||||
export interface Portfolio {
|
||||
type: PortfolioType
|
||||
name: string
|
||||
description: string
|
||||
capacity: number
|
||||
utilized: number
|
||||
}
|
||||
Reference in New Issue
Block a user