Implemented proposals from comprehensive QA review: 1. extend-csrf-protection - Add POST to CSRF protected methods in frontend - Global CSRF middleware for all state-changing operations - Update tests with CSRF token fixtures 2. tighten-cors-websocket-security - Replace wildcard CORS with explicit method/header lists - Disable query parameter auth in production (code 4002) - Add per-user WebSocket connection limit (max 5, code 4005) 3. shorten-jwt-expiry - Reduce JWT expiry from 7 days to 60 minutes - Add refresh token support with 7-day expiry - Implement token rotation on refresh - Frontend auto-refresh when token near expiry (<5 min) 4. fix-frontend-quality - Add React.lazy() code splitting for all pages - Fix useCallback dependency arrays (Dashboard, Comments) - Add localStorage data validation in AuthContext - Complete i18n for AttachmentUpload component 5. enhance-backend-validation - Add SecurityAuditMiddleware for access denied logging - Add ErrorSanitizerMiddleware for production error messages - Protect /health/detailed with admin authentication - Add input length validation (comment 5000, desc 10000) All 521 backend tests passing. Frontend builds successfully. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
270 lines
7.6 KiB
TypeScript
270 lines
7.6 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
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 { t } = useTranslation('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(t('common:messages.networkError'))
|
|
console.error('Dashboard fetch error:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [t])
|
|
|
|
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}>{t('welcome', { name: user?.name })}</h1>
|
|
</div>
|
|
|
|
<div style={styles.errorCard}>
|
|
<div style={styles.errorIcon}>!</div>
|
|
<h3 style={styles.errorTitle}>{t('common:messages.error')}</h3>
|
|
<p style={styles.errorMessage}>{error}</p>
|
|
<button
|
|
style={styles.retryButton}
|
|
onClick={fetchDashboard}
|
|
type="button"
|
|
>
|
|
{t('common:buttons.refresh')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Success state
|
|
return (
|
|
<div style={styles.container}>
|
|
{/* Welcome Section */}
|
|
<div style={styles.welcomeSection}>
|
|
<h1 style={styles.welcomeTitle}>{t('welcome', { name: user?.name })}</h1>
|
|
<p style={styles.welcomeSubtitle}>
|
|
{t('sections.projectOverview')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Statistics Cards */}
|
|
{data && (
|
|
<>
|
|
<div style={styles.statsGrid}>
|
|
<StatisticsCard
|
|
icon="✓"
|
|
value={data.task_stats.assigned_count}
|
|
label={t('stats.myTasks')}
|
|
color="#2196f3"
|
|
/>
|
|
<StatisticsCard
|
|
icon="⏰"
|
|
value={data.task_stats.due_this_week}
|
|
label={t('deadlines.thisWeek')}
|
|
color="#ff9800"
|
|
highlight={data.task_stats.due_this_week > 0}
|
|
/>
|
|
<StatisticsCard
|
|
icon="⚠"
|
|
value={data.task_stats.overdue_count}
|
|
label={t('deadlines.overdue')}
|
|
color="#f44336"
|
|
highlight={data.task_stats.overdue_count > 0}
|
|
/>
|
|
<StatisticsCard
|
|
icon="✅"
|
|
value={data.task_stats.completion_rate}
|
|
label={t('stats.completedTasks')}
|
|
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>
|
|
)
|
|
}
|
|
|
|
const styles: { [key: string]: React.CSSProperties } = {
|
|
container: {
|
|
padding: '24px',
|
|
maxWidth: '1200px',
|
|
margin: '0 auto',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '24px',
|
|
},
|
|
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',
|
|
borderRadius: '8px',
|
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
|
padding: '20px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '16px',
|
|
},
|
|
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,
|
|
color: 'white',
|
|
backgroundColor: '#2196f3',
|
|
border: 'none',
|
|
borderRadius: '6px',
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.2s ease',
|
|
},
|
|
}
|