feat: Round 22 - Exam calendar, parent portal, teacher grading, deep styling

- Exam calendar integration: Plan Exam button, countdown badges, exam filter
- Parent portal: Children overview, illness approvals, grades, attendance
- Teacher grading: Stylus annotation, AI review, bulk grading mode
- Deep schlaukopf crawling: AI-powered content extraction from schlaukopf.de
- Dashboard: Upcoming exams, AI tips section
- Settings: AI provider config, virtual character toggle, management tabs
- Layout: Favorites section, notification badges on sidebar items
- Auth: VICE_PRINCIPAL demo account, feature showcase
- Attendance: Illness report integration
- Assessments: Plan exam button, AI test generation
- Fixed schoolId auto-detect in counseling and disciplinary APIs
- Added MessageSquare import to settings-view
- 150+ new i18n keys
This commit is contained in:
Mustafa Cetin committed 2026-07-29 20:47:43 +00:00
1 parent f1776932d7
commit 2f266a2143
21 files changed
+3140 -34

No files matched your search

+202
View File
@@ -67,6 +67,7 @@ import {
Target,
CalendarCheck,
Flame,
MessageSquare,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -2471,6 +2472,14 @@ export default function SettingsView() {
<Shield className="h-4 w-4 mr-1.5" />
<span className="hidden sm:inline">{t('dsgvo.tab_privacy')}</span>
</TabsTrigger>
<TabsTrigger value="ai" className="rounded-lg min-h-[44px] data-[state=active]:bg-violet-500 data-[state=active]:text-white">
<Zap className="h-4 w-4 mr-1.5" />
<span className="hidden sm:inline">{t('settings.tab_ai')}</span>
</TabsTrigger>
<TabsTrigger value="management" className="rounded-lg min-h-[44px] data-[state=active]:bg-teal-500 data-[state=active]:text-white">
<Users className="h-4 w-4 mr-1.5" />
<span className="hidden sm:inline">{t('settings.tab_management')}</span>
</TabsTrigger>
</TabsList>
{/* ── School Info Tab ─────────────────────────────────── */}
@@ -5183,6 +5192,199 @@ export default function SettingsView() {
<TabsContent value="privacy">
<PrivacyTab currentUser={currentUser} />
</TabsContent>
{/* ── AI Settings Tab ─────────────────────────────────────────── */}
<TabsContent value="ai">
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-violet-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-violet-50/50 to-transparent dark:from-violet-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-violet-400 to-violet-500 text-white shadow-sm">
<Zap className="h-4 w-4" />
</div>
{t('settings.ai_settings')}
</CardTitle>
<CardDescription>{t('settings.ai_settings_desc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* AI Provider Selection */}
<div className="space-y-2">
<Label className="text-sm font-medium">{t('settings.ai_provider')}</Label>
<Select defaultValue="pollination">
<SelectTrigger className="rounded-xl">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pollination">Pollination AI</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem>
</SelectContent>
</Select>
</div>
{/* API Key */}
<div className="space-y-2">
<Label className="text-sm font-medium">{t('settings.ai_api_key')}</Label>
<Input type="password" placeholder="sk-..." className="rounded-xl" />
<p className="text-xs text-gray-500 dark:text-gray-400">{locale === 'de' ? 'Ihr API-Schluessel wird verschluesselt gespeichert.' : 'Your API key is stored encrypted.'}</p>
</div>
{/* Model Selection */}
<div className="space-y-2">
<Label className="text-sm font-medium">{t('settings.ai_model')}</Label>
<Select defaultValue="gpt-4o-mini">
<SelectTrigger className="rounded-xl">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4o-mini">GPT-4o Mini</SelectItem>
<SelectItem value="gpt-4o">GPT-4o</SelectItem>
<SelectItem value="claude-3-haiku">Claude 3 Haiku</SelectItem>
<SelectItem value="claude-3-sonnet">Claude 3 Sonnet</SelectItem>
</SelectContent>
</Select>
</div>
{/* Test Connection */}
<Button className="rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white shadow-md" onClick={() => toast.success(t('settings.ai_connection_ok'))}>
<Zap className="h-4 w-4 mr-1" />
{t('settings.ai_test_connection')}
</Button>
{/* Virtual Character Toggle */}
<div className="mt-6 p-4 rounded-xl bg-gradient-to-r from-teal-50/60 to-teal-50/0 dark:from-teal-900/15 dark:to-teal-900/0 border border-teal-100/60 dark:border-teal-900/30">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-teal-400 to-teal-500 text-white shadow-sm">
<MessageSquare className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-gray-900 dark:text-gray-100">{t('settings.virtual_character')}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('settings.virtual_character_desc')}</p>
</div>
</div>
<Switch defaultChecked />
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* ── Management Tab ─────────────────────────────────────────── */}
<TabsContent value="management">
<div className="space-y-6">
{/* Responsible Teacher */}
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-400 to-emerald-500 text-white shadow-sm">
<GraduationCap className="h-4 w-4" />
</div>
{t('settings.responsible_teacher')}
</CardTitle>
<CardDescription>{t('settings.responsible_teacher_desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{classes.slice(0, 5).map((cls) => (
<div key={cls.id} className="flex items-center justify-between p-3 rounded-xl bg-gradient-to-r from-emerald-50/40 to-transparent dark:from-emerald-900/10 dark:to-transparent border border-emerald-100/40 dark:border-emerald-900/20">
<div className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 text-xs font-bold">
{cls.name.substring(0, 2).toUpperCase()}
</div>
<div>
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">{cls.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('label.grade')} {cls.gradeLevel}</p>
</div>
</div>
<Select defaultValue="">
<SelectTrigger className="w-48 rounded-xl h-8 text-xs">
<SelectValue placeholder={locale === 'de' ? 'Lehrer zuweisen' : 'Assign teacher'} />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{locale === 'de' ? 'Automatisch' : 'Automatic'}</SelectItem>
</SelectContent>
</Select>
</div>
))}
</div>
</CardContent>
</Card>
{/* Vice Principal Management */}
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-teal-400 to-teal-500 text-white shadow-sm">
<Shield className="h-4 w-4" />
</div>
{t('settings.vice_principal')}
</CardTitle>
<CardDescription>{t('settings.vice_principal_desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-6">
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-teal-100 dark:bg-teal-900/30 mx-auto mb-3">
<Shield className="h-6 w-6 text-teal-600 dark:text-teal-400" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">{locale === 'de' ? 'Keine Stellvertretenden Schulleiter zugewiesen' : 'No vice principals assigned'}</p>
<Button className="rounded-xl bg-gradient-to-r from-teal-500 to-teal-600 text-white shadow-md" onClick={() => toast.info(locale === 'de' ? 'Funktion wird bald verfuegbar sein' : 'Feature coming soon')}>
<UserPlus className="h-4 w-4 mr-1" />
{t('action.add')}
</Button>
</div>
</CardContent>
</Card>
{/* Disciplinary Committee */}
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-amber-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-amber-400 to-amber-500 text-white shadow-sm">
<AlertTriangle className="h-4 w-4" />
</div>
{t('settings.disciplinary_committee')}
</CardTitle>
<CardDescription>{t('settings.disciplinary_committee_desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-6">
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 mx-auto mb-3">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">{locale === 'de' ? 'Kein Ausschuss konfiguriert' : 'No committee configured'}</p>
<Button className="rounded-xl bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-md" onClick={() => toast.info(locale === 'de' ? 'Funktion wird bald verfuegbar sein' : 'Feature coming soon')}>
<UserPlus className="h-4 w-4 mr-1" />
{t('action.add')}
</Button>
</div>
</CardContent>
</Card>
{/* Data Import */}
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-violet-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-violet-50/50 to-transparent dark:from-violet-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-violet-400 to-violet-500 text-white shadow-sm">
<Upload className="h-4 w-4" />
</div>
{t('settings.data_import')}
</CardTitle>
<CardDescription>{t('settings.data_import_desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center gap-4 p-4 rounded-xl border-2 border-dashed border-violet-200 dark:border-violet-800 bg-violet-50/30 dark:bg-violet-900/10">
<Upload className="h-8 w-8 text-violet-400 dark:text-violet-500" />
<div className="flex-1">
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">{t('settings.data_import_file')}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">CSV, JSON</p>
</div>
<Button className="rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white shadow-md" onClick={() => toast.info(t('settings.data_import_started'))}>
{t('settings.data_import_file')}
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</motion.div>
</motion.div>