From 4a7ee4cf963b0bd02b62d98deb1c903ac43c0c45 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 22 Jan 2026 14:45:18 +0000 Subject: [PATCH] Generated by Spark: Ok implement new features from ProductRoadmap.tsx, dont edit roadmap file, you can edit it to tick a box or two, thats about it. --- src/App.tsx | 38 +- src/components/AutomatedReportGeneration.tsx | 609 ++++++++++++++++ src/components/CollaborativeWorkshops.tsx | 628 ++++++++++++++++ src/components/CustomScorecard.tsx | 710 +++++++++++++++++++ src/components/ProductRoadmap.tsx | 12 +- 5 files changed, 1981 insertions(+), 16 deletions(-) create mode 100644 src/components/AutomatedReportGeneration.tsx create mode 100644 src/components/CollaborativeWorkshops.tsx create mode 100644 src/components/CustomScorecard.tsx diff --git a/src/App.tsx b/src/App.tsx index bcf0056..1b56038 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,18 @@ import { useKV } from '@github/spark/hooks' import { useState } from 'react' import { Card, CardContent } from '@/components/ui/card' -import { - Strategy, - ChartBar, - FolderOpen, - Target, - MapTrifold, - Rocket, - ChartLine, - TrendUp, - ArrowsLeftRight, - Tree, - GridFour, +import { + Strategy, + ChartBar, + FolderOpen, + Target, + MapTrifold, + Rocket, + ChartLine, + TrendUp, + ArrowsLeftRight, + Tree, + GridFour, Circle, House, CaretDown, @@ -21,7 +21,10 @@ import { ChartLineUp, GitBranch, ArrowsDownUp, - Gavel + Gavel, + Users, + Presentation, + FileText } from '@phosphor-icons/react' import { cn } from '@/lib/utils' import StrategyCards from './components/StrategyCards' @@ -42,6 +45,9 @@ import FinancialTracking from './components/FinancialTracking' import ExecutiveDashboard from './components/ExecutiveDashboard' import StrategyToInitiative from './components/StrategyToInitiative' import PortfolioGovernance from './components/PortfolioGovernance' +import CollaborativeWorkshops from './components/CollaborativeWorkshops' +import CustomScorecard from './components/CustomScorecard' +import AutomatedReportGeneration from './components/AutomatedReportGeneration' import type { StrategyCard, Initiative } from './types' type NavigationItem = { @@ -66,6 +72,7 @@ const navigationSections: NavigationSection[] = [ { id: 'comparison', label: 'Compare', icon: ArrowsLeftRight, component: StrategyComparison }, { id: 'traceability', label: 'Traceability', icon: Tree, component: StrategyTraceability }, { id: 'strategy-to-initiative', label: 'Strategy to Initiative', icon: ArrowsDownUp, component: StrategyToInitiative }, + { id: 'workshops', label: 'Collaborative Workshops', icon: Users, component: CollaborativeWorkshops }, ] }, { @@ -103,7 +110,9 @@ const navigationSections: NavigationSection[] = [ { 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: 'custom-scorecard', label: 'Custom Scorecards', icon: Presentation, component: CustomScorecard }, { id: 'financial', label: 'Financial Tracking', icon: CurrencyDollar, component: FinancialTracking }, + { id: 'automated-reports', label: 'Automated Reports', icon: FileText, component: AutomatedReportGeneration }, ] } ] @@ -298,6 +307,7 @@ function getModuleDescription(moduleId: string): string { 'comparison': 'Compare multiple strategic options side-by-side', 'traceability': 'Map relationships from goals to initiatives', 'strategy-to-initiative': 'AI-powered translation of strategy into executable initiatives', + 'workshops': 'Real-time collaboration and discussion on strategic initiatives', 'workbench': 'Execute and track strategic initiatives', 'tracker': 'Monitor initiative progress with real-time status', 'okr': 'Define and track Objectives and Key Results', @@ -311,7 +321,9 @@ function getModuleDescription(moduleId: string): string { 'executive-dashboard': 'Executive-level strategic performance overview', 'dashboard': 'Real-time performance metrics and insights', 'kpi': 'Monitor key performance indicators and metrics', + 'custom-scorecard': 'Create and manage configurable performance scorecards', 'financial': 'Track financial outcomes and value realization', + 'automated-reports': 'Generate comprehensive reports from your strategic data', } return descriptions[moduleId] || 'Manage your strategic initiatives' } diff --git a/src/components/AutomatedReportGeneration.tsx b/src/components/AutomatedReportGeneration.tsx new file mode 100644 index 0000000..3b86fba --- /dev/null +++ b/src/components/AutomatedReportGeneration.tsx @@ -0,0 +1,609 @@ +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 { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Checkbox } from '@/components/ui/checkbox' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { FilePdf, FileText, FileCsv, Download, Calendar, TrendUp, Rocket, CheckCircle, CurrencyDollar, Target } from '@phosphor-icons/react' +import { toast } from 'sonner' +import type { Initiative, StrategyCard } from '@/types' + +interface ReportTemplate { + id: string + name: string + description: string + sections: ReportSection[] +} + +interface ReportSection { + id: string + name: string + type: 'executive-summary' | 'strategy-overview' | 'initiative-status' | 'financial-summary' | 'kpi-dashboard' | 'portfolio-breakdown' + enabled: boolean +} + +const reportTemplates: ReportTemplate[] = [ + { + id: 'executive', + name: 'Executive Summary', + description: 'High-level overview for leadership', + sections: [ + { id: 'exec-1', name: 'Executive Summary', type: 'executive-summary', enabled: true }, + { id: 'exec-2', name: 'Key Initiatives', type: 'initiative-status', enabled: true }, + { id: 'exec-3', name: 'Financial Overview', type: 'financial-summary', enabled: true } + ] + }, + { + id: 'strategic', + name: 'Strategic Performance Report', + description: 'Comprehensive strategy and execution review', + sections: [ + { id: 'strat-1', name: 'Strategy Overview', type: 'strategy-overview', enabled: true }, + { id: 'strat-2', name: 'Initiative Status', type: 'initiative-status', enabled: true }, + { id: 'strat-3', name: 'KPI Dashboard', type: 'kpi-dashboard', enabled: true }, + { id: 'strat-4', name: 'Portfolio Breakdown', type: 'portfolio-breakdown', enabled: true } + ] + }, + { + id: 'operational', + name: 'Operational Dashboard', + description: 'Detailed operational metrics and progress', + sections: [ + { id: 'ops-1', name: 'Initiative Status', type: 'initiative-status', enabled: true }, + { id: 'ops-2', name: 'KPI Dashboard', type: 'kpi-dashboard', enabled: true }, + { id: 'ops-3', name: 'Portfolio Breakdown', type: 'portfolio-breakdown', enabled: true } + ] + }, + { + id: 'financial', + name: 'Financial Performance Report', + description: 'Budget, spending, and financial outcomes', + sections: [ + { id: 'fin-1', name: 'Financial Summary', type: 'financial-summary', enabled: true }, + { id: 'fin-2', name: 'Portfolio Breakdown', type: 'portfolio-breakdown', enabled: true }, + { id: 'fin-3', name: 'Initiative Status', type: 'initiative-status', enabled: true } + ] + } +] + +export default function AutomatedReportGeneration() { + const [strategyCards] = useKV('strategy-cards', []) + const [initiatives] = useKV('initiatives', []) + const [selectedTemplate, setSelectedTemplate] = useState('executive') + const [customSections, setCustomSections] = useState([]) + const [reportFormat, setReportFormat] = useState<'pdf' | 'html' | 'csv'>('html') + const [isGenerating, setIsGenerating] = useState(false) + + const generateReport = async () => { + setIsGenerating(true) + + const template = reportTemplates.find(t => t.id === selectedTemplate) + if (!template) { + toast.error('Please select a report template') + setIsGenerating(false) + return + } + + await new Promise(resolve => setTimeout(resolve, 1500)) + + const reportContent = buildReportContent(template) + + if (reportFormat === 'html') { + downloadHTMLReport(reportContent, template.name) + } else if (reportFormat === 'csv') { + downloadCSVReport(template.name) + } else { + toast.info('PDF generation would require a PDF library (not included in this demo)') + } + + setIsGenerating(false) + toast.success(`${template.name} generated successfully!`) + } + + const buildReportContent = (template: ReportTemplate): string => { + const enabledSections = template.sections.filter(s => s.enabled) + + let html = ` + + + + + + ${template.name} - ${new Date().toLocaleDateString()} + + + +
+

${template.name}

+
+ Generated: ${new Date().toLocaleString()} | + Period: ${new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })} +
+
+ ` + + enabledSections.forEach(section => { + html += generateSectionContent(section) + }) + + html += ` + + + + ` + + return html + } + + const generateSectionContent = (section: ReportSection): string => { + switch (section.type) { + case 'executive-summary': + return generateExecutiveSummary() + case 'strategy-overview': + return generateStrategyOverview() + case 'initiative-status': + return generateInitiativeStatus() + case 'financial-summary': + return generateFinancialSummary() + case 'kpi-dashboard': + return generateKPIDashboard() + case 'portfolio-breakdown': + return generatePortfolioBreakdown() + default: + return '' + } + } + + const generateExecutiveSummary = (): string => { + const totalStrategies = (strategyCards || []).length + const totalInitiatives = (initiatives || []).length + const completedInitiatives = (initiatives || []).filter(i => i.status === 'completed').length + const onTrackInitiatives = (initiatives || []).filter(i => i.status === 'on-track').length + const atRiskInitiatives = (initiatives || []).filter(i => i.status === 'at-risk').length + const blockedInitiatives = (initiatives || []).filter(i => i.status === 'blocked').length + + return ` +
+

Executive Summary

+
+
+
Active Strategies
+
${totalStrategies}
+
+
+
Total Initiatives
+
${totalInitiatives}
+
+
+
Completed
+
${completedInitiatives}
+
+
+
On Track
+
${onTrackInitiatives}
+
+
+
+
Health Overview
+

Completed: ${completedInitiatives} On Track: ${onTrackInitiatives} At Risk: ${atRiskInitiatives} Blocked: ${blockedInitiatives}

+
+
+ ` + } + + const generateStrategyOverview = (): string => { + return ` +
+

Strategy Overview

+ ${(strategyCards || []).map(card => ` +
+
${card.title}
+

Framework: ${card.framework}

+

Vision: ${card.vision}

+

Goals: ${card.goals.length} strategic goals defined

+

Metrics: ${card.metrics.length} success metrics tracked

+
+ `).join('')} +
+ ` + } + + const generateInitiativeStatus = (): string => { + return ` +
+

Initiative Status

+ + + + + + + + + + + + + ${(initiatives || []).map(init => ` + + + + + + + + + `).join('')} + +
InitiativeOwnerStatusProgressPriorityBudget
${init.title}${init.owner}${init.status}${init.progress}%${init.priority}$${init.budget.toLocaleString()}
+
+ ` + } + + const generateFinancialSummary = (): string => { + const totalBudget = (initiatives || []).reduce((sum, i) => sum + i.budget, 0) + const avgProgress = (initiatives || []).reduce((sum, i) => sum + i.progress, 0) / (initiatives || []).length || 0 + + return ` +
+

Financial Summary

+
+
+
Total Budget Allocated
+
$${totalBudget.toLocaleString()}
+
+
+
Average Progress
+
${Math.round(avgProgress)}%
+
+
+
Active Initiatives
+
${(initiatives || []).filter(i => i.status !== 'completed').length}
+
+
+
+ ` + } + + const generateKPIDashboard = (): string => { + return ` +
+

KPI Dashboard

+
+
Key Performance Indicators
+ ${(initiatives || []).slice(0, 5).map(init => ` +

${init.title}: ${init.kpis.length} KPIs tracked

+ `).join('')} +
+
+ ` + } + + const generatePortfolioBreakdown = (): string => { + const portfolios = ['operational-excellence', 'ma', 'financial-transformation', 'esg', 'innovation'] + + return ` +
+

Portfolio Breakdown

+ + + + + + + + + + + ${portfolios.map(portfolio => { + const portInitiatives = (initiatives || []).filter(i => i.portfolio === portfolio) + const portBudget = portInitiatives.reduce((sum, i) => sum + i.budget, 0) + const portProgress = portInitiatives.reduce((sum, i) => sum + i.progress, 0) / portInitiatives.length || 0 + + return ` + + + + + + + ` + }).join('')} + +
PortfolioInitiativesBudgetAvg Progress
${portfolio.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}${portInitiatives.length}$${portBudget.toLocaleString()}${Math.round(portProgress)}%
+
+ ` + } + + const getStatusBadgeClass = (status: string): string => { + switch (status) { + case 'completed': return 'success' + case 'on-track': return 'info' + case 'at-risk': return 'warning' + case 'blocked': return 'danger' + default: return 'info' + } + } + + const getPriorityBadgeClass = (priority: string): string => { + switch (priority) { + case 'critical': return 'danger' + case 'high': return 'warning' + case 'medium': return 'info' + case 'low': return 'success' + default: return 'info' + } + } + + const downloadHTMLReport = (content: string, templateName: string) => { + const blob = new Blob([content], { type: 'text/html' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${templateName.replace(/\s+/g, '-').toLowerCase()}-${Date.now()}.html` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + const downloadCSVReport = (templateName: string) => { + const csvRows = [ + ['Initiative', 'Owner', 'Status', 'Progress', 'Priority', 'Budget', 'Portfolio', 'Start Date', 'End Date'], + ...(initiatives || []).map(init => [ + init.title, + init.owner, + init.status, + `${init.progress}%`, + init.priority, + `$${init.budget}`, + init.portfolio, + init.startDate, + init.endDate + ]) + ] + + const csvContent = csvRows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n') + const blob = new Blob([csvContent], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${templateName.replace(/\s+/g, '-').toLowerCase()}-${Date.now()}.csv` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + const currentTemplate = reportTemplates.find(t => t.id === selectedTemplate) + + return ( +
+
+
+

Automated Report Generation

+

+ Generate comprehensive reports from your strategic data +

+
+
+ +
+ + + + + Strategies + + + +
{(strategyCards || []).length}
+
+
+ + + + + Initiatives + + + +
{(initiatives || []).length}
+
+
+ + + + + Completed + + + +
+ {(initiatives || []).filter(i => i.status === 'completed').length} +
+
+
+
+ +
+ + + Report Templates + Choose a pre-configured report + + +
+ {reportTemplates.map((template) => ( + setSelectedTemplate(template.id)} + > + +

{template.name}

+

{template.description}

+ + {template.sections.length} sections + +
+
+ ))} +
+
+
+ +
+ + + Report Configuration + + {currentTemplate ? currentTemplate.name : 'Select a template'} + + + + {currentTemplate ? ( +
+
+ +
+ {currentTemplate.sections.map((section) => ( +
+ +
+
{section.name}
+
+ {section.type.replace(/-/g, ' ')} +
+
+
+ ))} +
+
+ +
+ + +
+
+ ) : ( +
+ Select a report template to configure +
+ )} +
+
+ + + + Generate Report + Export your configured report + + +
+
+
+
+ {currentTemplate ? currentTemplate.name : 'No template selected'} +
+
+ Format: {reportFormat.toUpperCase()} • + Generated: {new Date().toLocaleDateString()} +
+
+ +
+ +
+

Report includes:

+
    +
  • {(strategyCards || []).length} strategic frameworks
  • +
  • {(initiatives || []).length} initiatives and projects
  • +
  • Financial data and budget allocation
  • +
  • Performance metrics and KPIs
  • +
  • Portfolio breakdowns and analysis
  • +
+
+
+
+
+
+
+
+ ) +} diff --git a/src/components/CollaborativeWorkshops.tsx b/src/components/CollaborativeWorkshops.tsx new file mode 100644 index 0000000..fc6c17d --- /dev/null +++ b/src/components/CollaborativeWorkshops.tsx @@ -0,0 +1,628 @@ +import { useKV } from '@github/spark/hooks' +import { useState, useEffect } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { Badge } from '@/components/ui/badge' +import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { ScrollArea } from '@/components/ui/scroll-area' +import { ChatCircleText, ThumbsUp, Plus, Users, PaperPlaneRight, Flag } from '@phosphor-icons/react' +import { toast } from 'sonner' +import type { StrategyCard } from '@/types' + +interface Comment { + id: string + strategyCardId: string + author: string + content: string + timestamp: number + replies: Reply[] + likes: string[] + type: 'comment' | 'question' | 'suggestion' | 'concern' +} + +interface Reply { + id: string + author: string + content: string + timestamp: number + likes: string[] +} + +interface Workshop { + id: string + name: string + description: string + strategyCardId: string + facilitator: string + participants: string[] + status: 'scheduled' | 'active' | 'completed' + startDate: string + endDate?: string + createdAt: number +} + +export default function CollaborativeWorkshops() { + const [strategyCards] = useKV('strategy-cards', []) + const [comments, setComments] = useKV('strategy-comments', []) + const [workshops, setWorkshops] = useKV('strategy-workshops', []) + const [currentUser, setCurrentUser] = useState('') + const [selectedCard, setSelectedCard] = useState('') + const [newComment, setNewComment] = useState('') + const [commentType, setCommentType] = useState('comment') + const [replyTo, setReplyTo] = useState(null) + const [replyContent, setReplyContent] = useState('') + const [isWorkshopDialogOpen, setIsWorkshopDialogOpen] = useState(false) + const [newWorkshop, setNewWorkshop] = useState({ + name: '', + description: '', + strategyCardId: '', + facilitator: '', + participants: '', + startDate: '' + }) + + useEffect(() => { + const loadUser = async () => { + const user = await window.spark.user() + setCurrentUser(user?.login || 'Anonymous') + } + loadUser() + }, []) + + const addComment = () => { + if (!newComment.trim() || !selectedCard) { + toast.error('Please select a strategy and enter a comment') + return + } + + const comment: Comment = { + id: `comment-${Date.now()}`, + strategyCardId: selectedCard, + author: currentUser, + content: newComment, + timestamp: Date.now(), + replies: [], + likes: [], + type: commentType + } + + setComments((current) => [...(current || []), comment]) + setNewComment('') + setCommentType('comment') + toast.success('Comment added!') + } + + const addReply = (commentId: string) => { + if (!replyContent.trim()) { + toast.error('Please enter a reply') + return + } + + setComments((current) => + (current || []).map(c => + c.id === commentId + ? { + ...c, + replies: [ + ...c.replies, + { + id: `reply-${Date.now()}`, + author: currentUser, + content: replyContent, + timestamp: Date.now(), + likes: [] + } + ] + } + : c + ) + ) + + setReplyContent('') + setReplyTo(null) + toast.success('Reply added!') + } + + const toggleLike = (commentId: string, isReply: boolean = false, replyId?: string) => { + setComments((current) => + (current || []).map(c => { + if (isReply && replyId) { + return c.id === commentId + ? { + ...c, + replies: c.replies.map(r => + r.id === replyId + ? { + ...r, + likes: r.likes.includes(currentUser) + ? r.likes.filter(u => u !== currentUser) + : [...r.likes, currentUser] + } + : r + ) + } + : c + } + return c.id === commentId + ? { + ...c, + likes: c.likes.includes(currentUser) + ? c.likes.filter(u => u !== currentUser) + : [...c.likes, currentUser] + } + : c + }) + ) + } + + const createWorkshop = () => { + if (!newWorkshop.name.trim() || !newWorkshop.strategyCardId || !newWorkshop.facilitator.trim()) { + toast.error('Please fill in all required fields') + return + } + + const workshop: Workshop = { + id: `workshop-${Date.now()}`, + name: newWorkshop.name, + description: newWorkshop.description, + strategyCardId: newWorkshop.strategyCardId, + facilitator: newWorkshop.facilitator, + participants: newWorkshop.participants.split(',').map(p => p.trim()).filter(p => p), + status: 'scheduled', + startDate: newWorkshop.startDate, + createdAt: Date.now() + } + + setWorkshops((current) => [...(current || []), workshop]) + setIsWorkshopDialogOpen(false) + setNewWorkshop({ + name: '', + description: '', + strategyCardId: '', + facilitator: '', + participants: '', + startDate: '' + }) + toast.success('Workshop created!') + } + + const updateWorkshopStatus = (workshopId: string, status: Workshop['status']) => { + setWorkshops((current) => + (current || []).map(w => + w.id === workshopId + ? { ...w, status, endDate: status === 'completed' ? new Date().toISOString() : undefined } + : w + ) + ) + toast.success(`Workshop ${status}`) + } + + const cardComments = selectedCard + ? (comments || []).filter(c => c.strategyCardId === selectedCard) + : [] + + const commentTypeConfig = { + comment: { label: 'Comment', color: 'default' }, + question: { label: 'Question', color: 'secondary' }, + suggestion: { label: 'Suggestion', color: 'default' }, + concern: { label: 'Concern', color: 'destructive' } + } + + const statusColors = { + scheduled: 'secondary', + active: 'default', + completed: 'outline' + } as const + + return ( +
+
+
+

Collaborative Workshops

+

+ Real-time collaboration and discussion on strategic initiatives +

+
+ + + + + + + Create Strategy Workshop + + Set up a collaborative workshop to discuss and refine a strategy + + +
+
+ + setNewWorkshop({ ...newWorkshop, name: e.target.value })} + placeholder="e.g., Q1 Strategy Review" + /> +
+
+ +