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 {
id Int @id @default(autoincrement())
anrede String
vorname String
nachname String
geburtsdatum String
strasse String
hausnummer String
ort String
plz String
telefon String
email String @unique
noteDeutsch Int
noteMathe Int
sozialverhalten String
schulart String
motivation String
id Int @id @default(autoincrement())
anrede String
vorname String
nachname String
email String
noteDeutsch String?
noteMathe String?
sozialverhalten String?
status Status @default(OFFEN)
zugewiesenId Int? // ID der zugewiesenen Dienststelle
zugewiesen Dienststelle? @relation(fields: [zugewiesenId], references: [id])
wunsch1Id Int?
wunsch1 Dienststelle? @relation("Wunsch1", fields: [wunsch1Id], references: [id])
wunsch2Id Int?
wunsch2 Dienststelle? @relation("Wunsch2", fields: [wunsch2Id], references: [id])
wunsch3Id Int?
wunsch3 Dienststelle? @relation("Wunsch3", fields: [wunsch3Id], references: [id])
timestamp BigInt
pdfs PdfDatei[]
praktikumId Int
wunsch1 Dienststelle @relation("Wunsch1", fields: [wunsch1Id], references: [id])
wunsch1Id Int
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")
@@map("anmeldungen")
}
model PdfDatei {

View File

@@ -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;
@@ -20,24 +26,26 @@
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

View File

@@ -2,6 +2,11 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
confirm: { dienststelleId: number };
cancel: void;
}>();
interface Wish {
id: number;
name: string;
@@ -10,113 +15,124 @@
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>
<!-- 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>
<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>
<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">
<p class="text-sm text-gray-500">Keine Wünsche verfügbar</p>
</div>
{/if}
</div>
</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>
{/if}
</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>
<!-- 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>

View File

@@ -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'
};
};

View File

@@ -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'
};
};

View File

@@ -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 });
}
};

View File

@@ -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 });
}
};

View File

@@ -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' }
}
);
};