Seite umbauen mit Komponenten und admin-auth

This commit is contained in:
titver968
2025-07-23 08:52:53 +02:00
parent 6fb118ac5b
commit 5516acb840
10 changed files with 757 additions and 209 deletions

View File

@@ -26,13 +26,14 @@
let dienststellen: any[];
let fileInputKey = 0;
let pdfDatein = [];
let noteDeutsch = '';
let noteMathe = '';
let sozialverhalten = '';
let schulklasse = '';
let ablehnungHinweis = '';
let showAblehnungModal = false;
let alter = '';
let startDatum = '';
$: hideSozialVerhalten =
Number(schulklasse) >= 11 &&
@@ -114,7 +115,7 @@
const deutsch = parseInt(noteDeutsch);
const mathe = parseInt(noteMathe);
if (['Gymnasium', 'Gymnasialzweig'].includes(schulart) ) {
if (['Gymnasium', 'KGS_Gymnasialzweig', 'IGS_Gymnasialzweig'].includes(schulart) ) {
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 4 && mathe > 4) {
ablehnungHinweis = 'Du brauchst mindestens eine 4 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
@@ -216,9 +217,14 @@
<select bind:value={zeitraum} required class="input">
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
{#each zeitraeume ?? [] as d}
<option value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})</option>
<option
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
</option>
startDatum = {new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
{/each}
</select>
</select>
<p>Startdatum: {startDatum}</p>
</div>
<!-- Wunschdienststellen -->
@@ -257,7 +263,7 @@
<!-- Mehrere PDF Upload -->
{#key fileInputKey}
<div>
<label for="pdf-upload" class="block text-gray-700 font-medium mb-1">PDFs hochladen (optional):</label>
<label for="pdf-upload" class="block text-gray-700 font-medium mb-1">Zeugnis hochladen:</label>
<input
id="pdf-upload"
type="file"

View File

@@ -1,65 +1,67 @@
<!-- src/routes/admin/+page.svelte -->
<script lang="ts">
let passwort = '';
let eingeloggt = false;
let fehler = false;
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import LoginForm from '$lib/components/LoginForm.svelte';
import AdminNavigation from '$lib/components/AdminNavigation.svelte';
let isAuthenticated = false;
let isLoading = true;
async function login() {
const res = await fetch('/api/admin/login', {
method: 'POST',
body: JSON.stringify({ passwort }),
headers: { 'Content-Type': 'application/json' }
});
onMount(async () => {
// Prüfe ob bereits eingeloggt
try {
const res = await fetch('/api/admin/check-auth');
isAuthenticated = res.ok;
} catch (error) {
console.error('Auth check failed:', error);
isAuthenticated = false;
}
isLoading = false;
});
if (res.ok) {
eingeloggt = true;
fehler = false;
} else {
fehler = true;
async function handleLogin(event: CustomEvent<{success: boolean}>) {
if (event.detail.success) {
isAuthenticated = true;
}
}
async function handleLogout() {
try {
await fetch('/api/admin/logout', { method: 'POST' });
isAuthenticated = false;
goto('/admin');
} catch (error) {
console.error('Logout failed:', error);
}
}
</script>
<div class="p-6 max-w-lg mx-auto">
{#if !eingeloggt}
<div class="space-y-4">
<h1 class="text-2xl font-bold">Admin Login</h1>
<input type="password" bind:value={passwort} placeholder="Passwort" class="input w-full" />
<button on:click={login} class="bg-blue-600 text-white px-4 py-2 rounded">Login</button>
{#if fehler}
<p class="text-red-600">Falsches Passwort</p>
{/if}
</div>
{:else}
<div class="space-y-4">
<h1 class="text-2xl font-bold mb-4">Admin-Bereich</h1>
<div class="flex flex-col gap-4">
<a href="/admin/anmeldungen" class="bg-blue-600 text-white px-4 py-3 rounded text-center hover:bg-blue-800">
📝 Anmeldungen anzeigen
</a>
<a href="/admin/dienststellen" class="bg-green-600 text-white px-4 py-3 rounded text-center hover:bg-green-800">
🏢 Dienststellen verwalten
</a>
<a href="/admin/zeitraeume" class="bg-yellow-600 text-white px-4 py-3 rounded text-center hover:bg-yellow-800">
🗓 Praktikumzeiträume verwalten
</a>
<a href="/admin/change-password" class="bg-cyan-600 text-white px-4 py-3 rounded text-center hover:bg-cyan-800">
👨‍💼 Passwort ädern
</a>
<div class="min-h-screen bg-gray-50 py-8">
<div class="max-w-2xl mx-auto px-4">
{#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>
</div>
<button
on:click={async () => {
await fetch('/api/admin/logout', { method: 'POST' });
location.reload();
}}
class="bg-red-600 text-white px-4 py-3 rounded text-center hiver:bg-red-700">
{:else if !isAuthenticated}
<div class="bg-white rounded-lg shadow-md p-6">
<h1 class="text-3xl font-bold text-gray-900 mb-6 text-center">Admin Login</h1>
<LoginForm on:loginResult={handleLogin} />
</div>
{:else}
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900">Admin-Bereich</h1>
<button
on:click={handleLogout}
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md transition-colors"
>
Logout
</button>
</button>
</div>
<AdminNavigation />
</div>
{/if}
</div>
<style>
.input {
@apply border rounded px-3 py-2 w-full;
}
</style>
{/if}
</div>
</div>

View File

@@ -1,8 +1,16 @@
// src/routes/admin/anmeldungen/+page.server.ts
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies }) => {
if (cookies.get('admin_session') !== 'true') {
// Korrigiere Cookie-Name um konsistent zu sein
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
throw redirect(303, '/admin');
}
return {
title: 'Anmeldungen verwalten'
};
};

View File

@@ -1,7 +1,13 @@
<!-- src/routes/admin/anmeldungen/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import AnmeldungenTable from '$lib/components/AnmeldungenTable.svelte';
import DienststellenDialog from '$lib/components/DienststellenDialog.svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
interface Anmeldung {
pdfs: any;
pdfs: { pfad: string }[];
anrede: string;
vorname: string;
nachname: string;
@@ -17,34 +23,58 @@
}
let anmeldungen: Anmeldung[] = [];
let auswahlDialogOffen = false;
let auswahlAnmeldungId: number | null = null;
let ausgewaehlteDienststelleId: number | null = null;
let aktuelleWuensche: { id: number, name: string }[] = [];
let isLoading = true;
let error = '';
// Dialog state
let showDialog = false;
let selectedAnmeldungId: number | null = null;
let selectedDienststelleId: number | null = null;
let availableWishes: { id: number, name: string }[] = [];
function oeffneAuswahl(id: number) {
const anmeldung = anmeldungen.find(a => a.id === id);
async function loadAnmeldungen() {
try {
isLoading = true;
error = '';
const res = await fetch('/api/admin/anmeldungen');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
}
anmeldungen = await res.json();
} catch (err) {
error = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Anmeldungen:', err);
} finally {
isLoading = false;
}
}
function handleAccept(event: CustomEvent<{id: number}>) {
const anmeldung = anmeldungen.find(a => a.id === event.detail.id);
if (!anmeldung) return;
aktuelleWuensche = [
availableWishes = [
anmeldung.wunsch1 && { id: anmeldung.wunsch1.id, name: `1. Wunsch: ${anmeldung.wunsch1.name}` },
anmeldung.wunsch2 && { id: anmeldung.wunsch2.id, name: `2. Wunsch: ${anmeldung.wunsch2.name}` },
anmeldung.wunsch3 && { id: anmeldung.wunsch3.id, name: `3. Wunsch: ${anmeldung.wunsch3.name}` }
].filter(Boolean) as { id: number, name: string }[];
ausgewaehlteDienststelleId = aktuelleWuensche[0]?.id ?? null;
auswahlAnmeldungId = id;
auswahlDialogOffen = true;
}
async function bestaetigeAnnahme() {
if (!auswahlAnmeldungId || !ausgewaehlteDienststelleId) return;
selectedDienststelleId = availableWishes[0]?.id ?? null;
selectedAnmeldungId = event.detail.id;
showDialog = true;
}
async function handleConfirmAccept(event: CustomEvent<{dienststelleId: number}>) {
if (!selectedAnmeldungId) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${auswahlAnmeldungId}`, {
const res = await fetch(`/api/admin/anmeldungen?id=${selectedAnmeldungId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dienststelleId: ausgewaehlteDienststelleId })
body: JSON.stringify({ dienststelleId: event.detail.dienststelleId })
});
if (!res.ok) {
@@ -52,154 +82,121 @@
throw new Error(`Fehler beim Annehmen (${res.status}): ${errorText}`);
}
auswahlDialogOffen = false;
auswahlAnmeldungId = null;
await ladeAnmeldungen();
} catch (error) {
console.error(error);
alert('Fehler beim Annehmen der Anmeldung.\n' + (error as Error).message);
showDialog = false;
selectedAnmeldungId = null;
await loadAnmeldungen();
} catch (err) {
error = err instanceof Error ? err.message : 'Fehler beim Annehmen';
console.error(err);
}
}
async function ladeAnmeldungen() {
const res = await fetch('/api/admin/anmeldungen');
anmeldungen = await res.json();
async function handleReject(event: CustomEvent<{id: number}>) {
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'reject' })
});
if (!res.ok) {
throw new Error(`Fehler beim Ablehnen: ${res.status}`);
}
await loadAnmeldungen();
} catch (err) {
error = err instanceof Error ? err.message : 'Fehler beim Ablehnen';
console.error(err);
}
}
async function loeschen(id: number) {
async function handleDelete(event: CustomEvent<{id: number}>) {
if (!confirm('Diese Anmeldung wirklich löschen?')) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${id}`, { method: 'DELETE' });
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
method: 'DELETE'
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Fehler beim Löschen (${res.status}): ${errorText}`);
throw new Error(`Fehler beim Löschen: ${res.status}`);
}
await ladeAnmeldungen();
} catch (error) {
console.error(error);
alert('Fehler beim Löschen der Anmeldung.\n' + (error as Error).message);
await loadAnmeldungen();
} catch (err) {
error = err instanceof Error ? err.message : 'Fehler beim Löschen';
console.error(err);
}
}
async function annehmen(id: number) {
if (!confirm('Diese Anmeldung wirklich annehmen?')) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${id}`, { method: 'POST' });
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Fehler beim Annehmen (${res.status}): ${errorText}`);
}
await ladeAnmeldungen();
} catch (error) {
console.error(error);
alert('Fehler beim Annehmen der Anmeldung.\n' + (error as Error).message);
}
function closeDialog() {
showDialog = false;
selectedAnmeldungId = null;
}
async function ablehnen(id: number) {
if (!confirm('Diese Anmeldung wirklich annehmen?')) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${id}`, { method: 'POST' });
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Fehler beim Annehmen (${res.status}): ${errorText}`);
}
await ladeAnmeldungen();
} catch (error) {
console.error(error);
alert('Fehler beim Annehmen der Anmeldung.\n' + (error as Error).message);
}
}
onMount(loadAnmeldungen);
</script>
onMount(ladeAnmeldungen);
</script>
<svelte:head>
<title>Anmeldungen verwalten - Admin</title>
</svelte:head>
<div class="p-6 max-w-8xl mx-auto">
<h1 class="text-2xl font-bold mb-4 text-center">Alle Anmeldungen</h1>
<table class="w-full border text-sm">
<thead>
<tr class="bg-gray-200">
<th class="p-2 text-left">Name</th>
<th class="p-2 text-left">E-Mail</th>
<th class="p-2 text-left">Wunsch Dienststelle</th>
<th class="p-2 text-left">Note Deutsch</th>
<th class="p-2 text-left">Note Mathe</th>
<th class="p-2 text-left">Sozialverhalten</th>
<th class="p-2 text-left">Datum</th>
<th class="p-2 text-left">Dateien</th>
<th class="p-2 text-left">Aktionen</th>
</tr>
</thead>
<tbody>
{#each anmeldungen as a}
<tr class="border-t">
<td class="p-2">{a.anrede} {a.vorname} {a.nachname}</td>
<td class="p-2">{a.email}</td>
<td class="p-2">
1: {a.wunsch1?.name}<br><br>
2: {a.wunsch2?.name}<br><br>
3: {a.wunsch3?.name}
</td>
<td class="p-2">{a.noteDeutsch || '—'}</td>
<td class="p-2">{a.noteMathe || '—'}</td>
<td class="p-2">{a.sozialverhalten || '—'}</td>
<td class="p-2">{new Date(a.timestamp).toLocaleDateString()}</td>
<td class="p-2">
{#each a.pdfs as pdf}
<li>
<a href={pdf.pfad} target="_blank" class="text-blue-600 hover:underline">
PDF ansehen
</a>
</li>
{/each}
</td>
<td class="p-2 text-right">
<button
class="text-green-600 hover:underline"
on:click={() => oeffneAuswahl(a.id)}>
Annehmen
</button><br><br>
<button
class="text-blue-600 hover:underline"
on:click={() => ablehnen(a.id)}>
Ablehnen
</button><br><br>
<button
class="text-red-600 hover:underline"
on:click={() => loeschen(a.id)}>
Löschen
</button>
</td>
</tr>
{/each}
</tbody>
</table>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Anmeldungen verwalten"
showBackButton={true}
/>
<button
on:click={async () => {
await fetch('/api/admin/logout', { method: 'POST' });
location.reload();
}}
class="bg-red-600 text-white px-4 py-3 rounded text-center hover:bg-red-700">
Logout
</button>
<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 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 Anmeldungen...</span>
</div>
{:else if anmeldungen.length === 0}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Anmeldungen</h3>
<p class="mt-1 text-sm text-gray-500">Es sind noch keine Praktikumsanmeldungen eingegangen.</p>
</div>
{:else}
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<AnmeldungenTable
{anmeldungen}
on:accept={handleAccept}
on:reject={handleReject}
on:delete={handleDelete}
/>
</div>
{/if}
</main>
</div>
{#if auswahlDialogOffen}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow max-w-sm w-full space-y-4">
<h2 class="text-lg font-bold">Dienststelle auswählen</h2>
<select bind:value={ausgewaehlteDienststelleId} class="w-full border p-2 rounded">
{#each aktuelleWuensche as w}
<option value={w.id}>{w.name}</option>
{/each}
</select>
<div class="flex justify-end gap-2">
<button on:click={() => auswahlDialogOffen = false} class="px-4 py-2 bg-gray-200 rounded">Abbrechen</button>
<button on:click={bestaetigeAnnahme} class="px-4 py-2 bg-green-600 text-white rounded">Zuweisen</button>
</div>
</div>
</div>
{#if showDialog}
<DienststellenDialog
wishes={availableWishes}
selectedId={selectedDienststelleId}
on:confirm={handleConfirmAccept}
on:cancel={closeDialog}
/>
{/if}

View File

@@ -0,0 +1,18 @@
// src/routes/api/admin/check-auth/+server.ts
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth === 'authenticated') {
return new Response(JSON.stringify({ authenticated: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({ authenticated: false }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
};