Dienstellen auf die neue cookie
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
accept: { id: number };
|
||||
reject: { id: number };
|
||||
delete: { id: number };
|
||||
}>();
|
||||
|
||||
interface Anmeldung {
|
||||
pdfs: { pfad: string }[];
|
||||
anrede: string;
|
||||
@@ -17,27 +23,29 @@
|
||||
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',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function downloadPdf(pdfPath: string) {
|
||||
window.open(pdfPath, '_blank');
|
||||
|
||||
function handleAccept(id: number) {
|
||||
dispatch('accept', { id });
|
||||
}
|
||||
|
||||
function handleReject(id: number) {
|
||||
dispatch('reject', { id });
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
dispatch('delete', { id });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -68,101 +76,111 @@
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each anmeldungen as anmeldung (anmeldung.id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<!-- Bewerber Info -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
|
||||
<div class="flex flex-col">
|
||||
<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>
|
||||
</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}
|
||||
|
||||
<!-- Noten -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900 space-y-1">
|
||||
<div><span class="font-medium">Deutsch:</span> {anmeldung.noteDeutsch || '—'}</div>
|
||||
<div><span class="font-medium">Mathe:</span> {anmeldung.noteMathe || '—'}</div>
|
||||
{#if anmeldung.sozialverhalten}
|
||||
<div>Sozialverhalten: <span class="font-medium">{anmeldung.sozialverhalten}</span></div>
|
||||
<div class="text-xs text-gray-600 mt-2">
|
||||
<span class="font-medium">Sozialverhalten:</span><br>
|
||||
{anmeldung.sozialverhalten}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 text-sm text-gray-900">
|
||||
<div class="space-y-1">
|
||||
|
||||
<!-- Wünsche -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="space-y-2 text-sm">
|
||||
{#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}
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-blue-100 text-blue-800 text-xs font-medium mr-2">1</span>
|
||||
<span class="text-gray-900">{anmeldung.wunsch1.name}</span>
|
||||
</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}
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-800 text-xs font-medium mr-2">2</span>
|
||||
<span class="text-gray-900">{anmeldung.wunsch2.name}</span>
|
||||
</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}
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-yellow-100 text-yellow-800 text-xs font-medium mr-2">3</span>
|
||||
<span class="text-gray-900">{anmeldung.wunsch3.name}</span>
|
||||
</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"
|
||||
|
||||
<!-- Dokumente -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="space-y-1">
|
||||
{#each anmeldung.pdfs as pdf, index}
|
||||
<div>
|
||||
<a
|
||||
href={pdf.pfad}
|
||||
target="_blank"
|
||||
class="inline-flex items-center text-sm text-blue-600 hover:text-blue-900 hover:underline"
|
||||
>
|
||||
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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}
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
<!-- Anmeldedatum -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(anmeldung.timestamp)}
|
||||
</td>
|
||||
|
||||
|
||||
<!-- Aktionen -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<div class="flex flex-col space-y-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"
|
||||
on:click={() => handleAccept(anmeldung.id)}
|
||||
class="inline-flex items-center px-3 py-1 border border-transparent text-xs 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"
|
||||
>
|
||||
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"
|
||||
on:click={() => handleReject(anmeldung.id)}
|
||||
class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded-md text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500"
|
||||
>
|
||||
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"
|
||||
on:click={() => handleDelete(anmeldung.id)}
|
||||
class="inline-flex items-center px-3 py-1 border border-transparent text-xs 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"
|
||||
>
|
||||
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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
|
||||
|
||||
@@ -2,121 +2,137 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
confirm: { dienststelleId: number };
|
||||
cancel: void;
|
||||
}>();
|
||||
|
||||
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;
|
||||
|
||||
|
||||
let isLoading = false;
|
||||
|
||||
function handleConfirm() {
|
||||
if (currentSelectedId !== null) {
|
||||
dispatch('confirm', { dienststelleId: currentSelectedId });
|
||||
if (selectedId !== null) {
|
||||
isLoading = true;
|
||||
dispatch('confirm', { dienststelleId: selectedId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleCancel() {
|
||||
dispatch('cancel', {});
|
||||
dispatch('cancel');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
|
||||
function handleBackdropClick(event: MouseEvent) {
|
||||
if (event.target === event.currentTarget) {
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackdropKeydown(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>
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4"
|
||||
on:click={handleBackdropClick}
|
||||
on:keydown={handleBackdropKeydown}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between p-6 border-b border-gray-200 rounded-t">
|
||||
<h3 class="text-lg font-semibold text-gray-900" id="modal-title">
|
||||
Dienststelle für Praktikum auswählen
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center"
|
||||
on:click={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span class="sr-only">Modal schließen</span>
|
||||
</button>
|
||||
</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>
|
||||
<!-- Body -->
|
||||
<div class="p-6">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Wählen Sie eine der Wunsch-Dienststellen für diese Anmeldung aus:
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each wishes as wish}
|
||||
<label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedId}
|
||||
value={wish.id}
|
||||
disabled={isLoading}
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||
/>
|
||||
<span class="ml-3 text-sm font-medium text-gray-900">
|
||||
{wish.name}
|
||||
</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if wishes.length === 0}
|
||||
<div class="text-center py-4">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">Keine Wünsche verfügbar</p>
|
||||
</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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 rounded-b">
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleCancel}
|
||||
disabled={isLoading}
|
||||
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleConfirm}
|
||||
disabled={selectedId === null || isLoading || wishes.length === 0}
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center"
|
||||
>
|
||||
{#if isLoading}
|
||||
<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 zugewiesen...
|
||||
{:else}
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Praktikum zuweisen
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +1,16 @@
|
||||
// src/routes/admin/dienstellen/+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') {
|
||||
throw redirect(303, '/admin'); // zurück zur Login-Seite
|
||||
// Korrigiere Cookie-Name um konsistent zu sein
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
|
||||
if (adminAuth !== 'authenticated') {
|
||||
throw redirect(303, '/admin');
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Dienstellen verwalten'
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,16 @@
|
||||
// src/routes/admin/zeitraeume/+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') {
|
||||
throw redirect(303, '/admin'); // zurück zur Login-Seite
|
||||
// Korrigiere Cookie-Name um konsistent zu sein
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
|
||||
if (adminAuth !== 'authenticated') {
|
||||
throw redirect(303, '/admin');
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Zetraeume verwalten'
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/routes/api/admin/anmeldungen/+server.ts
|
||||
import { PrismaClient, Status } from '@prisma/client';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
@@ -8,31 +9,55 @@ const prisma = new PrismaClient();
|
||||
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
|
||||
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
|
||||
function checkAuth(cookies: Cookies) {
|
||||
return cookies.get('admin_session') === 'true';
|
||||
return cookies.get('admin-auth') === 'authenticated';
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
|
||||
const anmeldungen = await prisma.anmeldung.findMany({
|
||||
include: {
|
||||
wunsch1: true,
|
||||
wunsch2: true,
|
||||
wunsch3: true,
|
||||
pdfs: true
|
||||
},
|
||||
orderBy: { timestamp: 'desc' }
|
||||
});
|
||||
return new Response(JSON.stringify(anmeldungen), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (!checkAuth(cookies)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Nicht autorisiert' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const anmeldungen = await prisma.anmeldung.findMany({
|
||||
include: {
|
||||
wunsch1: true,
|
||||
wunsch2: true,
|
||||
wunsch3: true,
|
||||
pdfs: true
|
||||
},
|
||||
orderBy: { timestamp: 'desc' }
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(anmeldungen), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anmeldungen:', error);
|
||||
return json({ error: 'Fehler beim Laden der Anmeldungen' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ url }) => {
|
||||
export const POST: RequestHandler = async ({ url, cookies, request }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
if (!id) return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
|
||||
try {
|
||||
// Prüfe ob eine spezifische Dienststelle zugewiesen werden soll
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const dienststelleId = body.dienststelleId;
|
||||
|
||||
const anmeldung = await prisma.anmeldung.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -42,8 +67,44 @@ export const POST: RequestHandler = async ({ url }) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (!anmeldung) return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
if (!anmeldung) {
|
||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Falls spezifische Dienststelle gewählt wurde
|
||||
if (dienststelleId) {
|
||||
const dienststelle = await prisma.dienststelle.findUnique({
|
||||
where: { id: dienststelleId }
|
||||
});
|
||||
|
||||
if (!dienststelle) {
|
||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (dienststelle.plaetze <= 0) {
|
||||
return json({ error: 'Keine verfügbaren Plätze bei dieser Dienststelle' }, { status: 409 });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.anmeldung.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: Status.ANGENOMMEN,
|
||||
zugewiesenId: dienststelleId
|
||||
}
|
||||
}),
|
||||
prisma.dienststelle.update({
|
||||
where: { id: dienststelleId },
|
||||
data: {
|
||||
plaetze: { decrement: 1 }
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
return json({ success: true, message: `Zugewiesen an: ${dienststelle.name}` });
|
||||
}
|
||||
|
||||
// Fallback: Automatische Zuweisung nach Wunschreihenfolge
|
||||
const wuensche = [anmeldung.wunsch1, anmeldung.wunsch2, anmeldung.wunsch3];
|
||||
|
||||
for (const wunsch of wuensche) {
|
||||
@@ -70,17 +131,51 @@ export const POST: RequestHandler = async ({ url }) => {
|
||||
|
||||
return json({ error: 'Keine verfügbaren Plätze bei Wunsch-Dienststellen' }, { status: 409 });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('Fehler beim Annehmen der Anmeldung:', err);
|
||||
return json({ error: 'Interner Serverfehler' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Neue PATCH-Route für Ablehnung
|
||||
export const PATCH: RequestHandler = async ({ url, cookies, request }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
if (!id) return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
|
||||
if (body.action === 'reject') {
|
||||
await prisma.anmeldung.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: Status.ABGELEHNT
|
||||
}
|
||||
});
|
||||
|
||||
return json({ success: true, message: 'Anmeldung abgelehnt' });
|
||||
}
|
||||
|
||||
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Ablehnen der Anmeldung:', err);
|
||||
return json({ error: 'Interner Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ cookies, url }) => {
|
||||
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Alle PDF-Einträge zur Anmeldung laden
|
||||
const pdfs = await prisma.pdfDatei.findMany({
|
||||
@@ -102,16 +197,18 @@ export const DELETE: RequestHandler = async ({ cookies, url }) => {
|
||||
}
|
||||
|
||||
// 3. PDF-Datensätze aus DB löschen
|
||||
await prisma.pdfDatei.deleteMany({
|
||||
where: {anmeldungId: id}
|
||||
});
|
||||
await prisma.pdfDatei.deleteMany({
|
||||
where: { anmeldungId: id }
|
||||
});
|
||||
|
||||
// Anmeldung löschen
|
||||
await prisma.anmeldung.delete({where: { id } });
|
||||
return json({ ok: true });
|
||||
// 4. Anmeldung löschen
|
||||
await prisma.anmeldung.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return json({ success: true, message: 'Anmeldung erfolgreich gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Anmeldung:', error);
|
||||
return json({ error: 'Löschen fehlgeschlagen' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/routes/api/admin/dienststellen/+server.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
@@ -6,73 +7,187 @@ const prisma = new PrismaClient();
|
||||
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
|
||||
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
|
||||
function checkAuth(cookies: Cookies) {
|
||||
return cookies.get('admin_session') === 'true';
|
||||
return cookies.get('admin-auth') === 'authenticated';
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
|
||||
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
|
||||
return json(dienststellen);
|
||||
if (!checkAuth(cookies)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Nicht autorisiert' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const dienststellen = await prisma.dienststelle.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
/*
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
Anmeldung: true // Use the correct relation name as defined in your Prisma schema
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
});
|
||||
return json(dienststellen);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dienststellen:', error);
|
||||
return json({ error: 'Fehler beim Laden der Dienststellen' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request }) => {
|
||||
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
|
||||
const { name, plaetze } = await request.json();
|
||||
if (typeof plaetze !== 'number' || plaetze < 0) {
|
||||
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await prisma.dienststelle.create({ data: {
|
||||
name,
|
||||
plaetze,
|
||||
} });
|
||||
return json(created);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Hinzufuegen:', e);
|
||||
return json({ error: 'Dienststelle existiert bereits' }, { status: 400 });
|
||||
const { name, plaetze } = await request.json();
|
||||
|
||||
// Validierung
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
|
||||
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Name bereits existiert
|
||||
const existing = await prisma.dienststelle.findFirst({
|
||||
where: { name: name.trim() }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
|
||||
}
|
||||
|
||||
const created = await prisma.dienststelle.create({
|
||||
data: {
|
||||
name: name.trim(),
|
||||
plaetze,
|
||||
}
|
||||
});
|
||||
|
||||
return json(created, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Dienststelle:', error);
|
||||
return json({ error: 'Fehler beim Erstellen der Dienststelle' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ cookies, request }) => {
|
||||
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
|
||||
|
||||
const { id, name, plaetze } = await request.json();
|
||||
|
||||
if (typeof id !== 'number' || isNaN(id) || !name || typeof plaetze !== 'number' || plaetze < 0) {
|
||||
return json({ error: 'Ungültige Eingabedaten' }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await prisma.dienststelle.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const konflikt = await prisma.dienststelle.findFirst({
|
||||
where: {
|
||||
name,
|
||||
NOT: { id },
|
||||
},
|
||||
});
|
||||
if (konflikt) {
|
||||
return json({ error: 'Eine andere Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, name, plaetze } = await request.json();
|
||||
|
||||
// Validierung
|
||||
if (typeof id !== 'number' || isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
|
||||
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Dienststelle existiert
|
||||
const existing = await prisma.dienststelle.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Prüfe ob neuer Name bereits bei anderer Dienststelle existiert
|
||||
const nameConflict = await prisma.dienststelle.findFirst({
|
||||
where: {
|
||||
name: name.trim(),
|
||||
NOT: { id },
|
||||
},
|
||||
});
|
||||
|
||||
if (nameConflict) {
|
||||
return json({ error: 'Eine andere Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Plätze reduziert werden und ob das möglich ist
|
||||
const assignedCount = await prisma.anmeldung.count({
|
||||
where: { zugewiesenId: id }
|
||||
});
|
||||
|
||||
if (plaetze < assignedCount) {
|
||||
return json({
|
||||
error: `Plätze können nicht auf ${plaetze} reduziert werden. ${assignedCount} Anmeldungen sind bereits zugewiesen.`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await prisma.dienststelle.update({
|
||||
where: { id },
|
||||
data: { name, plaetze },
|
||||
data: {
|
||||
name: name.trim(),
|
||||
plaetze
|
||||
},
|
||||
});
|
||||
|
||||
return json(updated);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Update:', e);
|
||||
return json({ error: 'Update fehlgeschlagen' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
|
||||
return json({ error: 'Fehler beim Aktualisieren der Dienststelle' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ cookies, url }) => {
|
||||
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 });
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
await prisma.dienststelle.delete({ where: { id } });
|
||||
return json({ success: true });
|
||||
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Prüfe ob Dienststelle existiert
|
||||
const existing = await prisma.dienststelle.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Prüfe ob noch Anmeldungen zugewiesen sind
|
||||
const assignedCount = await prisma.anmeldung.count({
|
||||
where: {
|
||||
OR: [
|
||||
{ zugewiesenId: id },
|
||||
{ wunsch1Id: id },
|
||||
{ wunsch2Id: id },
|
||||
{ wunsch3Id: id }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (assignedCount > 0) {
|
||||
return json({
|
||||
error: 'Dienststelle kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.dienststelle.delete({ where: { id } });
|
||||
return json({ success: true, message: 'Dienststelle erfolgreich gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Dienststelle:', error);
|
||||
return json({ error: 'Fehler beim Löschen der Dienststelle' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,15 @@
|
||||
// src/routes/api/admin/logout/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies }) => {
|
||||
cookies.delete('admin_session', { path: '/' });
|
||||
return new Response('Ausgeloggt');
|
||||
// Cookie löschen mit korrektem Namen
|
||||
cookies.delete('admin-auth', { path: '/' });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: 'Erfolgreich ausgeloggt' }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user