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,19 +1,42 @@
<!-- src/routes/admin/change-password/+page.svelte -->
<script lang="ts"> <script lang="ts">
import AdminHeader from '$lib/components/AdminHeader.svelte';
let oldPassword = ''; let oldPassword = '';
let newPassword = ''; let newPassword = '';
let confirmPassword = ''; let confirmPassword = '';
let message = ''; let message = '';
let error = ''; let error = '';
let isLoading = false;
async function changePassword() { async function changePassword() {
message = ''; message = '';
error = ''; 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) { if (newPassword !== confirmPassword) {
error = 'Die neuen Passwörter stimmen nicht überein.'; error = 'Die neuen Passwörter stimmen nicht überein.';
return; return;
} }
try {
isLoading = true;
const res = await fetch('/api/admin/change-password', { const res = await fetch('/api/admin/change-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -26,57 +49,180 @@
if (!res.ok) { if (!res.ok) {
error = data.error || 'Fehler beim Ändern des Passworts.'; error = data.error || 'Fehler beim Ändern des Passworts.';
} else { } else {
message = 'Passwort erfolgreich geändert.'; message = 'Passwort erfolgreich geändert.';
oldPassword = newPassword = confirmPassword = ''; 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;
} }
} }
function resetForm() {
<div class="max-w-lg mx-auto bg-white p-6 rounded-2xl shadow-md space-y-6 border border-gray-200"> oldPassword = '';
newPassword = '';
confirmPassword = '';
message = '';
error = '';
}
</script>
<svelte:head>
<title>Passwort ändern - Admin</title>
</svelte:head>
<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}
<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}
{#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> <div>
<div> <label for="old-password" class="block text-sm font-medium text-gray-700 mb-2">
Aktuelles Passwort
</label>
<input <input
id="old-password"
type="password" type="password"
bind:value={oldPassword} bind:value={oldPassword}
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>
<div> <div>
<div> <label for="new-password" class="block text-sm font-medium text-gray-700 mb-2">
Neues Passwort
</label>
<input <input
id="new-password"
type="password" type="password"
bind:value={newPassword} bind:value={newPassword}
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>
<div> <div>
<div> <label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-2">
Neues Passwort wiederholen
</label>
<input <input
id="confirm-password"
type="password" type="password"
bind:value={confirmPassword} bind:value={confirmPassword}
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>
</div>
<div class="flex items-center justify-between pt-4">
{#if error}
<div class="text-red-600 text-sm font-medium">{error}</div>
{/if}
{#if message}
<div class="text-green-600 text-sm font-medium">{message}</div>
{/if}
<button <button
<button type="button"
on:click={changePassword} 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 Passwort ändern
{/if}
</button> </button>
</div> </div>
</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>
</main>
</div>

View File

@@ -1,14 +1,33 @@
<!-- src/routes/admin/dienststellen/+page.svelte -->
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; 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; plaetze: number }[] = [];
let neuerName = ''; let neuerName = '';
let neuePlaetze = 0; let neuePlaetze = 0;
let fehlermeldung = ''; let fehlermeldung = '';
let bearbeiteId: number | null = null; let bearbeiteId: number | null = null;
let isLoading = true;
async function ladeDienststellen() { async function ladeDienststellen() {
try {
isLoading = true;
fehlermeldung = '';
const res = await fetch('/api/admin/dienststellen'); const res = await fetch('/api/admin/dienststellen');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
}
dienststellen = await res.json(); 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 }) { function bearbeiten(d: { id: number; name: string; plaetze: number }) {
@@ -19,8 +38,12 @@
async function speichern() { async function speichern() {
fehlermeldung = ''; fehlermeldung = '';
if (!neuerName.trim()) return; if (!neuerName.trim()) {
fehlermeldung = 'Name ist erforderlich';
return;
}
try {
const method = bearbeiteId ? 'PATCH' : 'POST'; const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId const body = bearbeiteId
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze } ? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
@@ -41,110 +64,181 @@
const err = await res.json(); const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern'; fehlermeldung = err.error || 'Fehler beim Speichern';
} }
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
}
} }
async function loeschen(id: number) { async function loeschen(id: number) {
if (!confirm('Diese Dienststelle wirklich löschen?')) return; if (!confirm('Diese Dienststelle wirklich löschen?')) return;
await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
await ladeDienststellen(); try {
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
if (!res.ok) {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Löschen';
return;
} }
onMount(ladeDienststellen); await ladeDienststellen();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Löschen';
console.error(err);
}
}
function resetForm() {
<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 = ''; neuerName = '';
neuePlaetze = 0; neuePlaetze = 0;
bearbeiteId = null; bearbeiteId = null;
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>
{/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 Zurücksetzen
</button> </button>
<button <button
on:click={speichern} on:click={speichern}
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'} {bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
</button> </button>
</div> </div>
</div> </div>
<!-- Fehlermeldung -->
{#if fehlermeldung}
<p class="text-red-600 text-sm">{fehlermeldung}</p>
{/if}
<!-- 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> </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 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} {#each dienststellen as d}
{#each dienststellen as d} <tr class="hover:bg-gray-50">
<div class="grid grid-cols-3 gap-x-8 items-center p-2 text-sm"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div>{d.name}</div> {d.name}
<div class="text-right">{d.plaetze}</div> </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 <button
<button on:click={() => bearbeiten(d)}
on:click={() => { class="text-blue-600 hover:text-blue-900 mr-4"
neuerName = d.name;
neuePlaetze = d.plaetze;
bearbeiteId = d.id;
}}
> >
Bearbeiten Bearbeiten
</button> </button>
<button <button
on:click={() => loeschen(d.id)} on:click={() => loeschen(d.id)}
on:click={() => loeschen(d.id)} class="text-red-600 hover:text-red-900"
> >
Löschen Löschen
</button> </button>
</button> </td>
</div> </tr>
{/each} {/each}
</tbody>
</table>
</div> </div>
</div> </div>
</div> {/if}
</main>
<!-- 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> </div>
</div>
<style>
/* You can add custom styles here if needed, or rely on Tailwind classes in your markup */

View File

@@ -1,41 +1,63 @@
<!-- src/routes/admin/zeitraeume/+page.svelte -->
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte';
let bezeichnung = ''; import AdminHeader from '$lib/components/AdminHeader.svelte';
let startDatum = '';
let endDatum = '';
interface Zeitraum { interface Zeitraum {
id: number; id: number;
bezeichnung: string; bezeichnung: string;
startDatum: string; startDatum: string;
endDatum: string; endDatum: string;
} }
let zeitraeume: Zeitraum[] = []; let zeitraeume: Zeitraum[] = [];
let neuerBezeichnung = ''; let neuerBezeichnung = '';
let neuerstartDatum = ''; let neuerstartDatum = '';
let neuerendDatum = ''; let neuerendDatum = '';
let fehlermeldung = ''; let fehlermeldung = '';
let bearbeiteId: number | null = null; let bearbeiteId: number | null = null;
let isLoading = true;
async function ladeZeitraeume() { async function ladeZeitraeume() {
try {
isLoading = true;
fehlermeldung = '';
const res = await fetch('/api/admin/zeitraeume'); const res = await fetch('/api/admin/zeitraeume');
zeitraeume = await res.json();
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
} }
function bearbeiten(d: { id: number; bezeichnung: string; startDatum: Date; endDatum: Date }) { 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; neuerBezeichnung = d.bezeichnung;
neuerstartDatum = d.startDatum instanceof Date neuerstartDatum = d.startDatum ? d.startDatum.slice(0, 10) : '';
? d.startDatum.toISOString().slice(0, 10) neuerendDatum = d.endDatum ? d.endDatum.slice(0, 10) : '';
: d.startDatum;
neuerendDatum = d.endDatum instanceof Date
? d.endDatum.toISOString().slice(0, 10)
: d.endDatum;
bearbeiteId = d.id; bearbeiteId = d.id;
} }
async function speichern() { async function speichern() {
fehlermeldung = ''; fehlermeldung = '';
if (!neuerBezeichnung.trim()) return; if (!neuerBezeichnung.trim()) {
fehlermeldung = 'Bezeichnung ist erforderlich';
return;
}
if (!neuerstartDatum || !neuerendDatum) {
fehlermeldung = 'Start- und Enddatum sind erforderlich';
return;
}
try {
const method = bearbeiteId ? 'PATCH' : 'POST'; const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId const body = bearbeiteId
? { id: bearbeiteId, bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum } ? { id: bearbeiteId, bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum }
@@ -57,119 +79,206 @@
const err = await res.json(); const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern'; fehlermeldung = err.error || 'Fehler beim Speichern';
} }
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
}
} }
async function loeschen(id: number) { async function loeschen(id: number) {
if (!confirm('Diese Zeitraum wirklich löschen?')) return; if (!confirm('Diesen Zeitraum wirklich löschen?')) return;
await fetch(`/api/admin/zeitraeume?id=${id}`, { method: 'DELETE' });
await ladeZeitraeume(); try {
const res = await fetch(`/api/admin/zeitraeume?id=${id}`, { method: 'DELETE' });
if (!res.ok) {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Löschen';
return;
} }
onMount(ladeZeitraeume); await ladeZeitraeume();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Löschen';
console.error(err);
}
}
</script> function resetForm() {
<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={() => {
neuerBezeichnung = ''; neuerBezeichnung = '';
neuerstartDatum = ''; neuerstartDatum = '';
neuerendDatum = ''; neuerendDatum = '';
bearbeiteId = null; bearbeiteId = null;
}} }
class="text-sm text-gray-500 hover:underline"
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>
</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 Zurücksetzen
</button> </button>
<button <button
on:click={speichern} on:click={speichern}
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm" 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'} {bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
</button> </button>
</div> </div>
</div>
<!-- Fehlermeldung -->
{#if fehlermeldung}
<p class="text-red-600 text-sm">{fehlermeldung}</p>
{/if}
<!-- 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> </div>
<!-- Einträge --> {#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} {#each zeitraeume as d}
<div class="grid grid-cols-4 gap-x-8 items-center p-2 text-sm"> <tr class="hover:bg-gray-50">
<div>{d.bezeichnung}</div> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="text-right">{new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</div> {d.bezeichnung}
<div class="text-right">{new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</div> </td>
<div class="flex justify-end gap-3"> <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 <button
on:click={() => { on:click={() => bearbeiten(d)}
neuerBezeichnung = d.bezeichnung; class="text-blue-600 hover:text-blue-900 mr-4"
//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 Bearbeiten
</button> </button>
<button <button
on:click={() => loeschen(d.id)} on:click={() => loeschen(d.id)}
class="text-red-600 hover:underline" class="text-red-600 hover:text-red-900"
> >
Löschen Löschen
</button> </button>
</div> </td>
</div> </tr>
{/each} {/each}
</tbody>
</table>
</div> </div>
</div> </div>
{/if}
<!-- Logout-Button --> </main>
<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>
</div> </div>
<style>
</style>

View File

@@ -6,8 +6,9 @@ const prisma = new PrismaClient();
import type { Cookies } from '@sveltejs/kit'; import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) { function checkAuth(cookies: Cookies) {
return cookies.get('admin_session') === 'true'; return cookies.get('admin-auth') === 'authenticated';
} }
function isValidDate(date: string | Date) { function isValidDate(date: string | Date) {
@@ -16,73 +17,144 @@ function isValidDate(date: string | Date) {
} }
export const GET: RequestHandler = async ({ cookies }) => { export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); 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(); const zeitraeume = await prisma.praktikumszeitraum.findMany();
return json(zeitraeume); 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 }) => { export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
const { bezeichnung, startDatum, endDatum } = await request.json(); return json({ error: 'Nicht autorisiert' }, { status: 401 });
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültige Datum' }, { status: 400 });
} }
try { try {
const created = await prisma.praktikumszeitraum.create({ data: { const { bezeichnung, startDatum, endDatum } = await request.json();
bezeichnung,
// 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), startDatum: new Date(startDatum),
endDatum: new Date(endDatum) endDatum: new Date(endDatum)
} }); }
return json(created); });
} catch (e) { return json(created, { status: 201 });
console.error('Fehler beim Hinzufuegen:', e); } catch (error) {
return json({ error: 'Zeitraum existiert bereits' }, { status: 400 }); 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 }) => { export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { 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 });
} }
try { 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({ const updated = await prisma.praktikumszeitraum.update({
where: { id }, where: { id },
data: { data: {
bezeichnung, bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum), startDatum: new Date(startDatum),
endDatum: new Date(endDatum) endDatum: new Date(endDatum)
}, },
}); });
return json(updated); return json(updated);
} catch (e) { } catch (error) {
console.error('Fehler beim Update:', e); console.error('Fehler beim Aktualisieren des Praktikumszeitraums:', error);
return json({ error: 'Update fehlgeschlagen' }, { status: 400 }); return json({ error: 'Fehler beim Aktualisieren des Praktikumszeitraums' }, { status: 500 });
} }
}; };
export const DELETE: RequestHandler = async ({ cookies, url }) => { 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')); 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 });
}
// 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 } }); await prisma.praktikumszeitraum.delete({ where: { id } });
return json({ success: true }); 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 });
}
}; };