praktikum refinemend Plaetze pro Dienstelle und Pro Zeitraum

This commit is contained in:
titver968
2025-11-26 15:27:21 +01:00
parent be9228b71d
commit 89bf0298ce
40 changed files with 2932 additions and 1247 deletions

View File

@@ -30,6 +30,13 @@
description: 'Praktikumszeiträume verwalten',
color: 'bg-purple-600 hover:bg-purple-700'
},
{
href: '/admin/plaetze',
title: 'Plätze verwalten',
icon: '📊',
description: 'Praktikumsplätze pro Zeitraum und Dienststelle festlegen',
color: 'bg-indigo-600 hover:bg-indigo-700'
},
{
href: '/admin/change-password',
title: 'Passwort ändern',
@@ -68,4 +75,4 @@
.group:hover {
transform: translateY(-2px);
}
</style>
</style>

View File

@@ -22,6 +22,15 @@
});
}
function formatGeburtsdatum(dateString: string | undefined): string {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
function formatProcessedDate(timestamp: number | undefined): string {
if (!timestamp) return '-';
return new Date(timestamp).toLocaleDateString('de-DE', {
@@ -33,7 +42,44 @@
});
}
// Vereinfachte Logik ohne processing Status
function formatZeitraum(zeitraum: any): string {
if (!zeitraum) return '-';
const start = new Date(zeitraum.startDatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
const end = new Date(zeitraum.endDatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
return `${zeitraum.bezeichnung} (${start} - ${end})`;
}
// Schulart formatieren
function formatSchulart(schulart: string | undefined): string {
if (!schulart) return '-';
const mapping: Record<string, string> = {
'Gymnasium': 'Gymnasium',
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig',
'Fachoberschule': 'Fachoberschule',
'Realschule': 'Realschule',
'KGSR': 'KGS Realschulzweig',
'IGSR': 'IGS Realschulzweig'
};
return mapping[schulart] || schulart;
}
// Sozialverhalten kürzen
function formatSozialverhalten(sv: string | undefined): string {
if (!sv) return '-';
if (sv === 'Entspricht den Erwartungen in vollem Umfang') return 'Voll entspr.';
if (sv === 'Entspricht den Erwartungen') return 'Entsprechend';
if (sv === 'Entspricht den Erwartungen mit Einschränkungen') return 'Mit Einschr.';
return sv;
}
function canBeAccepted(status: string): boolean {
return status === 'pending';
}
@@ -47,166 +93,234 @@
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="min-w-0 w-1/4 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Bewerber/in
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Persönliche Daten
</th>
<th class="w-16 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Noten
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kontakt & Adresse
</th>
<th class="min-w-0 w-1/3 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Wünsche / Zuweisung
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Schule & Noten
</th>
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Eingegangen
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Praktikum & Wünsche
</th>
<th class="w-40 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dokumente
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each anmeldungen as anmeldung (anmeldung.id)}
<tr class="hover:bg-gray-50">
<!-- Status (processing Styling entfernt) -->
<td class="px-4 py-4 whitespace-nowrap">
<tr class="hover:bg-gray-50 align-top">
<!-- Status -->
<td class="px-3 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {getStatusColor(anmeldung.status || 'pending')}">
{getStatusText(anmeldung.status || 'pending')}
</span>
<div class="text-xs text-gray-400 mt-2">
Eingang:<br>
{formatDate(anmeldung.timestamp)}
</div>
</td>
<!-- Bewerber/in -->
<td class="px-4 py-4">
<!-- Persönliche Daten -->
<td class="px-3 py-4">
<div class="text-sm font-medium text-gray-900">
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
</div>
<div class="text-sm text-gray-500 break-all">
{anmeldung.email}
<div class="text-xs text-gray-500 mt-1">
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
</div>
{#if anmeldung.alter}
<div class="text-xs text-gray-500">
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
</div>
{/if}
</td>
<!-- Kontakt & Adresse -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Adresse:</span><br>
<span class="text-gray-600">
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
{anmeldung.plz || ''} {anmeldung.ort || ''}
</span>
</div>
<div>
<span class="font-medium text-gray-700">Tel:</span>
{#if anmeldung.telefon}
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
{:else}
<span class="text-gray-400">-</span>
{/if}
</div>
<div>
<span class="font-medium text-gray-700">E-Mail:</span><br>
<a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a>
</div>
</div>
</td>
<!-- Schule & Noten -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Schulart:</span><br>
<span class="text-gray-900">{formatSchulart(anmeldung.schulart)}</span>
</div>
{#if anmeldung.schulklasse}
<div>
<span class="font-medium text-gray-700">Klasse:</span>
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
</div>
{/if}
<div class="pt-1 border-t border-gray-100">
<span class="font-medium text-gray-700">Noten:</span>
<div class="flex gap-2 mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
D: <span class="font-bold ml-1">{anmeldung.noteDeutsch || '-'}</span>
</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
M: <span class="font-bold ml-1">{anmeldung.noteMathe || '-'}</span>
</span>
</div>
</div>
{#if anmeldung.sozialverhalten}
<div>
<span class="font-medium text-gray-700">Sozialverh.:</span>
<span class="text-gray-900 text-xs">{formatSozialverhalten(anmeldung.sozialverhalten)}</span>
</div>
{/if}
</div>
</td>
<!-- Praktikum & Wünsche -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-2">
<!-- Zeitraum -->
{#if anmeldung.zeitraum}
<div>
<span class="font-medium text-gray-700">Zeitraum:</span><br>
<span class="text-gray-900">{formatZeitraum(anmeldung.zeitraum)}</span>
</div>
{/if}
<!-- Zugewiesene Dienststelle -->
{#if anmeldung.assignedDienststelle}
<div class="p-2 bg-green-50 border border-green-200 rounded">
<div class="flex items-center text-green-700">
<svg class="w-4 h-4 mr-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<span class="font-semibold">Zugewiesen:</span>
</div>
<div class="text-green-800 mt-1">{anmeldung.assignedDienststelle.name}</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1">
{formatProcessedDate(anmeldung.processedAt)}
</div>
{/if}
</div>
{/if}
<!-- Alle 3 Wünsche -->
<div>
<span class="font-medium text-gray-700">Wünsche:</span>
<div class="space-y-1 mt-1">
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-600 rounded-full mr-1 flex-shrink-0">1</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full mr-1 flex-shrink-0">2</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-400 rounded-full mr-1 flex-shrink-0">3</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400">Keine Wünsche</span>
{/if}
</div>
</div>
<!-- Motivation -->
{#if anmeldung.motivation}
<div>
<span class="font-medium text-gray-700">Motivation:</span>
<p class="text-gray-600 mt-1 text-xs whitespace-pre-wrap line-clamp-3">{anmeldung.motivation}</p>
</div>
{/if}
</div>
</td>
<!-- Dokumente / PDFs -->
<td class="px-3 py-4">
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
<div class="mt-2">
{#each anmeldung.pdfs as pdf}
<div class="space-y-1">
{#each anmeldung.pdfs as pdf, index}
<a
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
target="_blank"
class="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mr-2 mb-1"
class="flex items-center text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-1 rounded"
>
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-4 h-4 mr-1 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
</svg>
PDF ansehen
PDF {index + 1}
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
{/each}
</div>
{/if}
</td>
<!-- Noten -->
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
{#if anmeldung.noteDeutsch || anmeldung.noteMathe}
<div class="space-y-1">
{#if anmeldung.noteDeutsch}
<div class="text-xs">D: {anmeldung.noteDeutsch}</div>
{/if}
{#if anmeldung.noteMathe}
<div class="text-xs">M: {anmeldung.noteMathe}</div>
{/if}
</div>
{:else}
<span class="text-gray-400">-</span>
<span class="text-xs text-gray-400">Keine Dokumente</span>
{/if}
{#if anmeldung.sozialverhalten}
<div class="text-xs text-gray-500 mt-1">
SV: {anmeldung.sozialverhalten}
</div>
{/if}
</td>
<!-- Wünsche / Zuweisung -->
<td class="px-4 py-4 text-sm text-gray-900">
<!-- Zugewiesene Dienststelle (falls vorhanden) -->
{#if anmeldung.assignedDienststelle}
<div class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
<div class="flex items-center">
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<span class="font-medium text-green-800 text-sm">Zugewiesen:</span>
</div>
<div class="text-sm text-green-700 mt-1 ml-6">
{anmeldung.assignedDienststelle.name}
</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1 ml-6">
{formatProcessedDate(anmeldung.processedAt)}
{#if anmeldung.processedBy}
von {anmeldung.processedBy}
{/if}
</div>
{/if}
</div>
{/if}
<!-- Wünsche -->
<div class="space-y-2">
<div class="text-xs font-medium text-gray-500 uppercase tracking-wider">Wünsche:</div>
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-600 rounded-full mr-2 mt-0.5 flex-shrink-0">1</span>
<span class="text-sm leading-5">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-500 rounded-full mr-2 mt-0.5 flex-shrink-0">2</span>
<span class="text-sm leading-5">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-400 rounded-full mr-2 mt-0.5 flex-shrink-0">3</span>
<span class="text-sm leading-5">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400 text-sm">Keine Wünsche angegeben</span>
{/if}
</div>
</td>
<!-- Eingegangen -->
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="text-sm">{formatDate(anmeldung.timestamp)}</div>
</td>
<!-- Aktionen -->
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium">
<td class="px-3 py-4">
<div class="flex flex-col space-y-2">
{#if canBeAccepted(anmeldung.status || 'pending')}
<button
on:click={() => dispatch('accept', { id: anmeldung.id })}
class="text-green-600 hover:text-green-900 px-3 py-1 rounded text-xs font-medium border border-green-200 hover:bg-green-50 w-full text-center"
class="text-green-600 hover:text-green-900 px-3 py-1.5 rounded text-xs font-medium border border-green-300 hover:bg-green-50 text-center whitespace-nowrap"
>
Annehmen
Annehmen
</button>
{/if}
{#if canBeRejected(anmeldung.status || 'pending')}
<button
on:click={() => dispatch('reject', { id: anmeldung.id })}
class="text-red-600 hover:text-red-900 px-3 py-1 rounded text-xs font-medium border border-red-200 hover:bg-red-50 w-full text-center"
class="text-red-600 hover:text-red-900 px-3 py-1.5 rounded text-xs font-medium border border-red-300 hover:bg-red-50 text-center whitespace-nowrap"
>
Ablehnen
Ablehnen
</button>
{/if}
<button
on:click={() => dispatch('delete', { id: anmeldung.id })}
class="text-gray-600 hover:text-gray-900 px-3 py-1 rounded text-xs font-medium border border-gray-200 hover:bg-gray-50 w-full text-center"
class="text-gray-600 hover:text-gray-900 px-3 py-1.5 rounded text-xs font-medium border border-gray-300 hover:bg-gray-50 text-center whitespace-nowrap"
>
Löschen
🗑 Löschen
</button>
</div>
</td>
@@ -214,4 +328,13 @@
{/each}
</tbody>
</table>
</div>
</div>
<style>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

15
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV !== 'production' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

View File

@@ -32,8 +32,14 @@
let schulklasse = '';
let ablehnungHinweis = '';
let showAblehnungModal = false;
let showIgsHinweis = false;
let alter = '';
// Validierungsfehler für Echtzeit-Anzeige
let alterFehler = '';
let notenFehler = '';
let sozialverhaltenFehler = '';
// Berechnung des Alters
$: {
if (geburtsdatum && zeitraum && zeitraeume.length > 0) {
const gewaehlterZeitraum = zeitraeume.find(z => z.id == zeitraum);
@@ -47,34 +53,81 @@
if (monthDiff < 0 || (monthDiff === 0 && praktikumStart.getDate() < geburt.getDate())) {
altersberechnung--;
}
alter = altersberechnung.toString();
alter = altersberechnung.toString();
}
}
}
// Überwachung für IGS + Klasse 7 Kombination
// Echtzeit-Validierung: Alter
$: {
if (["KGSR", "IGSR"].includes(schulart) && schulklasse === '7') {
showIgsHinweis = true;
const altersWert = parseInt(alter);
if (alter && !isNaN(altersWert) && altersWert < 14) {
alterFehler = 'Du musst mindestens 14 Jahre alt sein, um ein Praktikum beginnen zu können.';
} else {
alterFehler = '';
}
}
// Echtzeit-Validierung: Noten
$: {
const deutsch = parseInt(noteDeutsch);
const mathe = parseInt(noteMathe);
const klasse = parseInt(schulklasse);
if (noteDeutsch && noteMathe && schulart) {
// Gymnasium oder KGS_Gymnasialzweig: mindestens 4 in Deutsch UND Mathe
if (['Gymnasium', 'KGS_Gymnasialzweig'].includes(schulart)) {
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 4 || mathe > 4)) {
notenFehler = 'Du brauchst mindestens eine 4 in Deutsch und Mathematik.';
} else {
notenFehler = '';
}
}
// Fachoberschule Klasse 11 oder 12: mindestens 4 in Deutsch UND Mathe
else if (schulart === 'Fachoberschule' && (klasse === 11 || klasse === 12)) {
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 4 || mathe > 4)) {
notenFehler = 'Du brauchst mindestens eine 4 in Deutsch und Mathematik.';
} else {
notenFehler = '';
}
}
// Alle anderen: mindestens 3 in Deutsch UND Mathe
else {
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 3 || mathe > 3)) {
notenFehler = 'Du brauchst mindestens eine 3 in Deutsch und Mathematik.';
} else {
notenFehler = '';
}
}
} else {
notenFehler = '';
}
}
// Echtzeit-Validierung: Sozialverhalten
$: {
if (sozialverhalten === 'Entspricht den Erwartungen mit Einschränkungen') {
sozialverhaltenFehler = 'Dein Sozialverhalten muss mindestens den Erwartungen entsprechen.';
} else {
sozialverhaltenFehler = '';
}
}
// Prüfen ob Formular gültig ist
$: formHatFehler = alterFehler !== '' || notenFehler !== '' || sozialverhaltenFehler !== '';
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
if (d.plaetze <= 0) return false;
// PK Mitte nur anzeigen wenn mindestens 18 Jahre alt
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
return parseInt(alter) >= 18;
}
return true;
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
return parseInt(alter) >= 18;
}
);
return true;
});
$: filteredZeitraeume = (zeitraeume ?? []).filter(zeitraum => {
const heute = new Date();
const startDatum = new Date(zeitraum.startDatum);
// Nur Zeiträume anzeigen, die noch nicht gestartet haben
return startDatum > heute;
});
@@ -82,7 +135,7 @@
$: hideSozialVerhalten =
Number(schulklasse) >= 11 &&
["Gymnasium", "KGS_Gymnasialzweig", "IGS_Gymnasialzweig"].includes(schulart);
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
onMount(async () => {
const resDienstelle = await fetch('/api/dienststellen');
@@ -91,6 +144,7 @@
const resZeitraeume = await fetch('/api/zeitraeume');
zeitraeume = await resZeitraeume.json();
});
interface Zeitraum {
id: number;
bezeichnung: string;
@@ -98,9 +152,6 @@
endDatum: string;
}
let zeitraeume: Zeitraum[] = [];
//let neuerBezeichnung = '';
//let neuerstartDatum = '';
//let neuerendDatum = '';
let fehlermeldung = '';
let bearbeiteId: number | null = null;
@@ -124,12 +175,21 @@
noteDeutsch = '';
noteMathe = '';
sozialverhalten = '';
schulklasse = '';
pdfDateien = [];
fileInputKey += 1;
success = false;
alterFehler = '';
notenFehler = '';
sozialverhaltenFehler = '';
}
async function anmelden() {
// Abbrechen wenn Validierungsfehler vorhanden
if (formHatFehler) {
return;
}
const data = new FormData();
data.append('anrede', anrede);
@@ -158,36 +218,6 @@
data.append('pdfs', pdf);
}
const altersWert = parseInt(alter);
if (isNaN(altersWert) || altersWert < 14) {
ablehnungHinweis = 'Du musst mindestens 14 Jahre alt sein, um ein Praktikum beginnen zu können. Bewirb dich gern erneut, wenn du das Mindestalter erreicht hast.';
showAblehnungModal = true;
return;
}
const deutsch = parseInt(noteDeutsch);
const mathe = parseInt(noteMathe);
if (['Gymnasium', 'KGS_Gymnasialzweig', 'IGS_Gymnasialzweig'].includes(schulart) ) {
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 4 && mathe > 4) {
ablehnungHinweis = 'Du brauchst mindestens eine 4 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
return;
}
} else {
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 3 && mathe > 3) {
ablehnungHinweis = 'Du brauchst mindestens eine 3 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
return;
}
}
if (sozialverhalten === 'Entspricht den Erwartungen mit Einschränkungen') {
ablehnungHinweis = 'Dein Sozialverhalten muss mindestens den Erwartungen entsprechen. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
return;
}
const res = await fetch('/api/anmelden', {
method: 'POST',
body: data
@@ -234,17 +264,13 @@
<option value="" disabled selected hidden>Schulart wählen</option>
<option value="Gymnasium">Gymnasium</option>
<option value="KGS_Gymnasialzweig">KGS Gymnasialzweig</option>
<option value="IGS_Gymnasialzweig">IGS Gymniasalzweig Fachoberschule</option>
<option value="Fachoberschule">Fachoberschule</option>
<option value="Realschule">Realschule</option>
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
</select>
<!-- Noten -->
<div class="grid grid-cols-2 gap-4">
<input bind:value={noteDeutsch} type="number" min="1" max="6" placeholder="Note in Deutsch" required class="input" />
<input bind:value={noteMathe} type="number" min="1" max="6" placeholder="Note in Mathe" required class="input" />
</div>
<!-- Schulklasse -->
<select bind:value={schulklasse} required class="input">
<option value="" disabled selected hidden>Schulklasse</option>
<option value="7">7. Klasse</option>
@@ -254,16 +280,56 @@
<option value="11">11. Klasse</option>
<option value="12">12. Klasse</option>
<option value="13">13. Klasse</option>
</select>
<!-- Sozialverhalten -->
</select>
<!-- Sozialverhalten mit Echtzeit-Validierung -->
{#if !hideSozialVerhalten}
<select bind:value={sozialverhalten} required class="input">
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</option>
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
</select>
{/if}
<div class="col-span-2">
<select
bind:value={sozialverhalten}
required
class="input"
class:input-error={sozialverhaltenFehler}
>
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</option>
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
</select>
{#if sozialverhaltenFehler}
<p class="text-red-600 text-sm mt-1">{sozialverhaltenFehler}</p>
{/if}
</div>
{/if}
<!-- Noten mit Echtzeit-Validierung -->
<div class="col-span-2">
<div class="grid grid-cols-2 gap-4">
<input
bind:value={noteDeutsch}
type="number"
min="1"
max="6"
placeholder="Note in Deutsch"
required
class="input"
class:input-error={notenFehler}
/>
<input
bind:value={noteMathe}
type="number"
min="1"
max="6"
placeholder="Note in Mathe"
required
class="input"
class:input-error={notenFehler}
/>
</div>
{#if notenFehler}
<p class="text-red-600 text-sm mt-1">{notenFehler}</p>
{/if}
</div>
</div>
<div class="grid grid-cols-1 gap-4">
@@ -273,13 +339,20 @@
<option
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
</option>
startDatum = {new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
{/each}
</select>
<p>Startdatum: {startDatum}</p>
</div>
<!-- Alter-Anzeige mit Echtzeit-Validierung -->
{#if alter}
<div class="text-sm" class:text-gray-600={!alterFehler} class:text-red-600={alterFehler}>
Alter zu Praktikumsbeginn: {alter} Jahre
{#if alterFehler}
<p class="font-semibold mt-1">{alterFehler}</p>
{/if}
</div>
{/if}
<!-- Wunschdienststellen -->
<div class="grid grid-cols-1 gap-4">
<select bind:value={wunsch1Id} required>
@@ -324,24 +397,6 @@
</div>
{/key}
<!-- IGS Klasse 7 Hinweis Modal -->
{#if showIgsHinweis}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-md w-full">
<h3 class="text-lg font-semibold text-blue-600">Wichtiger Hinweis</h3>
<p class="text-gray-700">
Bitte keinen Entwicklungsbericht beifügen, sondern Noten von der Schule bescheinigen lassen (zu Bewerbungszwecken).
</p>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
on:click={() => showIgsHinweis = false}
>
Verstanden
</button>
</div>
</div>
{/if}
{#if showAblehnungModal}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
@@ -356,12 +411,24 @@
</div>
{/if}
<!-- Button -->
<button type="submit"
class="w-full bg-blue-600 text-white py-3 rounded-xl hover:bg-blue-700 transition-all">
<!-- Button - deaktiviert bei Validierungsfehlern -->
<button
type="submit"
disabled={formHatFehler}
class="w-full py-3 rounded-xl transition-all"
class:bg-blue-600={!formHatFehler}
class:hover:bg-blue-700={!formHatFehler}
class:text-white={!formHatFehler}
class:bg-gray-400={formHatFehler}
class:cursor-not-allowed={formHatFehler}
>
Jetzt anmelden
</button>
{#if formHatFehler}
<p class="text-red-600 text-sm text-center">Bitte korrigiere die markierten Fehler, um fortzufahren.</p>
{/if}
{#if success}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
@@ -376,17 +443,6 @@
</div>
{/if}
{#if alter}
<div class="text-sm text-gray-600">
Alter zu Praktikumsbeginn: {alter} Jahre
{#if parseInt(alter) < 14}
<span class="text-red-600 font-semibold">
(Mindestalter 14 Jahre erforderlich)
</span>
{/if}
</div>
{/if}
{#if fehler}
<p class="text-red-600">{fehler}</p>
{/if}
@@ -396,15 +452,23 @@
<style>
.input {
width: 100%;
border: 1px solid #d1d5db; /* border-gray-300 */
border-radius: 0.75rem; /* rounded-xl */
padding: 0.75rem; /* p-3 */
border: 1px solid #d1d5db;
border-radius: 0.75rem;
padding: 0.75rem;
outline: none;
transition: box-shadow 0.2s;
transition: box-shadow 0.2s, border-color 0.2s;
}
.input:focus {
outline: none;
box-shadow: 0 0 0 2px #3b82f6; /* focus:ring-2 focus:ring-blue-500 */
box-shadow: 0 0 0 2px #3b82f6;
border-color: #3b82f6;
}
.input-error {
border-color: #dc2626;
background-color: #fef2f2;
}
.input-error:focus {
box-shadow: 0 0 0 2px #dc2626;
border-color: #dc2626;
}
</style>

View File

@@ -3,9 +3,8 @@
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
let dienststellen: { id: number; name: string }[] = [];
let neuerName = '';
let neuePlaetze = 0;
let fehlermeldung = '';
let bearbeiteId: number | null = null;
let isLoading = true;
@@ -30,9 +29,8 @@
}
}
function bearbeiten(d: { id: number; name: string; plaetze: number }) {
function bearbeiten(d: { id: number; name: string }) {
neuerName = d.name;
neuePlaetze = d.plaetze;
bearbeiteId = d.id;
}
@@ -46,8 +44,8 @@
try {
const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
: { name: neuerName, plaetze: neuePlaetze };
? { id: bearbeiteId, name: neuerName }
: { name: neuerName };
const res = await fetch('/api/admin/dienststellen', {
method,
@@ -57,7 +55,6 @@
if (res.ok) {
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
await ladeDienststellen();
} else {
@@ -71,7 +68,7 @@
}
async function loeschen(id: number) {
if (!confirm('Diese Dienststelle wirklich löschen?')) return;
if (!confirm('Diese Dienststelle wirklich löschen? Alle zugehörigen Platzangaben werden ebenfalls gelöscht.')) return;
try {
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
@@ -91,7 +88,6 @@
function resetForm() {
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
}
@@ -108,7 +104,7 @@
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
<main class="max-w-5xl mx-auto px-4 py-6">
{#if fehlermeldung}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
@@ -125,14 +121,30 @@
</div>
{/if}
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
Hier werden nur die Dienststellen verwaltet. Die Anzahl der Praktikumsplätze pro Zeitraum können Sie unter
<a href="/admin/plaetze" class="font-medium underline hover:text-blue-800">Plätze verwalten</a> festlegen.
</p>
</div>
</div>
</div>
<!-- Eingabeformular -->
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">
{bearbeiteId !== null ? 'Dienststelle bearbeiten' : 'Neue Dienststelle hinzufügen'}
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<div class="flex gap-4">
<div class="flex-1">
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
Dienststelle
</label>
@@ -145,20 +157,6 @@
/>
</div>
<div>
<label for="plaetze" class="block text-sm font-medium text-gray-700 mb-2">
Anzahl Plätze
</label>
<input
id="plaetze"
type="number"
bind:value={neuePlaetze}
placeholder="0"
min="0"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div class="flex items-end gap-2">
<button
on:click={resetForm}
@@ -192,7 +190,7 @@
{:else}
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen</h3>
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen ({dienststellen.length})</h3>
</div>
<div class="overflow-x-auto">
@@ -202,9 +200,6 @@
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dienststelle
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Plätze
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
@@ -216,9 +211,6 @@
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{d.name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-right">
{d.plaetze}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => bearbeiten(d)}

View File

@@ -0,0 +1,15 @@
// src/routes/admin/plaetze/+page.server.ts
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
throw redirect(303, '/admin');
}
return {
title: 'Plätze verwalten'
};
};

View File

@@ -0,0 +1,259 @@
<!-- src/routes/admin/plaetze/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
type ZeitraumPlaetzeData = {
zeitraumId: number;
zeitraumBezeichnung: string;
dienststellen: {
dienststelleId: number;
dienststelleName: string;
plaetze: number;
id: number; // ZeitraumPlaetze-ID
}[];
};
let data: ZeitraumPlaetzeData[] = [];
let isLoading = true;
let fehlermeldung = '';
let erfolgsmeldung = '';
let editingCell: { zeitraumId: number; dienststelleId: number } | null = null;
let editValue = '';
async function ladeDaten() {
try {
isLoading = true;
fehlermeldung = '';
const res = await fetch('/api/admin/plaetze');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
}
data = await res.json();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Plätze:', err);
} finally {
isLoading = false;
}
}
function startEdit(zeitraumId: number, dienststelleId: number, currentValue: number) {
editingCell = { zeitraumId, dienststelleId };
editValue = currentValue.toString();
}
async function speicherePlaetze(zeitraumId: number, dienststelleId: number) {
const plaetze = parseInt(editValue);
if (isNaN(plaetze) || plaetze < 0) {
fehlermeldung = 'Bitte geben Sie eine gültige Zahl ein (≥ 0)';
return;
}
try {
fehlermeldung = '';
erfolgsmeldung = '';
const res = await fetch('/api/admin/plaetze', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
zeitraumId,
dienststelleId,
plaetze
})
});
if (!res.ok) {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern';
return;
}
editingCell = null;
await ladeDaten();
erfolgsmeldung = 'Plätze erfolgreich aktualisiert';
setTimeout(() => erfolgsmeldung = '', 3000);
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
}
}
function abbrechen() {
editingCell = null;
editValue = '';
}
function handleKeydown(e: KeyboardEvent, zeitraumId: number, dienststelleId: number) {
if (e.key === 'Enter') {
speicherePlaetze(zeitraumId, dienststelleId);
} else if (e.key === 'Escape') {
abbrechen();
}
}
onMount(ladeDaten);
</script>
<svelte:head>
<title>Plätze verwalten - Admin</title>
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Praktikumsplätze verwalten"
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
{#if fehlermeldung}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
<p class="mt-1 text-sm text-red-700">{fehlermeldung}</p>
</div>
</div>
</div>
{/if}
{#if erfolgsmeldung}
<div class="bg-green-50 border border-green-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-green-700">{erfolgsmeldung}</p>
</div>
</div>
</div>
{/if}
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
Klicken Sie auf eine Zahl, um die Anzahl der Praktikumsplätze zu bearbeiten.
Drücken Sie Enter zum Speichern oder Esc zum Abbrechen.
</p>
</div>
</div>
</div>
{#if isLoading}
<div class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-3 text-gray-600">Lade Daten...</span>
</div>
{:else if data.length === 0}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Zeiträume vorhanden</h3>
<p class="mt-1 text-sm text-gray-500">
Erstellen Sie zunächst
<a href="/admin/zeitraeume" class="text-blue-600 hover:underline">Praktikumszeiträume</a>
und
<a href="/admin/dienststellen" class="text-blue-600 hover:underline">Dienststellen</a>.
</p>
</div>
{:else}
{#each data as zeitraum}
<div class="bg-white shadow-sm rounded-lg overflow-hidden mb-6">
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">{zeitraum.zeitraumBezeichnung}</h3>
</div>
{#if zeitraum.dienststellen.length === 0}
<div class="px-6 py-8 text-center text-gray-500 text-sm">
Keine Dienststellen vorhanden.
<a href="/admin/dienststellen" class="text-blue-600 hover:underline">Dienststellen erstellen</a>
</div>
{:else}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dienststelle
</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Anzahl Plätze
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each zeitraum.dienststellen as dienststelle}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{dienststelle.dienststelleName}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
{#if editingCell?.zeitraumId === zeitraum.zeitraumId && editingCell?.dienststelleId === dienststelle.dienststelleId}
<div class="flex items-center justify-center gap-2">
<input
type="number"
bind:value={editValue}
min="0"
on:keydown={(e) => handleKeydown(e, zeitraum.zeitraumId, dienststelle.dienststelleId)}
class="w-20 border border-blue-500 rounded px-2 py-1 text-sm text-center focus:ring-2 focus:ring-blue-500 focus:border-transparent"
autofocus
/>
<button
on:click={() => speicherePlaetze(zeitraum.zeitraumId, dienststelle.dienststelleId)}
class="text-green-600 hover:text-green-800"
title="Speichern"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
<button
on:click={abbrechen}
class="text-red-600 hover:text-red-800"
title="Abbrechen"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{:else}
<button
on:click={() => startEdit(zeitraum.zeitraumId, dienststelle.dienststelleId, dienststelle.plaetze)}
class="text-blue-600 hover:text-blue-800 hover:underline font-medium text-sm"
>
{dienststelle.plaetze}
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{/each}
{/if}
</main>
</div>

View File

@@ -30,6 +30,19 @@ export async function GET() {
nachname: anmeldung.nachname,
email: anmeldung.email,
// === FEHLENDE FELDER HINZUGEFÜGT ===
geburtsdatum: anmeldung.geburtsdatum,
strasse: anmeldung.strasse,
hausnummer: anmeldung.hausnummer,
ort: anmeldung.ort,
plz: anmeldung.plz,
telefon: anmeldung.telefon,
schulart: anmeldung.schulart,
schulklasse: anmeldung.schulklasse,
motivation: anmeldung.motivation,
alter: anmeldung.alter,
// === ENDE FEHLENDE FELDER ===
// Noten als String konvertieren (falls sie als Int gespeichert sind)
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
@@ -37,7 +50,6 @@ export async function GET() {
// Status-Mapping für Frontend
status: mapPrismaStatusToFrontend(anmeldung.status),
// processedBy: anmeldung.processedBy,
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
// Wünsche - sicherstellen dass sie existieren
@@ -60,6 +72,14 @@ export async function GET() {
name: anmeldung.zugewiesen.name
} : undefined,
// Praktikumszeitraum
zeitraum: anmeldung.praktikum ? {
id: anmeldung.praktikum.id,
bezeichnung: anmeldung.praktikum.bezeichnung,
startDatum: anmeldung.praktikum.startDatum.toISOString(),
endDatum: anmeldung.praktikum.endDatum.toISOString()
} : undefined,
// Timestamp
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
@@ -83,9 +103,12 @@ export async function POST({ request, url }) {
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
}
// Prüfen ob Anmeldung existiert
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id }
where: { id },
include: {
praktikum: true
}
});
if (!existingAnmeldung) {
@@ -96,8 +119,37 @@ export async function POST({ request, url }) {
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
}
// Anmeldung als angenommen markieren
const zeitraumId = existingAnmeldung.praktikumId;
if (!zeitraumId) {
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 });
}
// Prüfen ob ZeitraumPlaetze Eintrag existiert
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
});
if (!zeitraumPlaetze) {
return json({
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
}, { status: 400 });
}
if (zeitraumPlaetze.plaetze <= 0) {
return json({
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum'
}, { status: 400 });
}
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
await prisma.$transaction([
// Anmeldung aktualisieren
prisma.anmeldung.update({
where: { id },
data: {
@@ -107,13 +159,19 @@ export async function POST({ request, url }) {
}
}),
prisma.dienststelle.update({
where: { id: dienststelleId },
data: {
plaetze: {
decrement: 1
// Plätze in ZeitraumPlaetze reduzieren
prisma.zeitraumPlaetze.update({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
},
data: {
plaetze: {
decrement: 1
}
}
})
]);

View File

@@ -1,193 +1,124 @@
// src/routes/api/admin/dienststellen/+server.ts
import { PrismaClient } from '@prisma/client';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { prisma } from '$lib/prisma';
const prisma = new PrismaClient();
import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) {
return cookies.get('admin-auth') === 'authenticated';
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für eine neue Dienststelle
async function createZeitraumPlaetzeForDienststelle(dienststelleId: number) {
const zeitraeume = await prisma.praktikumszeitraum.findMany();
// Erstelle für jeden existierenden Zeitraum einen Eintrag mit 0 Plätzen
for (const zeitraum of zeitraeume) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraum.id,
dienststelleId: dienststelleId,
plaetze: 0 // Standardwert: 0 Plätze
}
});
}
}
export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) {
return new Response(
JSON.stringify({ error: 'Nicht autorisiert' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const dienststellen = await prisma.dienststelle.findMany({
orderBy: { name: 'asc' },
/*
include: {
_count: {
select: {
Anmeldung: true // Use the correct relation name as defined in your Prisma schema
}
}
}
*/
const dienststellen = await prisma.dienststelle.findMany({
orderBy: { name: 'asc' }
});
return json(dienststellen);
} catch (error) {
console.error('Fehler beim Laden der Dienststellen:', error);
return json({ error: 'Fehler beim Laden der Dienststellen' }, { status: 500 });
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { name, plaetze } = await request.json();
const { name } = await request.json();
// Validierung
if (!name || typeof name !== 'string' || name.trim().length === 0) {
if (!name) {
return json({ error: 'Name ist erforderlich' }, { status: 400 });
}
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
}
// Prüfe ob Name bereits existiert
const existing = await prisma.dienststelle.findFirst({
where: { name: name.trim() }
});
if (existing) {
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
const created = await prisma.dienststelle.create({
data: {
name: name.trim(),
plaetze,
}
});
return json(created, { status: 201 });
} catch (error) {
console.error('Fehler beim Erstellen der Dienststelle:', error);
return json({ error: 'Fehler beim Erstellen der Dienststelle' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { id, name, plaetze } = await request.json();
// Validierung
if (typeof id !== 'number' || isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
}
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return json({ error: 'Name ist erforderlich' }, { status: 400 });
}
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
}
// Prüfe ob Dienststelle existiert
const existing = await prisma.dienststelle.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
}
// Prüfe ob neuer Name bereits bei anderer Dienststelle existiert
const nameConflict = await prisma.dienststelle.findFirst({
where: {
name: name.trim(),
NOT: { id },
},
});
if (nameConflict) {
return json({ error: 'Eine andere Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
// Prüfe ob Plätze reduziert werden und ob das möglich ist
const assignedCount = await prisma.anmeldung.count({
where: { zugewiesenId: id }
});
if (plaetze < assignedCount) {
return json({
error: `Plätze können nicht auf ${plaetze} reduziert werden. ${assignedCount} Anmeldungen sind bereits zugewiesen.`
}, { status: 400 });
}
const updated = await prisma.dienststelle.update({
where: { id },
data: {
name: name.trim(),
plaetze
},
});
return json(updated);
} catch (error) {
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
return json({ error: 'Fehler beim Aktualisieren der Dienststelle' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id'));
if (isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
}
try {
// Prüfe ob Dienststelle existiert
const existing = await prisma.dienststelle.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
}
// Prüfe ob noch Anmeldungen zugewiesen sind
const assignedCount = await prisma.anmeldung.count({
where: {
OR: [
{ zugewiesenId: id },
{ wunsch1Id: id },
{ wunsch2Id: id },
{ wunsch3Id: id }
]
const dienststelle = await prisma.dienststelle.create({
data: {
name,
plaetze: 0 // Wird nicht mehr verwendet, aber bleibt im Schema für Kompatibilität
}
});
if (assignedCount > 0) {
return json({
error: 'Dienststelle kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
}, { status: 400 });
// Automatisch ZeitraumPlaetze für alle existierenden Zeiträume erstellen
await createZeitraumPlaetzeForDienststelle(dienststelle.id);
return json(dienststelle);
} catch (error: any) {
console.error('Fehler beim Erstellen der Dienststelle:', error);
if (error.code === 'P2002') {
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { id, name } = await request.json();
if (!id || !name) {
return json({ error: 'ID und Name sind erforderlich' }, { status: 400 });
}
await prisma.dienststelle.delete({ where: { id } });
return json({ success: true, message: 'Dienststelle erfolgreich gelöscht' });
const dienststelle = await prisma.dienststelle.update({
where: { id: parseInt(id) },
data: {
name
}
});
return json(dienststelle);
} catch (error: any) {
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
if (error.code === 'P2002') {
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const id = url.searchParams.get('id');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
await prisma.dienststelle.delete({
where: { id: parseInt(id) }
});
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen der Dienststelle:', error);
return json({ error: 'Fehler beim Löschen der Dienststelle' }, { status: 500 });
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -1,59 +1,37 @@
// src/routes/api/admin/login/+server.ts
import type { RequestHandler } from './$types';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
//const ADMIN_PASSWORD_HASH = 'your-hashed-password-here'; // Ersetze mit deinem Hash
const adminRecord = await prisma.admin.findUnique({ where: { id: 1 } });
if (!adminRecord || !adminRecord.password) {
throw new Error('Admin password hash not found in database');
} else {
console.log('Admin password hash loaded successfully');
}
const ADMIN_PASSWORD_HASH = adminRecord.password;
import { prisma } from '$lib/prisma';
export const POST: RequestHandler = async ({ request, cookies }) => {
try {
const adminRecord = await prisma.admin.findUnique({ where: { id: 1 } });
if (!adminRecord?.password) {
return new Response(JSON.stringify({ message: 'Admin password not found' }), { status: 500 });
}
const { passwort } = await request.json();
if (!passwort) {
return new Response(
JSON.stringify({ message: 'Passwort erforderlich' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
return new Response(JSON.stringify({ message: 'Passwort erforderlich' }), { status: 400 });
}
// Hier solltest du den Hash aus der Datenbank oder Umgebungsvariable laden
const isValid = await bcrypt.compare(passwort, ADMIN_PASSWORD_HASH);
const isValid = await bcrypt.compare(passwort, adminRecord.password);
if (isValid) {
// Setze konsistenten Cookie-Namen
cookies.set('admin-auth', 'authenticated', {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 // 24 Stunden
maxAge: 60 * 60 * 24,
});
return new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} else {
return new Response(
JSON.stringify({ message: 'Falsches Passwort' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
return new Response(JSON.stringify({ message: 'Falsches Passwort' }), { status: 401 });
} catch (error) {
console.error('Login error:', error);
return new Response(
JSON.stringify({ message: 'Serverfehler' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
return new Response(JSON.stringify({ message: 'Serverfehler' }), { status: 500 });
}
};

View File

@@ -0,0 +1,92 @@
// src/routes/api/admin/plaetze/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { prisma } from '$lib/prisma';
export const GET: RequestHandler = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
// Lade alle Zeiträume mit ihren ZeitraumPlaetze-Einträgen
const zeitraeume = await prisma.praktikumszeitraum.findMany({
include: {
zeitraumPlaetze: {
include: {
dienststelle: true
},
orderBy: {
dienststelle: {
name: 'asc'
}
}
}
},
orderBy: {
startDatum: 'desc'
}
});
// Transformiere die Daten in das gewünschte Format
const result = zeitraeume.map(zeitraum => ({
zeitraumId: zeitraum.id,
zeitraumBezeichnung: zeitraum.bezeichnung,
dienststellen: zeitraum.zeitraumPlaetze.map(zp => ({
id: zp.id,
dienststelleId: zp.dienststelleId,
dienststelleName: zp.dienststelle.name,
plaetze: zp.plaetze
}))
}));
return json(result);
} catch (error) {
console.error('Fehler beim Laden der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { zeitraumId, dienststelleId, plaetze } = await request.json();
if (!zeitraumId || !dienststelleId || plaetze === undefined) {
return json({ error: 'Zeitraum-ID, Dienststellen-ID und Plätze sind erforderlich' }, { status: 400 });
}
const plaetzeInt = parseInt(plaetze);
if (isNaN(plaetzeInt) || plaetzeInt < 0) {
return json({ error: 'Plätze muss eine gültige Zahl ≥ 0 sein' }, { status: 400 });
}
// Aktualisiere oder erstelle den Eintrag
const updated = await prisma.zeitraumPlaetze.upsert({
where: {
zeitraumId_dienststelleId: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId)
}
},
update: {
plaetze: plaetzeInt
},
create: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId),
plaetze: plaetzeInt
}
});
return json(updated);
} catch (error) {
console.error('Fehler beim Aktualisieren der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -1,160 +1,141 @@
import { PrismaClient } from '@prisma/client';
// src/routes/api/admin/zeitraeume/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { prisma } from '$lib/prisma';
const prisma = new PrismaClient();
import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) {
return cookies.get('admin-auth') === 'authenticated';
}
function isValidDate(date: string | Date) {
const parsed = new Date(date)
return !isNaN(parsed.getTime())
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
const dienststellen = await prisma.dienststelle.findMany();
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraumId,
dienststelleId: dienststelle.id,
plaetze: 0 // Standardwert: 0 Plätze
}
});
}
}
export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) {
return new Response(
JSON.stringify({ error: 'Nicht autorisiert' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const zeitraeume = await prisma.praktikumszeitraum.findMany();
const zeitraeume = await prisma.praktikumszeitraum.findMany({
orderBy: { startDatum: 'desc' }
});
return json(zeitraeume);
} catch (error) {
console.error('Fehler beim Laden der Praktikumszeiträume:', error);
return json({ error: 'Fehler beim Laden der Praktikumszeiträume' }, { status: 500 });
console.error('Fehler beim Laden der Zeiträume:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { bezeichnung, startDatum, endDatum } = await request.json();
// Validierung
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
if (!bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültiges Datum' }, { status: 400 });
const start = new Date(startDatum);
const end = new Date(endDatum);
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
const created = await prisma.praktikumszeitraum.create({
data: {
bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
}
const zeitraum = await prisma.praktikumszeitraum.create({
data: {
bezeichnung,
startDatum: start,
endDatum: end
}
});
return json(created, { status: 201 });
} catch (error) {
console.error('Fehler beim Erstellen des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Erstellen des Praktikumszeitraums' }, { status: 500 });
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
return json(zeitraum);
} catch (error: any) {
console.error('Fehler beim Erstellen des Zeitraums:', error);
if (error.code === 'P2002') {
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { id, bezeichnung, startDatum, endDatum } = await request.json();
// Validierung
if (typeof id !== 'number' || isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
if (!id || !bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
const start = new Date(startDatum);
const end = new Date(endDatum);
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültiges Datum' }, { status: 400 });
}
// Prüfe ob Praktikumszeitraum existiert
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
}
// Prüfe ob neue Bezeichnung bereits bei anderem Zeitraum existiert
const konflikt = await prisma.praktikumszeitraum.findFirst({
where: {
bezeichnung: bezeichnung.trim(),
NOT: { id },
},
const zeitraum = await prisma.praktikumszeitraum.update({
where: { id: parseInt(id) },
data: {
bezeichnung,
startDatum: start,
endDatum: end
}
});
if (konflikt) {
return json({ error: 'Ein anderer Praktikumszeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
return json(zeitraum);
} catch (error: any) {
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
if (error.code === 'P2002') {
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
}
const updated = await prisma.praktikumszeitraum.update({
where: { id },
data: {
bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
},
});
return json(updated);
} catch (error) {
console.error('Fehler beim Aktualisieren des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Aktualisieren des Praktikumszeitraums' }, { status: 500 });
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) {
export const DELETE: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id'));
if (isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
}
try {
// Prüfe ob Praktikumszeitraum existiert
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
const id = url.searchParams.get('id');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// Hier könntest du prüfen, ob noch Anmeldungen mit diesem Zeitraum verknüpft sind
// const assignedCount = await prisma.anmeldung.count({
// where: { praktikumszeitraumId: id }
// });
//
// if (assignedCount > 0) {
// return json({
// error: 'Praktikumszeitraum kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
// }, { status: 400 });
// }
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
await prisma.praktikumszeitraum.delete({
where: { id: parseInt(id) }
});
await prisma.praktikumszeitraum.delete({ where: { id } });
return json({ success: true, message: 'Praktikumszeitraum erfolgreich gelöscht' });
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Löschen des Praktikumszeitraums' }, { status: 500 });
console.error('Fehler beim Löschen des Zeitraums:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -0,0 +1,295 @@
<!-- src/routes/admin/zeitraum-plaetze/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
interface Zeitraum {
id: number;
bezeichnung: string;
startDatum: string;
endDatum: string;
}
interface ZeitraumPlatz {
id: number;
zeitraumId: number;
dienststelleId: number;
plaetze: number;
dienststelle: {
id: number;
name: string;
};
}
let zeitraeume: Zeitraum[] = [];
let selectedZeitraumId: number | null = null;
let plaetze: ZeitraumPlatz[] = [];
let isLoading = true;
let isSaving = false;
let fehlermeldung = '';
let erfolgsmeldung = '';
// Temporäre Änderungen speichern
let aenderungen: Map<number, number> = new Map();
async function ladeZeitraeume() {
try {
const res = await fetch('/api/admin/zeitraeume');
if (!res.ok) throw new Error('Fehler beim Laden');
zeitraeume = await res.json();
if (zeitraeume.length > 0 && !selectedZeitraumId) {
selectedZeitraumId = zeitraeume[0].id;
await ladePlaetze();
}
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Laden';
console.error(err);
}
}
async function ladePlaetze() {
if (!selectedZeitraumId) return;
try {
isLoading = true;
fehlermeldung = '';
erfolgsmeldung = '';
const res = await fetch(`/api/admin/zeitraum-plaetze?zeitraumId=${selectedZeitraumId}`);
if (!res.ok) throw new Error('Fehler beim Laden');
plaetze = await res.json();
aenderungen.clear();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Laden';
console.error(err);
} finally {
isLoading = false;
}
}
function handlePlaetzeChange(dienststelleId: number, wert: string) {
const plaetzeWert = parseInt(wert) || 0;
aenderungen.set(dienststelleId, plaetzeWert);
aenderungen = aenderungen; // Trigger reactivity
}
function getPlaetzeWert(platz: ZeitraumPlatz): number {
return aenderungen.has(platz.dienststelleId)
? aenderungen.get(platz.dienststelleId)!
: platz.plaetze;
}
async function speichernAlle() {
if (!selectedZeitraumId || aenderungen.size === 0) {
erfolgsmeldung = 'Keine Änderungen zu speichern';
return;
}
try {
isSaving = true;
fehlermeldung = '';
erfolgsmeldung = '';
// Alle Änderungen nacheinander speichern
for (const [dienststelleId, plaetzeWert] of aenderungen) {
const res = await fetch('/api/admin/zeitraum-plaetze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
zeitraumId: selectedZeitraumId,
dienststelleId,
plaetze: plaetzeWert
})
});
if (!res.ok) {
throw new Error(`Fehler beim Speichern für Dienststelle ${dienststelleId}`);
}
}
erfolgsmeldung = 'Alle Änderungen erfolgreich gespeichert';
aenderungen.clear();
await ladePlaetze();
// Erfolgsmeldung nach 3 Sekunden ausblenden
setTimeout(() => { erfolgsmeldung = ''; }, 3000);
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
} finally {
isSaving = false;
}
}
async function syncAlles() {
if (!confirm('Möchten Sie wirklich alle Zeitraum-Dienststellen-Kombinationen synchronisieren?')) {
return;
}
try {
isSaving = true;
fehlermeldung = '';
const res = await fetch('/api/admin/zeitraum-plaetze', {
method: 'PATCH'
});
if (!res.ok) throw new Error('Fehler beim Synchronisieren');
erfolgsmeldung = 'Synchronisierung erfolgreich';
await ladePlaetze();
setTimeout(() => { erfolgsmeldung = ''; }, 3000);
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Synchronisieren';
console.error(err);
} finally {
isSaving = false;
}
}
$: if (selectedZeitraumId) {
ladePlaetze();
}
onMount(ladeZeitraeume);
</script>
<svelte:head>
<title>Plätze pro Zeitraum verwalten - Admin</title>
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Plätze pro Zeitraum verwalten"
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
{#if fehlermeldung}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
<p class="mt-1 text-sm text-red-700">{fehlermeldung}</p>
</div>
</div>
</div>
{/if}
{#if erfolgsmeldung}
<div class="bg-green-50 border border-green-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800">{erfolgsmeldung}</p>
</div>
</div>
</div>
{/if}
<!-- Zeitraum Auswahl -->
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
<div class="flex justify-between items-center">
<div class="flex-1">
<label for="zeitraum" class="block text-sm font-medium text-gray-700 mb-2">
Praktikumszeitraum auswählen
</label>
<select
id="zeitraum"
bind:value={selectedZeitraumId}
class="w-full max-w-md border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
>
{#each zeitraeume as z}
<option value={z.id}>
{z.bezeichnung} ({new Date(z.startDatum).toLocaleDateString('de-DE')} - {new Date(z.endDatum).toLocaleDateString('de-DE')})
</option>
{/each}
</select>
</div>
<button
on:click={syncAlles}
disabled={isSaving}
class="px-4 py-2 text-sm text-blue-600 hover:text-blue-800 hover:underline disabled:opacity-50"
>
Alles synchronisieren
</button>
</div>
</div>
{#if isLoading}
<div class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-3 text-gray-600">Lade Plätze...</span>
</div>
{:else if plaetze.length === 0}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Dienststellen</h3>
<p class="mt-1 text-sm text-gray-500">Erstellen Sie zuerst Dienststellen, um Plätze zuzuweisen.</p>
</div>
{:else}
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">
Plätze pro Dienststelle
</h3>
<button
on:click={speichernAlle}
disabled={isSaving || aenderungen.size === 0}
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? 'Speichert...' : `Änderungen speichern ${aenderungen.size > 0 ? `(${aenderungen.size})` : ''}`}
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dienststelle
</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Verfügbare Plätze
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each plaetze as platz}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{platz.dienststelle.name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<input
type="number"
min="0"
value={getPlaetzeWert(platz)}
on:input={(e) => handlePlaetzeChange(platz.dienststelleId, e.currentTarget.value)}
class="w-24 border border-gray-300 rounded-md px-3 py-1 text-center focus:ring-blue-500 focus:border-blue-500 {aenderungen.has(platz.dienststelleId) ? 'bg-yellow-50 border-yellow-300' : ''}"
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</main>
</div>

View File

@@ -0,0 +1,114 @@
// src/routes/api/admin/zeitraum-plaetze/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET: Plätze für einen bestimmten Zeitraum abrufen
export const GET: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const zeitraumId = url.searchParams.get('zeitraumId');
if (!zeitraumId) {
return json({ error: 'zeitraumId erforderlich' }, { status: 400 });
}
const plaetze = await prisma.zeitraumPlaetze.findMany({
where: { zeitraumId: parseInt(zeitraumId) },
include: {
dienststelle: true
},
orderBy: {
dienststelle: { name: 'asc' }
}
});
return json(plaetze);
} catch (error) {
console.error('Fehler beim Laden der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
// POST: Plätze für einen Zeitraum und Dienststelle aktualisieren
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { zeitraumId, dienststelleId, plaetze } = await request.json();
if (!zeitraumId || !dienststelleId || plaetze === undefined) {
return json({ error: 'Fehlende Parameter' }, { status: 400 });
}
const result = await prisma.zeitraumPlaetze.upsert({
where: {
zeitraumId_dienststelleId: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId)
}
},
update: {
plaetze: parseInt(plaetze)
},
create: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId),
plaetze: parseInt(plaetze)
}
});
return json(result);
} catch (error) {
console.error('Fehler beim Speichern der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
// PATCH: Synchronisiert ZeitraumPlaetze wenn neue Dienststellen oder Zeiträume hinzugefügt werden
export const PATCH: RequestHandler = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
// Alle Zeiträume und Dienststellen holen
const zeitraeume = await prisma.praktikumszeitraum.findMany();
const dienststellen = await prisma.dienststelle.findMany();
// Für jede Kombination prüfen ob Eintrag existiert, sonst erstellen
for (const zeitraum of zeitraeume) {
for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.upsert({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraum.id,
dienststelleId: dienststelle.id
}
},
update: {}, // Nichts updaten wenn bereits vorhanden
create: {
zeitraumId: zeitraum.id,
dienststelleId: dienststelle.id,
plaetze: dienststelle.plaetze // Standardwert von Dienststelle übernehmen
}
});
}
}
return json({ success: true, message: 'Synchronisierung abgeschlossen' });
} catch (error) {
console.error('Fehler bei der Synchronisierung:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -1,10 +1,17 @@
import { PrismaClient } from '@prisma/client';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
const prisma = new PrismaClient();
let prismaPromise: Promise<any> | null = null;
async function getPrismaClient() {
if (!prismaPromise) {
prismaPromise = import('@prisma/client').then(({ PrismaClient }) => new PrismaClient());
}
return prismaPromise;
}
export const GET: RequestHandler = async () => {
const prisma = await getPrismaClient(); // Hier Prisma Client holen
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
return json(dienststellen);
};