Admin Bereich an das neuen layout geaendert.
This commit is contained in:
@@ -1,82 +1,228 @@
|
|||||||
|
<!-- src/routes/admin/change-password/+page.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let oldPassword = '';
|
import AdminHeader from '$lib/components/AdminHeader.svelte';
|
||||||
let newPassword = '';
|
|
||||||
let confirmPassword = '';
|
|
||||||
let message = '';
|
|
||||||
let error = '';
|
|
||||||
|
|
||||||
async function changePassword() {
|
|
||||||
message = '';
|
|
||||||
error = '';
|
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
error = 'Die neuen Passwörter stimmen nicht überein.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let oldPassword = '';
|
||||||
|
let newPassword = '';
|
||||||
|
let confirmPassword = '';
|
||||||
|
let message = '';
|
||||||
|
let error = '';
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
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', {
|
const res = await fetch('/api/admin/change-password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({ oldPassword, newPassword })
|
body: JSON.stringify({ oldPassword, newPassword })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
|
||||||
|
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 = '';
|
||||||
<h2 class="text-2xl font-bold text-gray-800">🔐 Admin-Passwort ändern</h2>
|
newPassword = '';
|
||||||
|
confirmPassword = '';
|
||||||
<div class="space-y-4">
|
message = '';
|
||||||
<div>
|
error = '';
|
||||||
<label class="block text-sm font-medium text-gray-700">Altes Passwort</label>
|
}
|
||||||
<input
|
</script>
|
||||||
type="password"
|
|
||||||
bind:value={oldPassword}
|
<svelte:head>
|
||||||
class="mt-1 w-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
|
<title>Passwort ändern - Admin</title>
|
||||||
/>
|
</svelte:head>
|
||||||
</div>
|
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
<div>
|
<AdminHeader
|
||||||
<label class="block text-sm font-medium text-gray-700">Neues Passwort</label>
|
title="Admin-Passwort ändern"
|
||||||
<input
|
showBackButton={true}
|
||||||
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"
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
{#if error}
|
{#if error}
|
||||||
{#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}
|
||||||
|
|
||||||
{#if message}
|
{#if message}
|
||||||
{#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}
|
{/if}
|
||||||
{/if}
|
|
||||||
|
<!-- Passwort ändern Formular -->
|
||||||
<div class="pt-4">
|
<div class="max-w-2xl mx-auto">
|
||||||
<button
|
<div class="bg-white shadow-sm rounded-lg p-6">
|
||||||
on:click={changePassword}
|
<div class="flex items-center mb-6">
|
||||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-xl transition duration-150"
|
<div class="flex-shrink-0">
|
||||||
>
|
<svg class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
Passwort ändern
|
<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>
|
||||||
</div>
|
</main>
|
||||||
|
</div>
|
||||||
@@ -1,26 +1,49 @@
|
|||||||
|
<!-- src/routes/admin/dienststellen/+page.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
|
import AdminHeader from '$lib/components/AdminHeader.svelte';
|
||||||
let neuerName = '';
|
|
||||||
let neuePlaetze = 0;
|
|
||||||
let fehlermeldung = '';
|
|
||||||
let bearbeiteId: number | null = null;
|
|
||||||
|
|
||||||
async function ladeDienststellen() {
|
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
|
||||||
const res = await fetch('/api/admin/dienststellen');
|
let neuerName = '';
|
||||||
dienststellen = await res.json();
|
let neuePlaetze = 0;
|
||||||
}
|
let fehlermeldung = '';
|
||||||
|
let bearbeiteId: number | null = null;
|
||||||
|
let isLoading = true;
|
||||||
|
|
||||||
function bearbeiten(d: { id: number; name: string; plaetze: number }) {
|
async function ladeDienststellen() {
|
||||||
neuerName = d.name;
|
try {
|
||||||
neuePlaetze = d.plaetze;
|
isLoading = true;
|
||||||
bearbeiteId = d.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function speichern() {
|
|
||||||
fehlermeldung = '';
|
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 method = bearbeiteId ? 'PATCH' : 'POST';
|
||||||
const body = bearbeiteId
|
const body = bearbeiteId
|
||||||
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
|
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
|
||||||
@@ -33,118 +56,189 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
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 = '';
|
neuerName = '';
|
||||||
neuePlaetze = 0;
|
neuePlaetze = 0;
|
||||||
bearbeiteId = null;
|
bearbeiteId = null;
|
||||||
bearbeiteId = null;
|
await ladeDienststellen();
|
||||||
}}
|
} else {
|
||||||
class="text-sm text-gray-500 hover:underline"
|
const err = await res.json();
|
||||||
>
|
fehlermeldung = err.error || 'Fehler beim Speichern';
|
||||||
Zurücksetzen
|
}
|
||||||
</button>
|
} catch (err) {
|
||||||
<button
|
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
|
||||||
on:click={speichern}
|
console.error(err);
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
}
|
||||||
>
|
}
|
||||||
{bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
async function loeschen(id: number) {
|
||||||
<!-- Fehlermeldung -->
|
if (!confirm('Diese Dienststelle wirklich löschen?')) return;
|
||||||
{#if fehlermeldung}
|
|
||||||
<p class="text-red-600 text-sm">{fehlermeldung}</p>
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ladeDienststellen();
|
||||||
|
} catch (err) {
|
||||||
|
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Löschen';
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
<!-- Tabellenähnliche Anzeige -->
|
neuerName = '';
|
||||||
<div class="overflow-x-auto">
|
neuePlaetze = 0;
|
||||||
<div class="min-w-[600px] divide-y border rounded">
|
bearbeiteId = null;
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
|
onMount(ladeDienststellen);
|
||||||
<!-- Einträge -->
|
</script>
|
||||||
{#each dienststellen as d}
|
|
||||||
<div class="grid grid-cols-3 gap-x-8 items-center p-2 text-sm">
|
<svelte:head>
|
||||||
<div>{d.name}</div>
|
<title>Dienststellen verwalten - Admin</title>
|
||||||
<div class="text-right">{d.plaetze}</div>
|
</svelte:head>
|
||||||
<div class="flex justify-end gap-3">
|
|
||||||
<button
|
<div class="min-h-screen bg-gray-50">
|
||||||
on:click={() => {
|
<AdminHeader
|
||||||
neuerName = d.name;
|
title="Dienststellen verwalten"
|
||||||
neuePlaetze = d.plaetze;
|
showBackButton={true}
|
||||||
bearbeiteId = d.id;
|
/>
|
||||||
}}
|
|
||||||
class="text-blue-600 hover:underline"
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||||
>
|
{#if fehlermeldung}
|
||||||
Bearbeiten
|
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||||
</button>
|
<div class="flex">
|
||||||
<button
|
<div class="flex-shrink-0">
|
||||||
on:click={() => loeschen(d.id)}
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
class="text-red-600 hover:underline"
|
<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>
|
||||||
Löschen
|
</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>
|
||||||
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
<!-- Logout-Button -->
|
<div class="flex justify-center items-center h-64">
|
||||||
<div class="pt-4 text-center">
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
<button
|
<span class="ml-3 text-gray-600">Lade Dienststellen...</span>
|
||||||
on:click={async () => {
|
</div>
|
||||||
await fetch('/api/admin/logout', { method: 'POST' });
|
{:else if dienststellen.length === 0}
|
||||||
location.reload();
|
<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">
|
||||||
class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded"
|
<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>
|
||||||
Logout
|
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Dienststellen</h3>
|
||||||
</button>
|
<p class="mt-1 text-sm text-gray-500">Erstellen Sie Ihre erste Dienststelle über das Formular oben.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
<style>
|
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen</h3>
|
||||||
/* You can add custom styles here if needed, or rely on Tailwind classes in your markup */
|
</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>
|
||||||
@@ -1,175 +1,284 @@
|
|||||||
|
<!-- 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() {
|
||||||
const res = await fetch('/api/admin/zeitraeume');
|
try {
|
||||||
zeitraeume = await res.json();
|
isLoading = true;
|
||||||
|
fehlermeldung = '';
|
||||||
|
|
||||||
|
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: Date; endDatum: Date }) {
|
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';
|
||||||
const method = bearbeiteId ? 'PATCH' : 'POST';
|
return;
|
||||||
const body = bearbeiteId
|
}
|
||||||
? { id: bearbeiteId, bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum }
|
|
||||||
: { bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum };
|
if (!neuerstartDatum || !neuerendDatum) {
|
||||||
|
fehlermeldung = 'Start- und Enddatum sind erforderlich';
|
||||||
const res = await fetch('/api/admin/zeitraeume', {
|
return;
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function loeschen(id: number) {
|
|
||||||
if (!confirm('Diese Zeitraum wirklich löschen?')) return;
|
|
||||||
await fetch(`/api/admin/zeitraeume?id=${id}`, { method: 'DELETE' });
|
|
||||||
await ladeZeitraeume();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(ladeZeitraeume);
|
|
||||||
|
|
||||||
</script>
|
try {
|
||||||
|
const method = bearbeiteId ? 'PATCH' : 'POST';
|
||||||
|
const body = bearbeiteId
|
||||||
|
? { id: bearbeiteId, bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum }
|
||||||
|
: { bezeichnung: neuerBezeichnung, startDatum: neuerstartDatum, endDatum: neuerendDatum };
|
||||||
|
|
||||||
<div class="p-6 max-w-6xl mx-auto space-y-8">
|
const res = await fetch('/api/admin/zeitraeume', {
|
||||||
<h1 class="text-2xl font-bold text-center">Praktikumszeiträume verwalten</h1>
|
method,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
<!-- Eingabefelder -->
|
if (res.ok) {
|
||||||
<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;
|
||||||
}}
|
await ladeZeitraeume();
|
||||||
class="text-sm text-gray-500 hover:underline"
|
} else {
|
||||||
>
|
const err = await res.json();
|
||||||
Zurücksetzen
|
fehlermeldung = err.error || 'Fehler beim Speichern';
|
||||||
</button>
|
}
|
||||||
<button
|
} catch (err) {
|
||||||
on:click={speichern}
|
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
console.error(err);
|
||||||
>
|
}
|
||||||
{bearbeiteId !== null ? 'Ändern' : 'Hinzufügen'}
|
}
|
||||||
</button>
|
|
||||||
</div>
|
async function loeschen(id: number) {
|
||||||
|
if (!confirm('Diesen Zeitraum wirklich löschen?')) return;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ladeZeitraeume();
|
||||||
|
} catch (err) {
|
||||||
|
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Löschen';
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Fehlermeldung -->
|
function resetForm() {
|
||||||
{#if fehlermeldung}
|
neuerBezeichnung = '';
|
||||||
<p class="text-red-600 text-sm">{fehlermeldung}</p>
|
neuerstartDatum = '';
|
||||||
{/if}
|
neuerendDatum = '';
|
||||||
|
bearbeiteId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(ladeZeitraeume);
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Tabellenähnliche Anzeige -->
|
<svelte:head>
|
||||||
<div class="overflow-x-auto">
|
<title>Praktikumszeiträume verwalten - Admin</title>
|
||||||
<div class="min-w-[600px] divide-y border rounded">
|
</svelte:head>
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- Einträge -->
|
<div class="min-h-screen bg-gray-50">
|
||||||
{#each zeitraeume as d}
|
<AdminHeader
|
||||||
<div class="grid grid-cols-4 gap-x-8 items-center p-2 text-sm">
|
title="Praktikumszeiträume verwalten"
|
||||||
<div>{d.bezeichnung}</div>
|
showBackButton={true}
|
||||||
<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">
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||||
<button
|
{#if fehlermeldung}
|
||||||
on:click={() => {
|
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||||
neuerBezeichnung = d.bezeichnung;
|
<div class="flex">
|
||||||
//neuerstartDatum = d.startDatum;
|
<div class="flex-shrink-0">
|
||||||
neuerstartDatum = d.startDatum ? d.startDatum.slice(0, 10) : '';
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
neuerendDatum = d.endDatum ? d.endDatum.slice(0, 10) : '';
|
<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" />
|
||||||
bearbeiteId = d.id;
|
</svg>
|
||||||
}}
|
</div>
|
||||||
class="text-blue-600 hover:underline"
|
<div class="ml-3">
|
||||||
>
|
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
|
||||||
Bearbeiten
|
<p class="mt-1 text-sm text-red-700">{fehlermeldung}</p>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
on:click={() => loeschen(d.id)}
|
|
||||||
class="text-red-600 hover:underline"
|
|
||||||
>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Logout-Button -->
|
{#if isLoading}
|
||||||
<div class="pt-4 text-center">
|
<div class="flex justify-center items-center h-64">
|
||||||
<button
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
on:click={async () => {
|
<span class="ml-3 text-gray-600">Lade Praktikumszeiträume...</span>
|
||||||
await fetch('/api/admin/logout', { method: 'POST' });
|
</div>
|
||||||
location.reload();
|
{:else if zeitraeume.length === 0}
|
||||||
}}
|
<div class="text-center py-12">
|
||||||
class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded"
|
<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" />
|
||||||
Logout
|
</svg>
|
||||||
</button>
|
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Praktikumszeiträume</h3>
|
||||||
</div>
|
<p class="mt-1 text-sm text-gray-500">Erstellen Sie Ihren ersten Praktikumszeitraum über das Formular oben.</p>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
<style>
|
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
</style>
|
<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>
|
||||||
@@ -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)) {
|
||||||
const zeitraeume = await prisma.praktikumszeitraum.findMany();
|
return new Response(
|
||||||
return json(zeitraeume);
|
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 }) => {
|
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,
|
|
||||||
startDatum: new Date(startDatum),
|
// Validierung
|
||||||
endDatum: new Date(endDatum)
|
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
|
||||||
} });
|
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
|
||||||
return json(created);
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Hinzufuegen:', e);
|
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
|
||||||
return json({ error: 'Zeitraum existiert bereits' }, { status: 400 });
|
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 }) => {
|
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'));
|
||||||
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 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user