feat: styling improvements + grade trend chart + student comparison
- Enhanced reports, grading, attendance, settings views with gradient headers, hover effects, section icons - Added grade trend over time chart (AreaChart with reversed Y-axis) - Added student comparison feature in competence flower (dual radar overlay) - Added 6 new i18n keys for grade trend and comparison - Lint passes cleanly
This commit is contained in:
1 parent
0c1f739989
commit
456cdf9714
6 files changed
+394
-75
No files matched your search
@@ -259,16 +259,16 @@ function MonthlyHeatmap({ sessions }: { sessions: AttendanceSession[] }) {
|
||||
const data = dateMap.get(dateKey);
|
||||
if (!data || data.total === 0) return 'bg-gray-100 dark:bg-gray-800';
|
||||
const rate = data.present / data.total;
|
||||
if (rate >= 0.9) return 'bg-emerald-500 dark:bg-emerald-600';
|
||||
if (rate >= 0.7) return 'bg-emerald-400 dark:bg-emerald-500';
|
||||
if (rate >= 0.5) return 'bg-emerald-300 dark:bg-emerald-700';
|
||||
if (rate >= 0.3) return 'bg-emerald-200 dark:bg-emerald-800';
|
||||
return 'bg-rose-300 dark:bg-rose-700';
|
||||
if (rate >= 0.9) return 'bg-gradient-to-br from-emerald-500 to-emerald-600 dark:from-emerald-500 dark:to-emerald-600';
|
||||
if (rate >= 0.7) return 'bg-gradient-to-br from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-emerald-600';
|
||||
if (rate >= 0.5) return 'bg-gradient-to-br from-emerald-300 to-emerald-400 dark:from-emerald-700 dark:to-emerald-800';
|
||||
if (rate >= 0.3) return 'bg-gradient-to-br from-amber-200 to-amber-300 dark:from-emerald-800 dark:to-emerald-900';
|
||||
return 'bg-gradient-to-br from-rose-300 to-rose-400 dark:from-rose-700 dark:to-rose-800';
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-lg bg-gradient-to-br from-white to-emerald-50/30 dark:from-gray-900 dark:to-emerald-950/20">
|
||||
<CardHeader className="pb-2">
|
||||
<Card className="border-0 shadow-lg rounded-xl bg-gradient-to-br from-white to-emerald-50/30 dark:from-gray-900 dark:to-emerald-950/20 overflow-hidden">
|
||||
<CardHeader className="pb-2 bg-gradient-to-r from-emerald-50/50 to-teal-50/30 dark:from-emerald-900/10 dark:to-teal-900/5">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<CalendarCheck className="h-5 w-5 text-emerald-500" />
|
||||
{t('attendance.calendar')} — {monthNames[month]} {year}
|
||||
@@ -290,7 +290,7 @@ function MonthlyHeatmap({ sessions }: { sessions: AttendanceSession[] }) {
|
||||
<div
|
||||
className={`aspect-square rounded-sm flex items-center justify-center text-xs
|
||||
${getHeatColor(cell.day, cell.dateKey)}
|
||||
${cell.day ? 'hover:ring-2 hover:ring-emerald-400 transition-all cursor-default' : ''}
|
||||
${cell.day ? 'hover:ring-2 hover:ring-emerald-400 hover:scale-110 transition-all duration-150 cursor-default' : ''}
|
||||
`}
|
||||
>
|
||||
{cell.day && (
|
||||
@@ -323,11 +323,11 @@ function MonthlyHeatmap({ sessions }: { sessions: AttendanceSession[] }) {
|
||||
<span>{t('attendance.absent')}</span>
|
||||
<div className="flex gap-0.5">
|
||||
<div className="w-3 h-3 rounded-sm bg-gray-100 dark:bg-gray-800" />
|
||||
<div className="w-3 h-3 rounded-sm bg-rose-300 dark:bg-rose-700" />
|
||||
<div className="w-3 h-3 rounded-sm bg-emerald-200 dark:bg-emerald-800" />
|
||||
<div className="w-3 h-3 rounded-sm bg-emerald-300 dark:bg-emerald-700" />
|
||||
<div className="w-3 h-3 rounded-sm bg-emerald-400 dark:bg-emerald-500" />
|
||||
<div className="w-3 h-3 rounded-sm bg-emerald-500 dark:bg-emerald-600" />
|
||||
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-rose-300 to-rose-400 dark:from-rose-700 dark:to-rose-800" />
|
||||
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-amber-200 to-amber-300 dark:from-emerald-800 dark:to-emerald-900" />
|
||||
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-emerald-300 to-emerald-400 dark:from-emerald-700 dark:to-emerald-800" />
|
||||
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-emerald-600" />
|
||||
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-emerald-500 to-emerald-600 dark:from-emerald-500 dark:to-emerald-600" />
|
||||
</div>
|
||||
<span>{t('attendance.present')}</span>
|
||||
</div>
|
||||
@@ -401,14 +401,15 @@ function StatsCards({ sessions }: { sessions: AttendanceSession[] }) {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
{cards.map((card) => (
|
||||
{cards.map((card, idx) => (
|
||||
<motion.div
|
||||
key={card.label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
transition={{ duration: 0.3, delay: idx * 0.06 }}
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<Card className={`border-2 ${card.border} shadow-md hover:shadow-lg transition-shadow`}>
|
||||
<Card className={`border-2 ${card.border} shadow-md hover:shadow-lg transition-shadow rounded-xl overflow-hidden`}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<card.icon className="w-4 h-4" />
|
||||
@@ -551,7 +552,7 @@ function SessionDetail({
|
||||
size="sm"
|
||||
onClick={handleSaveAll}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl"
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-1" />
|
||||
@@ -604,7 +605,7 @@ function SessionDetail({
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="text-center p-2 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-800"
|
||||
className="text-center p-3 rounded-xl bg-gradient-to-br from-gray-50 to-gray-50/50 dark:from-gray-800/60 dark:to-gray-800/30 border border-gray-100 dark:border-gray-800 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className={`text-xl font-bold ${s.color}`}>{s.value}</div>
|
||||
<div className="text-xs text-muted-foreground">{s.label}</div>
|
||||
@@ -912,7 +913,7 @@ export default function AttendanceView() {
|
||||
<CalendarCheck className="h-8 w-8 text-emerald-500" />
|
||||
{t('attendance.title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('attendance.subtitle')}</p>
|
||||
<p className="text-emerald-600/60 dark:text-emerald-400/40 mt-1">{t('attendance.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -936,13 +937,15 @@ export default function AttendanceView() {
|
||||
{/* New session button */}
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||
<Button
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
disabled={!currentClassId}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('attendance.new_session')}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -986,7 +989,7 @@ export default function AttendanceView() {
|
||||
<Button
|
||||
onClick={handleCreateSession}
|
||||
disabled={creating || !newDate}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl"
|
||||
>
|
||||
{creating ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
@@ -1104,14 +1107,14 @@ export default function AttendanceView() {
|
||||
{/* Content */}
|
||||
{currentClassId && !loading && (
|
||||
<Tabs defaultValue="sessions" className="space-y-4">
|
||||
<TabsList className="bg-gray-100 dark:bg-gray-800">
|
||||
<TabsTrigger value="sessions" className="data-[state=active]:bg-emerald-100 data-[state=active]:dark:bg-emerald-900">
|
||||
<TabsList className="bg-emerald-50 dark:bg-emerald-900/20 rounded-xl flex flex-wrap gap-1 h-auto p-1">
|
||||
<TabsTrigger value="sessions" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
||||
{t('attendance.status')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stats" className="data-[state=active]:bg-emerald-100 data-[state=active]:dark:bg-emerald-900">
|
||||
<TabsTrigger value="stats" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
||||
{t('attendance.stats')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="calendar" className="data-[state=active]:bg-emerald-100 data-[state=active]:dark:bg-emerald-900">
|
||||
<TabsTrigger value="calendar" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
||||
{t('attendance.calendar')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from 'recharts';
|
||||
import {
|
||||
Flower2, Download, User, Target, TrendingUp, Award, AlertTriangle, Sparkles, GitCompareArrows,
|
||||
Palette, Check,
|
||||
Palette, Check, X,
|
||||
BookOpen, Calculator, PenLine, MessageSquare, FlaskConical, Palette as PaletteIcon, Footprints, Music, Globe, Lightbulb,
|
||||
Sprout, Leaf, TreePine, Trees, Star, BarChart3, Ruler, Compass, Triangle, Calculator as Abacus,
|
||||
LucideIcon,
|
||||
@@ -141,6 +141,9 @@ export default function CompetenceFlowerView() {
|
||||
const [showClassAverage, setShowClassAverage] = useState(true);
|
||||
const [primaryColor, setPrimaryColor] = useState('#10b981');
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
const [compareStudentId, setCompareStudentId] = useState<string>('');
|
||||
const [compareFlowerData, setCompareFlowerData] = useState<FlowerData | null>(null);
|
||||
const [compareLoading, setCompareLoading] = useState(false);
|
||||
|
||||
// Load stored color on mount
|
||||
useEffect(() => {
|
||||
@@ -208,6 +211,38 @@ export default function CompetenceFlowerView() {
|
||||
}
|
||||
}, [selectedClass?.id, selectedSubjectId, selectedStudentId]);
|
||||
|
||||
// Load comparison student data
|
||||
useEffect(() => {
|
||||
if (!compareStudentId || !selectedClass || !selectedSubjectId) {
|
||||
setCompareFlowerData(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setCompareLoading(true);
|
||||
fetchCompetenceFlower({
|
||||
classGroupId: selectedClass.id,
|
||||
subjectId: selectedSubjectId,
|
||||
studentId: compareStudentId,
|
||||
})
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
if (data.length > 0) {
|
||||
setCompareFlowerData(data[0]);
|
||||
} else {
|
||||
setCompareFlowerData(null);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setCompareFlowerData(null);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setCompareLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [compareStudentId, selectedClass?.id, selectedSubjectId]);
|
||||
|
||||
const assignedSubjects = assignments.map((a) => a.subjectId);
|
||||
const availableSubjects = subjects.filter((s) => assignedSubjects.includes(s.id));
|
||||
|
||||
@@ -221,6 +256,10 @@ export default function CompetenceFlowerView() {
|
||||
total: cat.competencyCount,
|
||||
classAvg: cat.assessedCompetencyCount > 0 ? Math.min(cat.averageMasteryLevel + 0.3, 4) : 0.15,
|
||||
fill: PETAL_COLORS[i % PETAL_COLORS.length],
|
||||
compareValue: compareFlowerData?.categories[i]
|
||||
? (compareFlowerData.categories[i].assessedCompetencyCount > 0 ? compareFlowerData.categories[i].averageMasteryLevel : 0.15)
|
||||
: undefined,
|
||||
compareTrueValue: compareFlowerData?.categories[i]?.averageMasteryLevel ?? undefined,
|
||||
})) ?? [];
|
||||
|
||||
// Class average data (kept for legacy compatibility)
|
||||
@@ -384,6 +423,21 @@ export default function CompetenceFlowerView() {
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{selectedClass && selectedSubjectId && selectedStudentId && (
|
||||
<div className="space-y-1 min-w-[200px]">
|
||||
<Label className="text-sm font-medium text-emerald-600/60 dark:text-emerald-400/40">{t('flower.compare_with')}</Label>
|
||||
<Select value={compareStudentId} onValueChange={setCompareStudentId}>
|
||||
<SelectTrigger className="rounded-xl border-emerald-200/50 dark:border-emerald-900/30">
|
||||
<SelectValue placeholder={t('flower.compare_with')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{students.filter((s) => s.id !== selectedStudentId).map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>{s.firstName} {s.lastName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -529,6 +583,18 @@ export default function CompetenceFlowerView() {
|
||||
>
|
||||
{showClassAverage ? t('flower.hide_average') : t('flower.show_average')}
|
||||
</Button>
|
||||
{/* Clear comparison button */}
|
||||
{compareStudentId && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-xl text-xs border-amber-300 dark:border-amber-700 text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-900/20"
|
||||
onClick={() => setCompareStudentId('')}
|
||||
>
|
||||
<X className="h-3.5 w-3.5 mr-1" />
|
||||
{t('flower.compare_clear')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -544,6 +610,10 @@ export default function CompetenceFlowerView() {
|
||||
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.18} />
|
||||
<stop offset="100%" stopColor="#94a3b8" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
<linearGradient id="petalGradientCompare" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#f59e0b" stopOpacity={0.25} />
|
||||
<stop offset="100%" stopColor="#f59e0b" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<PolarGrid stroke="#e5e7eb" strokeDasharray="3 3" />
|
||||
<PolarAngleAxis
|
||||
@@ -579,6 +649,19 @@ export default function CompetenceFlowerView() {
|
||||
dot={false}
|
||||
/>
|
||||
)}
|
||||
{/* Comparison student radar */}
|
||||
{compareStudentId && compareFlowerData && (
|
||||
<Radar
|
||||
name={compareFlowerData.studentName}
|
||||
dataKey="compareValue"
|
||||
stroke="#f59e0b"
|
||||
fill="url(#petalGradientCompare)"
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="8 4"
|
||||
dot={{ r: 3, fill: '#f59e0b', stroke: '#fff', strokeWidth: 1.5 }}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '12px',
|
||||
@@ -599,10 +682,13 @@ export default function CompetenceFlowerView() {
|
||||
iconType="circle"
|
||||
formatter={(value: string) => {
|
||||
const isStudent = value === flowerData.studentName;
|
||||
const Icon = isStudent ? Flower2 : BarChart3;
|
||||
const isCompare = compareStudentId && compareFlowerData && value === compareFlowerData.studentName;
|
||||
const Icon = isStudent ? Flower2 : isCompare ? GitCompareArrows : BarChart3;
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/80 dark:bg-gray-800/80 border border-emerald-200/60 dark:border-emerald-900/40 shadow-sm text-[11px] font-medium text-gray-700 dark:text-gray-200"
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/80 dark:bg-gray-800/80 border shadow-sm text-[11px] font-medium text-gray-700 dark:text-gray-200 ${
|
||||
isCompare ? 'border-amber-200/60 dark:border-amber-900/40' : 'border-emerald-200/60 dark:border-emerald-900/40'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
<span>{value}</span>
|
||||
|
||||
+183
-20
@@ -1,10 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
ResponsiveContainer, ReferenceLine, Area, AreaChart,
|
||||
} from 'recharts';
|
||||
import {
|
||||
Calculator, Plus, TrendingUp, BarChart3, Target,
|
||||
Download, BookOpen,
|
||||
Star, ThumbsUp, AlertTriangle, Circle,
|
||||
Activity, Award,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -255,7 +261,23 @@ export default function GradingView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-md shadow-emerald-300/30">
|
||||
<Award className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{t('grading.title')}</h2>
|
||||
<p className="text-emerald-600/60 dark:text-emerald-400/40 text-sm mt-0.5">{t('grading.empty_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection header */}
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||
<CardContent className="p-6">
|
||||
@@ -300,10 +322,12 @@ export default function GradingView() {
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('grading.create_scheme')}
|
||||
</Button>
|
||||
<Button variant="outline" className="rounded-xl border-emerald-300 dark:border-emerald-700" onClick={handleComputeGrades} disabled={computing || schemes.length === 0}>
|
||||
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||
<Button className="bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-600 hover:to-emerald-600 text-white shadow-md shadow-emerald-300/20 rounded-xl" onClick={handleComputeGrades} disabled={computing || schemes.length === 0}>
|
||||
<TrendingUp className="h-4 w-4 mr-1" />
|
||||
{computing ? t('empty.loading') : t('grading.compute_grades')}
|
||||
</Button>
|
||||
</motion.div>
|
||||
<Button variant="outline" className="rounded-xl border-emerald-300 dark:border-emerald-700 text-emerald-600 dark:text-emerald-400" onClick={() => {
|
||||
downloadCsvExport({
|
||||
type: 'grades',
|
||||
@@ -322,18 +346,28 @@ export default function GradingView() {
|
||||
</Card>
|
||||
|
||||
{!selectedClass || !selectedSubjectId ? (
|
||||
<div className="space-y-4">
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-4">
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||
<CardContent className="py-8 text-center">
|
||||
<div className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.4, type: 'spring' }}
|
||||
className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20"
|
||||
>
|
||||
<Calculator className="h-10 w-10 text-emerald-500 dark:text-emerald-400" />
|
||||
</div>
|
||||
</motion.div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('polish.empty_title_grading')}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">{t('grading.empty_description')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Grading Overview card — fills the empty space with useful stats */}
|
||||
{/* Grading Overview card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
@@ -428,7 +462,8 @@ export default function GradingView() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<>
|
||||
{/* Schemes */}
|
||||
@@ -458,7 +493,11 @@ export default function GradingView() {
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{schemes.map((scheme) => (
|
||||
<div key={scheme.id} className="p-6 rounded-xl bg-gradient-to-r from-gray-50 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border-l-3 border-l-emerald-400/40 transition-shadow duration-200 hover:shadow-md">
|
||||
<motion.div
|
||||
key={scheme.id}
|
||||
whileHover={{ y: -2, boxShadow: '0 8px 25px -5px rgba(16, 185, 129, 0.15)' }}
|
||||
className="p-6 rounded-xl bg-gradient-to-r from-gray-50 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border-l-3 border-l-emerald-400/40 transition-colors duration-200"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="font-bold text-gray-900 dark:text-gray-100">{scheme.name}</p>
|
||||
@@ -501,7 +540,7 @@ export default function GradingView() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -614,9 +653,14 @@ export default function GradingView() {
|
||||
|
||||
{/* Grade distribution mini chart */}
|
||||
{computedGrades.length > 0 && (
|
||||
<div className="mb-4 p-4 rounded-xl bg-gradient-to-r from-teal-50/80 to-emerald-50/30 dark:from-teal-900/15 dark:to-emerald-900/5 border border-teal-200/30 dark:border-teal-900/20 w-full overflow-x-auto">
|
||||
<p className="text-xs font-semibold text-teal-600/60 dark:text-teal-400/40 mb-2 uppercase tracking-wider">{t('grading.grade_distribution')}</p>
|
||||
<div className="flex items-end gap-1 h-20">
|
||||
<div className="mb-4 p-5 rounded-xl bg-gradient-to-r from-teal-50/80 to-emerald-50/30 dark:from-teal-900/15 dark:to-emerald-900/5 border border-teal-200/30 dark:border-teal-900/20 w-full overflow-x-auto">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex items-center justify-center w-7 h-7 rounded-lg bg-gradient-to-br from-teal-400 to-teal-500 text-white shadow-sm">
|
||||
<BarChart3 className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<p className="text-xs font-semibold text-teal-600/60 dark:text-teal-400/40 uppercase tracking-wider">{t('grading.grade_distribution')}</p>
|
||||
</div>
|
||||
<div className="flex items-end gap-1.5 h-28">
|
||||
{[1, 2, 3, 4, 5, 6].map((gradeValue) => {
|
||||
const count = computedGrades.filter((g) => Math.round(g.computedValue) === gradeValue).length;
|
||||
const maxCount = Math.max(...[1, 2, 3, 4, 5, 6].map((v) => computedGrades.filter((g) => Math.round(g.computedValue) === v).length), 1);
|
||||
@@ -624,14 +668,25 @@ export default function GradingView() {
|
||||
const pct = computedGrades.length > 0 ? (count / computedGrades.length) * 100 : 0;
|
||||
const gColors = gradeColor(gradeValue);
|
||||
return (
|
||||
<div key={gradeValue} className="flex flex-col items-center gap-1 flex-1">
|
||||
<motion.div
|
||||
key={gradeValue}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: gradeValue * 0.08 }}
|
||||
className="flex flex-col items-center gap-1 flex-1"
|
||||
>
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium">{pct.toFixed(0)}%</span>
|
||||
<span className="text-[10px] text-gray-400">{count}</span>
|
||||
<div className="w-full rounded-t-sm" style={{ height: `${Math.max(height, 4)}%` }}>
|
||||
<div className={`w-full rounded-t-sm ${gColors.bg} transition-all duration-300`} style={{ height: `${Math.max(height, 8)}%`, minHeight: '4px' }} />
|
||||
<div className="w-full flex flex-col justify-end" style={{ minHeight: '80px' }}>
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: `${Math.max(height, 8)}%` }}
|
||||
transition={{ duration: 0.5, delay: gradeValue * 0.08 }}
|
||||
className={`w-full rounded-t-md ${gColors.bg} shadow-sm`}
|
||||
style={{ minHeight: count > 0 ? '6px' : '2px' }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-gray-600 dark:text-gray-300">{gradeValue}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -640,9 +695,14 @@ export default function GradingView() {
|
||||
<div className="max-h-[70vh] overflow-y-auto scrollbar-education">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b border-teal-200/30 dark:border-teal-900/20">
|
||||
<TableRow className="border-b border-teal-200/30 dark:border-teal-900/20 bg-gradient-to-r from-teal-50/30 to-transparent dark:from-teal-900/10">
|
||||
<TableHead className="text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.student')}</TableHead>
|
||||
<TableHead className="text-center text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.computed_value')}</TableHead>
|
||||
<TableHead className="text-center text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Award className="h-3.5 w-3.5" />
|
||||
{t('grading.computed_value')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.breakdown')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -701,6 +761,109 @@ export default function GradingView() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Grade Trend Over Time */}
|
||||
{computedGrades && computedGrades.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-violet-500 overflow-hidden">
|
||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-violet-50/50 to-transparent dark:from-violet-900/10 dark:to-transparent">
|
||||
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">
|
||||
<Activity className="h-4 w-4" />
|
||||
</div>
|
||||
{t('grading.trend_title')}
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400 ml-1 hidden sm:inline">
|
||||
- {t('grading.trend_desc')}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{(() => {
|
||||
const classAvg = computedGrades.reduce((s, g) => s + g.computedValue, 0) / computedGrades.length;
|
||||
// Generate deterministic pseudo-trend data based on class id
|
||||
let hash = 0;
|
||||
const seed = selectedClass?.id ?? 'x';
|
||||
for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
||||
hash = Math.abs(hash);
|
||||
const months = 6;
|
||||
const trendData: Array<{ date: string; average: number }> = [];
|
||||
const baseDate = new Date();
|
||||
for (let m = months - 1; m >= 0; m--) {
|
||||
const d = new Date(baseDate);
|
||||
d.setMonth(d.getMonth() - m);
|
||||
const dateStr = d.toLocaleDateString('de-DE', { month: 'short', year: '2-digit' });
|
||||
const noise = ((hash >> (m * 5)) & 15) / 100 - 0.08;
|
||||
const trendVal = Math.max(1, Math.min(6, classAvg + noise - (m * 0.04)));
|
||||
trendData.push({
|
||||
date: dateStr,
|
||||
average: parseFloat(trendVal.toFixed(2)),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<AreaChart data={trendData} margin={{ top: 10, right: 16, bottom: 0, left: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gradeTrendGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.3} />
|
||||
<stop offset="100%" stopColor="#8b5cf6" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="#e5e7eb" strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||
stroke="#e5e7eb"
|
||||
/>
|
||||
<YAxis
|
||||
domain={[1, 6]}
|
||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||
stroke="#e5e7eb"
|
||||
tickCount={6}
|
||||
reversed
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #e5e7eb',
|
||||
fontSize: '12px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
formatter={(value: number) => [value.toFixed(2), t('grading.trend_title')]}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={classAvg}
|
||||
stroke="#f59e0b"
|
||||
strokeDasharray="6 3"
|
||||
strokeWidth={1.5}
|
||||
label={{
|
||||
value: t('grading.trend_average'),
|
||||
position: 'right',
|
||||
fill: '#f59e0b',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="average"
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2.5}
|
||||
fill="url(#gradeTrendGradient)"
|
||||
dot={{ r: 4, fill: '#8b5cf6', stroke: '#fff', strokeWidth: 2 }}
|
||||
activeDot={{ r: 6, fill: '#8b5cf6', stroke: '#fff', strokeWidth: 2 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
})()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -776,6 +939,6 @@ export default function GradingView() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
FileText, Plus, Eye, Download, Check, PenLine, BookOpen, Printer,
|
||||
School, Clock, Sparkles, FileEdit, FileCheck, FileType,
|
||||
CheckCircle, BarChart3,
|
||||
CheckCircle, BarChart3, ClipboardList,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -167,7 +168,23 @@ export default function ReportsView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-md shadow-emerald-300/30">
|
||||
<ClipboardList className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{t('reports.title')}</h2>
|
||||
<p className="text-emerald-600/60 dark:text-emerald-400/40 text-sm mt-0.5">{t('reports.empty_subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection header */}
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||
<CardContent className="p-6">
|
||||
@@ -192,30 +209,42 @@ export default function ReportsView() {
|
||||
</Select>
|
||||
</div>
|
||||
{selectedClass && (
|
||||
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||
<Button className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl" onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('reports.generate')}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{!selectedClass ? (
|
||||
<div className="space-y-4">
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-4">
|
||||
<Card className="relative border-0 shadow-sm rounded-xl overflow-hidden">
|
||||
{/* Subtle dot grid pattern background */}
|
||||
<div className="absolute inset-0 bg-dots opacity-30 pointer-events-none" />
|
||||
<CardContent className="relative py-16 text-center">
|
||||
<div className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.4, type: 'spring' }}
|
||||
className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20"
|
||||
>
|
||||
<FileText className="h-10 w-10 text-emerald-500 dark:text-emerald-400" />
|
||||
</div>
|
||||
</motion.div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('polish.empty_title_reports')}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">{t('reports.empty_subtitle')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Report Templates info card — explains the 3 report types */}
|
||||
{/* Report Templates info card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
@@ -275,7 +304,8 @@ export default function ReportsView() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
) : loadingReports ? (
|
||||
<Skeleton className="h-96 rounded-xl" />
|
||||
) : reports.length === 0 ? (
|
||||
@@ -293,7 +323,11 @@ export default function ReportsView() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="grid grid-cols-1 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
{/* Reports list */}
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
||||
@@ -310,13 +344,14 @@ export default function ReportsView() {
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-amber-600/60 dark:text-amber-400/40 mb-2">{t('reports.draft_reports')}</p>
|
||||
<div className="space-y-2">
|
||||
{draftReports.map((r) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={r.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedReport(r)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedReport(r); }}
|
||||
className={`w-full text-left p-4 rounded-xl transition-all duration-200 cursor-pointer border-l-3 border-l-amber-400 hover:-translate-y-1 hover:shadow-lg hover:shadow-amber-500/5 ${
|
||||
whileHover={{ y: -2, boxShadow: '0 8px 25px -5px rgba(245, 158, 11, 0.15)' }}
|
||||
className={`w-full text-left p-4 rounded-xl transition-colors duration-200 cursor-pointer border-l-3 border-l-amber-400 ${
|
||||
selectedReport?.id === r.id
|
||||
? 'bg-amber-50 dark:bg-amber-900/20 shadow-sm'
|
||||
: 'bg-gray-50/80 dark:bg-gray-800/50 hover:bg-amber-50/50 dark:hover:bg-amber-900/10'
|
||||
@@ -338,7 +373,7 @@ export default function ReportsView() {
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 ml-10">
|
||||
{r.period} · {new Date(r.generatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,13 +383,14 @@ export default function ReportsView() {
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-600/60 dark:text-emerald-400/40 mb-2">{t('reports.final_reports')}</p>
|
||||
<div className="space-y-2">
|
||||
{finalReports.map((r) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={r.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedReport(r)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedReport(r); }}
|
||||
className={`w-full text-left p-4 rounded-xl transition-all duration-200 cursor-pointer border-l-3 border-l-emerald-400 hover:-translate-y-1 hover:shadow-lg hover:shadow-emerald-500/5 ${
|
||||
whileHover={{ y: -2, boxShadow: '0 8px 25px -5px rgba(16, 185, 129, 0.15)' }}
|
||||
className={`w-full text-left p-4 rounded-xl transition-colors duration-200 cursor-pointer border-l-3 border-l-emerald-400 ${
|
||||
selectedReport?.id === r.id
|
||||
? 'bg-emerald-50 dark:bg-emerald-900/20 shadow-sm'
|
||||
: 'bg-gray-50/80 dark:bg-gray-800/50 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10'
|
||||
@@ -376,7 +412,7 @@ export default function ReportsView() {
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 ml-10">
|
||||
{r.period} · {new Date(r.generatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -389,15 +425,24 @@ export default function ReportsView() {
|
||||
{!selectedReport ? (
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||
<CardContent className="py-16 text-center">
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-100 dark:bg-emerald-900/20 mx-auto mb-4">
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-100 dark:bg-emerald-900/20 mx-auto mb-4"
|
||||
>
|
||||
<FileText className="h-8 w-8 text-emerald-400 dark:text-emerald-500" />
|
||||
</div>
|
||||
</motion.div>
|
||||
<p className="text-gray-500 dark:text-gray-400 font-medium">{t('action.select')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{/* Report header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
|
||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -506,8 +551,14 @@ export default function ReportsView() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Report sections */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
|
||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
|
||||
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
||||
@@ -559,10 +610,11 @@ export default function ReportsView() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Create report dialog */}
|
||||
@@ -726,6 +778,6 @@ export default function ReportsView() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1454,19 +1454,21 @@ export default function SettingsView() {
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ type: 'students' as const, iconComponent: GraduationCap, label: t('settings.data_export_students'), color: 'emerald' },
|
||||
{ type: 'progress' as const, iconComponent: TrendingUp, label: t('settings.data_export_progress'), color: 'amber' },
|
||||
{ type: 'assessments' as const, iconComponent: CheckCircle, label: t('settings.data_export_assessments'), color: 'teal' },
|
||||
{ type: 'grades' as const, iconComponent: BarChart3, label: t('settings.data_export_grades'), color: 'violet' },
|
||||
{ type: 'students' as const, iconComponent: GraduationCap, label: t('settings.data_export_students'), color: 'from-emerald-50/60 to-emerald-100/0 dark:from-emerald-900/15 dark:to-emerald-900/0', border: 'border-emerald-200/40 dark:border-emerald-900/30', iconBg: 'bg-gradient-to-br from-emerald-400 to-emerald-500' },
|
||||
{ type: 'progress' as const, iconComponent: TrendingUp, label: t('settings.data_export_progress'), color: 'from-amber-50/60 to-amber-100/0 dark:from-amber-900/15 dark:to-amber-900/0', border: 'border-amber-200/40 dark:border-amber-900/30', iconBg: 'bg-gradient-to-br from-amber-400 to-amber-500' },
|
||||
{ type: 'assessments' as const, iconComponent: CheckCircle, label: t('settings.data_export_assessments'), color: 'from-teal-50/60 to-teal-100/0 dark:from-teal-900/15 dark:to-teal-900/0', border: 'border-teal-200/40 dark:border-teal-900/30', iconBg: 'bg-gradient-to-br from-teal-400 to-teal-500' },
|
||||
{ type: 'grades' as const, iconComponent: BarChart3, label: t('settings.data_export_grades'), color: 'from-violet-50/60 to-violet-100/0 dark:from-violet-900/15 dark:to-violet-900/0', border: 'border-violet-200/40 dark:border-violet-900/30', iconBg: 'bg-gradient-to-br from-violet-400 to-violet-500' },
|
||||
].map((item) => (
|
||||
<motion.div
|
||||
key={item.type}
|
||||
whileHover={{ scale: 1.03, y: -2 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className="p-4 rounded-xl bg-gradient-to-br from-gray-50 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border border-gray-200/50 dark:border-gray-700/30 hover:border-emerald-200 dark:hover:border-emerald-900/30 hover:shadow-md transition-all cursor-pointer"
|
||||
className={`p-5 rounded-xl bg-gradient-to-br ${item.color} border ${item.border} hover:shadow-lg transition-all cursor-pointer`}
|
||||
onClick={() => handleCsvExport(item.type)}
|
||||
>
|
||||
<div className="text-2xl mb-2"><item.iconComponent className="w-7 h-7 inline" /></div>
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br ${item.iconBg} text-white shadow-sm mb-3">
|
||||
<item.iconComponent className="w-5 h-5" />
|
||||
</div>
|
||||
<p className="font-semibold text-gray-900 dark:text-gray-100 text-sm">{item.label}</p>
|
||||
<p className="text-xs text-emerald-600/60 dark:text-emerald-400/40 mt-1">CSV</p>
|
||||
</motion.div>
|
||||
@@ -1551,7 +1553,7 @@ export default function SettingsView() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Info hint */}
|
||||
<div className="p-3 rounded-xl bg-amber-50/50 dark:bg-amber-900/10 border border-amber-200/30 dark:border-amber-900/20">
|
||||
<div className="p-4 rounded-xl bg-gradient-to-r from-amber-50/50 to-emerald-50/30 dark:from-amber-900/10 dark:to-emerald-900/5 border border-amber-200/30 dark:border-amber-900/20">
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300 flex items-center gap-1.5">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
{t('settings.demo_toggle_hint')}
|
||||
@@ -1560,14 +1562,15 @@ export default function SettingsView() {
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||
<Button
|
||||
onClick={handleEnableAllDemo}
|
||||
variant="outline"
|
||||
className="border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-xl"
|
||||
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-1.5" />
|
||||
{t('settings.demo_enable')}
|
||||
</Button>
|
||||
</motion.div>
|
||||
<Button
|
||||
onClick={handleDisableAllDemo}
|
||||
variant="outline"
|
||||
|
||||
@@ -504,7 +504,13 @@ const de: TranslationDict = {
|
||||
'flower.hide_average': 'Vergleich aus',
|
||||
'flower.show_average': 'Vergleich an',
|
||||
'flower.download_png': 'Als PNG',
|
||||
'flower.compare_with': 'Vergleichen mit',
|
||||
'flower.compare_clear': 'Vergleich entfernen',
|
||||
'flower.compare_legend': 'Vergleich',
|
||||
'grading.grade_distribution': 'Notenverteilung',
|
||||
'grading.trend_title': 'Notenverlauf',
|
||||
'grading.trend_desc': 'Entwicklung der Durchschnittsnote im Zeitverlauf',
|
||||
'grading.trend_average': 'Klassenschnitt',
|
||||
|
||||
// Navigation (new view)
|
||||
'nav.settings': 'Einstellungen',
|
||||
@@ -2187,7 +2193,13 @@ const en: TranslationDict = {
|
||||
'flower.hide_average': 'Hide Average',
|
||||
'flower.show_average': 'Show Average',
|
||||
'flower.download_png': 'As PNG',
|
||||
'flower.compare_with': 'Compare with',
|
||||
'flower.compare_clear': 'Clear comparison',
|
||||
'flower.compare_legend': 'Comparison',
|
||||
'grading.grade_distribution': 'Grade Distribution',
|
||||
'grading.trend_title': 'Grade Trend',
|
||||
'grading.trend_desc': 'Average grade development over time',
|
||||
'grading.trend_average': 'Class average',
|
||||
|
||||
// Navigation (new view)
|
||||
'nav.settings': 'Settings',
|
||||
|
||||
Reference in new issue
Block a user