Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6652dc6b96 |
No files matched your search
@@ -80,10 +80,3 @@ Caddyfile
|
|||||||
|
|
||||||
# lock files (optional - some projects include them)
|
# lock files (optional - some projects include them)
|
||||||
bun.lock
|
bun.lock
|
||||||
.dyad/
|
|
||||||
|
|
||||||
# Playwright test artifacts
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
.last-run.json
|
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
================================================================================
|
================================================================================
|
||||||
SCHULOS CODE AUDIT SUMMARY
|
EXAMETRA CODE AUDIT SUMMARY
|
||||||
PRD v1 → v2 + Module L Compliance Analysis
|
PRD v1 → v2 + Module L Compliance Analysis
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ AUDIT_SUMMARY.txt (this file)
|
|||||||
- Executive summary
|
- Executive summary
|
||||||
- Quick reference
|
- Quick reference
|
||||||
|
|
||||||
MEMORY (v0_memories/user/schulos-code-review.md)
|
MEMORY (v0_memories/user/exametra-code-review.md)
|
||||||
- Saved for future conversations
|
- Saved for future conversations
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS Code Review & Refactor Analysis
|
# ExaMetra Code Review & Refactor Analysis
|
||||||
## PRD v1 → v2 + Module L Compliance
|
## PRD v1 → v2 + Module L Compliance
|
||||||
|
|
||||||
**Date:** 2026-07-31
|
**Date:** 2026-07-31
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS Comprehensive Audit Report
|
# ExaMetra Comprehensive Audit Report
|
||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS Consolidation: Complete Index
|
# ExaMetra Consolidation: Complete Index
|
||||||
|
|
||||||
**Professional Deep Code Review & Consolidation Complete**
|
**Professional Deep Code Review & Consolidation Complete**
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
|
|
||||||
This session completed a **professional, deep investigation** of SchulOS's codebase, identifying and implementing **enterprise-grade consolidations** that eliminate duplication and add professional features.
|
This session completed a **professional, deep investigation** of ExaMetra's codebase, identifying and implementing **enterprise-grade consolidations** that eliminate duplication and add professional features.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: Deep Consolidation Analysis & Implementation Plan
|
# ExaMetra: Deep Consolidation Analysis & Implementation Plan
|
||||||
|
|
||||||
**Analysis Date:** August 1, 2026
|
**Analysis Date:** August 1, 2026
|
||||||
**Status:** Professional Consolidation Complete
|
**Status:** Professional Consolidation Complete
|
||||||
@@ -585,7 +585,7 @@ rm src/components/grade-analytics-view.tsx
|
|||||||
|
|
||||||
## CONCLUSION
|
## CONCLUSION
|
||||||
|
|
||||||
SchulOS consolidation is **professional-grade**, following industry best practices. The unified calendar and grading systems provide:
|
ExaMetra consolidation is **professional-grade**, following industry best practices. The unified calendar and grading systems provide:
|
||||||
|
|
||||||
✅ **84% code reduction** in affected areas
|
✅ **84% code reduction** in affected areas
|
||||||
✅ **Professional UX** with drag-drop and context menus
|
✅ **Professional UX** with drag-drop and context menus
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
# SchulOS Complete Refactoring & Implementation - FINAL HANDOFF
|
# ExaMetra Complete Refactoring & Implementation - FINAL HANDOFF
|
||||||
|
|
||||||
## Project Status: 100% ARCHITECTURE COMPLETE
|
## Project Status: 100% ARCHITECTURE COMPLETE
|
||||||
|
|
||||||
The SchulOS platform is now architecturally complete and ready for API implementation and deployment.
|
The ExaMetra platform is now architecturally complete and ready for API implementation and deployment.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ curl -X POST http://localhost:3000/api/v1/notifications/send \
|
|||||||
## File Organization Recap
|
## File Organization Recap
|
||||||
|
|
||||||
```
|
```
|
||||||
SchulOS/
|
ExaMetra/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── admin/
|
│ │ ├── admin/
|
||||||
@@ -344,7 +344,7 @@ Once live, measure:
|
|||||||
|
|
||||||
## Final Notes
|
## Final Notes
|
||||||
|
|
||||||
This project represents a complete, production-grade refactoring of SchulOS:
|
This project represents a complete, production-grade refactoring of ExaMetra:
|
||||||
|
|
||||||
1. **Code Quality:** Reduced duplication by 8% immediately, 30% with full migration
|
1. **Code Quality:** Reduced duplication by 8% immediately, 30% with full migration
|
||||||
2. **Architecture:** Standardized, composable, testable patterns throughout
|
2. **Architecture:** Standardized, composable, testable patterns throughout
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: Final Status Report
|
# ExaMetra: Final Status Report
|
||||||
|
|
||||||
**Date:** August 1, 2026
|
**Date:** August 1, 2026
|
||||||
**Time:** Consolidation & Bug Fixes Complete
|
**Time:** Consolidation & Bug Fixes Complete
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: Final Status & Production Roadmap
|
# ExaMetra: Final Status & Production Roadmap
|
||||||
|
|
||||||
**Status:** 100% Architecture Ready for Production
|
**Status:** 100% Architecture Ready for Production
|
||||||
**Date:** August 1, 2026
|
**Date:** August 1, 2026
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
SchulOS has been comprehensively refactored and is ready for production deployment. All PRD requirements are implemented, infrastructure is production-grade, and 3+ weeks of consolidation work will reduce codebase from 200K to 140K lines.
|
ExaMetra has been comprehensively refactored and is ready for production deployment. All PRD requirements are implemented, infrastructure is production-grade, and 3+ weeks of consolidation work will reduce codebase from 200K to 140K lines.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ First Contentful Paint: ~0.9s [25% improvement]
|
|||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
SchulOS is **production-ready** with comprehensive documentation, standardized infrastructure, and a clear path to 30% code reduction through planned consolidation. The system is secure, compliant, and performant.
|
ExaMetra is **production-ready** with comprehensive documentation, standardized infrastructure, and a clear path to 30% code reduction through planned consolidation. The system is secure, compliant, and performant.
|
||||||
|
|
||||||
**Deploy with confidence.** 🚀
|
**Deploy with confidence.** 🚀
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: Fixes & Improvements Complete
|
# ExaMetra: Fixes & Improvements Complete
|
||||||
|
|
||||||
**Date:** August 1, 2026
|
**Date:** August 1, 2026
|
||||||
**Status:** All Errors Fixed - Ready for Production
|
**Status:** All Errors Fixed - Ready for Production
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
================================================================================
|
================================================================================
|
||||||
SCHULOS REFACTORING - PHASE 1 COMPLETE
|
EXAMETRA REFACTORING - PHASE 1 COMPLETE
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
PROJECT STATUS: Production-Ready Infrastructure & Module B Foundation
|
PROJECT STATUS: Production-Ready Infrastructure & Module B Foundation
|
||||||
@@ -192,7 +192,7 @@ COMPLETED:
|
|||||||
✓ Usage examples provided
|
✓ Usage examples provided
|
||||||
✓ Error handling implemented
|
✓ Error handling implemented
|
||||||
✓ Uses shadcn/ui components consistently
|
✓ Uses shadcn/ui components consistently
|
||||||
✓ Follows SchulOS naming conventions
|
✓ Follows ExaMetra naming conventions
|
||||||
✓ Database migrations verified
|
✓ Database migrations verified
|
||||||
✓ No breaking changes to existing features
|
✓ No breaking changes to existing features
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ DOCUMENTATION PROVIDED
|
|||||||
SUMMARY
|
SUMMARY
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
Phase 1 represents a complete rewrite of SchulOS's core infrastructure,
|
Phase 1 represents a complete rewrite of ExaMetra's core infrastructure,
|
||||||
transforming it from a monolithic codebase with 40% duplication into a
|
transforming it from a monolithic codebase with 40% duplication into a
|
||||||
lean, modular system built on reusable components and standardized patterns.
|
lean, modular system built on reusable components and standardized patterns.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS Memory Optimization & Menu Consolidation Guide
|
# ExaMetra Memory Optimization & Menu Consolidation Guide
|
||||||
|
|
||||||
## Problem Analysis
|
## Problem Analysis
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -196,5 +196,5 @@ All code:
|
|||||||
- ✓ Includes usage examples
|
- ✓ Includes usage examples
|
||||||
- ✓ Handles error cases
|
- ✓ Handles error cases
|
||||||
- ✓ Uses shadcn/ui components
|
- ✓ Uses shadcn/ui components
|
||||||
- ✓ Follows existing SchulOS patterns
|
- ✓ Follows existing ExaMetra patterns
|
||||||
- ✓ Database migrations completed
|
- ✓ Database migrations completed
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS PRD: Nerede Olduğumuz ve Eksiklerimiz
|
# ExaMetra PRD: Nerede Olduğumuz ve Eksiklerimiz
|
||||||
|
|
||||||
**Tarih:** 1 Ağustos 2026
|
**Tarih:** 1 Ağustos 2026
|
||||||
**Durum:** 30% tamamlanmış, 70% eksik
|
**Durum:** 30% tamamlanmış, 70% eksik
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
## Özet
|
## Özet
|
||||||
|
|
||||||
SchulOS PRD'ye göre **12 modül** tanımlanmış. Bugünkü konsolidasyonda **Takvim** ve **Notlandırma** sistemini birleştirdik, ama **10 modül hala yapılacak**.
|
ExaMetra PRD'ye göre **12 modül** tanımlanmış. Bugünkü konsolidasyonda **Takvim** ve **Notlandırma** sistemini birleştirdik, ama **10 modül hala yapılacak**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# SchulOS PRD Status Report
|
# ExaMetra PRD Status Report
|
||||||
|
|
||||||
**Date:** August 1, 2026
|
**Date:** August 1, 2026
|
||||||
**PRD Reference:** SchulOS Consolidated PRD (12 Modules: A-L)
|
**PRD Reference:** SchoolOS Consolidated PRD (12 Modules: A-L)
|
||||||
**Current Status:** 25-30% implementation complete
|
**Current Status:** 25-30% implementation complete
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -457,7 +457,7 @@ AccessLog
|
|||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
SchulOS is **30% complete** against the 12-module PRD. Today's consolidation work (Calendar + Grading) was critical infrastructure. To reach **100% PRD compliance in 8 weeks**, the team must:
|
ExaMetra is **30% complete** against the 12-module PRD. Today's consolidation work (Calendar + Grading) was critical infrastructure. To reach **100% PRD compliance in 8 weeks**, the team must:
|
||||||
|
|
||||||
1. Complete Module A (Academics Core) — foundation for all others
|
1. Complete Module A (Academics Core) — foundation for all others
|
||||||
2. Build Module L (GDPR compliance) in parallel — non-negotiable
|
2. Build Module L (GDPR compliance) in parallel — non-negotiable
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: Professional Consolidation Complete ✨
|
# ExaMetra: Professional Consolidation Complete ✨
|
||||||
|
|
||||||
**Status:** Production Ready with Enterprise Features
|
**Status:** Production Ready with Enterprise Features
|
||||||
**Date:** August 1, 2026
|
**Date:** August 1, 2026
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: Professional Consolidation Complete ✨
|
# ExaMetra: Professional Consolidation Complete ✨
|
||||||
|
|
||||||
**Status:** Production Ready with Enterprise Features
|
**Status:** Production Ready with Enterprise Features
|
||||||
**Date:** August 1, 2026
|
**Date:** August 1, 2026
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS Project Status Report
|
# ExaMetra Project Status Report
|
||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ All endpoints follow middleware pattern and include:
|
|||||||
|
|
||||||
## Contact & Support
|
## Contact & Support
|
||||||
|
|
||||||
- **Architect:** SchulOS Core Team
|
- **Architect:** ExaMetra Core Team
|
||||||
- **Last Updated:** July 31, 2026
|
- **Last Updated:** July 31, 2026
|
||||||
- **Next Review:** After Phase 2 API implementation
|
- **Next Review:** After Phase 2 API implementation
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS
|
# ExaMetra
|
||||||
|
|
||||||
**Free & open-source competence & grading platform for schools**
|
**Free & open-source competence & grading platform for schools**
|
||||||
|
|
||||||
@@ -6,9 +6,9 @@ _Kompetenzorientierte Beurteilung fur alle_
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What is SchulOS?
|
## What is ExaMetra?
|
||||||
|
|
||||||
SchulOS is a comprehensive, free, and open-source web application designed for schools to track student competencies, manage assessments, compute grades, and generate reports. Built with modern web technologies, it provides a rich, professional interface for teachers, school administrators, students, and parents.
|
ExaMetra is a comprehensive, free, and open-source web application designed for schools to track student competencies, manage assessments, compute grades, and generate reports. Built with modern web technologies, it provides a rich, professional interface for teachers, school administrators, students, and parents.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ SchulOS is a comprehensive, free, and open-source web application designed for s
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/cetinmustafa83/schulos.git
|
git clone https://github.com/cetinmustafa83/exametra.git
|
||||||
cd schulos
|
cd exametra
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
@@ -108,7 +108,7 @@ After seeding, you can log in with these demo accounts:
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
schulos/
|
exametra/
|
||||||
src/
|
src/
|
||||||
app/ # Next.js App Router pages & API routes
|
app/ # Next.js App Router pages & API routes
|
||||||
api/ # 34+ API endpoint directories
|
api/ # 34+ API endpoint directories
|
||||||
@@ -152,4 +152,4 @@ Contributions are welcome! This is a free and open-source project aimed at makin
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**SchulOS** — Kompetenzorientierte Beurteilung fur alle
|
**ExaMetra** — Kompetenzorientierte Beurteilung fur alle
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS: READY FOR PRODUCTION
|
# ExaMetra: READY FOR PRODUCTION
|
||||||
|
|
||||||
## Project Completion Status: 100%
|
## Project Completion Status: 100%
|
||||||
|
|
||||||
@@ -254,8 +254,8 @@ pnpm run build
|
|||||||
vercel deploy --prod
|
vercel deploy --prod
|
||||||
|
|
||||||
# Or Docker/self-hosted
|
# Or Docker/self-hosted
|
||||||
docker build -t schulos:latest .
|
docker build -t exametra:latest .
|
||||||
docker run -e DATABASE_URL=$DB_URL schulos:latest
|
docker run -e DATABASE_URL=$DB_URL exametra:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Verify Deployment
|
### 4. Verify Deployment
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS Refactor Action Plan
|
# ExaMetra Refactor Action Plan
|
||||||
## PRD v1 → v2 → Production Ready
|
## PRD v1 → v2 → Production Ready
|
||||||
|
|
||||||
**Current State Analysis:**
|
**Current State Analysis:**
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# SchulOS PRD Implementation Todo — Visual Roadmap
|
# ExaMetra PRD Implementation Todo — Visual Roadmap
|
||||||
|
|
||||||
## Current Status: 30% Complete (Week 1)
|
## Current Status: 30% Complete (Week 1)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ The Processor shall process the following personal data on behalf of the Control
|
|||||||
- Exam monitoring logs
|
- Exam monitoring logs
|
||||||
- Communication/messaging data
|
- Communication/messaging data
|
||||||
|
|
||||||
**Purpose**: Operation and maintenance of the SchulOS school management platform
|
**Purpose**: Operation and maintenance of the ExaMetra school management platform
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -202,6 +202,6 @@ The Processor is authorized to engage the following sub-processors:
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Generated**: ISO 8601 timestamp
|
**Generated**: ISO 8601 timestamp
|
||||||
**Source**: SchulOS Module L - Compliance Framework
|
**Source**: ExaMetra Module L - Compliance Framework
|
||||||
|
|
||||||
**REMINDER**: This is a template. Consult legal counsel to customize for your jurisdiction and service provider.
|
**REMINDER**: This is a template. Consult legal counsel to customize for your jurisdiction and service provider.
|
||||||
+3
-8
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "schulos",
|
"name": "exametra",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Free & open-source competence & grading platform for schools — Kompetenzorientierte Beurteilung für alle",
|
"description": "Free & open-source competence & grading platform for schools — Kompetenzorientierte Beurteilung für alle",
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"author": "Mustafa Cetin",
|
"author": "Mustafa Cetin",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/cetinmustafa83/schulos"
|
"url": "https://github.com/cetinmustafa83/exametra"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"education",
|
"education",
|
||||||
@@ -23,15 +23,11 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"postinstall": "prisma generate",
|
|
||||||
"db:push": "prisma db push --accept-data-loss",
|
"db:push": "prisma db push --accept-data-loss",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:reset": "prisma migrate reset",
|
"db:reset": "prisma migrate reset",
|
||||||
"db:seed": "bun prisma/seed.ts",
|
"db:seed": "bun prisma/seed.ts"
|
||||||
"db:seed-demo": "bun prisma/seed-demo.ts",
|
|
||||||
"db:setup-demo": "bun run db:push && bun run db:seed-demo",
|
|
||||||
"test": "playwright test --config playwright-dyad.config.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -106,7 +102,6 @@
|
|||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.62.1",
|
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { defineConfig } from "@playwright/test";
|
|
||||||
|
|
||||||
// Generated by Dyad. The dev server is started separately by Dyad's
|
|
||||||
// preview, so we point baseURL at the already-running proxy URL (passed via
|
|
||||||
// env) rather than using Playwright's `webServer` (which would double-start
|
|
||||||
// the app).
|
|
||||||
// // Uses your installed Google Chrome (no extra browser download).
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: "./e2e-tests",
|
|
||||||
// Run serially against the single dev server.
|
|
||||||
workers: 1,
|
|
||||||
fullyParallel: false,
|
|
||||||
reporter: [["json", { outputFile: "test-results/results.json" }]],
|
|
||||||
use: {
|
|
||||||
baseURL: process.env.DYAD_TEST_BASE_URL || "http://localhost:32100",
|
|
||||||
channel: "chrome",
|
|
||||||
screenshot: "only-on-failure",
|
|
||||||
trace: "retain-on-failure",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Generated
+8
-47
@@ -148,13 +148,13 @@ importers:
|
|||||||
version: 0.525.0(react@19.2.8)
|
version: 0.525.0(react@19.2.8)
|
||||||
next:
|
next:
|
||||||
specifier: ^16.1.1
|
specifier: ^16.1.1
|
||||||
version: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
version: 16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: ^4.24.11
|
specifier: ^4.24.11
|
||||||
version: 4.24.15(next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
version: 4.24.15(next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
next-intl:
|
next-intl:
|
||||||
specifier: ^4.3.4
|
specifier: ^4.3.4
|
||||||
version: 4.13.4(next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@5.9.3)
|
version: 4.13.4(next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@5.9.3)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
@@ -219,9 +219,6 @@ importers:
|
|||||||
specifier: ^5.0.6
|
specifier: ^5.0.6
|
||||||
version: 5.0.14(@types/react@19.2.17)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
|
version: 5.0.14(@types/react@19.2.17)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@playwright/test':
|
|
||||||
specifier: ^1.62.1
|
|
||||||
version: 1.62.1
|
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
@@ -1121,11 +1118,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==}
|
resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@playwright/test@1.62.1':
|
|
||||||
resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@prisma/client@6.19.3':
|
'@prisma/client@6.19.3':
|
||||||
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
@@ -3043,11 +3035,6 @@ packages:
|
|||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.2:
|
|
||||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
@@ -3945,16 +3932,6 @@ packages:
|
|||||||
pkg-types@2.3.1:
|
pkg-types@2.3.1:
|
||||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||||
|
|
||||||
playwright-core@1.62.1:
|
|
||||||
resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
playwright@1.62.1:
|
|
||||||
resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
po-parser@2.1.1:
|
po-parser@2.1.1:
|
||||||
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
||||||
|
|
||||||
@@ -5741,10 +5718,6 @@ snapshots:
|
|||||||
'@parcel/watcher-win32-arm64': 2.6.0
|
'@parcel/watcher-win32-arm64': 2.6.0
|
||||||
'@parcel/watcher-win32-x64': 2.6.0
|
'@parcel/watcher-win32-x64': 2.6.0
|
||||||
|
|
||||||
'@playwright/test@1.62.1':
|
|
||||||
dependencies:
|
|
||||||
playwright: 1.62.1
|
|
||||||
|
|
||||||
'@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)':
|
'@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
prisma: 6.19.3(typescript@5.9.3)
|
prisma: 6.19.3(typescript@5.9.3)
|
||||||
@@ -7799,9 +7772,6 @@ snapshots:
|
|||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
react-dom: 19.2.8(react@19.2.8)
|
react-dom: 19.2.8(react@19.2.8)
|
||||||
|
|
||||||
fsevents@2.3.2:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
function.prototype.name@1.2.0:
|
function.prototype.name@1.2.0:
|
||||||
@@ -8745,13 +8715,13 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
next-auth@4.24.15(next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
next-auth@4.24.15(next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.7
|
||||||
'@panva/hkdf': 1.2.1
|
'@panva/hkdf': 1.2.1
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
jose: 4.15.9
|
jose: 4.15.9
|
||||||
next: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
next: 16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
oauth: 0.9.15
|
oauth: 0.9.15
|
||||||
openid-client: 5.7.1
|
openid-client: 5.7.1
|
||||||
preact: 10.29.7(preact-render-to-string@5.2.6)
|
preact: 10.29.7(preact-render-to-string@5.2.6)
|
||||||
@@ -8762,14 +8732,14 @@ snapshots:
|
|||||||
|
|
||||||
next-intl-swc-plugin-extractor@4.13.4: {}
|
next-intl-swc-plugin-extractor@4.13.4: {}
|
||||||
|
|
||||||
next-intl@4.13.4(next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@5.9.3):
|
next-intl@4.13.4(next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.8.13
|
'@formatjs/intl-localematcher': 0.8.13
|
||||||
'@parcel/watcher': 2.6.0
|
'@parcel/watcher': 2.6.0
|
||||||
'@swc/core': 1.15.47
|
'@swc/core': 1.15.47
|
||||||
icu-minify: 4.13.4
|
icu-minify: 4.13.4
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
next: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
next: 16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
next-intl-swc-plugin-extractor: 4.13.4
|
next-intl-swc-plugin-extractor: 4.13.4
|
||||||
po-parser: 2.1.1
|
po-parser: 2.1.1
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
@@ -8786,7 +8756,7 @@ snapshots:
|
|||||||
|
|
||||||
next-tick@1.1.0: {}
|
next-tick@1.1.0: {}
|
||||||
|
|
||||||
next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.2.12
|
'@next/env': 16.2.12
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -8805,7 +8775,6 @@ snapshots:
|
|||||||
'@next/swc-linux-x64-musl': 16.2.12
|
'@next/swc-linux-x64-musl': 16.2.12
|
||||||
'@next/swc-win32-arm64-msvc': 16.2.12
|
'@next/swc-win32-arm64-msvc': 16.2.12
|
||||||
'@next/swc-win32-x64-msvc': 16.2.12
|
'@next/swc-win32-x64-msvc': 16.2.12
|
||||||
'@playwright/test': 1.62.1
|
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -8958,14 +8927,6 @@ snapshots:
|
|||||||
exsolve: 1.1.1
|
exsolve: 1.1.1
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
playwright-core@1.62.1: {}
|
|
||||||
|
|
||||||
playwright@1.62.1:
|
|
||||||
dependencies:
|
|
||||||
playwright-core: 1.62.1
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.2
|
|
||||||
|
|
||||||
po-parser@2.1.1: {}
|
po-parser@2.1.1: {}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|||||||
@@ -1,382 +0,0 @@
|
|||||||
allowBuilds:
|
|
||||||
"@parcel/watcher": false # dyad-auto-denied
|
|
||||||
es5-ext: false # dyad-auto-denied
|
|
||||||
unrs-resolver: false # dyad-auto-denied
|
|
||||||
# dyad-default-allow-builds begin
|
|
||||||
# dyad-default-allow-builds-schema=v1
|
|
||||||
# dyad-default-allow-builds-data-version=2026-07-08.1
|
|
||||||
# dyad-default-allow-builds-channel=remote
|
|
||||||
"@airbnb/node-memwatch": true
|
|
||||||
"@anthropic-ai/claude-code": true
|
|
||||||
"@apollo/protobufjs": true
|
|
||||||
"@apollo/rover": true
|
|
||||||
"@appsignal/nodejs": true
|
|
||||||
"@arkweid/lefthook": true
|
|
||||||
"@aws-amplify/cli": true
|
|
||||||
"@bahmutov/add-typescript-to-cypress": true
|
|
||||||
"@bazel/concatjs": true
|
|
||||||
"@bazel/cypress": true
|
|
||||||
"@bazel/esbuild": true
|
|
||||||
"@bazel/hide-bazel-files": true
|
|
||||||
"@bazel/jasmine": true
|
|
||||||
"@bazel/protractor": true
|
|
||||||
"@bazel/rollup": true
|
|
||||||
"@bazel/terser": true
|
|
||||||
"@bazel/typescript": true
|
|
||||||
"@bufbuild/buf": true
|
|
||||||
"@cdktf/node-pty-prebuilt-multiarch": true
|
|
||||||
"@ckeditor/ckeditor5-vue": true
|
|
||||||
"@cloudflare/wrangler": true
|
|
||||||
"@contrast/fn-inspect": true
|
|
||||||
"@cubejs-backend/cubestore": true
|
|
||||||
"@cubejs-backend/native": true
|
|
||||||
"@cypress/snapshot": true
|
|
||||||
"@danmarshall/deckgl-typings": true
|
|
||||||
"@datadog/mobile-react-native": true
|
|
||||||
"@discordjs/opus": true
|
|
||||||
"@eversdk/lib-node": true
|
|
||||||
"@evilmartians/lefthook": true
|
|
||||||
"@ffmpeg-installer/darwin-arm64": true
|
|
||||||
"@ffmpeg-installer/darwin-x64": true
|
|
||||||
"@ffmpeg-installer/linux-arm": true
|
|
||||||
"@ffmpeg-installer/linux-arm64": true
|
|
||||||
"@ffmpeg-installer/linux-ia32": true
|
|
||||||
"@ffmpeg-installer/linux-x64": true
|
|
||||||
"@ffprobe-installer/darwin-arm64": true
|
|
||||||
"@ffprobe-installer/darwin-x64": true
|
|
||||||
"@ffprobe-installer/linux-arm": true
|
|
||||||
"@ffprobe-installer/linux-arm64": true
|
|
||||||
"@ffprobe-installer/linux-ia32": true
|
|
||||||
"@ffprobe-installer/linux-x64": true
|
|
||||||
"@fingerprintjs/fingerprintjs-pro-react": true
|
|
||||||
"@ghaiklor/x509": true
|
|
||||||
"@go-task/cli": true
|
|
||||||
"@injectivelabs/sdk-ts": true
|
|
||||||
"@instana/autoprofile": true
|
|
||||||
"@intlify/vue-i18n-bridge": true
|
|
||||||
"@intlify/vue-router-bridge": true
|
|
||||||
"@matteodisabatino/gc_info": true
|
|
||||||
"@memlab/cli": true
|
|
||||||
"@microsoft.azure/autorest-core": true
|
|
||||||
"@microsoft/teamsfx-cli": true
|
|
||||||
"@microsoft/ts-command-line": true
|
|
||||||
"@napi-rs/pinyin": true
|
|
||||||
"@nativescript/core": true
|
|
||||||
"@netlify/esbuild": true
|
|
||||||
"@newrelic/native-metrics": true
|
|
||||||
"@notarize/qlc-cli": true
|
|
||||||
"@nx-dotnet/core": true
|
|
||||||
"@opensearch-project/oui": true
|
|
||||||
"@pact-foundation/pact-node": true
|
|
||||||
"@paloaltonetworks/postman-code-generators": true
|
|
||||||
"@pdftron/pdfnet-node": true
|
|
||||||
"@percy/core": true
|
|
||||||
"@pnpm/exe": true
|
|
||||||
"@prisma/client": true
|
|
||||||
"@prisma/engines": true
|
|
||||||
"@progress/kendo-licensing": true
|
|
||||||
"@pulumi/aws-native": true
|
|
||||||
"@pulumi/awsx": true
|
|
||||||
"@pulumi/command": true
|
|
||||||
"@pulumi/kubernetes": true
|
|
||||||
"@railway/cli": true
|
|
||||||
"@replayio/cypress": true
|
|
||||||
"@replayio/playwright": true
|
|
||||||
"@roots/bud-framework": true
|
|
||||||
"@sap/hana-client": true
|
|
||||||
"@sap/hana-performance-tools": true
|
|
||||||
"@sap/hana-theme-vscode": true
|
|
||||||
"@scarf/scarf": true
|
|
||||||
"@sematext/gc-stats": true
|
|
||||||
"@sentry/capacitor": true
|
|
||||||
"@sentry/profiling-node": true
|
|
||||||
"@serialport/bindings": true
|
|
||||||
"@serialport/bindings-cpp": true
|
|
||||||
"@shopify/ngrok": true
|
|
||||||
"@shopify/plugin-cloudflare": true
|
|
||||||
"@sitespeed.io/chromedriver": true
|
|
||||||
"@sitespeed.io/edgedriver": true
|
|
||||||
"@softvisio/core": true
|
|
||||||
"@splunk/otel": true
|
|
||||||
"@strapi/strapi": true
|
|
||||||
"@sveltejs/kit": true
|
|
||||||
"@swc/core": true
|
|
||||||
"@syncfusion/ej2-angular-base": true
|
|
||||||
"@taquito/taquito": true
|
|
||||||
"@temporalio/core-bridge": true
|
|
||||||
"@tensorflow/tfjs-node": true
|
|
||||||
"@trufflesuite/bigint-buffer": true
|
|
||||||
"@typescript-tools/rust-implementation": true
|
|
||||||
"@vaadin/vaadin-usage-statistics": true
|
|
||||||
"@vscode/ripgrep": true
|
|
||||||
"@vscode/sqlite3": true
|
|
||||||
abstract-socket: true
|
|
||||||
admin-lte: true
|
|
||||||
appdynamics: true
|
|
||||||
appium-chromedriver: true
|
|
||||||
appium-windows-driver: true
|
|
||||||
applicationinsights-native-metrics: true
|
|
||||||
argon2: true
|
|
||||||
autorest: true
|
|
||||||
aws-crt: true
|
|
||||||
azure-functions-core-tools: true
|
|
||||||
azure-streamanalytics-cicd: true
|
|
||||||
backport: true
|
|
||||||
bcrypt: true
|
|
||||||
better-sqlite3: true
|
|
||||||
bigint-buffer: true
|
|
||||||
blake-hash: true
|
|
||||||
bs-platform: true
|
|
||||||
bufferutil: true
|
|
||||||
bun: true
|
|
||||||
canvacord: true
|
|
||||||
canvas: true
|
|
||||||
cbor-extract: true
|
|
||||||
chromedriver: true
|
|
||||||
chromium: true
|
|
||||||
classic-level: true
|
|
||||||
cld: true
|
|
||||||
cldr-data: true
|
|
||||||
clevertap-react-native: true
|
|
||||||
clientjs: true
|
|
||||||
cmark-gfm: true
|
|
||||||
compression: true
|
|
||||||
contentlayer: true
|
|
||||||
contextify: true
|
|
||||||
cordova.plugins.diagnostic: true
|
|
||||||
core-js: true
|
|
||||||
couchbase: true
|
|
||||||
cpu-features: true
|
|
||||||
cwebp-bin: true
|
|
||||||
cy2: true
|
|
||||||
cypress: true
|
|
||||||
dd-trace: true
|
|
||||||
deasync: true
|
|
||||||
detox: true
|
|
||||||
detox-recorder: true
|
|
||||||
diskusage: true
|
|
||||||
dotnet-2.0.0: true
|
|
||||||
dprint: true
|
|
||||||
drivelist: true
|
|
||||||
dtrace-provider: true
|
|
||||||
duckdb: true
|
|
||||||
dugite: true
|
|
||||||
eccrypto: true
|
|
||||||
egg-bin: true
|
|
||||||
egg-ci: true
|
|
||||||
electron: true
|
|
||||||
electron-chromedriver: true
|
|
||||||
electron-prebuilt: true
|
|
||||||
electron-winstaller: true
|
|
||||||
elm: true
|
|
||||||
elm-format: true
|
|
||||||
esbuild: true
|
|
||||||
esoftplay: true
|
|
||||||
event-loop-stats: true
|
|
||||||
exifreader: true
|
|
||||||
farmhash: true
|
|
||||||
fast-folder-size: true
|
|
||||||
faunadb: true
|
|
||||||
ffi: true
|
|
||||||
ffi-napi: true
|
|
||||||
ffmpeg-static: true
|
|
||||||
fibers: true
|
|
||||||
fmerge: true
|
|
||||||
free-email-domains: true
|
|
||||||
fs-xattr: true
|
|
||||||
full-icu: true
|
|
||||||
gatsby: true
|
|
||||||
gc-stats: true
|
|
||||||
gcstats.js: true
|
|
||||||
geckodriver: true
|
|
||||||
gentype: true
|
|
||||||
ghooks: true
|
|
||||||
gif2webp-bin: true
|
|
||||||
gifsicle: true
|
|
||||||
git-commit-msg-linter: true
|
|
||||||
git-validate: true
|
|
||||||
git-win: true
|
|
||||||
gl: true
|
|
||||||
go-ios: true
|
|
||||||
grpc: true
|
|
||||||
grpc-tools: true
|
|
||||||
handbrake-js: true
|
|
||||||
hasura-cli: true
|
|
||||||
heapdump: true
|
|
||||||
hiredis: true
|
|
||||||
hnswlib-node: true
|
|
||||||
hugo-bin: true
|
|
||||||
hummus: true
|
|
||||||
ibm_db: true
|
|
||||||
iconv: true
|
|
||||||
iedriver: true
|
|
||||||
iltorb: true
|
|
||||||
incremental-json-parser: true
|
|
||||||
install-peers: true
|
|
||||||
interruptor: true
|
|
||||||
iobroker.js-controller: true
|
|
||||||
iso-constants: true
|
|
||||||
isolated-vm: true
|
|
||||||
java: true
|
|
||||||
jest-preview: true
|
|
||||||
jpeg-recompress-bin: true
|
|
||||||
jpegtran-bin: true
|
|
||||||
keccak: true
|
|
||||||
kerberos: true
|
|
||||||
keytar: true
|
|
||||||
lefthook: true
|
|
||||||
leveldown: true
|
|
||||||
libpg-query: true
|
|
||||||
libpq: true
|
|
||||||
libxmljs: true
|
|
||||||
libxmljs2: true
|
|
||||||
lightningcss-cli: true
|
|
||||||
lint: true
|
|
||||||
lmdb: true
|
|
||||||
lmdb-store: true
|
|
||||||
local-cypress: true
|
|
||||||
lz4: true
|
|
||||||
lzma-native: true
|
|
||||||
lzo: true
|
|
||||||
macos-alias: true
|
|
||||||
mbt: true
|
|
||||||
memlab: true
|
|
||||||
microtime: true
|
|
||||||
minidump: true
|
|
||||||
mmmagic: true
|
|
||||||
modern-syslog: true
|
|
||||||
mongodb-client-encryption: true
|
|
||||||
mongodb-crypt-library-dummy: true
|
|
||||||
mongodb-crypt-library-version: true
|
|
||||||
mongodb-memory-server: true
|
|
||||||
mozjpeg: true
|
|
||||||
ms-chromium-edge-driver: true
|
|
||||||
msgpackr-extract: true
|
|
||||||
msnodesqlv8: true
|
|
||||||
msw: true
|
|
||||||
muhammara: true
|
|
||||||
netlify-cli: true
|
|
||||||
ngrok: true
|
|
||||||
ngx-popperjs: true
|
|
||||||
nice-napi: true
|
|
||||||
node: true
|
|
||||||
node-expat: true
|
|
||||||
node-hid: true
|
|
||||||
node-jq: true
|
|
||||||
node-libcurl: true
|
|
||||||
node-mac-contacts: true
|
|
||||||
node-pty: true
|
|
||||||
node-rdkafka: true
|
|
||||||
node-sass: true
|
|
||||||
node-webcrypto-ossl: true
|
|
||||||
node-zopfli: true
|
|
||||||
node-zopfli-es: true
|
|
||||||
nodegit: true
|
|
||||||
nodejieba: true
|
|
||||||
nodent-runtime: true
|
|
||||||
nx: true
|
|
||||||
odiff-bin: true
|
|
||||||
oniguruma: true
|
|
||||||
opencode-ai: true
|
|
||||||
optipng-bin: true
|
|
||||||
oracledb: true
|
|
||||||
os-dns-native: true
|
|
||||||
parse-server: true
|
|
||||||
phantomjs: true
|
|
||||||
phantomjs-prebuilt: true
|
|
||||||
pkcs11js: true
|
|
||||||
playwright-chromium: true
|
|
||||||
playwright-firefox: true
|
|
||||||
playwright-webkit: true
|
|
||||||
pngout-bin: true
|
|
||||||
pngquant-bin: true
|
|
||||||
posix: true
|
|
||||||
pprof: true
|
|
||||||
pre-commit: true
|
|
||||||
pre-push: true
|
|
||||||
prisma: true
|
|
||||||
protoc: true
|
|
||||||
protoc-gen-grpc-web: true
|
|
||||||
puppeteer: true
|
|
||||||
purescript: true
|
|
||||||
re2: true
|
|
||||||
react-jsx-parser: true
|
|
||||||
react-native-stylex: true
|
|
||||||
react-particles: true
|
|
||||||
react-tsparticles: true
|
|
||||||
react-vertical-timeline-component: true
|
|
||||||
realm: true
|
|
||||||
redis-memory-server: true
|
|
||||||
ref: true
|
|
||||||
ref-napi: true
|
|
||||||
registry-js: true
|
|
||||||
robotjs: true
|
|
||||||
sauce-connect-launcher: true
|
|
||||||
saucectl: true
|
|
||||||
secp256k1: true
|
|
||||||
segfault-handler: true
|
|
||||||
shared-git-hooks: true
|
|
||||||
sharp: true
|
|
||||||
simple-git-hooks: true
|
|
||||||
sleep: true
|
|
||||||
slice2js: true
|
|
||||||
snyk: true
|
|
||||||
sockopt: true
|
|
||||||
sodium-native: true
|
|
||||||
sonar-scanner: true
|
|
||||||
spago: true
|
|
||||||
spectron: true
|
|
||||||
spellchecker: true
|
|
||||||
sq-native: true
|
|
||||||
sqlite3: true
|
|
||||||
sse4_crc32: true
|
|
||||||
ssh2: true
|
|
||||||
storage-engine: true
|
|
||||||
subrequests: true
|
|
||||||
subrequests-express: true
|
|
||||||
subrequests-json-merger: true
|
|
||||||
supabase: true
|
|
||||||
svf-lib: true
|
|
||||||
swagger-ui: true
|
|
||||||
swiftlint: true
|
|
||||||
taiko: true
|
|
||||||
tldjs: true
|
|
||||||
tree-sitter: true
|
|
||||||
tree-sitter-cli: true
|
|
||||||
tree-sitter-json: true
|
|
||||||
tree-sitter-kotlin: true
|
|
||||||
tree-sitter-typescript: true
|
|
||||||
tree-sitter-yaml: true
|
|
||||||
truffle: true
|
|
||||||
tsparticles-engine: true
|
|
||||||
ttag-cli: true
|
|
||||||
ttf2woff2: true
|
|
||||||
typemoq: true
|
|
||||||
unix-dgram: true
|
|
||||||
ursa-optional: true
|
|
||||||
usb: true
|
|
||||||
utf-8-validate: true
|
|
||||||
v8-profiler-next: true
|
|
||||||
vue-demi: true
|
|
||||||
vue-echarts: true
|
|
||||||
vue-inbrowser-compiler-demi: true
|
|
||||||
wd: true
|
|
||||||
wdeasync: true
|
|
||||||
weak-napi: true
|
|
||||||
webdev-toolkit: true
|
|
||||||
windows-build-tools: true
|
|
||||||
wix-style-react: true
|
|
||||||
wordpos: true
|
|
||||||
workerd: true
|
|
||||||
wrtc: true
|
|
||||||
xxhash: true
|
|
||||||
yo: true
|
|
||||||
yorkie: true
|
|
||||||
zeromq: true
|
|
||||||
zlib-sync: true
|
|
||||||
zopflipng-bin: true
|
|
||||||
# dyad-default-allow-builds end
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- .
|
|
||||||
minimumReleaseAge: 1440
|
|
||||||
Binary file not shown.
+142
-3
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Full Database Schema
|
// CompetenceTrack — Full Database Schema
|
||||||
// SQLite via Prisma ORM | UUIDs for portability | UTC timestamps
|
// SQLite via Prisma ORM | UUIDs for portability | UTC timestamps
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
@@ -136,6 +136,10 @@ model School {
|
|||||||
dashboardWidgets DashboardWidget[]
|
dashboardWidgets DashboardWidget[]
|
||||||
dashboardLayouts DashboardLayout[]
|
dashboardLayouts DashboardLayout[]
|
||||||
widgetTemplates WidgetTemplate[]
|
widgetTemplates WidgetTemplate[]
|
||||||
|
// Module C escalation relations
|
||||||
|
escalationPolicies EscalationPolicy[]
|
||||||
|
escalations Escalation[]
|
||||||
|
escalationMessageTemplates EscalationMessageTemplate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@@ -241,6 +245,9 @@ model User {
|
|||||||
// Module J widgets relations
|
// Module J widgets relations
|
||||||
dashboardWidgets DashboardWidget[]
|
dashboardWidgets DashboardWidget[]
|
||||||
dashboardLayouts DashboardLayout[]
|
dashboardLayouts DashboardLayout[]
|
||||||
|
// Module C escalation relations
|
||||||
|
escalationsInitiated Escalation[] @relation("EscalationInitiator")
|
||||||
|
escalationLogsPerformed EscalationLog[] @relation("EscalationLogPerformer")
|
||||||
}
|
}
|
||||||
|
|
||||||
model SchoolYear {
|
model SchoolYear {
|
||||||
@@ -383,7 +390,7 @@ model Enrollment {
|
|||||||
schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id])
|
schoolYear SchoolYear @relation(fields: [schoolYearId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Subject & Competency Structure ────────────────────────────────
|
// ─── Subject & Competency Structure ─────────────────────────────��──
|
||||||
|
|
||||||
model Subject {
|
model Subject {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -1240,7 +1247,7 @@ model Backup {
|
|||||||
@@map("backups")
|
@@map("backups")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Behavior Interventions ─────────────────────────────────────────
|
// ──�� Behavior Interventions ─────────────────────────────────────────
|
||||||
|
|
||||||
model BehaviorIntervention {
|
model BehaviorIntervention {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -3330,6 +3337,138 @@ model SchoolComplianceStatus {
|
|||||||
school School @relation(fields: [schoolId], references: [id])
|
school School @relation(fields: [schoolId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Module C: Escalation Policy Engine ─────────────────────────────
|
||||||
|
|
||||||
|
model EscalationPolicy {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
schoolId String
|
||||||
|
name String // "Verhaltensauffälligkeiten", "Noten-Bedenken", etc.
|
||||||
|
description String?
|
||||||
|
triggerType String // BEHAVIOR_INCIDENT, GRADE_CONCERN, ATTENDANCE, WELLNESS, CUSTOM
|
||||||
|
severity String // MINOR, MODERATE, MAJOR
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
// Business days calculation
|
||||||
|
businessDaysUntilEscalation Int @default(5) // e.g., 5 business days until parent can escalate
|
||||||
|
timezone String @default("Europe/Berlin")
|
||||||
|
|
||||||
|
// Escalation chain configuration
|
||||||
|
escalationSteps EscalationStep[]
|
||||||
|
|
||||||
|
// Audit
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
school School @relation(fields: [schoolId], references: [id])
|
||||||
|
escalations Escalation[]
|
||||||
|
|
||||||
|
@@unique([schoolId, name])
|
||||||
|
@@index([schoolId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model EscalationStep {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
policyId String
|
||||||
|
stepNumber Int // 1, 2, 3, etc.
|
||||||
|
recipientRole String // TEACHER, CLASS_LEADER, VICE_PRINCIPAL, PRINCIPAL, COUNSELOR, SCHOOL_ADMIN
|
||||||
|
requiresConfirmation Boolean @default(true)
|
||||||
|
autoNotify Boolean @default(true)
|
||||||
|
messageTemplate String? // Reason why escalation necessary
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
policy EscalationPolicy @relation(fields: [policyId], references: [id], onDelete: Cascade)
|
||||||
|
escalationLogs EscalationLog[]
|
||||||
|
|
||||||
|
@@unique([policyId, stepNumber])
|
||||||
|
@@index([policyId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Escalation {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
schoolId String
|
||||||
|
policyId String
|
||||||
|
initiatedByUserId String
|
||||||
|
initiatorRole String // student, parent, teacher
|
||||||
|
subjectUserId String // Student being escalated
|
||||||
|
subjectType String // STUDENT, BEHAVIOR, GRADE, ATTENDANCE, WELLNESS
|
||||||
|
subjectId String // ID of the specific issue (incidentId, gradeId, etc.)
|
||||||
|
|
||||||
|
// Escalation state
|
||||||
|
status String @default("PENDING") // PENDING, IN_REVIEW, ESCALATED, RESOLVED, CLOSED
|
||||||
|
currentStep Int @default(1)
|
||||||
|
eligibleAtDateTime DateTime // Calculated by business-day logic
|
||||||
|
|
||||||
|
// Context
|
||||||
|
reason String // Why escalation was initiated
|
||||||
|
attachmentUrls String? // JSON array of file URLs
|
||||||
|
|
||||||
|
// Audit
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
resolvedAt DateTime?
|
||||||
|
|
||||||
|
school School @relation(fields: [schoolId], references: [id])
|
||||||
|
policy EscalationPolicy @relation(fields: [policyId], references: [id])
|
||||||
|
initiatedBy User @relation("EscalationInitiator", fields: [initiatedByUserId], references: [id])
|
||||||
|
subjectUser Student @relation(fields: [subjectUserId], references: [id])
|
||||||
|
logs EscalationLog[]
|
||||||
|
|
||||||
|
@@index([schoolId])
|
||||||
|
@@index([policyId])
|
||||||
|
@@index([subjectUserId])
|
||||||
|
@@index([status])
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model EscalationLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
escalationId String
|
||||||
|
stepId String
|
||||||
|
actionType String // INITIATED, NOTIFIED, CONFIRMED, ESCALATED_TO_NEXT_STEP, RESOLVED
|
||||||
|
performedByUserId String?
|
||||||
|
actionReason String?
|
||||||
|
confidentialNote String? // Only visible to authorized users
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
escalation Escalation @relation(fields: [escalationId], references: [id], onDelete: Cascade)
|
||||||
|
step EscalationStep @relation(fields: [stepId], references: [id])
|
||||||
|
performedBy User? @relation("EscalationLogPerformer", fields: [performedByUserId], references: [id])
|
||||||
|
|
||||||
|
@@index([escalationId])
|
||||||
|
@@index([performedByUserId])
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model EscalationMessageTemplate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
schoolId String
|
||||||
|
triggerType String // BEHAVIOR_INCIDENT, GRADE_CONCERN, ATTENDANCE, WELLNESS
|
||||||
|
severity String // MINOR, MODERATE, MAJOR
|
||||||
|
stepNumber Int // At which step this message is sent
|
||||||
|
|
||||||
|
// Template content
|
||||||
|
titleDe String // German
|
||||||
|
titleEn String? // English (optional)
|
||||||
|
bodyDe String
|
||||||
|
bodyEn String?
|
||||||
|
|
||||||
|
// Personalization placeholders: {{studentName}}, {{incidentDate}}, {{reason}}, {{nextStepRole}}
|
||||||
|
|
||||||
|
isDefault Boolean @default(false)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
school School @relation(fields: [schoolId], references: [id])
|
||||||
|
|
||||||
|
@@unique([schoolId, triggerType, severity, stepNumber])
|
||||||
|
@@index([schoolId])
|
||||||
|
}
|
||||||
|
|
||||||
model DeletionFlag {
|
model DeletionFlag {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
schoolId String
|
schoolId String
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
import bcrypt from 'bcryptjs'
|
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
const password = 'Demo2025!'
|
|
||||||
|
|
||||||
const demoUsers = [
|
|
||||||
{ email: 'demo@competencetrack.org', firstName: 'Anna', lastName: 'Müller', role: 'SCHOOL_ADMIN' },
|
|
||||||
{ email: 'demo.vice@competencetrack.org', firstName: 'Clara', lastName: 'Schmidt', role: 'VICE_PRINCIPAL' },
|
|
||||||
{ email: 'demo.teacher@competencetrack.org', firstName: 'Max', lastName: 'Lehrer', role: 'TEACHER' },
|
|
||||||
{ email: 'demo.student@competencetrack.org', firstName: 'Lena', lastName: 'Schüler', role: 'STUDENT' },
|
|
||||||
{ email: 'demo.parent@competencetrack.org', firstName: 'Thomas', lastName: 'Elter', role: 'PARENT' },
|
|
||||||
]
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const passwordHash = await bcrypt.hash(password, 10)
|
|
||||||
|
|
||||||
for (const user of demoUsers) {
|
|
||||||
await prisma.user.upsert({
|
|
||||||
where: { email: user.email },
|
|
||||||
update: { isDemo: true, passwordHash },
|
|
||||||
create: { ...user, passwordHash, locale: 'de', isDemo: true },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const seededCount = await prisma.user.count({
|
|
||||||
where: { email: { in: demoUsers.map(({ email }) => email) }, isDemo: true },
|
|
||||||
})
|
|
||||||
if (seededCount !== demoUsers.length) throw new Error(`Expected ${demoUsers.length} demo accounts, found ${seededCount}`)
|
|
||||||
|
|
||||||
console.log(`Ensured ${seededCount} demo accounts.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect()
|
|
||||||
})
|
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* SchulOS — Prisma Seed Script
|
* CompetenceTrack — Prisma Seed Script
|
||||||
* Populates the database with realistic demo data for a Berlin elementary school.
|
* Populates the database with realistic demo data for a Berlin elementary school.
|
||||||
*
|
*
|
||||||
* Usage: bun prisma/seed.ts
|
* Usage: bun prisma/seed.ts
|
||||||
@@ -29,7 +29,7 @@ function pick<T>(arr: T[]): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('🌱 Seeding SchulOS database...\n')
|
console.log('🌱 Seeding CompetenceTrack database...\n')
|
||||||
|
|
||||||
// Clean up existing data (order matters due to foreign keys)
|
// Clean up existing data (order matters due to foreign keys)
|
||||||
console.log(' 🧹 Cleaning existing data...')
|
console.log(' 🧹 Cleaning existing data...')
|
||||||
|
|||||||
+29
-1
@@ -1 +1,29 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" role="img" aria-labelledby="title"><title>SchulOS</title><rect width="48" height="48" rx="12" fill="#059669"/><path fill="#fff" d="M13 14.5c0-2.5 2-4.5 4.5-4.5h13A4.5 4.5 0 0 1 35 14.5v19c0 2.5-2 4.5-4.5 4.5h-13a4.5 4.5 0 0 1-4.5-4.5v-19Zm5 0v19h12v-19H18Zm3 4h6v3h-6v-3Zm0 6h6v3h-6v-3Z"/></svg>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||||
|
<defs>
|
||||||
|
<style type="text/css">
|
||||||
|
.st194{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.6317;stroke-miterlimit:10;}
|
||||||
|
.st23{fill:#FFFFFF;}
|
||||||
|
|
||||||
|
.z-breathe {
|
||||||
|
animation: breathe 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breathe {
|
||||||
|
0%, 100% { opacity: 0.7; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<path class="st194" d="M24.51,28.51H5.49c-2.21,0-4-1.79-4-4V5.49c0-2.21,1.79-4,4-4h19.03c2.21,0,4,1.79,4,4v19.03
|
||||||
|
C28.51,26.72,26.72,28.51,24.51,28.51z"/>
|
||||||
|
<g class="z-breathe">
|
||||||
|
<path class="st23" d="M15.47,7.1l-1.3,1.85c-0.2,0.29-0.54,0.47-0.9,0.47h-7.1V7.09C6.16,7.1,15.47,7.1,15.47,7.1z"/>
|
||||||
|
<polygon class="st23" points="24.3,7.1 13.14,22.91 5.7,22.91 16.86,7.1"/>
|
||||||
|
<path class="st23" d="M14.53,22.91l1.31-1.86c0.2-0.29,0.54-0.47,0.9-0.47h7.09v2.33H14.53z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "SchulOS",
|
"name": "CompetenceTrack",
|
||||||
"short_name": "SchulOS",
|
"short_name": "CompTrack",
|
||||||
"description": "Kompetenz- und Benotungsplattform für Schulen",
|
"description": "Kompetenz- und Benotungsplattform für Schulen",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
|||||||
+32
-64
@@ -1,12 +1,8 @@
|
|||||||
// SchulOS — Service Worker for PWA Offline Support
|
// CompetenceTrack — Service Worker for PWA Offline Support
|
||||||
const CACHE_NAME = 'schulos-v3';
|
const CACHE_NAME = 'competencetrack-v3';
|
||||||
const STATIC_CACHE = 'schulos-static-v3';
|
const STATIC_CACHE = 'competencetrack-static-v3';
|
||||||
const DATA_CACHE = 'schulos-data-v3';
|
const DATA_CACHE = 'competencetrack-data-v3';
|
||||||
const OFFLINE_CACHE = 'schulos-offline-v3';
|
const OFFLINE_CACHE = 'competencetrack-offline-v3';
|
||||||
const SYNC_TAG = 'schulos-sync';
|
|
||||||
const LEGACY_SYNC_TAG = 'competencetrack-sync';
|
|
||||||
const SYNC_DB = 'schulos-sync-db';
|
|
||||||
const LEGACY_SYNC_DB = 'competencetrack-sync-db';
|
|
||||||
|
|
||||||
// Static assets to cache on install
|
// Static assets to cache on install
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
@@ -37,17 +33,19 @@ self.addEventListener('install', (event) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Activate event — migrate queued writes before clearing legacy caches.
|
// Activate event — clean up old caches
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
migrateLegacyPendingRequests()
|
caches.keys().then((keys) => {
|
||||||
.then(() => caches.keys())
|
return Promise.all(
|
||||||
.then((keys) => Promise.all(
|
|
||||||
keys
|
keys
|
||||||
.filter((key) => key !== CACHE_NAME && key !== STATIC_CACHE && key !== DATA_CACHE && key !== OFFLINE_CACHE)
|
.filter((key) => key !== CACHE_NAME && key !== STATIC_CACHE && key !== DATA_CACHE && key !== OFFLINE_CACHE)
|
||||||
.map((key) => caches.delete(key))
|
.map((key) => caches.delete(key))
|
||||||
))
|
);
|
||||||
.then(() => self.clients.claim())
|
}).then(() => {
|
||||||
|
console.log('[SW] Activated and old caches cleaned');
|
||||||
|
return self.clients.claim();
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,7 +191,7 @@ async function networkFirstWithOfflineFallback(request) {
|
|||||||
|
|
||||||
// Store failed requests for background sync
|
// Store failed requests for background sync
|
||||||
async function storeRequestForSync(request) {
|
async function storeRequestForSync(request) {
|
||||||
const db = await openIndexedDB(SYNC_DB);
|
const db = await openIndexedDB();
|
||||||
const tx = db.transaction('pending-requests', 'readwrite');
|
const tx = db.transaction('pending-requests', 'readwrite');
|
||||||
const store = tx.objectStore('pending-requests');
|
const store = tx.objectStore('pending-requests');
|
||||||
const requestData = {
|
const requestData = {
|
||||||
@@ -204,13 +202,16 @@ async function storeRequestForSync(request) {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
store.add(requestData);
|
store.add(requestData);
|
||||||
await transactionDone(tx);
|
await tx.done;
|
||||||
self.registration.sync.register(SYNC_TAG);
|
|
||||||
|
// Register for background sync
|
||||||
|
self.registration.sync.register('competencetrack-sync');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openIndexedDB(name) {
|
// Open IndexedDB for storing pending requests
|
||||||
|
function openIndexedDB() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(name, 1);
|
const request = indexedDB.open('competencetrack-sync-db', 1);
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
if (!db.objectStoreNames.contains('pending-requests')) {
|
if (!db.objectStoreNames.contains('pending-requests')) {
|
||||||
@@ -222,54 +223,18 @@ function openIndexedDB(name) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function transactionDone(tx) {
|
// Background sync event — replay queued requests when back online
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
tx.oncomplete = () => resolve();
|
|
||||||
tx.onerror = () => reject(tx.error);
|
|
||||||
tx.onabort = () => reject(tx.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateLegacyPendingRequests() {
|
|
||||||
const legacyDb = await openIndexedDB(LEGACY_SYNC_DB);
|
|
||||||
const legacyTx = legacyDb.transaction('pending-requests', 'readonly');
|
|
||||||
const legacyStore = legacyTx.objectStore('pending-requests');
|
|
||||||
const requests = await new Promise((resolve, reject) => {
|
|
||||||
const request = legacyStore.getAll();
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
await transactionDone(legacyTx);
|
|
||||||
|
|
||||||
if (!requests.length) return;
|
|
||||||
|
|
||||||
const currentDb = await openIndexedDB(SYNC_DB);
|
|
||||||
const currentTx = currentDb.transaction('pending-requests', 'readwrite');
|
|
||||||
const currentStore = currentTx.objectStore('pending-requests');
|
|
||||||
for (const request of requests) currentStore.put(request);
|
|
||||||
await transactionDone(currentTx);
|
|
||||||
|
|
||||||
const deleteTx = legacyDb.transaction('pending-requests', 'readwrite');
|
|
||||||
deleteTx.objectStore('pending-requests').clear();
|
|
||||||
await transactionDone(deleteTx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background sync event — replay queues from both brand generations.
|
|
||||||
self.addEventListener('sync', (event) => {
|
self.addEventListener('sync', (event) => {
|
||||||
if (event.tag === SYNC_TAG || event.tag === LEGACY_SYNC_TAG) {
|
if (event.tag === 'competencetrack-sync') {
|
||||||
event.waitUntil(migrateLegacyPendingRequests().then(replayPendingRequests));
|
event.waitUntil(replayPendingRequests());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function replayPendingRequests() {
|
async function replayPendingRequests() {
|
||||||
const db = await openIndexedDB(SYNC_DB);
|
const db = await openIndexedDB();
|
||||||
const tx = db.transaction('pending-requests', 'readwrite');
|
const tx = db.transaction('pending-requests', 'readwrite');
|
||||||
const store = tx.objectStore('pending-requests');
|
const store = tx.objectStore('pending-requests');
|
||||||
const allRequests = await new Promise((resolve, reject) => {
|
const allRequests = await store.getAll();
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const reqData of allRequests) {
|
for (const reqData of allRequests) {
|
||||||
try {
|
try {
|
||||||
@@ -279,12 +244,15 @@ async function replayPendingRequests() {
|
|||||||
headers,
|
headers,
|
||||||
body: reqData.method !== 'GET' ? reqData.body : undefined,
|
body: reqData.method !== 'GET' ? reqData.body : undefined,
|
||||||
});
|
});
|
||||||
if (response.ok) store.delete(reqData.timestamp);
|
if (response.ok) {
|
||||||
|
store.delete(reqData.timestamp);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[SW] Failed to replay request:', error);
|
console.warn('[SW] Failed to replay request:', error);
|
||||||
|
// Keep the request in the store for next sync attempt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await transactionDone(tx);
|
await tx.done;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push event — show notification (if push notifications are configured)
|
// Push event — show notification (if push notifications are configured)
|
||||||
@@ -301,7 +269,7 @@ self.addEventListener('push', (event) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.registration.showNotification(data.title || 'SchulOS', options)
|
self.registration.showNotification(data.title || 'CompetenceTrack', options)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function CompliancePage() {
|
|||||||
<Card className="border-emerald-200 bg-emerald-50">
|
<Card className="border-emerald-200 bg-emerald-50">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<p className="text-emerald-800 mb-4">
|
<p className="text-emerald-800 mb-4">
|
||||||
✓ Compliance onboarding completed successfully! Your school is now ready to use SchulOS.
|
✓ Compliance onboarding completed successfully! Your school is now ready to use ExaMetra.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/admin/compliance-overview")}
|
onClick={() => router.push("/admin/compliance-overview")}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Announcement Read Receipt API
|
// CompetenceTrack — Announcement Read Receipt API
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export async function POST(request: Request) {
|
|||||||
teacherName: `${session.user?.firstName || 'Frau'} ${session.user?.lastName || 'Muster'}`,
|
teacherName: `${session.user?.firstName || 'Frau'} ${session.user?.lastName || 'Muster'}`,
|
||||||
date: new Date().toLocaleDateString('de-DE'),
|
date: new Date().toLocaleDateString('de-DE'),
|
||||||
score: '85%',
|
score: '85%',
|
||||||
schoolName: 'SchulOS Schule',
|
schoolName: 'CompetenceTrack Schule',
|
||||||
subjectName: 'Mathematik',
|
subjectName: 'Mathematik',
|
||||||
behaviorDescription: 'Beschreibung des Vorfalls',
|
behaviorDescription: 'Beschreibung des Vorfalls',
|
||||||
behaviorCategory: 'Störung',
|
behaviorCategory: 'Störung',
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export async function GET() {
|
|||||||
const exportData: Record<string, unknown> = {
|
const exportData: Record<string, unknown> = {
|
||||||
exportMetadata: {
|
exportMetadata: {
|
||||||
exportDate: new Date().toISOString(),
|
exportDate: new Date().toISOString(),
|
||||||
platform: 'SchulOS',
|
platform: 'CompetenceTrack',
|
||||||
legalBasis: 'DSGVO Art. 20 - Recht auf Datenübertragbarkeit',
|
legalBasis: 'DSGVO Art. 20 - Recht auf Datenübertragbarkeit',
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
@@ -285,7 +285,7 @@ export async function GET() {
|
|||||||
|
|
||||||
return NextResponse.json(exportData, {
|
return NextResponse.json(exportData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Disposition': `attachment; filename="schulos-data-export-${new Date().toISOString().split('T')[0]}.json"`,
|
'Content-Disposition': `attachment; filename="competencetrack-data-export-${new Date().toISOString().split('T')[0]}.json"`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Single Notification API
|
// CompetenceTrack — Single Notification API
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Notification Preferences API
|
// CompetenceTrack — Notification Preferences API
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Notifications API (DB-backed, Enhanced)
|
// CompetenceTrack — Notifications API (DB-backed, Enhanced)
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ function generatePrintHtml(report: Record<string, unknown>, template: string): s
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="watermark">SchulOS</div>
|
<div class="watermark">CompetenceTrack</div>
|
||||||
<button class="print-btn no-print" onclick="window.print()">Drucken / Print</button>
|
<button class="print-btn no-print" onclick="window.print()">Drucken / Print</button>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -364,7 +364,7 @@ function generatePrintHtml(report: Record<string, unknown>, template: string): s
|
|||||||
${gradesHtml}
|
${gradesHtml}
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>SchulOS — Kompetenzbericht · ${new Date().toLocaleDateString('de-DE')}</p>
|
<p>CompetenceTrack — Kompetenzbericht · ${new Date().toLocaleDateString('de-DE')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="eco-footer">
|
<div class="eco-footer">
|
||||||
Digital erstellt — Papier sparen, Umwelt schutzen
|
Digital erstellt — Papier sparen, Umwelt schutzen
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ export async function POST(request: Request) {
|
|||||||
{
|
{
|
||||||
schoolId: school.id,
|
schoolId: school.id,
|
||||||
name: 'welcome',
|
name: 'welcome',
|
||||||
subject: 'Willkommen bei SchulOS',
|
subject: 'Willkommen bei CompetenceTrack',
|
||||||
body: '<h1>Willkommen bei SchulOS!</h1><p>Hallo {{studentName}},</p><p>du wurdest für die Schule {{schoolName}} registriert.</p><p>Deine Zugangsdaten:</p><p>E-Mail: {{email}}</p><p>Bitte melde dich an und ändere dein Passwort.</p><p>Mit freundlichen Grüßen,<br>{{schoolName}}</p>',
|
body: '<h1>Willkommen bei CompetenceTrack!</h1><p>Hallo {{studentName}},</p><p>du wurdest für die Schule {{schoolName}} registriert.</p><p>Deine Zugangsdaten:</p><p>E-Mail: {{email}}</p><p>Bitte melde dich an und ändere dein Passwort.</p><p>Mit freundlichen Grüßen,<br>{{schoolName}}</p>',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ export async function GET(request: Request) {
|
|||||||
if (userId) {
|
if (userId) {
|
||||||
// For STUDENT role, also match by name + school as fallback for legacy data
|
// For STUDENT role, also match by name + school as fallback for legacy data
|
||||||
if (session.user?.role === 'STUDENT' && session.user.id === userId) {
|
if (session.user?.role === 'STUDENT' && session.user.id === userId) {
|
||||||
const legacyStudentFilter: Record<string, unknown> = {
|
userIdFilter = [
|
||||||
userId: null,
|
{ userId },
|
||||||
firstName: session.user.firstName,
|
{
|
||||||
lastName: session.user.lastName,
|
userId: null,
|
||||||
schoolId: session.user.schoolId,
|
firstName: session.user.firstName,
|
||||||
};
|
lastName: session.user.lastName,
|
||||||
userIdFilter = [{ userId }, legacyStudentFilter];
|
schoolId: session.user.schoolId,
|
||||||
|
},
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
where.userId = userId;
|
where.userId = userId;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -15,19 +15,19 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SchulOS — Free Competency & Grading Platform",
|
title: "CompetenceTrack — Free Competency & Grading Platform",
|
||||||
description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.",
|
description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.",
|
||||||
keywords: ["SchulOS", "competency", "grading", "school", "education", "Next.js", "TypeScript", "shadcn/ui"],
|
keywords: ["CompetenceTrack", "competency", "grading", "school", "education", "Next.js", "TypeScript", "shadcn/ui"],
|
||||||
authors: [{ name: "SchulOS Team" }],
|
authors: [{ name: "CompetenceTrack Team" }],
|
||||||
manifest: "/manifest.json",
|
manifest: "/manifest.json",
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/logo.svg",
|
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
||||||
apple: "/icon-192.png",
|
apple: "/icon-192.png",
|
||||||
},
|
},
|
||||||
appleWebApp: {
|
appleWebApp: {
|
||||||
capable: true,
|
capable: true,
|
||||||
statusBarStyle: "default",
|
statusBarStyle: "default",
|
||||||
title: "SchulOS",
|
title: "CompetenceTrack",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ function renderView(view: ViewName) {
|
|||||||
case 'matrix': return <MasteryMatrixView />;
|
case 'matrix': return <MasteryMatrixView />;
|
||||||
case 'analytics': return <AnalyticsView />;
|
case 'analytics': return <AnalyticsView />;
|
||||||
case 'assessments': return <AssessmentsView />;
|
case 'assessments': return <AssessmentsView />;
|
||||||
case 'grading': return <UnifiedGradingPanel />;
|
case 'grading': return <GradingView />;
|
||||||
case 'reports': return <ReportsView />;
|
case 'reports': return <ReportsView />;
|
||||||
case 'settings': return <SettingsView />;
|
case 'settings': return <SettingsView />;
|
||||||
case 'student-detail': return <StudentDetailView />;
|
case 'student-detail': return <StudentDetailView />;
|
||||||
@@ -975,7 +975,7 @@ export default function AppLayout() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="group-data-[collapsible=icon]:hidden">
|
<div className="group-data-[collapsible=icon]:hidden">
|
||||||
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 leading-tight">{schoolBranding?.name || 'SchulOS'}</h2>
|
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 leading-tight">{schoolBranding?.name || 'CompetenceTrack'}</h2>
|
||||||
<p className="text-[10px] text-emerald-600/70 dark:text-emerald-400/50 leading-tight">{t('app.subtitle')}</p>
|
<p className="text-[10px] text-emerald-600/70 dark:text-emerald-400/50 leading-tight">{t('app.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -1409,7 +1409,7 @@ export default function AppLayout() {
|
|||||||
<div className="flex items-center justify-between text-xs text-emerald-600/60 dark:text-emerald-400/40">
|
<div className="flex items-center justify-between text-xs text-emerald-600/60 dark:text-emerald-400/40">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Leaf className="h-3 w-3 text-emerald-500" />
|
<Leaf className="h-3 w-3 text-emerald-500" />
|
||||||
SchulOS — {t('footer.copyright')}
|
CompetenceTrack — {t('footer.copyright')}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="hidden sm:inline">{t('footer.version')} · {t('footer.oss')}</span>
|
<span className="hidden sm:inline">{t('footer.version')} · {t('footer.oss')}</span>
|
||||||
@@ -1609,7 +1609,7 @@ export default function AppLayout() {
|
|||||||
{t('polish.help')}
|
{t('polish.help')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs text-gray-500 dark:text-gray-400">
|
<DialogDescription className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
SchulOS · {t('footer.version')}
|
CompetenceTrack · {t('footer.version')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
|
|||||||
+148
-43
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence, type Variants } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { BookOpen, Mail, Lock, User, ArrowRight, GraduationCap, Heart, Sparkles, Flower2, Eye, EyeOff, KeyRound, Info, Shield, Leaf, Users as UsersIcon, HelpCircle, LockKeyhole, GitBranch, CheckCircle2, XCircle } from 'lucide-react';
|
import { BookOpen, Mail, Lock, User, ArrowRight, GraduationCap, Heart, Sparkles, Flower2, Eye, EyeOff, KeyRound, Info, Shield, Leaf, Users as UsersIcon, HelpCircle, BarChart3, LockKeyhole, GitBranch, CheckCircle2, XCircle, Globe, Github, Chrome } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -34,7 +34,7 @@ const containerVariants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 14 },
|
hidden: { opacity: 0, y: 14 },
|
||||||
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: 'easeOut' } },
|
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: 'easeOut' } },
|
||||||
};
|
};
|
||||||
@@ -160,7 +160,7 @@ function LeftIllustration({ loginRole }: { loginRole: LoginRole }) {
|
|||||||
initial={{ opacity: 0, x: -40 }}
|
initial={{ opacity: 0, x: -40 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.7, delay: 0.2, ease: 'easeOut' }}
|
transition={{ duration: 0.7, delay: 0.2, ease: 'easeOut' }}
|
||||||
className="relative z-10 hidden lg:flex flex-col items-center justify-center w-1/2 max-w-lg mr-12"
|
className="hidden lg:flex flex-col items-center justify-center w-1/2 max-w-lg mr-12"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Decorative orbiting circles */}
|
{/* Decorative orbiting circles */}
|
||||||
@@ -223,7 +223,9 @@ function LeftIllustration({ loginRole }: { loginRole: LoginRole }) {
|
|||||||
transition={{ duration: 0.5, delay: 0.7 }}
|
transition={{ duration: 0.5, delay: 0.7 }}
|
||||||
className="mt-12 space-y-4 text-center"
|
className="mt-12 space-y-4 text-center"
|
||||||
>
|
>
|
||||||
<p className="text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-100">SchulOS</p>
|
<h3 className="text-2xl font-bold text-gray-800 dark:text-gray-200 tracking-tight">
|
||||||
|
{t('auth.mission_title')}
|
||||||
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{features.map((f, i) => {
|
{features.map((f, i) => {
|
||||||
const c = colorClasses[f.color];
|
const c = colorClasses[f.color];
|
||||||
@@ -238,7 +240,7 @@ function LeftIllustration({ loginRole }: { loginRole: LoginRole }) {
|
|||||||
<div className={`flex items-center justify-center w-9 h-9 rounded-xl ${c.bg} ${c.text} ring-1 ${c.ring}`}>
|
<div className={`flex items-center justify-center w-9 h-9 rounded-xl ${c.bg} ${c.text} ring-1 ${c.ring}`}>
|
||||||
<f.icon className="w-4.5 h-4.5" />
|
<f.icon className="w-4.5 h-4.5" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-800 dark:text-gray-200 font-semibold">{t(f.labelKey)}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-medium">{t(f.labelKey)}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -249,23 +251,23 @@ function LeftIllustration({ loginRole }: { loginRole: LoginRole }) {
|
|||||||
initial={{ opacity: 0, y: 14 }}
|
initial={{ opacity: 0, y: 14 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 1.3 }}
|
transition={{ duration: 0.5, delay: 1.3 }}
|
||||||
className="relative z-10 mt-8 p-5 rounded-2xl bg-white dark:bg-gray-800 border border-emerald-300 dark:border-emerald-600 shadow-xl shadow-emerald-200/60 dark:shadow-emerald-950/50"
|
className="mt-8 p-5 rounded-2xl bg-white/60 dark:bg-gray-800/40 backdrop-blur-md border border-emerald-200/30 dark:border-emerald-900/20 shadow-xl shadow-emerald-100/20 dark:shadow-emerald-900/10"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-red-400" />
|
<div className="w-2.5 h-2.5 rounded-full bg-red-400" />
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-amber-400" />
|
<div className="w-2.5 h-2.5 rounded-full bg-amber-400" />
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-emerald-400" />
|
<div className="w-2.5 h-2.5 rounded-full bg-emerald-400" />
|
||||||
<div className="ml-3 flex-1 h-2.5 rounded-full bg-emerald-100 dark:bg-emerald-900" />
|
<div className="ml-3 flex-1 h-2.5 rounded-full bg-emerald-100 dark:bg-emerald-900/30" />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||||
<div key={i} className={`h-10 rounded-lg ${i % 3 === 0 ? 'bg-gradient-to-br from-emerald-100 to-emerald-200 dark:from-emerald-800 dark:to-emerald-900' : i % 3 === 1 ? 'bg-gradient-to-br from-teal-100 to-teal-200 dark:from-teal-800 dark:to-teal-900' : 'bg-gradient-to-br from-amber-100 to-amber-200 dark:from-amber-800 dark:to-amber-900'}`} />
|
<div key={i} className={`h-10 rounded-lg ${i % 3 === 0 ? 'bg-gradient-to-br from-emerald-100 to-emerald-200/50 dark:from-emerald-900/30 dark:to-emerald-800/20' : i % 3 === 1 ? 'bg-gradient-to-br from-teal-100 to-teal-200/50 dark:from-teal-900/30 dark:to-teal-800/20' : 'bg-gradient-to-br from-amber-100 to-amber-200/50 dark:from-amber-900/30 dark:to-amber-800/20'}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
<div className="h-2 w-full rounded-full bg-emerald-100 dark:bg-emerald-900" />
|
<div className="h-2 w-full rounded-full bg-emerald-100 dark:bg-emerald-900/30" />
|
||||||
<div className="h-2 w-3/4 rounded-full bg-teal-100 dark:bg-teal-900" />
|
<div className="h-2 w-3/4 rounded-full bg-teal-100 dark:bg-teal-900/30" />
|
||||||
<div className="h-2 w-1/2 rounded-full bg-amber-100 dark:bg-amber-900" />
|
<div className="h-2 w-1/2 rounded-full bg-amber-100 dark:bg-amber-900/30" />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -273,6 +275,56 @@ function LeftIllustration({ loginRole }: { loginRole: LoginRole }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Features section below form ───────────────────────────── */
|
||||||
|
function FeaturesSection() {
|
||||||
|
const features = [
|
||||||
|
{ icon: BarChart3, labelKey: 'auth.feature_grading', descKey: 'auth.feature_grading_desc', color: 'emerald' },
|
||||||
|
{ icon: GraduationCap, labelKey: 'auth.feature_attendance', descKey: 'auth.feature_attendance_desc', color: 'teal' },
|
||||||
|
{ icon: Sparkles, labelKey: 'auth.feature_ai', descKey: 'auth.feature_ai_desc', color: 'violet' },
|
||||||
|
{ icon: Heart, labelKey: 'auth.feature_collaboration', descKey: 'auth.feature_collaboration_desc', color: 'amber' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorClasses: Record<string, { bg: string; text: string; border: string; iconBg: string }> = {
|
||||||
|
emerald: { bg: 'bg-emerald-50 dark:bg-emerald-900/20', text: 'text-emerald-600 dark:text-emerald-400', border: 'border-emerald-200/60 dark:border-emerald-800/40', iconBg: 'bg-gradient-to-br from-emerald-400 to-emerald-500 text-white' },
|
||||||
|
teal: { bg: 'bg-teal-50 dark:bg-teal-900/20', text: 'text-teal-600 dark:text-teal-400', border: 'border-teal-200/60 dark:border-teal-800/40', iconBg: 'bg-gradient-to-br from-teal-400 to-teal-500 text-white' },
|
||||||
|
amber: { bg: 'bg-amber-50 dark:bg-amber-900/20', text: 'text-amber-600 dark:text-amber-400', border: 'border-amber-200/60 dark:border-amber-800/40', iconBg: 'bg-gradient-to-br from-amber-400 to-amber-500 text-white' },
|
||||||
|
violet: { bg: 'bg-violet-50 dark:bg-violet-900/20', text: 'text-violet-600 dark:text-violet-400', border: 'border-violet-200/60 dark:border-violet-800/40', iconBg: 'bg-gradient-to-br from-violet-400 to-violet-500 text-white' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.6 }}
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 text-center">{t('auth.feature_showcase')}</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{features.map((f, i) => {
|
||||||
|
const c = colorClasses[f.color];
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={f.labelKey}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.7 + i * 0.08 }}
|
||||||
|
className={`flex items-start gap-2.5 px-3 py-3 rounded-xl ${c.bg} border ${c.border}`}
|
||||||
|
>
|
||||||
|
<div className={`flex items-center justify-center w-7 h-7 rounded-lg ${c.iconBg} shrink-0 shadow-sm`}>
|
||||||
|
<f.icon className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-xs font-semibold text-gray-800 dark:text-gray-200 leading-tight">{t(f.labelKey)}</p>
|
||||||
|
<p className="text-[10px] text-gray-500 dark:text-gray-400 leading-tight mt-0.5">{t(f.descKey)}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Main Auth View ────────────────────────────────────────── */
|
/* ─── Main Auth View ────────────────────────────────────────── */
|
||||||
export default function AuthView() {
|
export default function AuthView() {
|
||||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||||
@@ -382,7 +434,6 @@ export default function AuthView() {
|
|||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
color: string;
|
color: string;
|
||||||
iconColor: string;
|
|
||||||
activeColor: string;
|
activeColor: string;
|
||||||
activeBg: string;
|
activeBg: string;
|
||||||
bgGradient: string;
|
bgGradient: string;
|
||||||
@@ -395,7 +446,6 @@ export default function AuthView() {
|
|||||||
icon: GraduationCap,
|
icon: GraduationCap,
|
||||||
labelKey: 'auth.login',
|
labelKey: 'auth.login',
|
||||||
color: 'text-emerald-600 dark:text-emerald-400',
|
color: 'text-emerald-600 dark:text-emerald-400',
|
||||||
iconColor: 'text-emerald-600 dark:text-emerald-400',
|
|
||||||
activeColor: 'text-white',
|
activeColor: 'text-white',
|
||||||
activeBg: 'bg-gradient-to-r from-emerald-500 to-teal-500',
|
activeBg: 'bg-gradient-to-r from-emerald-500 to-teal-500',
|
||||||
bgGradient: 'from-emerald-50/50 to-teal-50/50 dark:from-emerald-950/10 dark:to-teal-950/10',
|
bgGradient: 'from-emerald-50/50 to-teal-50/50 dark:from-emerald-950/10 dark:to-teal-950/10',
|
||||||
@@ -407,8 +457,7 @@ export default function AuthView() {
|
|||||||
key: 'student',
|
key: 'student',
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
labelKey: 'auth.student_login',
|
labelKey: 'auth.student_login',
|
||||||
color: 'text-emerald-600 dark:text-emerald-400',
|
color: 'text-amber-600 dark:text-amber-400',
|
||||||
iconColor: 'text-emerald-600 dark:text-emerald-400',
|
|
||||||
activeColor: 'text-white',
|
activeColor: 'text-white',
|
||||||
activeBg: 'bg-gradient-to-r from-amber-500 to-orange-500',
|
activeBg: 'bg-gradient-to-r from-amber-500 to-orange-500',
|
||||||
bgGradient: 'from-amber-50/50 to-orange-50/50 dark:from-amber-950/10 dark:to-orange-950/10',
|
bgGradient: 'from-amber-50/50 to-orange-50/50 dark:from-amber-950/10 dark:to-orange-950/10',
|
||||||
@@ -420,8 +469,7 @@ export default function AuthView() {
|
|||||||
key: 'parent',
|
key: 'parent',
|
||||||
icon: Heart,
|
icon: Heart,
|
||||||
labelKey: 'auth.parent_login',
|
labelKey: 'auth.parent_login',
|
||||||
color: 'text-emerald-600 dark:text-emerald-400',
|
color: 'text-violet-600 dark:text-violet-400',
|
||||||
iconColor: 'text-emerald-600 dark:text-emerald-400',
|
|
||||||
activeColor: 'text-white',
|
activeColor: 'text-white',
|
||||||
activeBg: 'bg-gradient-to-r from-violet-500 to-purple-500',
|
activeBg: 'bg-gradient-to-r from-violet-500 to-purple-500',
|
||||||
bgGradient: 'from-violet-50/50 to-purple-50/50 dark:from-violet-950/10 dark:to-purple-950/10',
|
bgGradient: 'from-violet-50/50 to-purple-50/50 dark:from-violet-950/10 dark:to-purple-950/10',
|
||||||
@@ -472,6 +520,14 @@ export default function AuthView() {
|
|||||||
>
|
>
|
||||||
{t('app.name')}
|
{t('app.name')}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.3 }}
|
||||||
|
className="mt-2 text-sm text-emerald-600/70 dark:text-emerald-400/50"
|
||||||
|
>
|
||||||
|
{t('app.subtitle')}
|
||||||
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auth Card - Glassmorphism */}
|
{/* Auth Card - Glassmorphism */}
|
||||||
@@ -499,7 +555,7 @@ export default function AuthView() {
|
|||||||
whileHover={{ scale: loginRole === tab.key ? 1 : 1.02 }}
|
whileHover={{ scale: loginRole === tab.key ? 1 : 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
<tab.icon className={`h-4.5 w-4.5 ${loginRole === tab.key ? 'text-white' : tab.iconColor}`} />
|
<tab.icon className="h-4.5 w-4.5" />
|
||||||
<span>{t(tab.labelKey)}</span>
|
<span>{t(tab.labelKey)}</span>
|
||||||
{/* Active indicator */}
|
{/* Active indicator */}
|
||||||
{loginRole === tab.key && (
|
{loginRole === tab.key && (
|
||||||
@@ -546,11 +602,11 @@ export default function AuthView() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="firstName" className="text-sm font-medium">{t('auth.firstName')}</Label>
|
<Label htmlFor="firstName" className="text-sm font-medium">{t('auth.firstName')}</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<User className="pointer-events-none absolute z-10 left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-400" />
|
||||||
<Input id="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} className={`pl-10 pr-9 h-12 min-h-[44px] bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 transition-all duration-200 ${
|
<Input id="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} className={`pl-10 pr-9 h-12 min-h-[44px] bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
|
||||||
firstName.length > 0 && firstNameValid ? 'input-valid border-emerald-400 dark:border-emerald-500'
|
firstName.length > 0 && firstNameValid ? 'input-valid border-emerald-400 dark:border-emerald-500'
|
||||||
: firstName.length > 0 && !firstNameValid ? 'input-invalid border-red-300 dark:border-red-700'
|
: firstName.length > 0 && !firstNameValid ? 'input-invalid border-red-300 dark:border-red-700'
|
||||||
: 'border-emerald-200 dark:border-emerald-800 focus:border-emerald-400 focus:ring-emerald-400/20'
|
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
|
||||||
}`} placeholder={t('auth.firstName')} required />
|
}`} placeholder={t('auth.firstName')} required />
|
||||||
{firstName.length > 0 && (
|
{firstName.length > 0 && (
|
||||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} className="absolute right-2.5 top-1/2 -translate-y-1/2">
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} className="absolute right-2.5 top-1/2 -translate-y-1/2">
|
||||||
@@ -562,10 +618,10 @@ export default function AuthView() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="lastName" className="text-sm font-medium">{t('auth.lastName')}</Label>
|
<Label htmlFor="lastName" className="text-sm font-medium">{t('auth.lastName')}</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input id="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} className={`pr-9 h-12 min-h-[44px] bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 transition-all duration-200 ${
|
<Input id="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} className={`pr-9 h-12 min-h-[44px] bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
|
||||||
lastName.length > 0 && lastNameValid ? 'input-valid border-emerald-400 dark:border-emerald-500'
|
lastName.length > 0 && lastNameValid ? 'input-valid border-emerald-400 dark:border-emerald-500'
|
||||||
: lastName.length > 0 && !lastNameValid ? 'input-invalid border-red-300 dark:border-red-700'
|
: lastName.length > 0 && !lastNameValid ? 'input-invalid border-red-300 dark:border-red-700'
|
||||||
: 'border-emerald-200 dark:border-emerald-800 focus:border-emerald-400 focus:ring-emerald-400/20'
|
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
|
||||||
}`} placeholder={t('auth.lastName')} required />
|
}`} placeholder={t('auth.lastName')} required />
|
||||||
{lastName.length > 0 && (
|
{lastName.length > 0 && (
|
||||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} className="absolute right-2.5 top-1/2 -translate-y-1/2">
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} className="absolute right-2.5 top-1/2 -translate-y-1/2">
|
||||||
@@ -583,8 +639,8 @@ export default function AuthView() {
|
|||||||
<motion.div key="school-id" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.25 }} className="space-y-2">
|
<motion.div key="school-id" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.25 }} className="space-y-2">
|
||||||
<Label htmlFor="schoolId" className="text-sm font-medium">{t('auth.school_id')}</Label>
|
<Label htmlFor="schoolId" className="text-sm font-medium">{t('auth.school_id')}</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Info className="pointer-events-none absolute z-10 left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
<Info className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-amber-400" />
|
||||||
<Input id="schoolId" value={schoolIdInput} onChange={(e) => setSchoolIdInput(e.target.value)} className="pl-10 h-12 min-h-[44px] border-emerald-200 dark:border-emerald-800 focus:border-emerald-400 focus:ring-emerald-400/20 text-base bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400" placeholder={t('auth.school_id')} />
|
<Input id="schoolId" value={schoolIdInput} onChange={(e) => setSchoolIdInput(e.target.value)} className="pl-10 h-12 min-h-[44px] border-amber-200/50 dark:border-amber-900/30 focus:border-amber-400 focus:ring-amber-400/20 text-base bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm" placeholder={t('auth.school_id')} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -592,9 +648,9 @@ export default function AuthView() {
|
|||||||
{/* Parent-specific: name fields on login */}
|
{/* Parent-specific: name fields on login */}
|
||||||
{loginRole === 'parent' && mode === 'login' && (
|
{loginRole === 'parent' && mode === 'login' && (
|
||||||
<motion.div key="parent-name" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.25 }}>
|
<motion.div key="parent-name" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.25 }}>
|
||||||
<div className="flex items-center gap-2 px-3 py-2.5 rounded-xl bg-emerald-50/50 dark:bg-emerald-900/10 border border-emerald-100/50 dark:border-emerald-900/20 backdrop-blur-sm">
|
<div className="flex items-center gap-2 px-3 py-2.5 rounded-xl bg-violet-50/50 dark:bg-violet-900/10 border border-violet-100/50 dark:border-violet-900/20 backdrop-blur-sm">
|
||||||
<Heart className="h-4 w-4 text-emerald-500" />
|
<Heart className="h-4 w-4 text-violet-500" />
|
||||||
<p className="text-xs text-emerald-700 dark:text-emerald-300 font-medium">{t('auth.parent_login_desc')}</p>
|
<p className="text-xs text-violet-700 dark:text-violet-300 font-medium">{t('auth.parent_login_desc')}</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -604,16 +660,16 @@ export default function AuthView() {
|
|||||||
<motion.div variants={itemVariants} className="space-y-2">
|
<motion.div variants={itemVariants} className="space-y-2">
|
||||||
<Label htmlFor="email" className="text-sm font-medium">{t('auth.email')}</Label>
|
<Label htmlFor="email" className="text-sm font-medium">{t('auth.email')}</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Mail className="pointer-events-none absolute z-10 left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
<Mail className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-400" />
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className={`pl-10 pr-10 h-12 min-h-[44px] text-base bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 transition-all duration-200 ${
|
className={`pl-10 pr-10 h-12 min-h-[44px] text-base bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
|
||||||
emailTouched && emailValid ? 'input-valid border-emerald-400 dark:border-emerald-500 focus:border-emerald-500 focus:ring-emerald-400/20'
|
emailTouched && emailValid ? 'input-valid border-emerald-400 dark:border-emerald-500 focus:border-emerald-500 focus:ring-emerald-400/20'
|
||||||
: emailTouched && !emailValid ? 'input-invalid border-red-300 dark:border-red-700 focus:border-red-400 focus:ring-red-400/20'
|
: emailTouched && !emailValid ? 'input-invalid border-red-300 dark:border-red-700 focus:border-red-400 focus:ring-red-400/20'
|
||||||
: 'border-emerald-200 dark:border-emerald-800 focus:border-emerald-400 focus:ring-emerald-400/20'
|
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
|
||||||
} ${
|
} ${
|
||||||
loginRole === 'student' ? 'focus:border-amber-400 focus:ring-amber-400/20'
|
loginRole === 'student' ? 'focus:border-amber-400 focus:ring-amber-400/20'
|
||||||
: loginRole === 'parent' ? 'focus:border-violet-400 focus:ring-violet-400/20'
|
: loginRole === 'parent' ? 'focus:border-violet-400 focus:ring-violet-400/20'
|
||||||
@@ -657,16 +713,16 @@ export default function AuthView() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="pointer-events-none absolute z-10 left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
<Lock className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-400" />
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className={`pl-9 pr-12 h-12 min-h-[44px] bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 transition-all duration-200 ${
|
className={`pl-9 pr-12 h-12 min-h-[44px] bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
|
||||||
passwordTouched && passwordValid ? 'input-valid border-emerald-400 dark:border-emerald-500 focus:border-emerald-500 focus:ring-emerald-400/20'
|
passwordTouched && passwordValid ? 'input-valid border-emerald-400 dark:border-emerald-500 focus:border-emerald-500 focus:ring-emerald-400/20'
|
||||||
: passwordTouched && !passwordValid ? 'input-invalid border-red-300 dark:border-red-700 focus:border-red-400 focus:ring-red-400/20'
|
: passwordTouched && !passwordValid ? 'input-invalid border-red-300 dark:border-red-700 focus:border-red-400 focus:ring-red-400/20'
|
||||||
: 'border-emerald-200 dark:border-emerald-800 focus:border-emerald-400 focus:ring-emerald-400/20'
|
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
|
||||||
}`}
|
}`}
|
||||||
placeholder="••••••"
|
placeholder="••••••"
|
||||||
minLength={6}
|
minLength={6}
|
||||||
@@ -803,13 +859,13 @@ export default function AuthView() {
|
|||||||
>
|
>
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-3">
|
||||||
<div className="flex items-center justify-center gap-1.5">
|
<div className="flex items-center justify-center gap-1.5">
|
||||||
<KeyRound className="h-3.5 w-3.5 text-emerald-500" />
|
<KeyRound className="h-3.5 w-3.5 text-amber-500" />
|
||||||
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('auth.demo_section_title')}</p>
|
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('auth.demo_section_title')}</p>
|
||||||
<Badge className="bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 border-emerald-200/50 dark:border-emerald-900/30 text-[10px] px-1.5 py-0">{t('badge.demo')}</Badge>
|
<Badge className="bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 border-amber-200/50 dark:border-amber-900/30 text-[10px] px-1.5 py-0">{t('badge.demo')}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{t('auth.demo_section_subtitle')}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{t('auth.demo_section_subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{DEMO_ACCOUNTS.map((demo, i) => (
|
{DEMO_ACCOUNTS.map((demo, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={demo.email}
|
key={demo.email}
|
||||||
@@ -822,12 +878,10 @@ export default function AuthView() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleDemoLogin(demo.email, demo.password)}
|
onClick={() => handleDemoLogin(demo.email, demo.password)}
|
||||||
disabled={loading || demoLoading}
|
disabled={loading || demoLoading}
|
||||||
className={`group min-h-12 h-auto w-full min-w-0 px-2 py-2 rounded-xl border-0 bg-gradient-to-r ${demo.colorClass} text-white font-semibold text-sm shadow-lg flex items-center justify-center gap-1.5 hover:shadow-xl hover:scale-[1.01] active:scale-[0.99] transition-all duration-200`}
|
className={`group h-auto min-h-[44px] py-3 px-3 rounded-xl border-0 bg-gradient-to-br ${demo.colorClass} text-white font-semibold text-xs shadow-lg flex flex-col items-center gap-1.5 hover:shadow-xl hover:scale-[1.03] active:scale-[0.97] transition-all duration-200`}
|
||||||
>
|
>
|
||||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white/90 text-emerald-600">
|
<demo.icon className="h-5 w-5 transition-transform duration-200 group-hover:scale-110" />
|
||||||
<demo.icon className="h-4 w-4 transition-transform duration-200 group-hover:scale-110" />
|
<span>{t(demo.labelKey)}</span>
|
||||||
</span>
|
|
||||||
<span className="min-w-0 break-words text-center leading-tight">{t(demo.labelKey)}</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
@@ -899,6 +953,57 @@ export default function AuthView() {
|
|||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Social login buttons (visual only) */}
|
||||||
|
{mode === 'login' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5, duration: 0.4 }}
|
||||||
|
className="mt-5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||||
|
<span className="text-xs text-gray-400 dark:text-gray-500 font-medium">{t('auth.or_continue_with') || 'or continue with'}</span>
|
||||||
|
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
className="social-btn flex items-center justify-center gap-2 h-11 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm text-gray-700 dark:text-gray-300 font-medium text-xs hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
onClick={() => toast.info(t('auth.social_coming_soon') || 'Social login coming soon')}
|
||||||
|
>
|
||||||
|
<Chrome className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="hidden sm:inline">Google</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
className="social-btn flex items-center justify-center gap-2 h-11 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm text-gray-700 dark:text-gray-300 font-medium text-xs hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
onClick={() => toast.info(t('auth.social_coming_soon') || 'Social login coming soon')}
|
||||||
|
>
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">GitHub</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
className="social-btn flex items-center justify-center gap-2 h-11 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm text-gray-700 dark:text-gray-300 font-medium text-xs hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
onClick={() => toast.info(t('auth.social_coming_soon') || 'Social login coming soon')}
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4 text-emerald-500" />
|
||||||
|
<span className="hidden sm:inline">Moodle</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Features section below form */}
|
||||||
|
<FeaturesSection />
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|||||||
@@ -3295,7 +3295,7 @@ export default function DashboardView() {
|
|||||||
<div className="hidden sm:flex flex-col items-end gap-1 shrink-0">
|
<div className="hidden sm:flex flex-col items-end gap-1 shrink-0">
|
||||||
<Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300 text-xs">
|
<Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300 text-xs">
|
||||||
<GraduationCap className="h-3 w-3 mr-1" />
|
<GraduationCap className="h-3 w-3 mr-1" />
|
||||||
{currentUser?.schoolId ? t('nav.classes') : 'SchulOS'}
|
{currentUser?.schoolId ? t('nav.classes') : 'CompetenceTrack'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-[10px] text-gray-400 dark:text-gray-500">
|
<span className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||||
{t('dashboard.total_students')}: {data.stats.totalStudents} · {t('dashboard.total_classes')}: {data.stats.totalClasses}
|
{t('dashboard.total_students')}: {data.stats.totalStudents} · {t('dashboard.total_classes')}: {data.stats.totalClasses}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export default function OnboardingTour({ open, onClose }: OnboardingTourProps) {
|
|||||||
{/* Hidden title for accessibility */}
|
{/* Hidden title for accessibility */}
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
<DialogTitle>SchulOS Onboarding Tour</DialogTitle>
|
<DialogTitle>CompetenceTrack Onboarding Tour</DialogTitle>
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
|
|||||||
@@ -1762,7 +1762,7 @@ function generatePrintHTML(
|
|||||||
return `
|
return `
|
||||||
<div class="report-card-page">
|
<div class="report-card-page">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>${school?.name ?? 'SchulOS'}</h1>
|
<h1>${school?.name ?? 'CompetenceTrack'}</h1>
|
||||||
${school?.motto ? `<p class="motto">${school.motto}</p>` : ''}
|
${school?.motto ? `<p class="motto">${school.motto}</p>` : ''}
|
||||||
<h2>Zeugnis / Report Card</h2>
|
<h2>Zeugnis / Report Card</h2>
|
||||||
<p>Schuljahr: ${report.schoolYear.label}</p>
|
<p>Schuljahr: ${report.schoolYear.label}</p>
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ export default function SchoolLibraryView() {
|
|||||||
<p><strong>${t('library.due_date')}:</strong> ${new Date(checkout.dueDate).toLocaleDateString()}</p>
|
<p><strong>${t('library.due_date')}:</strong> ${new Date(checkout.dueDate).toLocaleDateString()}</p>
|
||||||
<p><strong>${t('library.condition')}:</strong> ${checkout.condition}</p>
|
<p><strong>${t('library.condition')}:</strong> ${checkout.condition}</p>
|
||||||
<hr/>
|
<hr/>
|
||||||
<p style="text-align:center; font-size: 12px;">SchulOS - ${t('library.title')}</p>
|
<p style="text-align:center; font-size: 12px;">CompetenceTrack - ${t('library.title')}</p>
|
||||||
</body></html>
|
</body></html>
|
||||||
`);
|
`);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ function PrivacyTab({ currentUser }: { currentUser: { id: string; role: string;
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `schulos-data-export-${new Date().toISOString().split('T')[0]}.json`;
|
a.download = `competencetrack-data-export-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
@@ -866,7 +866,7 @@ function PrivacyTab({ currentUser }: { currentUser: { id: string; role: string;
|
|||||||
<Mail className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
<Mail className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('dsgvo.contact_dpo')}</h4>
|
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('dsgvo.contact_dpo')}</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">datenschutz@schulos.org</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">datenschutz@competencetrack.org</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1070,12 +1070,12 @@ function PrivacyTab({ currentUser }: { currentUser: { id: string; role: string;
|
|||||||
className="min-h-[44px] rounded-xl border-violet-200 dark:border-violet-900/30 text-violet-700 dark:text-violet-300 hover:bg-violet-50 dark:hover:bg-violet-900/20 font-semibold"
|
className="min-h-[44px] rounded-xl border-violet-200 dark:border-violet-900/30 text-violet-700 dark:text-violet-300 hover:bg-violet-50 dark:hover:bg-violet-900/20 font-semibold"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Generate a simple DPA template as a downloadable text file
|
// Generate a simple DPA template as a downloadable text file
|
||||||
const dpaTemplate = `Auftragsverarbeitungsvertrag (AVV)\ngemäß Art. 28 DSGVO\n\nVerantwortliche Stelle (Schule):\n[Name der Schule]\n[Adresse]\n\nAuftragsverarbeiter:\nSchulOS\n[Adresse]\n\n1. Gegenstand und Dauer der Verarbeitung\nDer Auftragsverarbeiter verarbeitet personenbezogene Daten im Auftrag der verantwortlichen Stelle gemäß den Bestimmungen dieses Vertrages.\n\n2. Art und Zweck der Verarbeitung\n- Bereitstellung und Betrieb der SchulOS-Plattform\n- Verwaltung von Schüler- und Klassendaten\n- Dokumentation von Lernfortschritten\n- Erstellung von Zeugnissen und Berichten\n\n3. Art der personenbezogenen Daten\n- Name, E-Mail-Adresse\n- Schülereinschreibungsdaten\n- Lernfortschrittsdaten\n- Bewertungsdaten\n- Anwesenheitsdaten\n\n4. Betroffene Personenkreise\n- Schülerinnen und Schüler\n- Lehrkräfte\n- Eltern / Erziehungsberechtigte\n\n5. Technisch-organisatorische Maßnahmen\nDer Auftragsverarbeiter implementiert angemessene technisch-organisatorische Maßnahmen gemäß Art. 32 DSGVO.\n\n6. Löschung und Rückgabe von Daten\nNach Beendigung des Auftragsverhältnisses werden alle personenbezogenen Daten gelöscht.\n\n7. Kontrolle durch die verantwortliche Stelle\nDie verantwortliche Stelle hat das Recht, die Einhaltung der Maßnahmen zu kontrollieren.\n\n8. Unterauftragsverhältnisse\nEine Weitergabe an Unterauftragnehmer bedarf der vorherigen Zustimmung der verantwortlichen Stelle.\n\nOrt, Datum: _______________\n\nUnterschrift Verantwortliche Stelle: _______________\nUnterschrift Auftragsverarbeiter: _______________`;
|
const dpaTemplate = `Auftragsverarbeitungsvertrag (AVV)\ngemäß Art. 28 DSGVO\n\nVerantwortliche Stelle (Schule):\n[Name der Schule]\n[Adresse]\n\nAuftragsverarbeiter:\nCompetenceTrack\n[Adresse]\n\n1. Gegenstand und Dauer der Verarbeitung\nDer Auftragsverarbeiter verarbeitet personenbezogene Daten im Auftrag der verantwortlichen Stelle gemäß den Bestimmungen dieses Vertrages.\n\n2. Art und Zweck der Verarbeitung\n- Bereitstellung und Betrieb der CompetenceTrack-Plattform\n- Verwaltung von Schüler- und Klassendaten\n- Dokumentation von Lernfortschritten\n- Erstellung von Zeugnissen und Berichten\n\n3. Art der personenbezogenen Daten\n- Name, E-Mail-Adresse\n- Schülereinschreibungsdaten\n- Lernfortschrittsdaten\n- Bewertungsdaten\n- Anwesenheitsdaten\n\n4. Betroffene Personenkreise\n- Schülerinnen und Schüler\n- Lehrkräfte\n- Eltern / Erziehungsberechtigte\n\n5. Technisch-organisatorische Maßnahmen\nDer Auftragsverarbeiter implementiert angemessene technisch-organisatorische Maßnahmen gemäß Art. 32 DSGVO.\n\n6. Löschung und Rückgabe von Daten\nNach Beendigung des Auftragsverhältnisses werden alle personenbezogenen Daten gelöscht.\n\n7. Kontrolle durch die verantwortliche Stelle\nDie verantwortliche Stelle hat das Recht, die Einhaltung der Maßnahmen zu kontrollieren.\n\n8. Unterauftragsverhältnisse\nEine Weitergabe an Unterauftragnehmer bedarf der vorherigen Zustimmung der verantwortlichen Stelle.\n\nOrt, Datum: _______________\n\nUnterschrift Verantwortliche Stelle: _______________\nUnterschrift Auftragsverarbeiter: _______________`;
|
||||||
const blob = new Blob([dpaTemplate], { type: 'text/plain;charset=utf-8' });
|
const blob = new Blob([dpaTemplate], { type: 'text/plain;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = 'AVV_SchulOS_Vorlage.txt';
|
a.download = 'AVV_CompetenceTrack_Vorlage.txt';
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
@@ -1102,7 +1102,7 @@ function PrivacyTab({ currentUser }: { currentUser: { id: string; role: string;
|
|||||||
title: 'Verzeichnis von Verarbeitungstätigkeiten',
|
title: 'Verzeichnis von Verarbeitungstätigkeiten',
|
||||||
legalBasis: 'Art. 30 DSGVO',
|
legalBasis: 'Art. 30 DSGVO',
|
||||||
controller: '[Name der Schule]',
|
controller: '[Name der Schule]',
|
||||||
processor: 'SchulOS',
|
processor: 'CompetenceTrack',
|
||||||
processingActivities: [
|
processingActivities: [
|
||||||
{ name: 'Benutzerverwaltung', purpose: 'Bereitstellung der Plattform', dataCategories: 'Name, E-Mail, Rolle', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
{ name: 'Benutzerverwaltung', purpose: 'Bereitstellung der Plattform', dataCategories: 'Name, E-Mail, Rolle', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||||
{ name: 'Schülerverwaltung', purpose: 'Verwaltung von Klassen und Schülern', dataCategories: 'Name, Geburtsdatum, Geschlecht', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
{ name: 'Schülerverwaltung', purpose: 'Verwaltung von Klassen und Schülern', dataCategories: 'Name, Geburtsdatum, Geschlecht', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||||
@@ -5085,7 +5085,7 @@ export default function SettingsView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-gray-900 dark:text-gray-100" style={{ color: brandingForm.primaryColor }}>{selectedSchool?.name || 'SchulOS'}</p>
|
<p className="font-bold text-gray-900 dark:text-gray-100" style={{ color: brandingForm.primaryColor }}>{selectedSchool?.name || 'CompetenceTrack'}</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{brandingForm.motto || t('app.subtitle')}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">{brandingForm.motto || t('app.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5444,7 +5444,7 @@ export default function SettingsView() {
|
|||||||
<div className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
<div className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||||
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">{t('email.template_subject')}: {emailPreviewTemplate.subject}</p>
|
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">{t('email.template_subject')}: {emailPreviewTemplate.subject}</p>
|
||||||
<div className="mt-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 text-sm max-h-64 overflow-y-auto scrollbar-education">
|
<div className="mt-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 text-sm max-h-64 overflow-y-auto scrollbar-education">
|
||||||
{emailPreviewTemplate.body.replace(/\{\{studentName\}\}/g, 'Max Mustermann').replace(/\{\{className\}\}/g, 'Klasse 5a').replace(/\{\{teacherName\}\}/g, 'Frau Muster').replace(/\{\{date\}\}/g, new Date().toLocaleDateString()).replace(/\{\{schoolName\}\}/g, 'SchulOS Schule').replace(/\{\{subjectName\}\}/g, 'Mathematik').replace(/\{\{score\}\}/g, '85%')}
|
{emailPreviewTemplate.body.replace(/\{\{studentName\}\}/g, 'Max Mustermann').replace(/\{\{className\}\}/g, 'Klasse 5a').replace(/\{\{teacherName\}\}/g, 'Frau Muster').replace(/\{\{date\}\}/g, new Date().toLocaleDateString()).replace(/\{\{schoolName\}\}/g, 'CompetenceTrack Schule').replace(/\{\{subjectName\}\}/g, 'Mathematik').replace(/\{\{score\}\}/g, '85%')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ function exportStudentCsv(data: StudentDetailData) {
|
|||||||
const rows: string[] = [];
|
const rows: string[] = [];
|
||||||
|
|
||||||
// Header section
|
// Header section
|
||||||
rows.push('SchulOS - Student Export');
|
rows.push('CompetenceTrack - Student Export');
|
||||||
rows.push(`Student,${studentName}`);
|
rows.push(`Student,${studentName}`);
|
||||||
rows.push(`Class,${primaryClass}`);
|
rows.push(`Class,${primaryClass}`);
|
||||||
rows.push(`School,${schoolName}`);
|
rows.push(`School,${schoolName}`);
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — API Client
|
// CompetenceTrack — API Client
|
||||||
// All API calls go through these helpers
|
// All API calls go through these helpers
|
||||||
|
|
||||||
/* ── Generic helpers ──────────────────────────────────────────────── */
|
/* ── Generic helpers ──────────────────────────────────────────────── */
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Audit Logging Middleware
|
// CompetenceTrack — Audit Logging Middleware
|
||||||
// Provides logAudit() and withAuditLog() helpers for recording all data modifications
|
// Provides logAudit() and withAuditLog() helpers for recording all data modifications
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Badge Auto-Award Check Utility
|
// CompetenceTrack — Badge Auto-Award Check Utility
|
||||||
// Checks if a student meets requirements for auto-awarded badges
|
// Checks if a student meets requirements for auto-awarded badges
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — In-memory API response cache utility
|
// CompetenceTrack — In-memory API response cache utility
|
||||||
// Provides TTL-based caching for API responses to reduce redundant fetches
|
// Provides TTL-based caching for API responses to reduce redundant fetches
|
||||||
|
|
||||||
interface CacheEntry<T> {
|
interface CacheEntry<T> {
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
/**
|
||||||
|
* Escalation Policy Engine
|
||||||
|
* Handles business-day calculations, escalation eligibility, and workflow management
|
||||||
|
* Module C: Communication & Escalation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
isWeekend,
|
||||||
|
differenceInCalendarDays,
|
||||||
|
setHours,
|
||||||
|
setMinutes,
|
||||||
|
isBefore,
|
||||||
|
isAfter,
|
||||||
|
parseISO,
|
||||||
|
} from 'date-fns';
|
||||||
|
import { de as deLocale } from 'date-fns/locale';
|
||||||
|
|
||||||
|
export interface SchoolDayConfig {
|
||||||
|
timezone: string;
|
||||||
|
publicHolidays: Date[]; // German public holidays for the year
|
||||||
|
schoolHolidays: { start: Date; end: Date }[]; // Specific school holidays
|
||||||
|
schoolDayStartTime: string; // "08:00"
|
||||||
|
schoolDayEndTime: string; // "16:00"
|
||||||
|
businessDaysPerWeek: number; // Usually 5
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EscalationPolicyRule {
|
||||||
|
triggerType: string; // BEHAVIOR_INCIDENT, GRADE_CONCERN, ATTENDANCE, WELLNESS
|
||||||
|
severity: string; // MINOR, MODERATE, MAJOR
|
||||||
|
businessDaysUntilEscalation: number;
|
||||||
|
escalationChain: {
|
||||||
|
stepNumber: number;
|
||||||
|
recipientRole: string; // TEACHER, CLASS_LEADER, VICE_PRINCIPAL, PRINCIPAL, COUNSELOR
|
||||||
|
requiresConfirmation: boolean;
|
||||||
|
autoNotify: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate business days (excluding weekends, public holidays, school holidays)
|
||||||
|
*/
|
||||||
|
export function getNextBusinessDay(
|
||||||
|
fromDate: Date,
|
||||||
|
config: SchoolDayConfig,
|
||||||
|
daysToAdd: number = 1
|
||||||
|
): Date {
|
||||||
|
let currentDate = new Date(fromDate);
|
||||||
|
let businessDaysAdded = 0;
|
||||||
|
|
||||||
|
while (businessDaysAdded < daysToAdd) {
|
||||||
|
currentDate = addDays(currentDate, 1);
|
||||||
|
|
||||||
|
// Skip weekends
|
||||||
|
if (isWeekend(currentDate, { locale: deLocale })) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip public holidays (German holidays)
|
||||||
|
if (isGermanPublicHoliday(currentDate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip school holidays
|
||||||
|
if (isSchoolHoliday(currentDate, config)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
businessDaysAdded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to end of school day (e.g., 16:00)
|
||||||
|
const [hours, minutes] = config.schoolDayEndTime.split(':').map(Number);
|
||||||
|
return setMinutes(setHours(currentDate, hours), minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate business days between two dates
|
||||||
|
*/
|
||||||
|
export function calculateBusinessDaysBetween(
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date,
|
||||||
|
config: SchoolDayConfig
|
||||||
|
): number {
|
||||||
|
let businessDays = 0;
|
||||||
|
let currentDate = new Date(startDate);
|
||||||
|
|
||||||
|
while (isBefore(currentDate, endDate)) {
|
||||||
|
// Skip weekends
|
||||||
|
if (!isWeekend(currentDate, { locale: deLocale })) {
|
||||||
|
// Skip public holidays
|
||||||
|
if (!isGermanPublicHoliday(currentDate)) {
|
||||||
|
// Skip school holidays
|
||||||
|
if (!isSchoolHoliday(currentDate, config)) {
|
||||||
|
businessDays++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDate = addDays(currentDate, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return businessDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date is a German public holiday
|
||||||
|
*/
|
||||||
|
function isGermanPublicHoliday(date: Date): boolean {
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
|
||||||
|
// German public holidays (simplified, doesn't account for Easter-dependent dates)
|
||||||
|
const publicHolidays = [
|
||||||
|
{ month: 1, day: 1 }, // Neujahrstag
|
||||||
|
{ month: 3, day: 8 }, // Internationaler Frauentag (varies by state)
|
||||||
|
{ month: 5, day: 1 }, // Tag der Arbeit
|
||||||
|
{ month: 5, day: 9 }, // Europatag (varies)
|
||||||
|
{ month: 10, day: 3 }, // Tag der Deutschen Einheit
|
||||||
|
{ month: 10, day: 31 }, // Reformationstag (varies by state)
|
||||||
|
{ month: 11, day: 1 }, // Allerheiligen (varies by state)
|
||||||
|
{ month: 12, day: 25 }, // Weihnachtstag
|
||||||
|
{ month: 12, day: 26 }, // Zweiter Weihnachtstag
|
||||||
|
];
|
||||||
|
|
||||||
|
return publicHolidays.some((h) => h.month === month && h.day === day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date falls within school holidays
|
||||||
|
*/
|
||||||
|
function isSchoolHoliday(date: Date, config: SchoolDayConfig): boolean {
|
||||||
|
return config.schoolHolidays.some((holiday) => {
|
||||||
|
return isBefore(date, holiday.end) && isAfter(date, holiday.start);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if an escalation can be initiated
|
||||||
|
*/
|
||||||
|
export function canInitiateEscalation(
|
||||||
|
escalationCreatedAt: Date,
|
||||||
|
eligibleAtDateTime: Date
|
||||||
|
): boolean {
|
||||||
|
return isBefore(new Date(), eligibleAtDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has permission to escalate at current step
|
||||||
|
*/
|
||||||
|
export function canEscalateToNextStep(
|
||||||
|
currentStep: number,
|
||||||
|
totalSteps: number,
|
||||||
|
userRole: string,
|
||||||
|
escalationPolicy: { escalationSteps: { recipientRole: string }[] }
|
||||||
|
): boolean {
|
||||||
|
if (currentStep >= totalSteps) {
|
||||||
|
return false; // Already at final step
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = escalationPolicy.escalationSteps[currentStep];
|
||||||
|
return userRole === nextStep.recipientRole || userRole === 'SCHOOL_ADMIN';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format escalation eligibility message
|
||||||
|
*/
|
||||||
|
export function formatEscalationEligibility(
|
||||||
|
eligibleAtDateTime: Date,
|
||||||
|
now: Date = new Date(),
|
||||||
|
locale: 'de' | 'en' = 'de'
|
||||||
|
): string {
|
||||||
|
const businessDaysRemaining = calculateBusinessDaysBetween(now, eligibleAtDateTime, {
|
||||||
|
timezone: 'Europe/Berlin',
|
||||||
|
publicHolidays: [],
|
||||||
|
schoolHolidays: [],
|
||||||
|
schoolDayStartTime: '08:00',
|
||||||
|
schoolDayEndTime: '16:00',
|
||||||
|
businessDaysPerWeek: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (locale === 'de') {
|
||||||
|
if (businessDaysRemaining <= 0) {
|
||||||
|
return 'Sie können jetzt eskalieren.';
|
||||||
|
}
|
||||||
|
return `Sie können in ${businessDaysRemaining} Geschäftstag(en) eskalieren.`;
|
||||||
|
} else {
|
||||||
|
if (businessDaysRemaining <= 0) {
|
||||||
|
return 'You can escalate now.';
|
||||||
|
}
|
||||||
|
return `You can escalate in ${businessDaysRemaining} business day(s).`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get escalation severity level
|
||||||
|
*/
|
||||||
|
export function getEscalationSeverity(
|
||||||
|
triggerType: string,
|
||||||
|
incident?: { severity?: string }
|
||||||
|
): string {
|
||||||
|
if (incident?.severity) {
|
||||||
|
return incident.severity; // MINOR, MODERATE, MAJOR
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default severity based on trigger type
|
||||||
|
const severityMap: Record<string, string> = {
|
||||||
|
BEHAVIOR_INCIDENT: 'MODERATE',
|
||||||
|
GRADE_CONCERN: 'MINOR',
|
||||||
|
ATTENDANCE: 'MODERATE',
|
||||||
|
WELLNESS: 'MAJOR',
|
||||||
|
CUSTOM: 'MINOR',
|
||||||
|
};
|
||||||
|
|
||||||
|
return severityMap[triggerType] || 'MINOR';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate escalation request
|
||||||
|
*/
|
||||||
|
export function validateEscalationRequest(
|
||||||
|
initiatorRole: string,
|
||||||
|
escalationPolicy: any,
|
||||||
|
currentStep: number
|
||||||
|
): { valid: boolean; error?: string } {
|
||||||
|
// Students can only initiate escalations if policy allows
|
||||||
|
if (initiatorRole === 'STUDENT' && !escalationPolicy.allowStudentInitiation) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Students cannot initiate escalations for this policy.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parents can only escalate after specific step
|
||||||
|
if (initiatorRole === 'PARENT' && currentStep < (escalationPolicy.parentCanEscalateAtStep || 1)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Parents cannot escalate at this stage.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next recipient in escalation chain
|
||||||
|
*/
|
||||||
|
export function getNextRecipientRole(
|
||||||
|
escalationPolicy: any,
|
||||||
|
currentStep: number
|
||||||
|
): string | null {
|
||||||
|
const nextStep = escalationPolicy.escalationSteps[currentStep];
|
||||||
|
return nextStep?.recipientRole || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create audit trail entry for escalation action
|
||||||
|
*/
|
||||||
|
export function createEscalationAuditEntry(
|
||||||
|
escalationId: string,
|
||||||
|
actionType: string,
|
||||||
|
performedByUserId: string | null,
|
||||||
|
actionReason: string,
|
||||||
|
confidentialNote?: string
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
escalationId,
|
||||||
|
actionType, // INITIATED, NOTIFIED, CONFIRMED, ESCALATED_TO_NEXT_STEP, RESOLVED
|
||||||
|
performedByUserId,
|
||||||
|
actionReason,
|
||||||
|
confidentialNote,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getNextBusinessDay,
|
||||||
|
calculateBusinessDaysBetween,
|
||||||
|
canInitiateEscalation,
|
||||||
|
canEscalateToNextStep,
|
||||||
|
formatEscalationEligibility,
|
||||||
|
getEscalationSeverity,
|
||||||
|
validateEscalationRequest,
|
||||||
|
getNextRecipientRole,
|
||||||
|
createEscalationAuditEntry,
|
||||||
|
};
|
||||||
+25
-25
@@ -1,11 +1,11 @@
|
|||||||
// SchulOS — i18n (German + English)
|
// CompetenceTrack — i18n (German + English)
|
||||||
// Simple translation system: t(key) returns the string for the current locale
|
// Simple translation system: t(key) returns the string for the current locale
|
||||||
|
|
||||||
type TranslationDict = Record<string, string>;
|
type TranslationDict = Record<string, string>;
|
||||||
|
|
||||||
const de: TranslationDict = {
|
const de: TranslationDict = {
|
||||||
// App branding
|
// App branding
|
||||||
'app.name': 'SchulOS',
|
'app.name': 'CompetenceTrack',
|
||||||
'app.subtitle': 'Freie Kompetenz- & Benotungsplattform für Schulen',
|
'app.subtitle': 'Freie Kompetenz- & Benotungsplattform für Schulen',
|
||||||
'app.tagline': 'Open-Source Software für kompetenzorientierte Beurteilung',
|
'app.tagline': 'Open-Source Software für kompetenzorientierte Beurteilung',
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ const de: TranslationDict = {
|
|||||||
'auth.password': 'Passwort',
|
'auth.password': 'Passwort',
|
||||||
'auth.firstName': 'Vorname',
|
'auth.firstName': 'Vorname',
|
||||||
'auth.lastName': 'Nachname',
|
'auth.lastName': 'Nachname',
|
||||||
'auth.login_title': 'Bei SchulOS anmelden',
|
'auth.login_title': 'Bei CompetenceTrack anmelden',
|
||||||
'auth.register_title': 'Neues Konto erstellen',
|
'auth.register_title': 'Neues Konto erstellen',
|
||||||
'auth.login_subtitle': 'Geben Sie Ihre Zugangsdaten ein',
|
'auth.login_subtitle': 'Geben Sie Ihre Zugangsdaten ein',
|
||||||
'auth.register_subtitle': 'Erstellen Sie ein kostenloses Konto',
|
'auth.register_subtitle': 'Erstellen Sie ein kostenloses Konto',
|
||||||
@@ -440,7 +440,7 @@ const de: TranslationDict = {
|
|||||||
|
|
||||||
// Onboarding checklist
|
// Onboarding checklist
|
||||||
'onboarding.title': 'Erste Schritte',
|
'onboarding.title': 'Erste Schritte',
|
||||||
'onboarding.subtitle': 'Follow these steps to set up SchulOS',
|
'onboarding.subtitle': 'Follow these steps to set up CompetenceTrack',
|
||||||
'onboarding.step_create_class': 'Erstellen Sie Ihre erste Klasse',
|
'onboarding.step_create_class': 'Erstellen Sie Ihre erste Klasse',
|
||||||
'onboarding.step_assign_grid': 'Weisen Sie ein Kompetenzraster zu',
|
'onboarding.step_assign_grid': 'Weisen Sie ein Kompetenzraster zu',
|
||||||
'onboarding.step_add_students': 'Fügen Sie Schüler hinzu',
|
'onboarding.step_add_students': 'Fügen Sie Schüler hinzu',
|
||||||
@@ -478,7 +478,7 @@ const de: TranslationDict = {
|
|||||||
'classes.assign_no_templates': 'Keine passenden Raster gefunden.',
|
'classes.assign_no_templates': 'Keine passenden Raster gefunden.',
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
'footer.copyright': 'SchulOS — Freie Schulsoftware',
|
'footer.copyright': 'CompetenceTrack — Freie Schulsoftware',
|
||||||
'footer.version': 'Version 1.0',
|
'footer.version': 'Version 1.0',
|
||||||
'footer.oss': 'Open Source',
|
'footer.oss': 'Open Source',
|
||||||
'sidebar.eco_message': 'Papier sparen, Umwelt schutzen',
|
'sidebar.eco_message': 'Papier sparen, Umwelt schutzen',
|
||||||
@@ -1732,7 +1732,7 @@ const de: TranslationDict = {
|
|||||||
'notifications.days_ago': 'vor {days} Tagen',
|
'notifications.days_ago': 'vor {days} Tagen',
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
'onboarding.welcome_title': 'Willkommen bei SchulOS!',
|
'onboarding.welcome_title': 'Willkommen bei CompetenceTrack!',
|
||||||
'onboarding.welcome_desc': 'Ihre freie Kompetenz- & Benotungsplattform für Schulen. Lassen Sie uns eine kurze Einführung machen.',
|
'onboarding.welcome_desc': 'Ihre freie Kompetenz- & Benotungsplattform für Schulen. Lassen Sie uns eine kurze Einführung machen.',
|
||||||
'onboarding.dashboard_title': 'Dashboard',
|
'onboarding.dashboard_title': 'Dashboard',
|
||||||
'onboarding.dashboard_desc': 'Hier sehen Sie einen Überblick über alle Klassen, Schüler und aktuelle Aktivitäten. Nutzen Sie die Schnellaktionen für häufige Aufgaben.',
|
'onboarding.dashboard_desc': 'Hier sehen Sie einen Überblick über alle Klassen, Schüler und aktuelle Aktivitäten. Nutzen Sie die Schnellaktionen für häufige Aufgaben.',
|
||||||
@@ -1743,7 +1743,7 @@ const de: TranslationDict = {
|
|||||||
'onboarding.reports_title': 'Berichte',
|
'onboarding.reports_title': 'Berichte',
|
||||||
'onboarding.reports_desc': 'Erstellen Sie Zeugnis- und Entwicklungsberichte. Exportieren Sie diese als PDF oder drucken Sie sie direkt.',
|
'onboarding.reports_desc': 'Erstellen Sie Zeugnis- und Entwicklungsberichte. Exportieren Sie diese als PDF oder drucken Sie sie direkt.',
|
||||||
'onboarding.done_title': 'Fertig!',
|
'onboarding.done_title': 'Fertig!',
|
||||||
'onboarding.done_desc': 'Sie sind bereit, SchulOS zu nutzen. Bei Fragen klicken Sie auf das Hilfe-Symbol in der Kopfzeile.',
|
'onboarding.done_desc': 'Sie sind bereit, CompetenceTrack zu nutzen. Bei Fragen klicken Sie auf das Hilfe-Symbol in der Kopfzeile.',
|
||||||
'onboarding.skip': 'Überspringen',
|
'onboarding.skip': 'Überspringen',
|
||||||
'onboarding.next': 'Weiter',
|
'onboarding.next': 'Weiter',
|
||||||
'onboarding.back': 'Zurück',
|
'onboarding.back': 'Zurück',
|
||||||
@@ -1764,7 +1764,7 @@ const de: TranslationDict = {
|
|||||||
'onboarding.parents_title': 'Elternkommunikation',
|
'onboarding.parents_title': 'Elternkommunikation',
|
||||||
'onboarding.parents_desc': 'Kommunizieren Sie mit Eltern über den Lernfortschritt. Senden Sie Berichte und halten Sie den Dialog aufrecht.',
|
'onboarding.parents_desc': 'Kommunizieren Sie mit Eltern über den Lernfortschritt. Senden Sie Berichte und halten Sie den Dialog aufrecht.',
|
||||||
'onboarding.environmental_title': 'Umweltschutz',
|
'onboarding.environmental_title': 'Umweltschutz',
|
||||||
'onboarding.environmental_desc': 'SchulOS spart Papier: Jeder digitale Eintrag ersetzt ca. 50 Seiten Papier. Danke für Ihren Beitrag zum Umweltschutz!',
|
'onboarding.environmental_desc': 'CompetenceTrack spart Papier: Jeder digitale Eintrag ersetzt ca. 50 Seiten Papier. Danke für Ihren Beitrag zum Umweltschutz!',
|
||||||
|
|
||||||
// Batch deletion (progress entries)
|
// Batch deletion (progress entries)
|
||||||
'progress.batch_select': 'Auswählen',
|
'progress.batch_select': 'Auswählen',
|
||||||
@@ -2728,10 +2728,10 @@ const de: TranslationDict = {
|
|||||||
'pwa.back_online_desc': 'Ihre Verbindung wurde wiederhergestellt.',
|
'pwa.back_online_desc': 'Ihre Verbindung wurde wiederhergestellt.',
|
||||||
'pwa.cached_data': 'Cache-Daten',
|
'pwa.cached_data': 'Cache-Daten',
|
||||||
'pwa.install_app': 'App installieren',
|
'pwa.install_app': 'App installieren',
|
||||||
'pwa.install_app_desc': 'SchulOS als App auf Ihrem Gerät installieren.',
|
'pwa.install_app_desc': 'CompetenceTrack als App auf Ihrem Gerät installieren.',
|
||||||
'pwa.install': 'Installieren',
|
'pwa.install': 'Installieren',
|
||||||
'pwa.installed': 'App installiert',
|
'pwa.installed': 'App installiert',
|
||||||
'pwa.installed_desc': 'SchulOS wurde erfolgreich als App installiert.',
|
'pwa.installed_desc': 'CompetenceTrack wurde erfolgreich als App installiert.',
|
||||||
'pwa.install_accepted': 'Installation gestartet',
|
'pwa.install_accepted': 'Installation gestartet',
|
||||||
'pwa.update_available': 'Update verfügbar',
|
'pwa.update_available': 'Update verfügbar',
|
||||||
'pwa.update_available_desc': 'Eine neue Version ist verfügbar. Laden Sie die Seite neu.',
|
'pwa.update_available_desc': 'Eine neue Version ist verfügbar. Laden Sie die Seite neu.',
|
||||||
@@ -3300,7 +3300,7 @@ const de: TranslationDict = {
|
|||||||
'events.view_all': 'Alle Veranstaltungen',
|
'events.view_all': 'Alle Veranstaltungen',
|
||||||
|
|
||||||
// Onboarding Tour (24 steps) - DE
|
// Onboarding Tour (24 steps) - DE
|
||||||
'onboarding.step_1_title': 'Willkommen bei SchulOS!',
|
'onboarding.step_1_title': 'Willkommen bei CompetenceTrack!',
|
||||||
'onboarding.step_1_desc': 'Ihre freie Kompetenz- & Benotungsplattform für Schulen. Lassen Sie uns eine kurze Einführung machen.',
|
'onboarding.step_1_desc': 'Ihre freie Kompetenz- & Benotungsplattform für Schulen. Lassen Sie uns eine kurze Einführung machen.',
|
||||||
'onboarding.step_2_title': 'Dashboard',
|
'onboarding.step_2_title': 'Dashboard',
|
||||||
'onboarding.step_2_desc': 'Hier sehen Sie einen Überblick über alle Klassen, Schüler und aktuelle Aktivitäten. Nutzen Sie die Schnellaktionen und Kennzahlen.',
|
'onboarding.step_2_desc': 'Hier sehen Sie einen Überblick über alle Klassen, Schüler und aktuelle Aktivitäten. Nutzen Sie die Schnellaktionen und Kennzahlen.',
|
||||||
@@ -3349,7 +3349,7 @@ const de: TranslationDict = {
|
|||||||
'onboarding.step_24_title': 'Einstellungen',
|
'onboarding.step_24_title': 'Einstellungen',
|
||||||
'onboarding.step_24_desc': 'Passen Sie die Plattform an: Schulprofil, Branding, Backup und Datenverwaltung.',
|
'onboarding.step_24_desc': 'Passen Sie die Plattform an: Schulprofil, Branding, Backup und Datenverwaltung.',
|
||||||
'onboarding.celebration': 'Sie sind bereit!',
|
'onboarding.celebration': 'Sie sind bereit!',
|
||||||
'onboarding.complete': 'Sie haben die Tour abgeschlossen. Viel Erfolg mit SchulOS!',
|
'onboarding.complete': 'Sie haben die Tour abgeschlossen. Viel Erfolg mit CompetenceTrack!',
|
||||||
'onboarding.previous': 'Zurück',
|
'onboarding.previous': 'Zurück',
|
||||||
'onboarding.dont_show_again': 'Nicht mehr anzeigen',
|
'onboarding.dont_show_again': 'Nicht mehr anzeigen',
|
||||||
'onboarding.restart_tour': 'Tour erneut starten',
|
'onboarding.restart_tour': 'Tour erneut starten',
|
||||||
@@ -3676,7 +3676,7 @@ const de: TranslationDict = {
|
|||||||
'dsgvo.dpa_desc': 'Vertrag zur Auftragsdatenverarbeitung gemäß Art. 28 DSGVO',
|
'dsgvo.dpa_desc': 'Vertrag zur Auftragsdatenverarbeitung gemäß Art. 28 DSGVO',
|
||||||
'dsgvo.data_register': 'Verzeichnis von Verarbeitungstätigkeiten',
|
'dsgvo.data_register': 'Verzeichnis von Verarbeitungstätigkeiten',
|
||||||
'dsgvo.data_register_desc': 'Übersicht aller Verarbeitungstätigkeiten gemäß Art. 30 DSGVO',
|
'dsgvo.data_register_desc': 'Übersicht aller Verarbeitungstätigkeiten gemäß Art. 30 DSGVO',
|
||||||
'dsgvo.privacy_policy_text': 'Die nachfolgende Datenschutzerklärung gilt für die Nutzung der Plattform SchulOS. Verantwortlich für die Datenverarbeitung ist die jeweilige Schule.',
|
'dsgvo.privacy_policy_text': 'Die nachfolgende Datenschutzerklärung gilt für die Nutzung der Plattform CompetenceTrack. Verantwortlich für die Datenverarbeitung ist die jeweilige Schule.',
|
||||||
'dsgvo.data_collected_list': 'Name, E-Mail-Adresse, Passwort (verschlüsselt), Rollenzuordnung, Klassen- und Fächerverknüpfungen, Lernfortschritte, Bewertungen, Anwesenheitsdaten, Notizen und Zeugnisse.',
|
'dsgvo.data_collected_list': 'Name, E-Mail-Adresse, Passwort (verschlüsselt), Rollenzuordnung, Klassen- und Fächerverknüpfungen, Lernfortschritte, Bewertungen, Anwesenheitsdaten, Notizen und Zeugnisse.',
|
||||||
'dsgvo.data_purpose_list': 'Bereitstellung und Betrieb der Plattform, Verwaltung von Schülern und Klassen, Dokumentation von Lernfortschritten, Erstellung von Zeugnissen und Berichten, Kommunikation mit Eltern.',
|
'dsgvo.data_purpose_list': 'Bereitstellung und Betrieb der Plattform, Verwaltung von Schülern und Klassen, Dokumentation von Lernfortschritten, Erstellung von Zeugnissen und Berichten, Kommunikation mit Eltern.',
|
||||||
'dsgvo.data_retention_list': 'Nutzerdaten werden für die Dauer der Nutzung gespeichert. Nach Löschung des Kontos werden die Daten nach Ablauf der 30-tägigen Widerspruchsfrist unwiderruflich gelöscht. Auswertungsdaten werden nach 3 Jahren anonymisiert.',
|
'dsgvo.data_retention_list': 'Nutzerdaten werden für die Dauer der Nutzung gespeichert. Nach Löschung des Kontos werden die Daten nach Ablauf der 30-tägigen Widerspruchsfrist unwiderruflich gelöscht. Auswertungsdaten werden nach 3 Jahren anonymisiert.',
|
||||||
@@ -6654,7 +6654,7 @@ const de: TranslationDict = {
|
|||||||
|
|
||||||
const en: TranslationDict = {
|
const en: TranslationDict = {
|
||||||
// App branding
|
// App branding
|
||||||
'app.name': 'SchulOS',
|
'app.name': 'CompetenceTrack',
|
||||||
'app.subtitle': 'Free Competency & Grading Platform for Schools',
|
'app.subtitle': 'Free Competency & Grading Platform for Schools',
|
||||||
'app.tagline': 'Open-Source Software for Competency-Based Assessment',
|
'app.tagline': 'Open-Source Software for Competency-Based Assessment',
|
||||||
|
|
||||||
@@ -6705,7 +6705,7 @@ const en: TranslationDict = {
|
|||||||
'auth.password': 'Password',
|
'auth.password': 'Password',
|
||||||
'auth.firstName': 'First Name',
|
'auth.firstName': 'First Name',
|
||||||
'auth.lastName': 'Last Name',
|
'auth.lastName': 'Last Name',
|
||||||
'auth.login_title': 'Sign in to SchulOS',
|
'auth.login_title': 'Sign in to CompetenceTrack',
|
||||||
'auth.register_title': 'Create a new account',
|
'auth.register_title': 'Create a new account',
|
||||||
'auth.login_subtitle': 'Enter your credentials',
|
'auth.login_subtitle': 'Enter your credentials',
|
||||||
'auth.register_subtitle': 'Create a free account',
|
'auth.register_subtitle': 'Create a free account',
|
||||||
@@ -7089,7 +7089,7 @@ const en: TranslationDict = {
|
|||||||
|
|
||||||
// Onboarding checklist
|
// Onboarding checklist
|
||||||
'onboarding.title': 'Getting Started',
|
'onboarding.title': 'Getting Started',
|
||||||
'onboarding.subtitle': 'Follow these steps to set up SchulOS',
|
'onboarding.subtitle': 'Follow these steps to set up CompetenceTrack',
|
||||||
'onboarding.step_create_class': 'Create your first class',
|
'onboarding.step_create_class': 'Create your first class',
|
||||||
'onboarding.step_assign_grid': 'Assign a competency grid',
|
'onboarding.step_assign_grid': 'Assign a competency grid',
|
||||||
'onboarding.step_add_students': 'Add students',
|
'onboarding.step_add_students': 'Add students',
|
||||||
@@ -7127,7 +7127,7 @@ const en: TranslationDict = {
|
|||||||
'classes.assign_no_templates': 'No matching templates found.',
|
'classes.assign_no_templates': 'No matching templates found.',
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
'footer.copyright': 'SchulOS — Free School Software',
|
'footer.copyright': 'CompetenceTrack — Free School Software',
|
||||||
'footer.version': 'Version 1.0',
|
'footer.version': 'Version 1.0',
|
||||||
'footer.oss': 'Open Source',
|
'footer.oss': 'Open Source',
|
||||||
'sidebar.eco_message': 'Save paper, protect the environment',
|
'sidebar.eco_message': 'Save paper, protect the environment',
|
||||||
@@ -8381,7 +8381,7 @@ const en: TranslationDict = {
|
|||||||
'notifications.days_ago': '{days} days ago',
|
'notifications.days_ago': '{days} days ago',
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
'onboarding.welcome_title': 'Welcome to SchulOS!',
|
'onboarding.welcome_title': 'Welcome to CompetenceTrack!',
|
||||||
'onboarding.welcome_desc': 'Your free competency & grading platform for schools. Let\'s take a quick tour.',
|
'onboarding.welcome_desc': 'Your free competency & grading platform for schools. Let\'s take a quick tour.',
|
||||||
'onboarding.dashboard_title': 'Dashboard',
|
'onboarding.dashboard_title': 'Dashboard',
|
||||||
'onboarding.dashboard_desc': 'Here you see an overview of all classes, students and current activities. Use quick actions for common tasks.',
|
'onboarding.dashboard_desc': 'Here you see an overview of all classes, students and current activities. Use quick actions for common tasks.',
|
||||||
@@ -8392,7 +8392,7 @@ const en: TranslationDict = {
|
|||||||
'onboarding.reports_title': 'Reports',
|
'onboarding.reports_title': 'Reports',
|
||||||
'onboarding.reports_desc': 'Create report cards and development reports. Export as PDF or print directly.',
|
'onboarding.reports_desc': 'Create report cards and development reports. Export as PDF or print directly.',
|
||||||
'onboarding.done_title': 'Done!',
|
'onboarding.done_title': 'Done!',
|
||||||
'onboarding.done_desc': 'You\'re ready to use SchulOS. For questions, click the help icon in the header.',
|
'onboarding.done_desc': 'You\'re ready to use CompetenceTrack. For questions, click the help icon in the header.',
|
||||||
'onboarding.skip': 'Skip',
|
'onboarding.skip': 'Skip',
|
||||||
'onboarding.next': 'Next',
|
'onboarding.next': 'Next',
|
||||||
'onboarding.back': 'Back',
|
'onboarding.back': 'Back',
|
||||||
@@ -8413,7 +8413,7 @@ const en: TranslationDict = {
|
|||||||
'onboarding.parents_title': 'Parent Communication',
|
'onboarding.parents_title': 'Parent Communication',
|
||||||
'onboarding.parents_desc': 'Communicate with parents about learning progress. Send reports and maintain an ongoing dialogue.',
|
'onboarding.parents_desc': 'Communicate with parents about learning progress. Send reports and maintain an ongoing dialogue.',
|
||||||
'onboarding.environmental_title': 'Environmental Protection',
|
'onboarding.environmental_title': 'Environmental Protection',
|
||||||
'onboarding.environmental_desc': 'SchulOS saves paper: Each digital entry replaces ~50 pages of paper. Thank you for contributing to environmental protection!',
|
'onboarding.environmental_desc': 'CompetenceTrack saves paper: Each digital entry replaces ~50 pages of paper. Thank you for contributing to environmental protection!',
|
||||||
|
|
||||||
// Batch deletion (progress entries)
|
// Batch deletion (progress entries)
|
||||||
'progress.batch_select': 'Select',
|
'progress.batch_select': 'Select',
|
||||||
@@ -9376,10 +9376,10 @@ const en: TranslationDict = {
|
|||||||
'pwa.back_online_desc': 'Your connection has been restored.',
|
'pwa.back_online_desc': 'Your connection has been restored.',
|
||||||
'pwa.cached_data': 'Cached data',
|
'pwa.cached_data': 'Cached data',
|
||||||
'pwa.install_app': 'Install App',
|
'pwa.install_app': 'Install App',
|
||||||
'pwa.install_app_desc': 'Install SchulOS as an app on your device.',
|
'pwa.install_app_desc': 'Install CompetenceTrack as an app on your device.',
|
||||||
'pwa.install': 'Install',
|
'pwa.install': 'Install',
|
||||||
'pwa.installed': 'App installed',
|
'pwa.installed': 'App installed',
|
||||||
'pwa.installed_desc': 'SchulOS has been successfully installed as an app.',
|
'pwa.installed_desc': 'CompetenceTrack has been successfully installed as an app.',
|
||||||
'pwa.install_accepted': 'Installation started',
|
'pwa.install_accepted': 'Installation started',
|
||||||
'pwa.update_available': 'Update available',
|
'pwa.update_available': 'Update available',
|
||||||
'pwa.update_available_desc': 'A new version is available. Reload the page to update.',
|
'pwa.update_available_desc': 'A new version is available. Reload the page to update.',
|
||||||
@@ -9948,7 +9948,7 @@ const en: TranslationDict = {
|
|||||||
'events.view_all': 'View All Events',
|
'events.view_all': 'View All Events',
|
||||||
|
|
||||||
// Onboarding Tour (24 steps) - EN
|
// Onboarding Tour (24 steps) - EN
|
||||||
'onboarding.step_1_title': 'Welcome to SchulOS!',
|
'onboarding.step_1_title': 'Welcome to CompetenceTrack!',
|
||||||
'onboarding.step_1_desc': 'Your free competency & grading platform for schools. Let\'s take a quick tour.',
|
'onboarding.step_1_desc': 'Your free competency & grading platform for schools. Let\'s take a quick tour.',
|
||||||
'onboarding.step_2_title': 'Dashboard',
|
'onboarding.step_2_title': 'Dashboard',
|
||||||
'onboarding.step_2_desc': 'Here you see an overview of all classes, students and current activities. Use quick actions and key metrics.',
|
'onboarding.step_2_desc': 'Here you see an overview of all classes, students and current activities. Use quick actions and key metrics.',
|
||||||
@@ -9997,7 +9997,7 @@ const en: TranslationDict = {
|
|||||||
'onboarding.step_24_title': 'Settings',
|
'onboarding.step_24_title': 'Settings',
|
||||||
'onboarding.step_24_desc': 'Customize the platform: school profile, branding, backup and data management.',
|
'onboarding.step_24_desc': 'Customize the platform: school profile, branding, backup and data management.',
|
||||||
'onboarding.celebration': 'You\'re all set!',
|
'onboarding.celebration': 'You\'re all set!',
|
||||||
'onboarding.complete': 'You\'ve completed the tour. Good luck with SchulOS!',
|
'onboarding.complete': 'You\'ve completed the tour. Good luck with CompetenceTrack!',
|
||||||
'onboarding.previous': 'Previous',
|
'onboarding.previous': 'Previous',
|
||||||
'onboarding.dont_show_again': 'Don\'t show again',
|
'onboarding.dont_show_again': 'Don\'t show again',
|
||||||
'onboarding.restart_tour': 'Restart tour',
|
'onboarding.restart_tour': 'Restart tour',
|
||||||
@@ -10324,7 +10324,7 @@ const en: TranslationDict = {
|
|||||||
'dsgvo.dpa_desc': 'Data processing agreement pursuant to Art. 28 GDPR',
|
'dsgvo.dpa_desc': 'Data processing agreement pursuant to Art. 28 GDPR',
|
||||||
'dsgvo.data_register': 'Register of Processing Activities',
|
'dsgvo.data_register': 'Register of Processing Activities',
|
||||||
'dsgvo.data_register_desc': 'Overview of all processing activities pursuant to Art. 30 GDPR',
|
'dsgvo.data_register_desc': 'Overview of all processing activities pursuant to Art. 30 GDPR',
|
||||||
'dsgvo.privacy_policy_text': 'The following privacy policy applies to the use of the SchulOS platform. The respective school is responsible for data processing.',
|
'dsgvo.privacy_policy_text': 'The following privacy policy applies to the use of the CompetenceTrack platform. The respective school is responsible for data processing.',
|
||||||
'dsgvo.data_collected_list': 'Name, email address, password (encrypted), role assignment, class and subject associations, learning progress, assessments, attendance data, notes and reports.',
|
'dsgvo.data_collected_list': 'Name, email address, password (encrypted), role assignment, class and subject associations, learning progress, assessments, attendance data, notes and reports.',
|
||||||
'dsgvo.data_purpose_list': 'Provision and operation of the platform, management of students and classes, documentation of learning progress, creation of reports, communication with parents.',
|
'dsgvo.data_purpose_list': 'Provision and operation of the platform, management of students and classes, documentation of learning progress, creation of reports, communication with parents.',
|
||||||
'dsgvo.data_retention_list': 'User data is stored for the duration of use. After account deletion, data is permanently deleted after the 30-day objection period. Analytics data is anonymized after 3 years.',
|
'dsgvo.data_retention_list': 'User data is stored for the duration of use. After account deletion, data is permanently deleted after the 30-day objection period. Analytics data is anonymized after 3 years.',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Notification Helper
|
// CompetenceTrack — Notification Helper
|
||||||
// Server-side utility for creating notifications in the database
|
// Server-side utility for creating notifications in the database
|
||||||
// Also pushes notifications via WebSocket service
|
// Also pushes notifications via WebSocket service
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Offline Cache for Notebook Data
|
// CompetenceTrack — Offline Cache for Notebook Data
|
||||||
// Stores notebook data in localStorage for offline access
|
// Stores notebook data in localStorage for offline access
|
||||||
|
|
||||||
const NOTEBOOK_CACHE_KEY = 'ct_notebook_cache';
|
const NOTEBOOK_CACHE_KEY = 'ct_notebook_cache';
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — QR Code Generation Utility
|
// CompetenceTrack — QR Code Generation Utility
|
||||||
// Generates QR codes as base64 PNG images using canvas
|
// Generates QR codes as base64 PNG images using canvas
|
||||||
|
|
||||||
export type QRCodeType = 'student' | 'class' | 'attendance' | 'event';
|
export type QRCodeType = 'student' | 'class' | 'attendance' | 'event';
|
||||||
@@ -17,7 +17,7 @@ export interface QRCodeData {
|
|||||||
* Build a URL string from QR code data that can be encoded into a QR code
|
* Build a URL string from QR code data that can be encoded into a QR code
|
||||||
*/
|
*/
|
||||||
export function buildQRCodeUrl(data: QRCodeData): string {
|
export function buildQRCodeUrl(data: QRCodeData): string {
|
||||||
const base = typeof window !== 'undefined' ? window.location.origin : 'https://schulos.app';
|
const base = typeof window !== 'undefined' ? window.location.origin : 'https://competencetrack.app';
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'student':
|
case 'student':
|
||||||
return `${base}/?student=${data.id}`;
|
return `${base}/?student=${data.id}`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — API Rate Limiting Middleware
|
// CompetenceTrack — API Rate Limiting Middleware
|
||||||
// In-memory rate limiting with configurable limits per endpoint
|
// In-memory rate limiting with configurable limits per endpoint
|
||||||
|
|
||||||
interface RateLimitEntry {
|
interface RateLimitEntry {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — Zustand Store
|
// CompetenceTrack — Zustand Store
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { setLocale, getLocale } from '@/lib/i18n';
|
import { setLocale, getLocale } from '@/lib/i18n';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SchulOS — WebSocket Hook
|
// CompetenceTrack — WebSocket Hook
|
||||||
// Real-time collaboration and push notifications via Socket.IO
|
// Real-time collaboration and push notifications via Socket.IO
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|||||||
+9
-9
@@ -38,7 +38,7 @@ Stage Summary:
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Built the Subject Content Management System for SchulOS - a "Schlaukopf clone" educational content platform. Includes full CRUD API routes, comprehensive frontend component with role-based views, i18n support (90+ keys in DE+EN), and schlaukopf.de-style seed data.
|
Built the Subject Content Management System for CompetenceTrack - a "Schlaukopf clone" educational content platform. Includes full CRUD API routes, comprehensive frontend component with role-based views, i18n support (90+ keys in DE+EN), and schlaukopf.de-style seed data.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ Unresolved issues / Next phase priorities:
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Built the **Illness Reporting with Parent Approval** and **Student-Teacher Communication Rooms** features for SchulOS. These are critical features for a German school platform: illness reports require parent approval before becoming visible to teachers (privacy compliance), and communication rooms provide private student-teacher conversation channels.
|
Built the **Illness Reporting with Parent Approval** and **Student-Teacher Communication Rooms** features for CompetenceTrack. These are critical features for a German school platform: illness reports require parent approval before becoming visible to teachers (privacy compliance), and communication rooms provide private student-teacher conversation channels.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -444,7 +444,7 @@ Added 80+ keys to both DE and EN sections:
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Implemented the Counseling/Guidance System, Disciplinary Committee System, and AI Test Generation features for SchulOS. This includes 12 API route files, 3 view components, 130+ i18n keys, and store/navigation updates.
|
Implemented the Counseling/Guidance System, Disciplinary Committee System, and AI Test Generation features for CompetenceTrack. This includes 12 API route files, 3 view components, 130+ i18n keys, and store/navigation updates.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -533,7 +533,7 @@ Implemented the Counseling/Guidance System, Disciplinary Committee System, and A
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Implemented the complete AI Integration system and Virtual Character/Chatbot system for SchulOS. This includes 6 API routes, 2 major frontend components, 100+ i18n keys, and integration into the app layout.
|
Implemented the complete AI Integration system and Virtual Character/Chatbot system for CompetenceTrack. This includes 6 API routes, 2 major frontend components, 100+ i18n keys, and integration into the app layout.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -673,7 +673,7 @@ Unresolved issues / Next phase priorities:
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Enhanced the visual design and polish of 8 components across SchulOS. Focused on adding gradient headers, animated progress bars, skeleton loading states, empty state illustrations, typing indicators, online/offline status indicators, circular progress visualizations, shimmer effects, and improved message bubbles. All changes are purely visual - no functionality was modified.
|
Enhanced the visual design and polish of 8 components across CompetenceTrack. Focused on adding gradient headers, animated progress bars, skeleton loading states, empty state illustrations, typing indicators, online/offline status indicators, circular progress visualizations, shimmer effects, and improved message bubbles. All changes are purely visual - no functionality was modified.
|
||||||
|
|
||||||
### Files Modified
|
### Files Modified
|
||||||
|
|
||||||
@@ -711,7 +711,7 @@ Enhanced the visual design and polish of 8 components across SchulOS. Focused on
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Enhanced the SchulOS platform with teacher grading features (annotation, AI review, bulk grading) and deep styling improvements across all major components.
|
Enhanced the CompetenceTrack platform with teacher grading features (annotation, AI review, bulk grading) and deep styling improvements across all major components.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -840,7 +840,7 @@ Unresolved issues / Next phase priorities:
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Built the Inter-School Competition Federation system and Enhanced Communication System for SchulOS. The federation system allows schools in the same district to compete against each other with school rankings and team scores. The enhanced communication system adds voice chat, note sharing, file sharing, and teacher notes to the existing communication rooms.
|
Built the Inter-School Competition Federation system and Enhanced Communication System for CompetenceTrack. The federation system allows schools in the same district to compete against each other with school rankings and team scores. The enhanced communication system adds voice chat, note sharing, file sharing, and teacher notes to the existing communication rooms.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -972,7 +972,7 @@ Unresolved issues / Next phase priorities:
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Built the Responsible Teacher (Klassenlehrer) management, Vice Principal management, enhanced calendar exam integration, student portal exam/study tips, and dashboard Klassenlehrer section for SchulOS. Includes full API routes, comprehensive frontend components with role-based views, i18n support (90+ keys in DE+EN), and deep styling improvements.
|
Built the Responsible Teacher (Klassenlehrer) management, Vice Principal management, enhanced calendar exam integration, student portal exam/study tips, and dashboard Klassenlehrer section for CompetenceTrack. Includes full API routes, comprehensive frontend components with role-based views, i18n support (90+ keys in DE+EN), and deep styling improvements.
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
@@ -2091,7 +2091,7 @@ Stage Summary:
|
|||||||
|
|
||||||
## Task ID: 2-a
|
## Task ID: 2-a
|
||||||
Agent: Grade Analytics & Reporting Builder
|
Agent: Grade Analytics & Reporting Builder
|
||||||
Task: Build Grade Analytics & Reporting View for SchulOS
|
Task: Build Grade Analytics & Reporting View for CompetenceTrack
|
||||||
|
|
||||||
Work Log:
|
Work Log:
|
||||||
- Added GradeReport model to Prisma schema with fields: schoolId, generatedBy, title, type, dateRange, classIds, subjectIds, metrics, status, fileData, createdAt
|
- Added GradeReport model to Prisma schema with fields: schoolId, generatedBy, title, type, dateRange, classIds, subjectIds, metrics, status, fileData, createdAt
|
||||||
|
|||||||
Reference in new issue
Block a user