From 231d8449e7746a44640147782269abfb63949d62 Mon Sep 17 00:00:00 2001 From: Z User Date: Sun, 6 Sep 2026 18:47:48 +0000 Subject: [PATCH] Add Environment & Archive module: Eco metrics, Digital Archive, Government Portal, NAS settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Module: 'Umwelt & Archiv' (Eco & Archive) — 4 tabs: 1. Umwelt (Eco Dashboard): - Paper savings tracker (sheets, grams, CO2, water, trees) - Monthly trend bar chart - Hero card with environmental mission statement - EcoMetric model: daily tracking of paper/CO2/water/trees saved 2. Archiv (Digital Archive): - Full-text searchable document archive (OCR text search) - Document types: Report Card, Certificate, Enrollment, Letter, Contract - Categories: Student, Staff, Finance, Admin, Legal - Confidential documents with lock indicator - Upload dialog with drag-and-drop area - ArchivedDocument model with checksum, retention date, access level 3. Behörden (Government Portal): - Report types: Schulstatistik, Schulbesuch, Förderung, Personal, Bildungsbericht - Status tracking: DRAFT → SUBMITTED → ACKNOWLEDGED/REJECTED - Info box about German government portal integration - GovernmentReport model with submission tracking 4. Speicher (NAS/Storage Settings): - NAS server connection (SMB/NFS/WebDAV/S3) - Host, port, share, credentials configuration - Backup frequency (hourly/daily/weekly) - Optional cloud backup (Nextcloud/ownCloud/S3) - StorageConfig model New Prisma models: ArchivedDocument, GovernmentReport, EcoMetric, StorageConfig New API routes: /api/archived-documents, /api/government-reports, /api/eco-metrics, /api/storage-config New view: EcoArchiveView registered in nav, store, role-access --- prisma/schema.prisma | 111 ++++ src/app/api/archived-documents/[id]/route.ts | 28 + src/app/api/archived-documents/route.ts | 56 ++ src/app/api/eco-metrics/route.ts | 45 ++ src/app/api/government-reports/[id]/route.ts | 16 + src/app/api/government-reports/route.ts | 38 ++ src/app/api/storage-config/route.ts | 39 ++ src/components/app-layout.tsx | 4 + src/components/eco-archive-view.tsx | 509 +++++++++++++++++++ src/lib/i18n.ts | 2 + src/lib/role-access.ts | 2 +- src/lib/store.ts | 3 +- 12 files changed, 851 insertions(+), 2 deletions(-) create mode 100644 src/app/api/archived-documents/[id]/route.ts create mode 100644 src/app/api/archived-documents/route.ts create mode 100644 src/app/api/eco-metrics/route.ts create mode 100644 src/app/api/government-reports/[id]/route.ts create mode 100644 src/app/api/government-reports/route.ts create mode 100644 src/app/api/storage-config/route.ts create mode 100644 src/components/eco-archive-view.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b5bdb0a..19d1281 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,6 +36,10 @@ model School { users User[] schoolYears SchoolYear[] + archivedDocuments ArchivedDocument[] + governmentReports GovernmentReport[] + ecoMetrics EcoMetric[] + storageConfig StorageConfig? classGroups ClassGroup[] students Student[] subjects Subject[] @@ -235,6 +239,8 @@ model User { dashboardWidgets DashboardWidget[] dashboardLayouts DashboardLayout[] passwordResetTokens PasswordResetToken[] + uploadedDocuments ArchivedDocument[] @relation("DocumentUploader") + createdReports GovernmentReport[] @relation("ReportCreator") twoFactorRecoveryCodes TwoFactorRecoveryCode[] } @@ -3558,3 +3564,108 @@ model DashboardLayout { @@map("dashboard_layouts") } + +// ─── Digital Archive (Papierloses Archiv) ──────────────────────────── +model ArchivedDocument { + id String @id @default(cuid()) + schoolId String + title String + documentType String // REPORT_CARD, CERTIFICATE, ENROLLMENT, LETTER, CONTRACT, OTHER + category String // STUDENT, STAFF, FINANCE, ADMIN, LEGAL + studentId String? + staffId String? + filePath String // NAS path or cloud URL + fileName String + fileSize Int // bytes + mimeType String + scanDate DateTime @default(now()) + documentDate DateTime? // date on the document itself + checksum String? // SHA-256 for integrity verification + ocrText String? // extracted text via OCR (searchable) + tags String? // JSON array of tags + retentionDate DateTime? // when this document can be deleted per policy + isConfidential Boolean @default(false) + accessLevel String @default("STAFF") // STAFF, ADMIN, DPO + uploadedBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + school School @relation(fields: [schoolId], references: [id]) + uploader User @relation("DocumentUploader", fields: [uploadedBy], references: [id]) + + @@index([schoolId, category]) + @@index([schoolId, documentType]) + @@index([studentId]) + @@map("archived_documents") +} + +// ─── Government Portal (Behördenportal) ────────────────────────────── +model GovernmentReport { + id String @id @default(cuid()) + schoolId String + reportType String // STATISTIK_SCHULamt, SCHULBESUCH, FUERDERUNG, PERSONAL, BILDUNGSBERICHT + reportingPeriod String // e.g., "2025-Q3", "2025/2026-S1" + status String @default("DRAFT") // DRAFT, SUBMITTED, ACKNOWLEDGED, REJECTED + data String // JSON: the report payload + generatedAt DateTime @default(now()) + submittedAt DateTime? + acknowledgedAt DateTime? + submissionId String? // government portal confirmation ID + notes String? + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + school School @relation(fields: [schoolId], references: [id]) + creator User @relation("ReportCreator", fields: [createdBy], references: [id]) + + @@index([schoolId, reportType]) + @@index([schoolId, status]) + @@map("government_reports") +} + +// ─── Eco Metrics (Papiereinsparung) ────────────────────────────────── +model EcoMetric { + id String @id @default(cuid()) + schoolId String + date DateTime @default(now()) + paperSavedSheets Int // sheets of paper saved that day + paperSavedGrams Int // grams of paper saved + co2SavedKg Float // CO2 saved in kg (production + transport) + waterSavedLiters Float // water saved in liters + treesSaved Float // trees saved (approx) + source String // NOTEBOOK, REPORT_CARD, WORKSHEET, ANNOUNCEMENT, OTHER + notes String? + + school School @relation(fields: [schoolId], references: [id]) + + @@index([schoolId, date]) + @@map("eco_metrics") +} + +// ─── NAS / Storage Settings ────────────────────────────────────────── +model StorageConfig { + id String @id @default(cuid()) + schoolId String @unique + nasEnabled Boolean @default(false) + nasHost String? // e.g., "192.168.1.100" or "nas.schule.de" + nasPort Int @default(445) // SMB port + nasShare String? // e.g., "/SchulOS" + nasUsername String? + nasPasswordEnc String? // encrypted password + nasProtocol String @default("SMB") // SMB, NFS, WebDAV, S3 + cloudEnabled Boolean @default(false) + cloudProvider String? // "nextcloud", "owncloud", "s3" + cloudUrl String? + cloudApiKey String? + backupFrequency String @default("daily") // hourly, daily, weekly + lastBackup DateTime? + lastBackupSize Int? // bytes + backupStatus String @default("never") // never, success, failed, in_progress + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + school School @relation(fields: [schoolId], references: [id]) + + @@map("storage_configs") +} diff --git a/src/app/api/archived-documents/[id]/route.ts b/src/app/api/archived-documents/[id]/route.ts new file mode 100644 index 0000000..4865d8e --- /dev/null +++ b/src/app/api/archived-documents/[id]/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const { id } = await params + const doc = await db.archivedDocument.findUnique({ where: { id } }) + if (!doc) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return NextResponse.json(doc) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} + +export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const { id } = await params + await db.archivedDocument.delete({ where: { id } }) + return NextResponse.json({ success: true }) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} diff --git a/src/app/api/archived-documents/route.ts b/src/app/api/archived-documents/route.ts new file mode 100644 index 0000000..c522d86 --- /dev/null +++ b/src/app/api/archived-documents/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export async function GET(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + + const { searchParams } = new URL(request.url) + const schoolId = searchParams.get('schoolId') || session.user.schoolId + if (!schoolId) return NextResponse.json([]) + + const category = searchParams.get('category') + const documentType = searchParams.get('documentType') + const search = searchParams.get('search') + + const where: any = { schoolId } + if (category) where.category = category + if (documentType) where.documentType = documentType + if (search) where.OR = [ + { title: { contains: search } }, + { ocrText: { contains: search } }, + { tags: { contains: search } }, + ] + + const docs = await db.archivedDocument.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: 100, + }) + return NextResponse.json(docs) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} + +export async function POST(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + + const role = session.user.role + if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN' && role !== 'TEACHER') { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const body = await request.json() + const doc = await db.archivedDocument.create({ + data: { ...body, schoolId: session.user.schoolId, uploadedBy: session.user.id }, + }) + return NextResponse.json(doc, { status: 201 }) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} diff --git a/src/app/api/eco-metrics/route.ts b/src/app/api/eco-metrics/route.ts new file mode 100644 index 0000000..4fa06fe --- /dev/null +++ b/src/app/api/eco-metrics/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export async function GET(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const schoolId = session.user.schoolId + if (!schoolId) return NextResponse.json([]) + + const metrics = await db.ecoMetric.findMany({ + where: { schoolId }, + orderBy: { date: 'desc' }, + take: 365, + }) + + // Aggregate totals + const totals = metrics.reduce((acc, m) => ({ + paperSavedSheets: acc.paperSavedSheets + m.paperSavedSheets, + paperSavedGrams: acc.paperSavedGrams + m.paperSavedGrams, + co2SavedKg: acc.co2SavedKg + m.co2SavedKg, + waterSavedLiters: acc.waterSavedLiters + m.waterSavedLiters, + treesSaved: acc.treesSaved + m.treesSaved, + }), { paperSavedSheets: 0, paperSavedGrams: 0, co2SavedKg: 0, waterSavedLiters: 0, treesSaved: 0 }) + + return NextResponse.json({ metrics, totals }) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} + +export async function POST(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const body = await request.json() + const metric = await db.ecoMetric.create({ + data: { ...body, schoolId: session.user.schoolId! }, + }) + return NextResponse.json(metric, { status: 201 }) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} diff --git a/src/app/api/government-reports/[id]/route.ts b/src/app/api/government-reports/[id]/route.ts new file mode 100644 index 0000000..43aec0d --- /dev/null +++ b/src/app/api/government-reports/[id]/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const { id } = await params + const body = await request.json() + const report = await db.governmentReport.update({ where: { id }, data: body }) + return NextResponse.json(report) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} diff --git a/src/app/api/government-reports/route.ts b/src/app/api/government-reports/route.ts new file mode 100644 index 0000000..b112d1d --- /dev/null +++ b/src/app/api/government-reports/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export async function GET(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const schoolId = session.user.schoolId + if (!schoolId) return NextResponse.json([]) + + const reports = await db.governmentReport.findMany({ + where: { schoolId }, + orderBy: { createdAt: 'desc' }, + }) + return NextResponse.json(reports) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} + +export async function POST(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const role = session.user.role + if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + const body = await request.json() + const report = await db.governmentReport.create({ + data: { ...body, schoolId: session.user.schoolId!, createdBy: session.user.id }, + }) + return NextResponse.json(report, { status: 201 }) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} diff --git a/src/app/api/storage-config/route.ts b/src/app/api/storage-config/route.ts new file mode 100644 index 0000000..3819184 --- /dev/null +++ b/src/app/api/storage-config/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth' + +export async function GET() { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const schoolId = session.user.schoolId + if (!schoolId) return NextResponse.json(null) + + const config = await db.storageConfig.findUnique({ where: { schoolId } }) + return NextResponse.json(config) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} + +export async function POST(request: Request) { + try { + const session = await getSession() + if (!session?.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + const role = session.user.role + if (role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + const schoolId = session.user.schoolId! + const body = await request.json() + + const config = await db.storageConfig.upsert({ + where: { schoolId }, + update: { ...body }, + create: { ...body, schoolId }, + }) + return NextResponse.json(config) + } catch (e: any) { + return NextResponse.json({ error: e.message }, { status: 500 }) + } +} diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx index 5552010..ca41bc1 100755 --- a/src/components/app-layout.tsx +++ b/src/components/app-layout.tsx @@ -178,6 +178,7 @@ import CounselingView from './counseling-view'; import DisciplinaryView from './disciplinary-view'; import AITestsView from './ai-tests-view'; import AIStudioView from './ai-studio-view'; +import EcoArchiveView from './eco-archive-view'; import ParentPortalView from './parent-portal-view'; import NotificationCenterView from './notification-center-view'; import SchoolAnnouncementsView from './school-announcements-view'; @@ -285,6 +286,7 @@ const navSections: NavSection[] = [ { key: 'competencies', icon: BookOpen, labelKey: 'nav.competencies' }, { key: 'districts', icon: Building2, labelKey: 'nav.districts' }, { key: 'settings', icon: Settings, labelKey: 'nav.settings' }, + { key: 'eco-archive', icon: Leaf, labelKey: 'nav.eco-archive' }, ], }, ]; @@ -318,6 +320,7 @@ const studentNavSections: NavSection[] = [ { key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' }, { key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' }, { key: 'settings', icon: Settings, labelKey: 'nav.settings' }, + { key: 'eco-archive', icon: Leaf, labelKey: 'nav.eco-archive' }, ], }, ]; @@ -482,6 +485,7 @@ function renderView(view: ViewName) { case 'disciplinary': return ; case 'ai-tests': return ; case 'ai-studio': return ; + case 'eco-archive': return ; case 'parent-portal': return ; case 'notification-center': return ; case 'announcements': return ; diff --git a/src/components/eco-archive-view.tsx b/src/components/eco-archive-view.tsx new file mode 100644 index 0000000..dec756e --- /dev/null +++ b/src/components/eco-archive-view.tsx @@ -0,0 +1,509 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { motion } from 'framer-motion'; +import { + Archive, FileText, Search, Upload, Trash2, Download, Filter, + Shield, Lock, FileCheck, Calendar, Tag, HardDrive, Cloud, RefreshCw, + Leaf, TreePine, Droplets, Wind, BarChart3, TrendingDown, + Building2, Send, CheckCircle2, Clock, AlertCircle, FileSpreadsheet, +} from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { useAppStore } from '@/lib/store'; +import { apiGet, apiPost, apiPut, apiDelete } from '@/lib/api'; +import { toast } from 'sonner'; +import { cn } from '@/lib/utils'; +import { format } from 'date-fns'; +import { de as deLocale } from 'date-fns/locale'; + +// ─── Eco Dashboard Tab ────────────────────────────────────────────── + +function EcoDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiGet('/api/eco-metrics').then((d: any) => { + setData(d); + setLoading(false); + }).catch(() => setLoading(false)); + }, []); + + if (loading) return
; + + const totals = data?.totals || { paperSavedSheets: 0, paperSavedGrams: 0, co2SavedKg: 0, waterSavedLiters: 0, treesSaved: 0 }; + + const ecoCards = [ + { icon: FileText, label: 'Papier gespart', value: totals.paperSavedSheets.toLocaleString('de-DE'), unit: 'Blätter', color: 'text-emerald-600 bg-emerald-50 dark:bg-emerald-950/30' }, + { icon: TreePine, label: 'Bäume gerettet', value: totals.treesSaved.toFixed(2), unit: 'Bäume', color: 'text-green-600 bg-green-50 dark:bg-green-950/30' }, + { icon: Wind, label: 'CO₂-Einsparung', value: totals.co2SavedKg.toFixed(1), unit: 'kg', color: 'text-sky-600 bg-sky-50 dark:bg-sky-950/30' }, + { icon: Droplets, label: 'Wasser gespart', value: totals.waterSavedLiters.toFixed(0), unit: 'Liter', color: 'text-blue-600 bg-blue-50 dark:bg-blue-950/30' }, + ]; + + return ( +
+ {/* Hero */} + + +
+ +

Umweltbeitrag der Schule

+
+

+ Durch die Digitalisierung mit SchulOS sparen wir Papier, schützen Bäume und reduzieren den CO₂-Fußabdruck. + Jedes digitale Heft, jede digitale Mitteilung und jedes Zeugnis als PDF sp wertvolle Ressourcen. +

+
+
+ + {/* Stats */} +
+ {ecoCards.map((card, i) => { + const Icon = card.icon; + return ( + + + +
+ +
+

{card.value}

+

{card.unit}

+

{card.label}

+
+
+
+ ); + })} +
+ + {/* Monthly trend */} + + + + + Monatliche Papiereinsparung + + Blätter Papier pro Monat durch Digitalisierung + + +
+ {(data?.metrics || []).slice(-6).reverse().map((m: any, i: number) => { + const max = Math.max(...(data?.metrics || []).map((x: any) => x.paperSavedSheets), 1); + const height = (m.paperSavedSheets / max) * 100; + return ( +
+ {m.paperSavedSheets} +
+ {format(new Date(m.date), 'MMM', { locale: deLocale })} +
+ ); + })} +
+ + +
+ ); +} + +// ─── Digital Archive Tab ──────────────────────────────────────────── + +const DOC_TYPES = [ + { value: 'REPORT_CARD', label: 'Zeugnis' }, + { value: 'CERTIFICATE', label: 'Bescheinigung' }, + { value: 'ENROLLMENT', label: 'Einschulung' }, + { value: 'LETTER', label: 'Brief' }, + { value: 'CONTRACT', label: 'Vertrag' }, + { value: 'OTHER', label: 'Sonstiges' }, +]; + +const DOC_CATEGORIES = [ + { value: 'STUDENT', label: 'Schüler' }, + { value: 'STAFF', label: 'Personal' }, + { value: 'FINANCE', label: 'Finanzen' }, + { value: 'ADMIN', label: 'Verwaltung' }, + { value: 'LEGAL', label: 'Recht' }, +]; + +function DigitalArchive() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [category, setCategory] = useState('ALL'); + const [uploadOpen, setUploadOpen] = useState(false); + + const loadDocs = useCallback(async () => { + setLoading(true); + try { + const params = new URLSearchParams(); + if (search) params.set('search', search); + if (category !== 'ALL') params.set('category', category); + const data = await apiGet(`/api/archived-documents?${params}`); + setDocuments(data || []); + } catch { + setDocuments([]); + } finally { + setLoading(false); + } + }, [search, category]); + + useEffect(() => { loadDocs(); }, [loadDocs]); + + return ( +
+ {/* Header */} +
+
+

+ + Digitales Archiv +

+

Papierlose Dokumentenverwaltung — alles digital, alles durchsuchbar

+
+ +
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} className="pl-9" /> +
+ +
+ + {/* Documents list */} + + + {loading ? ( +
+ ) : documents.length === 0 ? ( +
+ +

Keine Dokumente im Archiv

+

Scannen Sie Dokumente ein oder laden Sie PDFs hoch

+
+ ) : ( + +
+ {documents.map((doc) => ( +
+
+ +
+
+
+

{doc.title}

+ {doc.isConfidential && } +
+
+ {DOC_TYPES.find(t => t.value === doc.documentType)?.label || doc.documentType} + {DOC_CATEGORIES.find(c => c.value === doc.category)?.label || doc.category} + {format(new Date(doc.scanDate), 'dd.MM.yyyy')} +
+
+
+

{(doc.fileSize / 1024).toFixed(0)} KB

+
+ +
+ ))} +
+
+ )} +
+
+ + {/* Upload Dialog placeholder */} + {uploadOpen && ( +
setUploadOpen(false)}> + e.stopPropagation()}> + + Dokument archivieren + Dokument einlesen und ins digitale Archiv aufnehmen + + +
+ +

Datei hierher ziehen oder klicken

+

PDF, JPG, PNG, DOCX — max 50 MB

+
+
+
+
+
+
+
+ + +
+
+
+ )} +
+ ); +} + +// ─── Government Portal Tab ────────────────────────────────────────── + +const REPORT_TYPES = [ + { value: 'STATISTIK_SCHULAMT', label: 'Schulstatistik (Schulamt)' }, + { value: 'SCHULBESUCH', label: 'Schulbesuchsstatistik' }, + { value: 'FUERDERUNG', label: 'Fördermittelbericht' }, + { value: 'PERSONAL', label: 'Personalstatistik' }, + { value: 'BILDUNGSBERICHT', label: 'Bildungsbericht' }, +]; + +function GovernmentPortal() { + const [reports, setReports] = useState([]); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + setLoading(true); + try { + const data = await apiGet('/api/government-reports'); + setReports(data || []); + } catch { setReports([]); } + finally { setLoading(false); } + }, []); + + useEffect(() => { load(); }, [load]); + + const statusConfig: Record = { + DRAFT: { color: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-900/40 dark:text-zinc-300', icon: FileText, label: 'Entwurf' }, + SUBMITTED: { color: 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300', icon: Send, label: 'Eingereicht' }, + ACKNOWLEDGED: { color: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300', icon: CheckCircle2, label: 'Bestätigt' }, + REJECTED: { color: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300', icon: AlertCircle, label: 'Abgelehnt' }, + }; + + return ( +
+ {/* Hero */} + + +
+ +

Behördenportal

+
+

+ Vorbereitung der elektronischen Berichterstattung an Schulamt, Bildungsministerium und weitere Behörden. + Statistiken und Berichte werden digital erstellt und können exportiert werden. +

+
+ Vorbereitungsphase + Manuelle Übermittlung +
+
+
+ + {/* Reports list */} + + + + + Berichte an Behörden + + + + {loading ? ( +
+ ) : reports.length === 0 ? ( +
+ +

Noch keine Berichte erstellt

+
+ ) : ( +
+ {reports.map(r => { + const sc = statusConfig[r.status] || statusConfig.DRAFT; + const SIcon = sc.icon; + return ( +
+
+ +
+
+

{REPORT_TYPES.find(t => t.value === r.reportType)?.label || r.reportType}

+

Zeitraum: {r.reportingPeriod}

+
+ {sc.label} + {format(new Date(r.createdAt), 'dd.MM.yyyy')} +
+ ); + })} +
+ )} +
+
+ + {/* Info box */} + + +
+ +
+

Hinweis zur Behördenanbindung

+

+ Die elektronische Anbindung an Behördenportale (z.B. BILDUNGS-Portal NRW, Schulportal Bayern) + erfordert eine offizielle Vereinbarung mit dem jeweiligen Bundesland. + SchulOS bereitet die Daten vor — die Übermittlung erfolgt aktuell manuell via Export. + Eine direkte API-Anbindung ist für eine spätere Version geplant. +

+
+
+
+
+
+ ); +} + +// ─── NAS/Storage Settings Tab ────────────────────────────────────── + +function StorageSettings() { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + apiGet('/api/storage-config').then((d) => { + setConfig(d); + setLoading(false); + }).catch(() => setLoading(false)); + }, []); + + const handleSave = async () => { + setSaving(true); + try { + await apiPost('/api/storage-config', config); + toast.success('Speichereinstellungen gespeichert'); + } catch { + toast.error('Fehler beim Speichern'); + } finally { + setSaving(false); + } + }; + + if (loading) return
; + + return ( +
+ + + + + NAS-Server Anbindung + + Verbinden Sie einen lokalen NAS-Server für automatische Backups und Archivierung + + +
+
+ +

Automatische Backups auf NAS-Server

+
+ +
+ + {config?.nasEnabled && ( + <> +
+
setConfig({ ...config, nasHost: e.target.value })} placeholder="192.168.1.100" className="mt-1" />
+
setConfig({ ...config, nasPort: Number(e.target.value) })} className="mt-1" />
+
+
setConfig({ ...config, nasShare: e.target.value })} placeholder="/SchulOS-Backup" className="mt-1" />
+
+
setConfig({ ...config, nasUsername: e.target.value })} className="mt-1" />
+
setConfig({ ...config, nasPasswordEnc: e.target.value })} className="mt-1" />
+
+
+
+ + )} +
+
+ + + + + + Cloud-Speicher (optional) + + + +
+
+ +

Zusätzliche Sicherigung in der Cloud

+
+ +
+ {config?.cloudEnabled && ( +
+ )} +
+
+ + +
+ ); +} + +// ─── Main Component ───────────────────────────────────────────────── + +export default function EcoArchiveView() { + const [tab, setTab] = useState('eco'); + + return ( +
+
+

+ + Umwelt & Archiv +

+

+ Papierlose Verwaltung, digitale Archivierung und Behördenberichterstattung +

+
+ + + + Umwelt + Archiv + Behörden + Speicher + + + + + + +
+ ); +} diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 0c68e8a..4af169a 100755 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -819,6 +819,7 @@ const de: TranslationDict = { 'polish.nav_insights': 'Erkenntnisse', 'polish.nav_administration': 'Verwaltung', 'nav.ai-studio': 'KI-Studio', + 'nav.eco-archive': 'Umwelt & Archiv', 'action.upload': 'Hochladen', 'attendance.daily_timeline': 'Tagesverlauf', 'auth.back_to_login': 'Zurück zum Login', @@ -7468,6 +7469,7 @@ const en: TranslationDict = { 'polish.nav_insights': 'Insights', 'polish.nav_administration': 'Administration', 'nav.ai-studio': 'AI Studio', + 'nav.eco-archive': 'Environment & Archive', 'action.upload': 'Upload', 'attendance.daily_timeline': 'Daily Timeline', 'auth.back_to_login': 'Back to Login', diff --git a/src/lib/role-access.ts b/src/lib/role-access.ts index 15973fc..d0d1590 100755 --- a/src/lib/role-access.ts +++ b/src/lib/role-access.ts @@ -31,7 +31,7 @@ const teacherViews: ViewName[] = [ 'peer-assessment', 'subjects', 'competitions', 'parents', 'announcements', 'school-library', 'school-transport', 'counseling', 'student-wellness', 'student-career', - 'ai-tests', 'ai-studio', 'settings', 'data-import-export', + 'ai-tests', 'ai-studio', 'settings', 'data-import-export', 'eco-archive', ]; const vicePrincipalViews: ViewName[] = [ diff --git a/src/lib/store.ts b/src/lib/store.ts index 88f2124..cd20d05 100755 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -53,7 +53,8 @@ export type ViewName = | 'school-newsletter' | 'school-transport' | 'peer-assessment' - | 'ai-studio'; + | 'ai-studio' + | 'eco-archive'; export interface CurrentUser { id: string;