diff --git a/package.json b/package.json index 600f1f0..ef1a7b5 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ ], "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --webpack", "start": "next start", "lint": "eslint .", "postinstall": "prisma generate", @@ -31,7 +31,7 @@ "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" + "test": "bun test" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/scripts/generate-data-model.mjs b/scripts/generate-data-model.mjs index 114833e..ee90a7f 100644 --- a/scripts/generate-data-model.mjs +++ b/scripts/generate-data-model.mjs @@ -3,20 +3,11 @@ * This document lists all data entities, their purposes, legal basis, and retention categories */ -const fs = require("fs"); -const path = require("path"); - -interface DataEntity { - name: string; - dataCategories: string[]; - legalBasis: string; - purpose: string; - retentionCategory: string; - notes?: string; -} +import fs from "node:fs"; +import path from "node:path"; // Core data model definition -const dataModel: DataEntity[] = [ +const dataModel = [ { name: "User", dataCategories: ["firstName", "lastName", "email", "role", "createdAt"], @@ -112,12 +103,12 @@ const dataModel: DataEntity[] = [ }, ]; -export function generateDataModel(outputPath: string = "docs/legal"): void { +function generateDataModel(outputPath = "docs/legal") { if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath, { recursive: true }); } - const lines: string[] = [ + const lines = [ "# Data Model Documentation", "", "**Status**: Schema Reference for ExaMetra (Module L)", @@ -166,7 +157,7 @@ export function generateDataModel(outputPath: string = "docs/legal"): void { "", `Generated: ${new Date().toISOString()}`, "Source: ExaMetra Prisma schema (Module L)", - ]; + ); const filePath = path.join(outputPath, "DATA_MODEL.md"); fs.writeFileSync(filePath, lines.join("\n"), "utf-8"); diff --git a/scripts/generate-privacy-notice.mjs b/scripts/generate-privacy-notice.mjs index 5e2a84a..e2a344d 100644 --- a/scripts/generate-privacy-notice.mjs +++ b/scripts/generate-privacy-notice.mjs @@ -3,26 +3,15 @@ * Plain-language German, based on school configuration */ -const fs = require("fs"); -const path = require("path"); +import fs from "node:fs"; +import path from "node:path"; -interface PrivacyNoticeParams { - schoolName: string; - dpoName: string; - dpoEmail: string; - schultragerName: string; - bundesland: string; -} - -export function generatePrivacyNotice( - params: PrivacyNoticeParams, - outputPath: string = "docs/legal" -): void { +function generatePrivacyNotice(params, outputPath = "docs/legal") { if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath, { recursive: true }); } - const lines: string[] = [ + const lines = [ "# Datenschutzerklärung", "# (Privacy Notice for Parents & Students)", "", @@ -139,7 +128,7 @@ export function generatePrivacyNotice( console.log(`[v0] Generated privacy notice at ${filePath}`); } -if (require.main === module) { +if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) { generatePrivacyNotice({ schoolName: "Gymnasium Berlin", dpoName: "Max Mustermann", diff --git a/scripts/generate-ropa.mjs b/scripts/generate-ropa.mjs index 122a38b..1dbfaa4 100644 --- a/scripts/generate-ropa.mjs +++ b/scripts/generate-ropa.mjs @@ -4,20 +4,10 @@ * German legal document for school's DPO review */ -const fs = require("fs"); -const path = require("path"); +import fs from "node:fs"; +import path from "node:path"; -interface RopaEntry { - name: string; - purposes: string[]; - dataCategories: string[]; - recipients: string[]; - retentionPeriod: string; - legalBasis: string; - securityMeasures: string[]; -} - -const ropas: RopaEntry[] = [ +const ropas = [ { name: "Student Information Management", purposes: [ @@ -126,12 +116,12 @@ const ropas: RopaEntry[] = [ }, ]; -export function generateRopa(outputPath: string = "docs/legal"): void { +function generateRopa(outputPath = "docs/legal") { if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath, { recursive: true }); } - const lines: string[] = [ + const lines = [ "# Verzeichnis von Verarbeitungstätigkeiten", "# (Records of Processing Activities - Art. 30 GDPR)", "", @@ -198,7 +188,7 @@ export function generateRopa(outputPath: string = "docs/legal"): void { "", `**Generiert**: ${new Date().toISOString()}`, "**Quelle**: ExaMetra Module L - Compliance Framework", - ]; + ); const filePath = path.join( outputPath, diff --git a/scripts/run-retention-job.mjs b/scripts/run-retention-job.mjs index 02cb2fc..b43b99e 100644 --- a/scripts/run-retention-job.mjs +++ b/scripts/run-retention-job.mjs @@ -6,18 +6,11 @@ * Usage: npx ts-node scripts/run-retention-job.ts */ -const { PrismaClient } = require("@prisma/client"); +import { PrismaClient } from "@prisma/client"; +import path from "node:path"; const prisma = new PrismaClient(); -interface FlaggedRecord { - entityType: string; - entityId: string; - dataCategory: string; - flaggedAt: Date; - reason: string; -} - /** * Check records for each school and flag those exceeding retention period */ @@ -32,7 +25,7 @@ async function runRetentionJob() { }); let totalFlagged = 0; - const report: { school: string; flagged: number }[] = []; + const report = []; for (const school of schools) { const schoolFlagged = await flagExpiredRecordsForSchool(school.id); @@ -75,7 +68,7 @@ async function runRetentionJob() { /** * Flag expired records for a specific school */ -async function flagExpiredRecordsForSchool(schoolId: string): Promise { +async function flagExpiredRecordsForSchool(schoolId) { // Get all retention policies for this school const policies = await prisma.dataRetentionPolicy.findMany({ where: { schoolId }, @@ -106,11 +99,7 @@ async function flagExpiredRecordsForSchool(schoolId: string): Promise { /** * Flag records of a specific category that are older than expiryDate */ -async function flagRecordsByCategory( - schoolId: string, - dataCategory: string, - expiryDate: Date -): Promise { +async function flagRecordsByCategory(schoolId, dataCategory, expiryDate) { let flaggedCount = 0; try { @@ -201,11 +190,7 @@ async function flagRecordsByCategory( /** * Generic function to flag expired records by entity type */ -async function flagRecordsByEntityType( - schoolId: string, - entityType: string, - expiryDate: Date -): Promise { +async function flagRecordsByEntityType(schoolId, entityType, expiryDate) { // Check if records of this type already have deletion flags const alreadyFlagged = await prisma.deletionFlag.findMany({ where: { @@ -255,7 +240,7 @@ async function flagRecordsByEntityType( } // Run the job if this script is executed directly -if (require.main === module) { +if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) { runRetentionJob() .then((result) => { console.log( @@ -270,4 +255,4 @@ if (require.main === module) { }); } -module.exports = { runRetentionJob, flagExpiredRecordsForSchool }; +export { runRetentionJob, flagExpiredRecordsForSchool }; diff --git a/src/app/api/v1/compliance/module-access/route.ts b/src/app/api/v1/compliance/module-access/route.ts index 76c3eb9..c25d0f0 100644 --- a/src/app/api/v1/compliance/module-access/route.ts +++ b/src/app/api/v1/compliance/module-access/route.ts @@ -8,16 +8,16 @@ import { checkModuleAccess } from "@/lib/compliance"; export async function GET(req: NextRequest) { try { const schoolId = req.nextUrl.searchParams.get("schoolId"); - const module = req.nextUrl.searchParams.get("module"); + const moduleName = req.nextUrl.searchParams.get("module"); - if (!schoolId || !module) { + if (!schoolId || !moduleName) { return NextResponse.json( { error: "Missing schoolId or module parameter" }, { status: 400 } ); } - const result = await checkModuleAccess(schoolId, module); + const result = await checkModuleAccess(schoolId, moduleName); return NextResponse.json(result); } catch (error) { diff --git a/src/app/globals.css b/src/app/globals.css index 473505e..b4d7855 100755 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,8 +6,8 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: Arial, Helvetica, sans-serif; + --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 36e9909..435ee7b 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,19 +1,8 @@ import type { Metadata, Viewport } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "next-themes"; import { Toaster } from "@/components/ui/sonner"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { title: "SchulOS — Free Competency & Grading Platform", description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.", @@ -48,9 +37,7 @@ export default function RootLayout({ - + { /** * Wrap handler with automatic error handling */ -export const withErrorHandling = (handler: Function) => { +export const withErrorHandling = ( + handler: (...args: any[]) => any +) => { return async (...args: any[]) => { try { return await handler(...args); diff --git a/src/lib/hooks/useApi.ts b/src/lib/hooks/useApi.ts index d1b44e9..58dcfd2 100644 --- a/src/lib/hooks/useApi.ts +++ b/src/lib/hooks/useApi.ts @@ -145,23 +145,17 @@ export function useApiQueries( queries: Array<{ url: string | null; key?: string }>, options?: SWRConfiguration ) { - const results = queries.map((query) => { - const { data, error, isLoading, mutate } = useApiGet(query.url, options); - return { - key: query.key || query.url, - data, - error, - isLoading, - mutate, - }; - }); + const activeQueries = queries.filter((query) => query.url); + const cacheKey = activeQueries.map((query) => query.url).join("|") || null; + const { data: values, error, isLoading } = useSWR( + cacheKey, + () => Promise.all(activeQueries.map((query) => apiFetcher(query.url!))), + options + ); - const isLoading = results.some((r) => r.isLoading); - const error = results.find((r) => r.error); - const data = Object.fromEntries(results.map((r) => [r.key, r.data])) as Record< - string, - T | undefined - >; + const data = Object.fromEntries( + activeQueries.map((query, index) => [query.key || query.url, values?.[index]]) + ) as Record; return { data, error, isLoading }; }