Seite umbauen mit Komponenten und admin-auth
This commit is contained in:
57
src/lib/components/AdminHeader.svelte
Normal file
57
src/lib/components/AdminHeader.svelte
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!-- src/lib/components/AdminHeader.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
export let title: string;
|
||||||
|
export let showBackButton: boolean = false;
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
goto('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/logout', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
goto('/admin');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout fehler:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between items-center h-16">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{#if showBackButton}
|
||||||
|
<button
|
||||||
|
on:click={goBack}
|
||||||
|
class="mr-4 p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors"
|
||||||
|
aria-label="Zurück"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">{title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="text-sm text-gray-500">Admin-Bereich</span>
|
||||||
|
<button
|
||||||
|
on:click={logout}
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
71
src/lib/components/AdminNavigation.svelte
Normal file
71
src/lib/components/AdminNavigation.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<!-- src/lib/components/AdminNavigation.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
interface NavigationItem {
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
description: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationItems: NavigationItem[] = [
|
||||||
|
{
|
||||||
|
href: '/admin/anmeldungen',
|
||||||
|
title: 'Anmeldungen',
|
||||||
|
icon: '📝',
|
||||||
|
description: 'Praktikumsanmeldungen anzeigen und verwalten',
|
||||||
|
color: 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/admin/dienststellen',
|
||||||
|
title: 'Dienststellen',
|
||||||
|
icon: '🏢',
|
||||||
|
description: 'Dienststellen hinzufügen und bearbeiten',
|
||||||
|
color: 'bg-green-600 hover:bg-green-700'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/admin/zeitraeume',
|
||||||
|
title: 'Zeiträume',
|
||||||
|
icon: '🗓️',
|
||||||
|
description: 'Praktikumszeiträume verwalten',
|
||||||
|
color: 'bg-purple-600 hover:bg-purple-700'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/admin/change-password',
|
||||||
|
title: 'Passwort ändern',
|
||||||
|
icon: '🔒',
|
||||||
|
description: 'Admin-Passwort aktualisieren',
|
||||||
|
color: 'bg-orange-600 hover:bg-orange-700'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{#each navigationItems as item}
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
class="group {item.color} text-white p-6 rounded-lg shadow-md transition-all duration-200 hover:shadow-lg hover:transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<div class="flex items-start space-x-4">
|
||||||
|
<div class="text-3xl">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p class="text-white/90 text-sm">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Zusätzliche Hover-Effekte */
|
||||||
|
.group:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
176
src/lib/components/AnmeldungenTable.svelte
Normal file
176
src/lib/components/AnmeldungenTable.svelte
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<!-- src/lib/components/AnmeldungenTable.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
interface Anmeldung {
|
||||||
|
pdfs: { pfad: string }[];
|
||||||
|
anrede: string;
|
||||||
|
vorname: string;
|
||||||
|
nachname: string;
|
||||||
|
email: string;
|
||||||
|
noteDeutsch?: string;
|
||||||
|
noteMathe?: string;
|
||||||
|
sozialverhalten?: string;
|
||||||
|
wunsch1?: { id: number; name: string };
|
||||||
|
wunsch2?: { id: number; name: string };
|
||||||
|
wunsch3?: { id: number; name: string };
|
||||||
|
timestamp: number;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let anmeldungen: Anmeldung[];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
accept: { id: number };
|
||||||
|
reject: { id: number };
|
||||||
|
delete: { id: number };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function formatDate(timestamp: number): string {
|
||||||
|
return new Date(timestamp).toLocaleDateString('de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadPdf(pdfPath: string) {
|
||||||
|
window.open(pdfPath, '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
Bewerber
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Noten
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Wünsche
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Dokumente
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Anmeldung
|
||||||
|
</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 anmeldungen as anmeldung (anmeldung.id)}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm font-medium text-gray-900">
|
||||||
|
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">{anmeldung.email}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#if anmeldung.noteDeutsch}
|
||||||
|
<div>Deutsch: <span class="font-medium">{anmeldung.noteDeutsch}</span></div>
|
||||||
|
{/if}
|
||||||
|
{#if anmeldung.noteMathe}
|
||||||
|
<div>Mathe: <span class="font-medium">{anmeldung.noteMathe}</span></div>
|
||||||
|
{/if}
|
||||||
|
{#if anmeldung.sozialverhalten}
|
||||||
|
<div>Sozialverhalten: <span class="font-medium">{anmeldung.sozialverhalten}</span></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900">
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#if anmeldung.wunsch1}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mr-2">1</span>
|
||||||
|
{anmeldung.wunsch1.name}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if anmeldung.wunsch2}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 mr-2">2</span>
|
||||||
|
{anmeldung.wunsch2.name}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if anmeldung.wunsch3}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 mr-2">3</span>
|
||||||
|
{anmeldung.wunsch3.name}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{#if anmeldung.pdfs.length > 0}
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#each anmeldung.pdfs as pdf, index}
|
||||||
|
<button
|
||||||
|
on:click={() => downloadPdf(pdf.pfad)}
|
||||||
|
class="flex items-center text-blue-600 hover:text-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||||
|
PDF {index + 1}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<span class="text-gray-400">Keine Dokumente</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{formatDate(anmeldung.timestamp)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
on:click={() => dispatch('accept', { id: anmeldung.id })}
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
Annehmen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => dispatch('reject', { id: anmeldung.id })}
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
Ablehnen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => dispatch('delete', { id: anmeldung.id })}
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
122
src/lib/components/DienststellenDialog.svelte
Normal file
122
src/lib/components/DienststellenDialog.svelte
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<!-- src/lib/components/DienststellenDialog.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
interface Wish {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let wishes: Wish[];
|
||||||
|
export let selectedId: number | null;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
confirm: { dienststelleId: number };
|
||||||
|
cancel: {};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let currentSelectedId = selectedId;
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
if (currentSelectedId !== null) {
|
||||||
|
dispatch('confirm', { dienststelleId: currentSelectedId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
dispatch('cancel', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
handleCancel();
|
||||||
|
} else if (event.key === 'Enter' && currentSelectedId !== null) {
|
||||||
|
handleConfirm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity z-50"
|
||||||
|
on:click={handleCancel}
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && handleCancel()}
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Dialog -->
|
||||||
|
<div class="fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<div
|
||||||
|
class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
|
||||||
|
on:click|stopPropagation
|
||||||
|
on:keydown|stopPropagation
|
||||||
|
role="dialog"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
|
||||||
|
Praktikumsstelle zuweisen
|
||||||
|
</h3>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm text-gray-500 mb-4">
|
||||||
|
Bitte wählen Sie eine der gewünschten Praktikumsstellen aus:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<fieldset class="space-y-3">
|
||||||
|
<legend class="sr-only">Praktikumsstelle auswählen</legend>
|
||||||
|
{#each wishes as wish}
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={currentSelectedId}
|
||||||
|
value={wish.id}
|
||||||
|
class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600"
|
||||||
|
/>
|
||||||
|
<span class="ml-3 text-sm font-medium text-gray-700">
|
||||||
|
{wish.name}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{#if wishes.length === 0}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<p class="text-sm text-gray-500">Keine Wünsche verfügbar</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={handleConfirm}
|
||||||
|
disabled={currentSelectedId === null}
|
||||||
|
class="inline-flex w-full justify-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 disabled:bg-gray-300 disabled:cursor-not-allowed sm:ml-3 sm:w-auto transition-colors"
|
||||||
|
>
|
||||||
|
Bestätigen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={handleCancel}
|
||||||
|
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
91
src/lib/components/LoginForm.svelte
Normal file
91
src/lib/components/LoginForm.svelte
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<!-- src/lib/components/LoginForm.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
loginResult: { success: boolean; error?: string };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let password = '';
|
||||||
|
let isLoading = false;
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!password.trim()) {
|
||||||
|
error = 'Bitte Passwort eingeben';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ passwort: password }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
dispatch('loginResult', { success: true });
|
||||||
|
} else {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
error = data.message || 'Falsches Passwort';
|
||||||
|
dispatch('loginResult', { success: false, error });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = 'Verbindungsfehler. Bitte versuchen Sie es erneut.';
|
||||||
|
dispatch('loginResult', { success: false, error });
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Passwort
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
bind:value={password}
|
||||||
|
on:keydown={handleKeydown}
|
||||||
|
placeholder="Admin-Passwort eingeben"
|
||||||
|
disabled={isLoading}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-md p-3">
|
||||||
|
<p class="text-sm text-red-800">{error}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !password.trim()}
|
||||||
|
class="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
{#if isLoading}
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
Wird eingeloggt...
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
Anmelden
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@@ -26,13 +26,14 @@
|
|||||||
let dienststellen: any[];
|
let dienststellen: any[];
|
||||||
|
|
||||||
let fileInputKey = 0;
|
let fileInputKey = 0;
|
||||||
let pdfDatein = [];
|
|
||||||
let noteDeutsch = '';
|
let noteDeutsch = '';
|
||||||
let noteMathe = '';
|
let noteMathe = '';
|
||||||
let sozialverhalten = '';
|
let sozialverhalten = '';
|
||||||
let schulklasse = '';
|
let schulklasse = '';
|
||||||
let ablehnungHinweis = '';
|
let ablehnungHinweis = '';
|
||||||
let showAblehnungModal = false;
|
let showAblehnungModal = false;
|
||||||
|
let alter = '';
|
||||||
|
let startDatum = '';
|
||||||
|
|
||||||
$: hideSozialVerhalten =
|
$: hideSozialVerhalten =
|
||||||
Number(schulklasse) >= 11 &&
|
Number(schulklasse) >= 11 &&
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
const deutsch = parseInt(noteDeutsch);
|
const deutsch = parseInt(noteDeutsch);
|
||||||
const mathe = parseInt(noteMathe);
|
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) {
|
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.';
|
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;
|
showAblehnungModal = true;
|
||||||
@@ -216,9 +217,14 @@
|
|||||||
<select bind:value={zeitraum} required class="input">
|
<select bind:value={zeitraum} required class="input">
|
||||||
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
||||||
{#each zeitraeume ?? [] as d}
|
{#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}
|
{/each}
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
|
<p>Startdatum: {startDatum}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Wunschdienststellen -->
|
<!-- Wunschdienststellen -->
|
||||||
@@ -257,7 +263,7 @@
|
|||||||
<!-- Mehrere PDF Upload -->
|
<!-- Mehrere PDF Upload -->
|
||||||
{#key fileInputKey}
|
{#key fileInputKey}
|
||||||
<div>
|
<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
|
<input
|
||||||
id="pdf-upload"
|
id="pdf-upload"
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -1,65 +1,67 @@
|
|||||||
|
<!-- src/routes/admin/+page.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let passwort = '';
|
import { onMount } from 'svelte';
|
||||||
let eingeloggt = false;
|
import { goto } from '$app/navigation';
|
||||||
let fehler = false;
|
import LoginForm from '$lib/components/LoginForm.svelte';
|
||||||
|
import AdminNavigation from '$lib/components/AdminNavigation.svelte';
|
||||||
|
|
||||||
async function login() {
|
let isAuthenticated = false;
|
||||||
const res = await fetch('/api/admin/login', {
|
let isLoading = true;
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ passwort }),
|
onMount(async () => {
|
||||||
headers: { 'Content-Type': 'application/json' }
|
// 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) {
|
async function handleLogin(event: CustomEvent<{success: boolean}>) {
|
||||||
eingeloggt = true;
|
if (event.detail.success) {
|
||||||
fehler = false;
|
isAuthenticated = true;
|
||||||
} else {
|
}
|
||||||
fehler = true;
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
await fetch('/api/admin/logout', { method: 'POST' });
|
||||||
|
isAuthenticated = false;
|
||||||
|
goto('/admin');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout failed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6 max-w-lg mx-auto">
|
<div class="min-h-screen bg-gray-50 py-8">
|
||||||
{#if !eingeloggt}
|
<div class="max-w-2xl mx-auto px-4">
|
||||||
<div class="space-y-4">
|
{#if isLoading}
|
||||||
<h1 class="text-2xl font-bold">Admin Login</h1>
|
<div class="flex justify-center items-center h-64">
|
||||||
<input type="password" bind:value={passwort} placeholder="Passwort" class="input w-full" />
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
<button on:click={login} class="bg-blue-600 text-white px-4 py-2 rounded">Login</button>
|
</div>
|
||||||
{#if fehler}
|
{:else if !isAuthenticated}
|
||||||
<p class="text-red-600">Falsches Passwort</p>
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
{/if}
|
<h1 class="text-3xl font-bold text-gray-900 mb-6 text-center">Admin Login</h1>
|
||||||
|
<LoginForm on:loginResult={handleLogin} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
<h1 class="text-2xl font-bold mb-4">Admin-Bereich</h1>
|
<div class="flex justify-between items-center mb-6">
|
||||||
<div class="flex flex-col gap-4">
|
<h1 class="text-3xl font-bold text-gray-900">Admin-Bereich</h1>
|
||||||
<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>
|
|
||||||
<button
|
<button
|
||||||
on:click={async () => {
|
on:click={handleLogout}
|
||||||
await fetch('/api/admin/logout', { method: 'POST' });
|
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md transition-colors"
|
||||||
location.reload();
|
>
|
||||||
}}
|
|
||||||
class="bg-red-600 text-white px-4 py-3 rounded text-center hiver:bg-red-700">
|
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AdminNavigation />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<style>
|
|
||||||
.input {
|
|
||||||
@apply border rounded px-3 py-2 w-full;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
|
// src/routes/admin/anmeldungen/+page.server.ts
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ cookies }) => {
|
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');
|
throw redirect(303, '/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Anmeldungen verwalten'
|
||||||
|
};
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
<!-- src/routes/admin/anmeldungen/+page.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
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 {
|
interface Anmeldung {
|
||||||
pdfs: any;
|
pdfs: { pfad: string }[];
|
||||||
anrede: string;
|
anrede: string;
|
||||||
vorname: string;
|
vorname: string;
|
||||||
nachname: string;
|
nachname: string;
|
||||||
@@ -17,34 +23,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let anmeldungen: Anmeldung[] = [];
|
let anmeldungen: Anmeldung[] = [];
|
||||||
let auswahlDialogOffen = false;
|
let isLoading = true;
|
||||||
let auswahlAnmeldungId: number | null = null;
|
let error = '';
|
||||||
let ausgewaehlteDienststelleId: number | null = null;
|
|
||||||
let aktuelleWuensche: { id: number, name: string }[] = [];
|
|
||||||
|
|
||||||
function oeffneAuswahl(id: number) {
|
// Dialog state
|
||||||
const anmeldung = anmeldungen.find(a => a.id === id);
|
let showDialog = false;
|
||||||
|
let selectedAnmeldungId: number | null = null;
|
||||||
|
let selectedDienststelleId: number | null = null;
|
||||||
|
let availableWishes: { id: number, name: string }[] = [];
|
||||||
|
|
||||||
|
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;
|
if (!anmeldung) return;
|
||||||
|
|
||||||
aktuelleWuensche = [
|
availableWishes = [
|
||||||
anmeldung.wunsch1 && { id: anmeldung.wunsch1.id, name: `1. Wunsch: ${anmeldung.wunsch1.name}` },
|
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.wunsch2 && { id: anmeldung.wunsch2.id, name: `2. Wunsch: ${anmeldung.wunsch2.name}` },
|
||||||
anmeldung.wunsch3 && { id: anmeldung.wunsch3.id, name: `3. Wunsch: ${anmeldung.wunsch3.name}` }
|
anmeldung.wunsch3 && { id: anmeldung.wunsch3.id, name: `3. Wunsch: ${anmeldung.wunsch3.name}` }
|
||||||
].filter(Boolean) as { id: number, name: string }[];
|
].filter(Boolean) as { id: number, name: string }[];
|
||||||
|
|
||||||
ausgewaehlteDienststelleId = aktuelleWuensche[0]?.id ?? null;
|
selectedDienststelleId = availableWishes[0]?.id ?? null;
|
||||||
auswahlAnmeldungId = id;
|
selectedAnmeldungId = event.detail.id;
|
||||||
auswahlDialogOffen = true;
|
showDialog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bestaetigeAnnahme() {
|
async function handleConfirmAccept(event: CustomEvent<{dienststelleId: number}>) {
|
||||||
if (!auswahlAnmeldungId || !ausgewaehlteDienststelleId) return;
|
if (!selectedAnmeldungId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/anmeldungen?id=${auswahlAnmeldungId}`, {
|
const res = await fetch(`/api/admin/anmeldungen?id=${selectedAnmeldungId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ dienststelleId: ausgewaehlteDienststelleId })
|
body: JSON.stringify({ dienststelleId: event.detail.dienststelleId })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -52,154 +82,121 @@
|
|||||||
throw new Error(`Fehler beim Annehmen (${res.status}): ${errorText}`);
|
throw new Error(`Fehler beim Annehmen (${res.status}): ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
auswahlDialogOffen = false;
|
showDialog = false;
|
||||||
auswahlAnmeldungId = null;
|
selectedAnmeldungId = null;
|
||||||
await ladeAnmeldungen();
|
await loadAnmeldungen();
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error(error);
|
error = err instanceof Error ? err.message : 'Fehler beim Annehmen';
|
||||||
alert('Fehler beim Annehmen der Anmeldung.\n' + (error as Error).message);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ladeAnmeldungen() {
|
async function handleReject(event: CustomEvent<{id: number}>) {
|
||||||
const res = await fetch('/api/admin/anmeldungen');
|
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
|
||||||
anmeldungen = await res.json();
|
|
||||||
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loeschen(id: number) {
|
await loadAnmeldungen();
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Fehler beim Ablehnen';
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(event: CustomEvent<{id: number}>) {
|
||||||
if (!confirm('Diese Anmeldung wirklich löschen?')) return;
|
if (!confirm('Diese Anmeldung wirklich löschen?')) return;
|
||||||
|
|
||||||
try {
|
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) {
|
if (!res.ok) {
|
||||||
const errorText = await res.text();
|
throw new Error(`Fehler beim Löschen: ${res.status}`);
|
||||||
throw new Error(`Fehler beim Löschen (${res.status}): ${errorText}`);
|
|
||||||
}
|
}
|
||||||
await ladeAnmeldungen();
|
|
||||||
} catch (error) {
|
await loadAnmeldungen();
|
||||||
console.error(error);
|
} catch (err) {
|
||||||
alert('Fehler beim Löschen der Anmeldung.\n' + (error as Error).message);
|
error = err instanceof Error ? err.message : 'Fehler beim Löschen';
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function annehmen(id: number) {
|
function closeDialog() {
|
||||||
if (!confirm('Diese Anmeldung wirklich annehmen?')) return;
|
showDialog = false;
|
||||||
try {
|
selectedAnmeldungId = null;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ablehnen(id: number) {
|
onMount(loadAnmeldungen);
|
||||||
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(ladeAnmeldungen);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6 max-w-8xl mx-auto">
|
<svelte:head>
|
||||||
<h1 class="text-2xl font-bold mb-4 text-center">Alle Anmeldungen</h1>
|
<title>Anmeldungen verwalten - Admin</title>
|
||||||
<table class="w-full border text-sm">
|
</svelte:head>
|
||||||
<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>
|
|
||||||
|
|
||||||
<button
|
<div class="min-h-screen bg-gray-50">
|
||||||
on:click={async () => {
|
<AdminHeader
|
||||||
await fetch('/api/admin/logout', { method: 'POST' });
|
title="Anmeldungen verwalten"
|
||||||
location.reload();
|
showBackButton={true}
|
||||||
}}
|
/>
|
||||||
class="bg-red-600 text-white px-4 py-3 rounded text-center hover:bg-red-700">
|
|
||||||
Logout
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||||
</button>
|
{#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>
|
||||||
|
<div class="ml-3">
|
||||||
{#if auswahlDialogOffen}
|
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<p class="mt-1 text-sm text-red-700">{error}</p>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 showDialog}
|
||||||
|
<DienststellenDialog
|
||||||
|
wishes={availableWishes}
|
||||||
|
selectedId={selectedDienststelleId}
|
||||||
|
on:confirm={handleConfirmAccept}
|
||||||
|
on:cancel={closeDialog}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
18
src/routes/api/admin/check-auth/+server.ts
Normal file
18
src/routes/api/admin/check-auth/+server.ts
Normal 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' }
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user