feat: enhance weekly report and realtime notifications
Weekly Report (fix-weekly-report): - Remove 5-task limit, show all tasks per category - Add blocked tasks with blocker_reason and blocked_since - Add next week tasks (due in coming week) - Add assignee_name, completed_at, days_overdue to task details - Frontend collapsible sections for each task category - 8 new tests for enhanced report content Realtime Notifications (fix-realtime-notifications): - SQLAlchemy event-based notification publishing - Redis Pub/Sub for multi-process broadcast - Fix soft rollback handler stacking issue - Fix ping scheduling drift (send immediately when interval expires) - Frontend NotificationContext with WebSocket reconnection Spec Fixes: - Add missing ## Purpose sections to 5 specs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,145 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { reportsApi, WeeklyReportContent } from '../services/reports'
|
||||
import { reportsApi, WeeklyReportContent, ProjectSummary } from '../services/reports'
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string
|
||||
count: number
|
||||
colorClass: string
|
||||
defaultOpen?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function CollapsibleSection({ title, count, colorClass, defaultOpen = false, children }: CollapsibleSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen)
|
||||
|
||||
if (count === 0) return null
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center justify-between w-full text-left text-sm font-medium hover:bg-gray-50 p-2 rounded"
|
||||
>
|
||||
<span className={colorClass}>
|
||||
{title} ({count})
|
||||
</span>
|
||||
<span className="text-gray-400">{isOpen ? '▼' : '▶'}</span>
|
||||
</button>
|
||||
{isOpen && <div className="pl-2 mt-1 space-y-1">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TaskItemProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
highlight?: 'overdue' | 'blocked'
|
||||
}
|
||||
|
||||
function TaskItem({ title, subtitle, highlight }: TaskItemProps) {
|
||||
const bgClass = highlight === 'overdue'
|
||||
? 'bg-red-50 border-l-2 border-red-400'
|
||||
: highlight === 'blocked'
|
||||
? 'bg-orange-50 border-l-2 border-orange-400'
|
||||
: 'bg-gray-50'
|
||||
|
||||
return (
|
||||
<div className={`p-2 rounded text-sm ${bgClass}`}>
|
||||
<div className="font-medium text-gray-800">{title}</div>
|
||||
{subtitle && <div className="text-xs text-gray-500 mt-0.5">{subtitle}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectCard({ project }: { project: ProjectSummary }) {
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleDateString('zh-TW', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h5 className="font-medium">{project.project_title}</h5>
|
||||
<span className="text-sm text-gray-500">
|
||||
{project.completed_count}/{project.total_tasks} completed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Summary row */}
|
||||
<div className="flex flex-wrap gap-3 text-sm mb-2">
|
||||
<span className="text-green-600">{project.completed_count} done</span>
|
||||
<span className="text-blue-600">{project.in_progress_count} in progress</span>
|
||||
{project.overdue_count > 0 && (
|
||||
<span className="text-red-600">{project.overdue_count} overdue</span>
|
||||
)}
|
||||
{project.blocked_count > 0 && (
|
||||
<span className="text-orange-600">{project.blocked_count} blocked</span>
|
||||
)}
|
||||
{project.next_week_count > 0 && (
|
||||
<span className="text-purple-600">{project.next_week_count} next week</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Completed Tasks */}
|
||||
<CollapsibleSection title="Completed" count={project.completed_count} colorClass="text-green-700">
|
||||
{project.completed_tasks.map(task => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
title={task.title}
|
||||
subtitle={`${task.assignee_name || 'Unassigned'} • ${formatDate(task.completed_at)}`}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* In Progress Tasks */}
|
||||
<CollapsibleSection title="In Progress" count={project.in_progress_count} colorClass="text-blue-700">
|
||||
{project.in_progress_tasks.map(task => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
title={task.title}
|
||||
subtitle={`${task.assignee_name || 'Unassigned'}${task.due_date ? ` • Due ${formatDate(task.due_date)}` : ''}`}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Overdue Tasks */}
|
||||
<CollapsibleSection title="Overdue" count={project.overdue_count} colorClass="text-red-700" defaultOpen>
|
||||
{project.overdue_tasks.map(task => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
title={task.title}
|
||||
subtitle={`${task.assignee_name || 'Unassigned'} • ${task.days_overdue} days overdue`}
|
||||
highlight="overdue"
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Blocked Tasks */}
|
||||
<CollapsibleSection title="Blocked" count={project.blocked_count} colorClass="text-orange-700" defaultOpen>
|
||||
{project.blocked_tasks.map(task => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
title={task.title}
|
||||
subtitle={`${task.blocker_reason || 'No reason provided'}${task.blocked_since ? ` • Since ${formatDate(task.blocked_since)}` : ''}`}
|
||||
highlight="blocked"
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Next Week Tasks */}
|
||||
<CollapsibleSection title="Next Week" count={project.next_week_count} colorClass="text-purple-700">
|
||||
{project.next_week_tasks.map(task => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
title={task.title}
|
||||
subtitle={`${task.assignee_name || 'Unassigned'} • Due ${formatDate(task.due_date)}`}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WeeklyReportPreview() {
|
||||
const [report, setReport] = useState<WeeklyReportContent | null>(null)
|
||||
@@ -82,22 +222,30 @@ export function WeeklyReportPreview() {
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-3xl font-bold text-green-600">{report.summary.completed_count}</p>
|
||||
<p className="text-sm text-green-800">Completed</p>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-green-600">{report.summary.completed_count}</p>
|
||||
<p className="text-xs text-green-800">Completed</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-3xl font-bold text-blue-600">{report.summary.in_progress_count}</p>
|
||||
<p className="text-sm text-blue-800">In Progress</p>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-blue-600">{report.summary.in_progress_count}</p>
|
||||
<p className="text-xs text-blue-800">In Progress</p>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-3xl font-bold text-red-600">{report.summary.overdue_count}</p>
|
||||
<p className="text-sm text-red-800">Overdue</p>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-red-600">{report.summary.overdue_count}</p>
|
||||
<p className="text-xs text-red-800">Overdue</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<p className="text-3xl font-bold text-gray-600">{report.summary.total_tasks}</p>
|
||||
<p className="text-sm text-gray-800">Total Tasks</p>
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-orange-600">{report.summary.blocked_count}</p>
|
||||
<p className="text-xs text-orange-800">Blocked</p>
|
||||
</div>
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-purple-600">{report.summary.next_week_count}</p>
|
||||
<p className="text-xs text-purple-800">Next Week</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-gray-600">{report.summary.total_tasks}</p>
|
||||
<p className="text-xs text-gray-800">Total</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,21 +254,7 @@ export function WeeklyReportPreview() {
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">Projects</h4>
|
||||
{report.projects.map(project => (
|
||||
<div key={project.project_id} className="border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h5 className="font-medium">{project.project_title}</h5>
|
||||
<span className="text-sm text-gray-500">
|
||||
{project.completed_count}/{project.total_tasks} completed
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-4 text-sm">
|
||||
<span className="text-green-600">{project.completed_count} done</span>
|
||||
<span className="text-blue-600">{project.in_progress_count} in progress</span>
|
||||
{project.overdue_count > 0 && (
|
||||
<span className="text-red-600">{project.overdue_count} overdue</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ProjectCard key={project.project_id} project={project} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user