diff --git a/src/App.tsx b/src/App.tsx index 9d0bad6..7a0e20a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -120,7 +120,7 @@ export default function App() { filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} /> )} {displayPage === 'teamtasks' && } - {displayPage === 'reports' && } + {displayPage === 'reports' && } {displayPage === 'members' && } diff --git a/src/Reports.tsx b/src/Reports.tsx index 7a352c3..cbf716d 100644 --- a/src/Reports.tsx +++ b/src/Reports.tsx @@ -1,20 +1,69 @@ -import type { Task } from './data'; -import { USERS, STATUS_COLORS, PRIORITY_COLORS } from './data'; -import { BarChart, Bar, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; +import { useState } from 'react'; +import type { Task, User } from './data'; +import { USERS, STATUS_COLORS, STATUS_LABELS, PRIORITY_COLORS } from './data'; +import { + BarChart, Bar, PieChart, Pie, Cell, AreaChart, Area, Line, + XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, RadarChart, Radar, + PolarGrid, PolarAngleAxis, PolarRadiusAxis, +} from 'recharts'; +/* ── dark tooltip shared across all charts ── */ const tooltipStyle = { contentStyle: { background: '#0f172a', border: '1px solid #334155', borderRadius: 8, color: '#e2e8f0', fontSize: 12 }, itemStyle: { color: '#e2e8f0' }, labelStyle: { color: '#94a3b8' }, + cursor: { fill: 'rgba(99,102,241,0.08)' }, // ← FIX: was white + wrapperStyle: { outline: 'none' }, }; +const tooltipLine = { ...tooltipStyle, cursor: { stroke: '#6366f1', strokeWidth: 1 } }; + +/* ── CSV export helper (CTO only) ── */ +function exportToCSV(tasks: Task[], filename: string) { + const header = 'ID,Title,Status,Priority,Assignee,Due Date,Tags,Subtasks Done,Subtasks Total,Comments\n'; + const rows = tasks.map(t => { + const assignee = USERS.find(u => u.id === t.assignee)?.name ?? t.assignee; + const subDone = t.subtasks.filter(s => s.done).length; + return `"${t.id}","${t.title}","${STATUS_LABELS[t.status]}","${t.priority}","${assignee}","${t.dueDate}","${t.tags.join('; ')}",${subDone},${t.subtasks.length},${t.comments.length}`; + }).join('\n'); + const blob = new Blob([header + rows], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = filename; a.click(); + URL.revokeObjectURL(url); +} + +function exportJSON(tasks: Task[], filename: string) { + const blob = new Blob([JSON.stringify(tasks, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = filename; a.click(); + URL.revokeObjectURL(url); +} + +/* ── main component ── */ +interface ReportsProps { tasks: Task[]; currentUser: User; } + +export function ReportsPage({ tasks, currentUser }: ReportsProps) { + const [exportOpen, setExportOpen] = useState(false); + const isCTO = currentUser.role === 'cto' || currentUser.role === 'manager'; -export function ReportsPage({ tasks }: { tasks: Task[] }) { const total = tasks.length; const completed = tasks.filter(t => t.status === 'done').length; + const inProgress = tasks.filter(t => t.status === 'inprogress').length; + const inReview = tasks.filter(t => t.status === 'review').length; const overdue = tasks.filter(t => new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length; const critical = tasks.filter(t => t.priority === 'critical' && t.status !== 'done').length; + const avgSubtaskCompletion = total + ? Math.round(tasks.reduce((acc, t) => { + if (!t.subtasks.length) return acc; + return acc + (t.subtasks.filter(s => s.done).length / t.subtasks.length) * 100; + }, 0) / tasks.filter(t => t.subtasks.length > 0).length || 0) + : 0; + const totalComments = tasks.reduce((a, t) => a + t.comments.length, 0); - // Tasks per member (stacked by status) + /* ── chart data ── */ + + // 1 · Tasks per member (stacked bar) const memberData = USERS.map(u => { const ut = tasks.filter(t => t.assignee === u.id); return { @@ -26,93 +75,282 @@ export function ReportsPage({ tasks }: { tasks: Task[] }) { }; }); - // Priority distribution + // 2 · Priority donut const prioData = (['critical', 'high', 'medium', 'low'] as const).map(p => ({ - name: p, value: tasks.filter(t => t.priority === p).length, color: PRIORITY_COLORS[p].color, + name: p.charAt(0).toUpperCase() + p.slice(1), value: tasks.filter(t => t.priority === p).length, color: PRIORITY_COLORS[p].color, })); - // Completions mock + // 3 · Completions this week (area chart) const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - const completionData = days.map((d, i) => ({ name: d, completed: [1, 0, 2, 1, 3, 0, 1][i] })); + const completionData = days.map((d, i) => ({ name: d, completed: [1, 0, 2, 1, 3, 0, 1][i], target: 2 })); - // Overdue by member + // 4 · Overdue by member (horizontal bar) const overdueData = USERS.map(u => ({ name: u.name.split(' ')[0], overdue: tasks.filter(t => t.assignee === u.id && new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length, })).filter(d => d.overdue > 0); + // 5 · Member performance radar + const radarData = USERS.map(u => { + const ut = tasks.filter(t => t.assignee === u.id); + const done = ut.filter(t => t.status === 'done').length; + const ot = ut.filter(t => new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length; + const subDone = ut.reduce((a, t) => a + t.subtasks.filter(s => s.done).length, 0); + const subTotal = ut.reduce((a, t) => a + t.subtasks.length, 0); + return { + name: u.name.split(' ')[0], + tasks: ut.length, + completed: done, + onTime: ut.length - ot, + subtasks: subTotal ? Math.round((subDone / subTotal) * 100) : 0, + }; + }); + + // 6 · Status flow (what % of tasks in each status) + const statusFlow = (['todo', 'inprogress', 'review', 'done'] as const).map(s => ({ + name: STATUS_LABELS[s], + count: tasks.filter(t => t.status === s).length, + color: STATUS_COLORS[s], + pct: total ? Math.round((tasks.filter(t => t.status === s).length / total) * 100) : 0, + })); + + // 7 · Tag frequency + const tagMap: Record = {}; + tasks.forEach(t => t.tags.forEach(tag => { tagMap[tag] = (tagMap[tag] || 0) + 1; })); + const tagData = Object.entries(tagMap).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([name, count]) => ({ name, count })); + const tagColors = ['#6366f1', '#818cf8', '#a78bfa', '#c4b5fd', '#22c55e', '#f59e0b', '#ef4444', '#ec4899']; + return (
-
+ {/* HEADER with export */} +
+

Reports & Analytics

+ {isCTO && ( +
+ + {exportOpen && ( +
+ + + +
+ )} +
+ )} +
+ + {/* STAT CARDS (extended) */} +
{[ - { label: 'Total Tasks', num: total, border: '#6366f1' }, - { label: 'Completed', num: completed, border: '#22c55e' }, - { label: 'Overdue', num: overdue, border: '#ef4444' }, - { label: 'Critical Open', num: critical, border: '#f97316' }, + { label: 'Total Tasks', num: total, border: '#6366f1', icon: '📋' }, + { label: 'Completed', num: completed, border: '#22c55e', icon: '✅' }, + { label: 'In Progress', num: inProgress, border: '#818cf8', icon: '⏳' }, + { label: 'In Review', num: inReview, border: '#f59e0b', icon: '👀' }, + { label: 'Overdue', num: overdue, border: '#ef4444', icon: '🔴' }, + { label: 'Critical', num: critical, border: '#f97316', icon: '🔥' }, + { label: 'Subtask %', num: `${avgSubtaskCompletion}%`, border: '#a78bfa', icon: '📊' }, + { label: 'Comments', num: totalComments, border: '#ec4899', icon: '💬' }, ].map(s => (
-
{s.num}
+
+
{s.num}
+ {s.icon} +
{s.label}
))}
+ {/* STATUS PIPELINE */} +
+
Status Pipeline
+
+ {statusFlow.map(s => ( + s.pct > 0 ? ( +
+ {s.pct}% +
+ ) : null + ))} +
+
+ {statusFlow.map(s => ( + + + {s.name} ({s.count}) + + ))} +
+
+ + {/* CHARTS GRID */}
+ {/* 1 · Tasks per Member */}
Tasks per Member
- - - - + + + + - + - +
+ {/* 2 · Priority Distribution */}
Priority Distribution
- + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - `${entry.name} ${((entry.percent ?? 0) * 100).toFixed(0)}%`) as any}> - {prioData.map(d => )} + `${entry.name} ${((entry.percent ?? 0) * 100).toFixed(0)}%`) as any} + labelLine={{ stroke: '#334155' }}> + {prioData.map(d => )} -
{total}
+
+ {total} +
tasks
+
+ {/* 3 · Completion Trend (Area) */}
-
Completions This Week
- - - - - - - +
Completion Trend vs target
+ + + + + + + + + + + + + +
+ {/* 4 · Overdue by Member */}
-
Overdue by Member
- - - - +
Overdue by Member ⚠ needs attention
+ + + + - + + {overdueData.length === 0 &&
🎉 No overdue tasks!
} +
+ + {/* 5 · Member Performance Radar */} +
+
Member Performance
+ + + + + + + + + + + + +
+ + {/* 6 · Tag Distribution */} +
+
Top Tags
+ {tagData.length > 0 ? ( + + + + + + + {tagData.map((_d, i) => )} + + + + ) :
No tags assigned yet
}
+ + {/* INSIGHTS SECTION (CTO/Manager only) */} + {isCTO && ( +
+
💡 Key Insights
+
+ {(() => { + const insights: { icon: string; text: string; type: 'warning' | 'success' | 'info' }[] = []; + // Completion rate + const compRate = total ? Math.round((completed / total) * 100) : 0; + if (compRate >= 70) insights.push({ icon: '🎯', text: `Great completion rate: ${compRate}% of tasks are done.`, type: 'success' }); + else if (compRate < 40) insights.push({ icon: '⚠️', text: `Low completion rate: only ${compRate}% of tasks are done.`, type: 'warning' }); + else insights.push({ icon: '📈', text: `Completion rate is ${compRate}% — keep pushing!`, type: 'info' }); + + // Overdue + if (overdue > 0) insights.push({ icon: '🔴', text: `${overdue} task${overdue > 1 ? 's are' : ' is'} overdue and needs attention.`, type: 'warning' }); + else insights.push({ icon: '✅', text: 'No overdue tasks — the team is on track!', type: 'success' }); + + // Busiest member + const busiest = USERS.map(u => ({ name: u.name, count: tasks.filter(t => t.assignee === u.id && t.status !== 'done').length })) + .sort((a, b) => b.count - a.count)[0]; + if (busiest && busiest.count > 3) insights.push({ icon: '🏋️', text: `${busiest.name} has the heaviest load with ${busiest.count} active tasks.`, type: 'warning' }); + + // Critical items + if (critical > 0) insights.push({ icon: '🔥', text: `${critical} critical task${critical > 1 ? 's' : ''} still open — prioritize these.`, type: 'warning' }); + + // Comments activity + if (totalComments > 5) insights.push({ icon: '💬', text: `Good collaboration: ${totalComments} comments across all tasks.`, type: 'success' }); + else insights.push({ icon: '🤫', text: `Only ${totalComments} comments total — encourage more team communication.`, type: 'info' }); + + return insights.map((ins, i) => ( +
+ {ins.icon} + {ins.text} +
+ )); + })()} +
+
+ )}
); } diff --git a/src/index.css b/src/index.css index 4ed30d8..9f14c38 100644 --- a/src/index.css +++ b/src/index.css @@ -1839,6 +1839,13 @@ body { padding: 20px; } +.reports-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + .charts-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -1858,6 +1865,51 @@ body { margin-bottom: 16px; } +/* INSIGHTS */ +.insights-section { + margin-top: 20px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; +} + +.insights-grid { + display: flex; + flex-direction: column; + gap: 10px; +} + +.insight-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: 8px; + background: var(--bg-card); + border-left: 3px solid var(--border); + transition: all 0.15s; +} + +.insight-card:hover { + background: rgba(255, 255, 255, 0.03); +} + +.insight-warning { + border-left-color: #f59e0b; + background: rgba(245, 158, 11, 0.05); +} + +.insight-success { + border-left-color: #22c55e; + background: rgba(34, 197, 94, 0.05); +} + +.insight-info { + border-left-color: #6366f1; + background: rgba(99, 102, 241, 0.05); +} + /* MEMBERS */ .members-page { padding: 20px;