Admin Bereich an das neuen layout geaendert.

This commit is contained in:
titver968
2025-07-24 09:31:02 +02:00
parent 24dd912f77
commit aeabd91d2d
4 changed files with 789 additions and 368 deletions

View File

@@ -1,18 +1,41 @@
<!-- src/routes/admin/change-password/+page.svelte -->
<script lang="ts">
let oldPassword = '';
let newPassword = '';
let confirmPassword = '';
let message = '';
let error = '';
import AdminHeader from '$lib/components/AdminHeader.svelte';
async function changePassword() {
message = '';
error = '';
let oldPassword = '';
let newPassword = '';
let confirmPassword = '';
let message = '';
let error = '';
let isLoading = false;
if (newPassword !== confirmPassword) {
error = 'Die neuen Passwörter stimmen nicht überein.';
return;
}
async function changePassword() {
message = '';
error = '';
// Validierung
if (!oldPassword.trim()) {
error = 'Altes Passwort ist erforderlich.';
return;
}
if (!newPassword.trim()) {
error = 'Neues Passwort ist erforderlich.';
return;
}
if (newPassword.length < 6) {
error = 'Das neue Passwort muss mindestens 6 Zeichen lang sein.';
return;
}
if (newPassword !== confirmPassword) {
error = 'Die neuen Passwörter stimmen nicht überein.';
return;
}
try {
isLoading = true;
const res = await fetch('/api/admin/change-password', {
method: 'POST',
@@ -26,57 +49,180 @@
if (!res.ok) {
error = data.error || 'Fehler beim Ändern des Passworts.';
} else {
message = 'Passwort erfolgreich geändert.';
oldPassword = newPassword = confirmPassword = '';
message = 'Passwort erfolgreich geändert.';
oldPassword = '';
newPassword = '';
confirmPassword = '';
}
} catch (err) {
error = err instanceof Error ? err.message : 'Unbekannter Fehler beim Ändern des Passworts.';
console.error('Fehler beim Passwort ändern:', err);
} finally {
isLoading = false;
}
}
<div class="max-w-lg mx-auto bg-white p-6 rounded-2xl shadow-md space-y-6 border border-gray-200">
function resetForm() {
oldPassword = '';
newPassword = '';
confirmPassword = '';
message = '';
error = '';
}
</script>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Altes Passwort</label>
<input
type="password"
bind:value={oldPassword}
class="mt-1 w-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<svelte:head>
<title>Passwort ändern - Admin</title>
</svelte:head>
<div>
<label class="block text-sm font-medium text-gray-700">Neues Passwort</label>
<input
type="password"
bind:value={newPassword}
class="mt-1 w-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Neues Passwort wiederholen</label>
<input
type="password"
bind:value={confirmPassword}
class="mt-1 w-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Admin-Passwort ändern"
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
{#if error}
{#if error}
<div class="text-red-600 text-sm font-medium">{error}</div>
{/if}
{#if message}
<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">{error}</p>
</div>
</div>
</div>
{/if}
<div class="pt-4">
<button
on:click={changePassword}
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-xl transition duration-150"
>
Passwort ändern
{#if message}
<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">
<h3 class="text-sm font-medium text-green-800">Erfolg</h3>
<p class="mt-1 text-sm text-green-700">{message}</p>
</div>
</div>
</div>
{/if}
<!-- Passwort ändern Formular -->
<div class="max-w-2xl mx-auto">
<div class="bg-white shadow-sm rounded-lg p-6">
<div class="flex items-center mb-6">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<div class="ml-4">
<h2 class="text-lg font-medium text-gray-900">Passwort ändern</h2>
<p class="text-sm text-gray-500">Aus Sicherheitsgründen sollten Sie Ihr Passwort regelmäßig ändern.</p>
</div>
</div>
<div class="space-y-6">
<div>
<label for="old-password" class="block text-sm font-medium text-gray-700 mb-2">
Aktuelles Passwort
</label>
<input
id="old-password"
type="password"
bind:value={oldPassword}
placeholder="Geben Sie Ihr aktuelles Passwort ein"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isLoading}
/>
</div>
<div>
<label for="new-password" class="block text-sm font-medium text-gray-700 mb-2">
Neues Passwort
</label>
<input
id="new-password"
type="password"
bind:value={newPassword}
placeholder="Geben Sie Ihr neues Passwort ein (mindestens 6 Zeichen)"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isLoading}
/>
</div>
<div>
<label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-2">
Neues Passwort wiederholen
</label>
<input
id="confirm-password"
type="password"
bind:value={confirmPassword}
placeholder="Wiederholen Sie Ihr neues Passwort"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isLoading}
/>
</div>
<div class="flex items-center justify-between pt-4">
<button
type="button"
on:click={resetForm}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 hover:underline"
disabled={isLoading}
>
Formular zurücksetzen
</button>
<button
type="button"
on:click={changePassword}
disabled={isLoading}
class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white px-6 py-2 rounded-md text-sm font-medium inline-flex items-center"
>
{#if isLoading}
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Wird geändert...
{:else}
Passwort ändern
{/if}
</button>
</div>
</div>
<!-- Sicherheitshinweise -->
<div class="mt-8 p-4 bg-blue-50 rounded-md">
<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">
<h3 class="text-sm font-medium text-blue-800">Sicherheitshinweise</h3>
<div class="mt-2 text-sm text-blue-700">
<ul class="list-disc pl-5 space-y-1">
<li>Verwenden Sie ein starkes Passwort mit mindestens 6 Zeichen</li>
<li>Kombinieren Sie Buchstaben, Zahlen und Sonderzeichen</li>
<li>Verwenden Sie dieses Passwort nicht für andere Dienste</li>
<li>Ändern Sie Ihr Passwort regelmäßig</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>

View File

@@ -1,26 +1,49 @@
<!-- src/routes/admin/dienststellen/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
let neuerName = '';
let neuePlaetze = 0;
let fehlermeldung = '';
let bearbeiteId: number | null = null;
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
async function ladeDienststellen() {
const res = await fetch('/api/admin/dienststellen');
dienststellen = await res.json();
}
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
let neuerName = '';
let neuePlaetze = 0;
let fehlermeldung = '';
let bearbeiteId: number | null = null;
let isLoading = true;
function bearbeiten(d: { id: number; name: string; plaetze: number }) {
neuerName = d.name;
neuePlaetze = d.plaetze;
bearbeiteId = d.id;
}
async function speichern() {
async function ladeDienststellen() {
try {
isLoading = true;
fehlermeldung = '';
if (!neuerName.trim()) return;
const res = await fetch('/api/admin/dienststellen');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
}
dienststellen = await res.json();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Dienststellen:', err);
} finally {
isLoading = false;
}
}
function bearbeiten(d: { id: number; name: string; plaetze: number }) {
neuerName = d.name;
neuePlaetze = d.plaetze;
bearbeiteId = d.id;
}
async function speichern() {
fehlermeldung = '';
if (!neuerName.trim()) {
fehlermeldung = 'Name ist erforderlich';
return;
}
try {
const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
@@ -33,118 +56,189 @@
});
if (res.ok) {
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
await ladeDienststellen();
} else {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern';
}
}
async function loeschen(id: number) {
if (!confirm('Diese Dienststelle wirklich löschen?')) return;
await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
await ladeDienststellen();
}
onMount(ladeDienststellen);
</script>
<div class="p-6 max-w-6xl mx-auto space-y-8">
<h1 class="text-2xl font-bold text-center">Dienststellen verwalten</h1>
<!-- Eingabefelder -->
<div class="flex flex-wrap gap-4 items-center">
<input
bind:value={neuerName}
placeholder="Dienststelle"
class="input w-full sm:w-[55%] border rounded px-3 py-2"
/>
<input
type="number"
bind:value={neuePlaetze}
placeholder="Anzahl Plätze"
class="input w-full sm:w-[20%] border rounded px-3 py-2"
min="0"
/>
<button
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
bearbeiteId = null;
}}
class="text-sm text-gray-500 hover:underline"
>
Zurücksetzen
</button>
<button
on:click={speichern}
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
>
{bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
</button>
await ladeDienststellen();
} else {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern';
}
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
}
}
<!-- Fehlermeldung -->
{#if fehlermeldung}
<p class="text-red-600 text-sm">{fehlermeldung}</p>
async function loeschen(id: number) {
if (!confirm('Diese Dienststelle wirklich löschen?')) return;
<!-- Tabellenähnliche Anzeige -->
<div class="overflow-x-auto">
<div class="min-w-[600px] divide-y border rounded">
<!-- Kopfzeile -->
<div class="grid grid-cols-3 gap-x-8 font-semibold text-sm bg-gray-100 p-2">
<div>Dienststelle</div>
<div class="text-right">Plätze</div>
<div class="text-right">Aktionen</div>
try {
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
<!-- Einträge -->
{#each dienststellen as d}
<div class="grid grid-cols-3 gap-x-8 items-center p-2 text-sm">
<div>{d.name}</div>
<div class="text-right">{d.plaetze}</div>
<div class="flex justify-end gap-3">
<button
on:click={() => {
neuerName = d.name;
neuePlaetze = d.plaetze;
bearbeiteId = d.id;
}}
class="text-blue-600 hover:underline"
>
Bearbeiten
</button>
<button
on:click={() => loeschen(d.id)}
class="text-red-600 hover:underline"
>
Löschen
if (!res.ok) {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Löschen';
return;
}
await ladeDienststellen();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Löschen';
console.error(err);
}
}
function resetForm() {
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
}
onMount(ladeDienststellen);
</script>
<svelte:head>
<title>Dienststellen verwalten - Admin</title>
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Dienststellen 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>
</div>
{/if}
<!-- 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>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
Dienststelle
</label>
<input
id="name"
type="text"
bind:value={neuerName}
placeholder="Name der Dienststelle"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
/>
</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}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 hover:underline"
>
Zurücksetzen
</button>
<button
on:click={speichern}
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md text-sm font-medium"
>
{bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
</button>
</div>
</div>
</div>
</div>
<!-- Logout-Button -->
<div class="pt-4 text-center">
<button
on:click={async () => {
await fetch('/api/admin/logout', { method: 'POST' });
location.reload();
}}
class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded"
>
Logout
</button>
{#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 Dienststellen...</span>
</div>
{:else if dienststellen.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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-4m-5 0H9m0 0H7m2 0v-5a2 2 0 012-2h2a2 2 0 012 2v5M7 7h.01M7 11h.01M11 7h.01M11 11h.01" />
</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 Ihre erste Dienststelle über das Formular oben.</p>
</div>
{: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>
</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-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>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each dienststellen as d}
<tr class="hover:bg-gray-50">
<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)}
class="text-blue-600 hover:text-blue-900 mr-4"
>
Bearbeiten
</button>
<button
on:click={() => loeschen(d.id)}
class="text-red-600 hover:text-red-900"
>
Löschen
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</main>
</div>
</div>
<style>
/* You can add custom styles here if needed, or rely on Tailwind classes in your markup */

View File

@@ -1,175 +1,284 @@
<!-- src/routes/admin/zeitraeume/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte'
let bezeichnung = '';
let startDatum = '';
let endDatum = '';
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
interface Zeitraum {
id: number;
bezeichnung: string;
startDatum: string;
endDatum: string;
}
let zeitraeume: Zeitraum[] = [];
let neuerBezeichnung = '';
let neuerstartDatum = '';
let neuerendDatum = '';
let fehlermeldung = '';
let bearbeiteId: number | null = null;
let isLoading = true;
async function ladeZeitraeume() {
const res = await fetch('/api/admin/zeitraeume');
zeitraeume = await res.json();
}
try {
isLoading = true;
fehlermeldung = '';
function bearbeiten(d: { id: number; bezeichnung: string; startDatum: Date; endDatum: Date }) {
const res = await fetch('/api/admin/zeitraeume');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
}
zeitraeume = await res.json();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Zeiträume:', err);
} finally {
isLoading = false;
}
}
function bearbeiten(d: { id: number; bezeichnung: string; startDatum: string; endDatum: string }) {
neuerBezeichnung = d.bezeichnung;
neuerstartDatum = d.startDatum instanceof Date
? d.startDatum.toISOString().slice(0, 10)
: d.startDatum;
neuerendDatum = d.endDatum instanceof Date
? d.endDatum.toISOString().slice(0, 10)
: d.endDatum;
neuerstartDatum = d.startDatum ? d.startDatum.slice(0, 10) : '';
neuerendDatum = d.endDatum ? d.endDatum.slice(0, 10) : '';
bearbeiteId = d.id;
}
async function speichern() {
fehlermeldung = '';
if (!neuerBezeichnung.trim()) return;
const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId
? { id: bearbeiteId, bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum }
: { bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum };
const res = await fetch('/api/admin/zeitraeume', {
method,
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' }
});
if (res.ok) {
neuerBezeichnung = '';
neuerstartDatum = '';
neuerendDatum = '';
bearbeiteId = null;
await ladeZeitraeume();
} else {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern';
if (!neuerBezeichnung.trim()) {
fehlermeldung = 'Bezeichnung ist erforderlich';
return;
}
}
async function loeschen(id: number) {
if (!confirm('Diese Zeitraum wirklich löschen?')) return;
await fetch(`/api/admin/zeitraeume?id=${id}`, { method: 'DELETE' });
await ladeZeitraeume();
}
if (!neuerstartDatum || !neuerendDatum) {
fehlermeldung = 'Start- und Enddatum sind erforderlich';
return;
}
onMount(ladeZeitraeume);
try {
const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId
? { id: bearbeiteId, bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum }
: { bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum };
</script>
const res = await fetch('/api/admin/zeitraeume', {
method,
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' }
});
<div class="p-6 max-w-6xl mx-auto space-y-8">
<h1 class="text-2xl font-bold text-center">Praktikumszeiträume verwalten</h1>
<!-- Eingabefelder -->
<div class="flex flex-wrap gap-4 items-center">
<input
bind:value={neuerBezeichnung}
placeholder="Bezeichnung"
class="input w-full sm:w-[35%] border rounded px-3 py-2"
/>
<input
type="date"
bind:value={neuerstartDatum}
placeholder="Startdatum"
class="input w-full sm:w-[20%] border rounded px-3 py-2"
/>
<input
type="date"
bind:value={neuerendDatum}
placeholder="Enddatum"
class="input w-full sm:w-[20%] border rounded px-3 py-2"
/>
<button
on:click={() => {
if (res.ok) {
neuerBezeichnung = '';
neuerstartDatum = '';
neuerendDatum = '';
bearbeiteId = null;
}}
class="text-sm text-gray-500 hover:underline"
>
Zurücksetzen
</button>
<button
on:click={speichern}
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
>
{bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
</button>
</div>
await ladeZeitraeume();
} else {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern';
}
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
}
}
<!-- Fehlermeldung -->
{#if fehlermeldung}
<p class="text-red-600 text-sm">{fehlermeldung}</p>
{/if}
async function loeschen(id: number) {
if (!confirm('Diesen Zeitraum wirklich löschen?')) return;
<!-- Tabellenähnliche Anzeige -->
<div class="overflow-x-auto">
<div class="min-w-[600px] divide-y border rounded">
<!-- Kopfzeile -->
<div class="grid grid-cols-4 gap-x-8 font-semibold text-sm bg-gray-100 p-2">
<div>Bezeichnung</div>
<div class="text-right">Startdatum</div>
<div class="text-right">Enddatum</div>
<div class="text-right">Aktionen</div>
</div>
try {
const res = await fetch(`/api/admin/zeitraeume?id=${id}`, { method: 'DELETE' });
<!-- Einträge -->
{#each zeitraeume as d}
<div class="grid grid-cols-4 gap-x-8 items-center p-2 text-sm">
<div>{d.bezeichnung}</div>
<div class="text-right">{new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</div>
<div class="text-right">{new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</div>
<div class="flex justify-end gap-3">
<button
on:click={() => {
neuerBezeichnung = d.bezeichnung;
//neuerstartDatum = d.startDatum;
neuerstartDatum = d.startDatum ? d.startDatum.slice(0, 10) : '';
neuerendDatum = d.endDatum ? d.endDatum.slice(0, 10) : '';
bearbeiteId = d.id;
}}
class="text-blue-600 hover:underline"
>
Bearbeiten
</button>
<button
on:click={() => loeschen(d.id)}
class="text-red-600 hover:underline"
>
Löschen
</button>
if (!res.ok) {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Löschen';
return;
}
await ladeZeitraeume();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Löschen';
console.error(err);
}
}
function resetForm() {
neuerBezeichnung = '';
neuerstartDatum = '';
neuerendDatum = '';
bearbeiteId = null;
}
onMount(ladeZeitraeume);
</script>
<svelte:head>
<title>Praktikumszeiträume verwalten - Admin</title>
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Praktikumszeiträume 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>
{/each}
</div>
{/if}
<!-- 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 ? 'Praktikumszeitraum bearbeiten' : 'Neuen Praktikumszeitraum hinzufügen'}
</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="bezeichnung" class="block text-sm font-medium text-gray-700 mb-2">
Bezeichnung
</label>
<input
id="bezeichnung"
type="text"
bind:value={neuerBezeichnung}
placeholder="z.B. Sommerpraktikum 2024"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label for="startdatum" class="block text-sm font-medium text-gray-700 mb-2">
Startdatum
</label>
<input
id="startdatum"
type="date"
bind:value={neuerstartDatum}
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label for="enddatum" class="block text-sm font-medium text-gray-700 mb-2">
Enddatum
</label>
<input
id="enddatum"
type="date"
bind:value={neuerendDatum}
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}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 hover:underline"
>
Zurücksetzen
</button>
<button
on:click={speichern}
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md text-sm font-medium"
>
{bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
</button>
</div>
</div>
</div>
</div>
<!-- Logout-Button -->
<div class="pt-4 text-center">
<button
on:click={async () => {
await fetch('/api/admin/logout', { method: 'POST' });
location.reload();
}}
class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded"
>
Logout
</button>
</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 Praktikumszeiträume...</span>
</div>
{:else if zeitraeume.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="M8 7V3a4 4 0 118 0v4m-4 6v6m-1 0h2m-1 0V9a4 4 0 00-8 0v2M7 9h2m-2 0v6" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Praktikumszeiträume</h3>
<p class="mt-1 text-sm text-gray-500">Erstellen Sie Ihren ersten Praktikumszeitraum über das Formular oben.</p>
</div>
{: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 Praktikumszeiträume</h3>
</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">
Bezeichnung
</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Startdatum
</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Enddatum
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each zeitraeume as d}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{d.bezeichnung}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
{new Date(d.startDatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
{new Date(d.endDatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => bearbeiten(d)}
class="text-blue-600 hover:text-blue-900 mr-4"
>
Bearbeiten
</button>
<button
on:click={() => loeschen(d.id)}
class="text-red-600 hover:text-red-900"
>
Löschen
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</main>
</div>
<style>
</style>

View File

@@ -6,8 +6,9 @@ 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_session') === 'true';
return cookies.get('admin-auth') === 'authenticated';
}
function isValidDate(date: string | Date) {
@@ -16,73 +17,144 @@ function isValidDate(date: string | Date) {
}
export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
const zeitraeume = await prisma.praktikumszeitraum.findMany();
return json(zeitraeume);
if (!checkAuth(cookies)) {
return new Response(
JSON.stringify({ error: 'Nicht autorisiert' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
}
try {
const zeitraeume = await prisma.praktikumszeitraum.findMany();
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 });
}
};
export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
const { bezeichnung, startDatum, endDatum } = await request.json();
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültige Datum' }, { status: 400 });
if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const created = await prisma.praktikumszeitraum.create({ data: {
bezeichnung,
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
} });
return json(created);
} catch (e) {
console.error('Fehler beim Hinzufuegen:', e);
return json({ error: 'Zeitraum existiert bereits' }, { status: 400 });
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 (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültiges Datum' }, { status: 400 });
}
const created = await prisma.praktikumszeitraum.create({
data: {
bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
}
});
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 });
}
};
export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
const { id, bezeichnung, startDatum, endDatum } = await request.json();
if (typeof id !== 'number' || isNaN(id) || !bezeichnung || !isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültige Eingabedaten' }, { status: 400 });
}
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
}
const konflikt = await prisma.praktikumszeitraum.findFirst({
where: {
bezeichnung,
NOT: { id },
},
});
if (konflikt) {
return json({ error: 'Eine andere Praktikumszeitraum mit diesem Namen existiert bereits' }, { status: 400 });
if (!checkAuth(cookies)) {
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 (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
return json({ error: 'Bezeichnung ist erforderlich' }, { 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 },
},
});
if (konflikt) {
return json({ error: 'Ein anderer Praktikumszeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
}
const updated = await prisma.praktikumszeitraum.update({
where: { id },
data: {
bezeichnung,
bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
},
});
return json(updated);
} catch (e) {
console.error('Fehler beim Update:', e);
return json({ error: 'Update fehlgeschlagen' }, { status: 400 });
} catch (error) {
console.error('Fehler beim Aktualisieren des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Aktualisieren des Praktikumszeitraums' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id'));
await prisma.praktikumszeitraum.delete({ where: { id } });
return json({ success: true });
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 });
}
// 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 });
// }
await prisma.praktikumszeitraum.delete({ where: { id } });
return json({ success: true, message: 'Praktikumszeitraum erfolgreich gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Löschen des Praktikumszeitraums' }, { status: 500 });
}
};