feat: implement dashboard widgets functionality
Backend: - Add dashboard API router with widget endpoints - Create dashboard schemas for widget data - Add dashboard tests Frontend: - Enhance Dashboard page with widget components - Add dashboard service for API calls - Create reusable dashboard components OpenSpec: - Archive add-dashboard-widgets change - Add dashboard capability 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,28 +1,171 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { dashboardApi, DashboardResponse } from '../services/dashboard'
|
||||
import {
|
||||
StatisticsCard,
|
||||
WorkloadWidget,
|
||||
HealthSummaryWidget,
|
||||
QuickActions,
|
||||
} from '../components/dashboard'
|
||||
import { Skeleton } from '../components/Skeleton'
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user } = useAuth()
|
||||
const [data, setData] = useState<DashboardResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchDashboard = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await dashboardApi.getDashboard()
|
||||
setData(response)
|
||||
} catch (err) {
|
||||
setError('Failed to load dashboard data. Please try again.')
|
||||
console.error('Dashboard fetch error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboard()
|
||||
}, [fetchDashboard])
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.welcomeSection}>
|
||||
<Skeleton variant="text" width={200} height={28} />
|
||||
<Skeleton variant="text" width={150} height={16} style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards Skeleton */}
|
||||
<div style={styles.statsGrid}>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} style={styles.skeletonCard}>
|
||||
<Skeleton variant="circle" width={48} height={48} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Skeleton variant="text" width="60%" height={28} />
|
||||
<Skeleton variant="text" width="80%" height={14} style={{ marginTop: 4 }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Widgets Skeleton */}
|
||||
<div style={styles.widgetsGrid}>
|
||||
<div style={styles.skeletonWidget}>
|
||||
<Skeleton variant="text" width={120} height={20} />
|
||||
<Skeleton variant="text" width="100%" height={32} style={{ marginTop: 16 }} />
|
||||
<Skeleton variant="rect" width="100%" height={8} style={{ marginTop: 12, borderRadius: 4 }} />
|
||||
</div>
|
||||
<div style={styles.skeletonWidget}>
|
||||
<Skeleton variant="text" width={120} height={20} />
|
||||
<div style={{ display: 'flex', gap: 24, marginTop: 16 }}>
|
||||
<Skeleton variant="circle" width={80} height={80} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Skeleton variant="text" width="100%" height={16} />
|
||||
<Skeleton variant="text" width="100%" height={16} style={{ marginTop: 8 }} />
|
||||
<Skeleton variant="text" width="100%" height={16} style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Skeleton */}
|
||||
<div style={styles.skeletonWidget}>
|
||||
<Skeleton variant="text" width={120} height={20} />
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 16 }}>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} variant="rect" width={100} height={80} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.welcomeSection}>
|
||||
<h1 style={styles.welcomeTitle}>Welcome, {user?.name}!</h1>
|
||||
</div>
|
||||
|
||||
<div style={styles.errorCard}>
|
||||
<div style={styles.errorIcon}>!</div>
|
||||
<h3 style={styles.errorTitle}>Unable to Load Dashboard</h3>
|
||||
<p style={styles.errorMessage}>{error}</p>
|
||||
<button
|
||||
style={styles.retryButton}
|
||||
onClick={fetchDashboard}
|
||||
type="button"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Success state
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.welcomeCard}>
|
||||
<h2>Welcome, {user?.name}!</h2>
|
||||
<p>Email: {user?.email}</p>
|
||||
<p>Role: {user?.role || 'No role assigned'}</p>
|
||||
{user?.is_system_admin && (
|
||||
<p style={styles.adminNote}>
|
||||
You have system administrator privileges.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={styles.infoCard}>
|
||||
<h3>Getting Started</h3>
|
||||
<p>
|
||||
This is the Project Control system dashboard. Features will be
|
||||
added as development progresses.
|
||||
{/* Welcome Section */}
|
||||
<div style={styles.welcomeSection}>
|
||||
<h1 style={styles.welcomeTitle}>Welcome, {user?.name}!</h1>
|
||||
<p style={styles.welcomeSubtitle}>
|
||||
Here is your work overview for today
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
{data && (
|
||||
<>
|
||||
<div style={styles.statsGrid}>
|
||||
<StatisticsCard
|
||||
icon="✓"
|
||||
value={data.task_stats.assigned_count}
|
||||
label="My Tasks"
|
||||
color="#2196f3"
|
||||
/>
|
||||
<StatisticsCard
|
||||
icon="⏰"
|
||||
value={data.task_stats.due_this_week}
|
||||
label="Due This Week"
|
||||
color="#ff9800"
|
||||
highlight={data.task_stats.due_this_week > 0}
|
||||
/>
|
||||
<StatisticsCard
|
||||
icon="⚠"
|
||||
value={data.task_stats.overdue_count}
|
||||
label="Overdue"
|
||||
color="#f44336"
|
||||
highlight={data.task_stats.overdue_count > 0}
|
||||
/>
|
||||
<StatisticsCard
|
||||
icon="✅"
|
||||
value={data.task_stats.completion_rate}
|
||||
label="Completion Rate"
|
||||
color="#4caf50"
|
||||
suffix="%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Widgets Grid */}
|
||||
<div style={styles.widgetsGrid}>
|
||||
<WorkloadWidget workload={data.workload} />
|
||||
<HealthSummaryWidget health={data.health_summary} />
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<QuickActions isAdmin={user?.is_system_admin} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -32,23 +175,93 @@ const styles: { [key: string]: React.CSSProperties } = {
|
||||
padding: '24px',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '24px',
|
||||
},
|
||||
welcomeCard: {
|
||||
welcomeSection: {
|
||||
marginBottom: '8px',
|
||||
},
|
||||
welcomeTitle: {
|
||||
margin: 0,
|
||||
fontSize: '24px',
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
},
|
||||
welcomeSubtitle: {
|
||||
margin: '8px 0 0 0',
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
},
|
||||
statsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
||||
gap: '16px',
|
||||
},
|
||||
widgetsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '16px',
|
||||
},
|
||||
skeletonCard: {
|
||||
backgroundColor: 'white',
|
||||
padding: '24px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
marginBottom: '24px',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
},
|
||||
adminNote: {
|
||||
color: '#0066cc',
|
||||
skeletonWidget: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
padding: '20px',
|
||||
},
|
||||
errorCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
padding: '40px',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
},
|
||||
errorIcon: {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ffebee',
|
||||
color: '#f44336',
|
||||
fontSize: '32px',
|
||||
fontWeight: 700,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
errorTitle: {
|
||||
margin: 0,
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
},
|
||||
errorMessage: {
|
||||
margin: 0,
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
maxWidth: '400px',
|
||||
},
|
||||
retryButton: {
|
||||
padding: '10px 24px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
marginTop: '12px',
|
||||
},
|
||||
infoCard: {
|
||||
backgroundColor: 'white',
|
||||
padding: '24px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
color: 'white',
|
||||
backgroundColor: '#2196f3',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s ease',
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user