Dienstellen auf die neue cookie

This commit is contained in:
titver968
2025-07-23 17:53:07 +02:00
parent 1e6c3b1703
commit 24dd912f77
8 changed files with 528 additions and 268 deletions

View File

@@ -38,38 +38,27 @@ enum Status {
} }
model Anmeldung { model Anmeldung {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
anrede String anrede String
vorname String vorname String
nachname String nachname String
geburtsdatum String email String
strasse String noteDeutsch String?
hausnummer String noteMathe String?
ort String sozialverhalten String?
plz String status Status @default(OFFEN)
telefon String zugewiesenId Int? // ID der zugewiesenen Dienststelle
email String @unique zugewiesen Dienststelle? @relation(fields: [zugewiesenId], references: [id])
noteDeutsch Int wunsch1Id Int?
noteMathe Int wunsch1 Dienststelle? @relation("Wunsch1", fields: [wunsch1Id], references: [id])
sozialverhalten String wunsch2Id Int?
schulart String wunsch2 Dienststelle? @relation("Wunsch2", fields: [wunsch2Id], references: [id])
motivation String wunsch3Id Int?
wunsch3 Dienststelle? @relation("Wunsch3", fields: [wunsch3Id], references: [id])
praktikumId Int timestamp BigInt
pdfs PdfDatei[]
wunsch1 Dienststelle @relation("Wunsch1", fields: [wunsch1Id], references: [id])
wunsch1Id Int @@map("anmeldungen")
wunsch2 Dienststelle @relation("Wunsch2", fields: [wunsch2Id], references: [id])
wunsch2Id Int
wunsch3 Dienststelle @relation("Wunsch3", fields: [wunsch3Id], references: [id])
wunsch3Id Int
status Status @default(OFFEN)
zugewiesenId Int?
zugewiesen Dienststelle? @relation("Zugewiesen", fields: [zugewiesenId], references: [id])
timestamp DateTime @default(now())
pdfs PdfDatei[] @relation("AnmeldungPdfs")
} }
model PdfDatei { model PdfDatei {

View File

@@ -2,6 +2,12 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
accept: { id: number };
reject: { id: number };
delete: { id: number };
}>();
interface Anmeldung { interface Anmeldung {
pdfs: { pfad: string }[]; pdfs: { pfad: string }[];
anrede: string; anrede: string;
@@ -17,27 +23,29 @@
timestamp: number; timestamp: number;
id: number; id: number;
} }
export let anmeldungen: Anmeldung[]; export let anmeldungen: Anmeldung[];
const dispatch = createEventDispatcher<{
accept: { id: number };
reject: { id: number };
delete: { id: number };
}>();
function formatDate(timestamp: number): string { function formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleDateString('de-DE', { return new Date(timestamp).toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit', day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}); });
} }
function downloadPdf(pdfPath: string) { function handleAccept(id: number) {
window.open(pdfPath, '_blank'); dispatch('accept', { id });
}
function handleReject(id: number) {
dispatch('reject', { id });
}
function handleDelete(id: number) {
dispatch('delete', { id });
} }
</script> </script>
@@ -68,101 +76,111 @@
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
{#each anmeldungen as anmeldung (anmeldung.id)} {#each anmeldungen as anmeldung (anmeldung.id)}
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50">
<!-- Bewerber Info -->
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900"> <div class="flex flex-col">
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname} <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>
<div class="text-sm text-gray-500">{anmeldung.email}</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <!-- Noten -->
<div class="space-y-1"> <td class="px-6 py-4 whitespace-nowrap">
{#if anmeldung.noteDeutsch} <div class="text-sm text-gray-900 space-y-1">
<div>Deutsch: <span class="font-medium">{anmeldung.noteDeutsch}</span></div> <div><span class="font-medium">Deutsch:</span> {anmeldung.noteDeutsch || '—'}</div>
{/if} <div><span class="font-medium">Mathe:</span> {anmeldung.noteMathe || '—'}</div>
{#if anmeldung.noteMathe}
<div>Mathe: <span class="font-medium">{anmeldung.noteMathe}</span></div>
{/if}
{#if anmeldung.sozialverhalten} {#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} {/if}
</div> </div>
</td> </td>
<td class="px-6 py-4 text-sm text-gray-900"> <!-- Wünsche -->
<div class="space-y-1"> <td class="px-6 py-4">
<div class="space-y-2 text-sm">
{#if anmeldung.wunsch1} {#if anmeldung.wunsch1}
<div class="flex items-center"> <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> <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>
{anmeldung.wunsch1.name} <span class="text-gray-900">{anmeldung.wunsch1.name}</span>
</div> </div>
{/if} {/if}
{#if anmeldung.wunsch2} {#if anmeldung.wunsch2}
<div class="flex items-center"> <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> <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>
{anmeldung.wunsch2.name} <span class="text-gray-900">{anmeldung.wunsch2.name}</span>
</div> </div>
{/if} {/if}
{#if anmeldung.wunsch3} {#if anmeldung.wunsch3}
<div class="flex items-center"> <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> <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>
{anmeldung.wunsch3.name} <span class="text-gray-900">{anmeldung.wunsch3.name}</span>
</div> </div>
{/if} {/if}
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <!-- Dokumente -->
{#if anmeldung.pdfs.length > 0} <td class="px-6 py-4 whitespace-nowrap">
<div class="space-y-1"> <div class="space-y-1">
{#each anmeldung.pdfs as pdf, index} {#each anmeldung.pdfs as pdf, index}
<button <div>
on:click={() => downloadPdf(pdf.pfad)} <a
class="flex items-center text-blue-600 hover:text-blue-800 transition-colors" 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" /> <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> </svg>
PDF {index + 1} PDF {index + 1}
</button> </a>
{/each} </div>
</div> {/each}
{:else} </div>
<span class="text-gray-400">Keine Dokumente</span>
{/if}
</td> </td>
<!-- Anmeldedatum -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(anmeldung.timestamp)} {formatDate(anmeldung.timestamp)}
</td> </td>
<!-- Aktionen -->
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <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 <button
on:click={() => dispatch('accept', { id: anmeldung.id })} on:click={() => handleAccept(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" 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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg> </svg>
Annehmen Annehmen
</button> </button>
<button <button
on:click={() => dispatch('reject', { id: anmeldung.id })} on:click={() => handleReject(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" 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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
Ablehnen Ablehnen
</button> </button>
<button <button
on:click={() => dispatch('delete', { id: anmeldung.id })} on:click={() => handleDelete(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" 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" /> <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> </svg>
Löschen Löschen

View File

@@ -2,121 +2,137 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
confirm: { dienststelleId: number };
cancel: void;
}>();
interface Wish { interface Wish {
id: number; id: number;
name: string; name: string;
} }
export let wishes: Wish[]; export let wishes: Wish[];
export let selectedId: number | null; export let selectedId: number | null;
const dispatch = createEventDispatcher<{ let isLoading = false;
confirm: { dienststelleId: number };
cancel: {};
}>();
let currentSelectedId = selectedId;
function handleConfirm() { function handleConfirm() {
if (currentSelectedId !== null) { if (selectedId !== null) {
dispatch('confirm', { dienststelleId: currentSelectedId }); isLoading = true;
dispatch('confirm', { dienststelleId: selectedId });
} }
} }
function handleCancel() { 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') { if (event.key === 'Escape') {
handleCancel(); handleCancel();
} else if (event.key === 'Enter' && currentSelectedId !== null) {
handleConfirm();
} }
} }
</script> </script>
<svelte:window on:keydown={handleKeydown} />
<!-- Backdrop -->
<div <div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity z-50" 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={handleCancel} on:click={handleBackdropClick}
on:keydown={(e) => e.key === 'Enter' && handleCancel()} on:keydown={handleBackdropKeydown}
role="button" role="dialog"
tabindex="-1" aria-modal="true"
></div> 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 --> <!-- Body -->
<div class="fixed inset-0 z-50 overflow-y-auto"> <div class="p-6">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> <p class="text-sm text-gray-600 mb-4">
<div Wählen Sie eine der Wunsch-Dienststellen für diese Anmeldung aus:
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" </p>
on:click|stopPropagation
on:keydown|stopPropagation <div class="space-y-3">
role="dialog" {#each wishes as wish}
tabindex="-1" <label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
> <input
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4"> type="radio"
<div class="sm:flex sm:items-start"> bind:group={selectedId}
<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"> value={wish.id}
<svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> disabled={isLoading}
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
</svg> />
</div> <span class="ml-3 text-sm font-medium text-gray-900">
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full"> {wish.name}
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> </span>
Praktikumsstelle zuweisen </label>
</h3> {/each}
<div class="mt-4"> </div>
<p class="text-sm text-gray-500 mb-4">
Bitte wählen Sie eine der gewünschten Praktikumsstellen aus: {#if wishes.length === 0}
</p> <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">
<fieldset class="space-y-3"> <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" />
<legend class="sr-only">Praktikumsstelle auswählen</legend> </svg>
{#each wishes as wish} <p class="mt-2 text-sm text-gray-500">Keine Wünsche verfügbar</p>
<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> {/if}
</div>
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button <!-- Footer -->
type="button" <div class="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 rounded-b">
on:click={handleConfirm} <button
disabled={currentSelectedId === null} type="button"
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" on:click={handleCancel}
> disabled={isLoading}
Bestätigen 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"
</button> >
<button Abbrechen
type="button" </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" <button
> type="button"
Abbrechen on:click={handleConfirm}
</button> disabled={selectedId === null || isLoading || wishes.length === 0}
</div> 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> </div>
</div> </div>

View File

@@ -1,8 +1,16 @@
// src/routes/admin/dienstellen/+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
throw redirect(303, '/admin'); // zurück zur Login-Seite const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
throw redirect(303, '/admin');
} }
return {
title: 'Dienstellen verwalten'
};
}; };

View File

@@ -1,8 +1,16 @@
// src/routes/admin/zeitraeume/+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
throw redirect(303, '/admin'); // zurück zur Login-Seite const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
throw redirect(303, '/admin');
} }
return {
title: 'Zetraeume verwalten'
};
}; };

View File

@@ -1,3 +1,4 @@
// src/routes/api/admin/anmeldungen/+server.ts
import { PrismaClient, Status } from '@prisma/client'; import { PrismaClient, Status } from '@prisma/client';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
@@ -8,31 +9,55 @@ const prisma = new PrismaClient();
import type { Cookies } from '@sveltejs/kit'; import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) { function checkAuth(cookies: Cookies) {
return cookies.get('admin_session') === 'true'; return cookies.get('admin-auth') === 'authenticated';
} }
export const GET: RequestHandler = async ({ cookies }) => { export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
const anmeldungen = await prisma.anmeldung.findMany({ return new Response(
include: { JSON.stringify({ error: 'Nicht autorisiert' }),
wunsch1: true, {
wunsch2: true, status: 401,
wunsch3: true, headers: { 'Content-Type': 'application/json' }
pdfs: true }
}, );
orderBy: { timestamp: 'desc' } }
});
return new Response(JSON.stringify(anmeldungen), { try {
headers: { 'Content-Type': 'application/json' } 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')); const id = Number(url.searchParams.get('id'));
if (!id) return json({ error: 'Ungültige ID' }, { status: 400 }); if (!id) return json({ error: 'Ungültige ID' }, { status: 400 });
try { 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({ const anmeldung = await prisma.anmeldung.findUnique({
where: { id }, where: { id },
include: { 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]; const wuensche = [anmeldung.wunsch1, anmeldung.wunsch2, anmeldung.wunsch3];
for (const wunsch of wuensche) { 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 }); return json({ error: 'Keine verfügbaren Plätze bei Wunsch-Dienststellen' }, { status: 409 });
} catch (err) { } catch (err) {
console.error(err); console.error('Fehler beim Annehmen der Anmeldung:', err);
return json({ error: 'Interner Serverfehler' }, { status: 500 }); 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 }) => { export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id')); const id = Number(url.searchParams.get('id'));
if (isNaN(id)) { if (isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 }); return json({ error: 'Ungültige ID' }, { status: 400 });
} }
try { try {
// 1. Alle PDF-Einträge zur Anmeldung laden // 1. Alle PDF-Einträge zur Anmeldung laden
const pdfs = await prisma.pdfDatei.findMany({ 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 // 3. PDF-Datensätze aus DB löschen
await prisma.pdfDatei.deleteMany({ await prisma.pdfDatei.deleteMany({
where: {anmeldungId: id} where: { anmeldungId: id }
}); });
// Anmeldung löschen // 4. Anmeldung löschen
await prisma.anmeldung.delete({where: { id } }); await prisma.anmeldung.delete({
return json({ ok: true }); where: { id }
});
return json({ success: true, message: 'Anmeldung erfolgreich gelöscht' });
} catch (error) { } catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error); console.error('Fehler beim Löschen der Anmeldung:', error);
return json({ error: 'Löschen fehlgeschlagen' }, { status: 500 }); return json({ error: 'Löschen fehlgeschlagen' }, { status: 500 });
} }
}; };

View File

@@ -1,3 +1,4 @@
// src/routes/api/admin/dienststellen/+server.ts
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
@@ -6,73 +7,187 @@ const prisma = new PrismaClient();
import type { Cookies } from '@sveltejs/kit'; import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) { function checkAuth(cookies: Cookies) {
return cookies.get('admin_session') === 'true'; return cookies.get('admin-auth') === 'authenticated';
} }
export const GET: RequestHandler = async ({ cookies }) => { export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } }); return new Response(
return json(dienststellen); 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 }) => { export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
const { name, plaetze } = await request.json(); return json({ error: 'Nicht autorisiert' }, { status: 401 });
if (typeof plaetze !== 'number' || plaetze < 0) {
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
} }
try { try {
const created = await prisma.dienststelle.create({ data: { const { name, plaetze } = await request.json();
name,
plaetze, // Validierung
} }); if (!name || typeof name !== 'string' || name.trim().length === 0) {
return json(created); return json({ error: 'Name ist erforderlich' }, { status: 400 });
} catch (e) { }
console.error('Fehler beim Hinzufuegen:', e);
return json({ error: 'Dienststelle existiert bereits' }, { 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 }) => { export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
const { id, 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 });
} }
try { 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({ const updated = await prisma.dienststelle.update({
where: { id }, where: { id },
data: { name, plaetze }, data: {
name: name.trim(),
plaetze
},
}); });
return json(updated); return json(updated);
} catch (e) { } catch (error) {
console.error('Fehler beim Update:', e); console.error('Fehler beim Aktualisieren der Dienststelle:', error);
return json({ error: 'Update fehlgeschlagen' }, { status: 400 }); return json({ error: 'Fehler beim Aktualisieren der Dienststelle' }, { status: 500 });
} }
}; };
export const DELETE: RequestHandler = async ({ cookies, url }) => { export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) return new Response('Nicht erlaubt', { status: 401 }); if (!checkAuth(cookies)) {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id')); const id = Number(url.searchParams.get('id'));
await prisma.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 });
}
}; };

View File

@@ -1,6 +1,15 @@
// src/routes/api/admin/logout/+server.ts
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ cookies }) => { export const POST: RequestHandler = async ({ cookies }) => {
cookies.delete('admin_session', { path: '/' }); // Cookie löschen mit korrektem Namen
return new Response('Ausgeloggt'); cookies.delete('admin-auth', { path: '/' });
return new Response(
JSON.stringify({ success: true, message: 'Erfolgreich ausgeloggt' }),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}; };