Anmeldungen Bearbeitung hinzugefuegt, leider die Anzeige ist noch nicht voll funktionsfaehig

This commit is contained in:
titver968
2025-07-26 12:10:45 +02:00
parent 1b4f37ec87
commit 043704d0a4
5 changed files with 584 additions and 274 deletions

View File

@@ -20,6 +20,10 @@
wunsch3?: { id: number; name: string };
timestamp: number;
id: number;
status?: 'pending' | 'accepted' | 'rejected' | 'processing'; // Neuer Status
assignedDienststelle?: { id: number; name: string }; // Zugewiesene Dienststelle
processedBy?: string; // Wer die Anmeldung bearbeitet
processedAt?: number; // Wann bearbeitet
}
interface EmailConfig {
@@ -31,6 +35,10 @@
let isLoading = true;
let error = '';
// Filter für Status
let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' | 'processing' = 'all';
let filteredAnmeldungen: Anmeldung[] = [];
// Dialog state
let showDialog = false;
let selectedAnmeldungId: number | null = null;
@@ -55,6 +63,40 @@ Ihr Praktikumsteam`;
let isLoadingEmailConfig = false;
let isSavingEmailConfig = false;
// Status-Badge Funktionen
function getStatusColor(status: string): string {
switch (status) {
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'processing': return 'bg-blue-100 text-blue-800';
case 'accepted': return 'bg-green-100 text-green-800';
case 'rejected': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
}
function getStatusText(status: string): string {
switch (status) {
case 'pending': return 'Offen';
case 'processing': return 'In Bearbeitung';
case 'accepted': return 'Angenommen';
case 'rejected': return 'Abgelehnt';
default: return 'Unbekannt';
}
}
// Filter-Funktionen
function filterAnmeldungen() {
if (statusFilter === 'all') {
filteredAnmeldungen = anmeldungen;
} else {
filteredAnmeldungen = anmeldungen.filter(a => (a.status || 'pending') === statusFilter);
}
}
$: {
filterAnmeldungen();
}
async function loadAnmeldungen() {
try {
isLoading = true;
@@ -67,6 +109,8 @@ Ihr Praktikumsteam`;
}
anmeldungen = await res.json();
// Standardstatus setzen falls nicht vorhanden
anmeldungen = anmeldungen.map(a => ({ ...a, status: a.status || 'pending' }));
} catch (err) {
error = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Anmeldungen:', err);
@@ -75,6 +119,28 @@ Ihr Praktikumsteam`;
}
}
async function setProcessingStatus(anmeldungId: number) {
try {
const res = await fetch(`/api/admin/anmeldungen?id=${anmeldungId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'set_processing',
processedBy: 'current_user' // Hier sollte der aktuelle Benutzer stehen
})
});
if (!res.ok) {
throw new Error(`Fehler beim Setzen des Status: ${res.status}`);
}
await loadAnmeldungen();
} catch (err) {
error = err instanceof Error ? err.message : 'Fehler beim Setzen des Status';
console.error(err);
}
}
async function loadEmailConfig() {
try {
isLoadingEmailConfig = true;
@@ -127,6 +193,16 @@ Ihr Praktikumsteam`;
const anmeldung = anmeldungen.find(a => a.id === event.detail.id);
if (!anmeldung) return;
// Prüfen ob bereits bearbeitet wird
if (anmeldung.status === 'processing') {
if (!confirm('Diese Anmeldung wird bereits bearbeitet. Trotzdem fortfahren?')) {
return;
}
}
// Status auf "in Bearbeitung" setzen
setProcessingStatus(event.detail.id);
availableWishes = [
anmeldung.wunsch1 && { id: anmeldung.wunsch1.id, name: `1. Wunsch: ${anmeldung.wunsch1.name}` },
anmeldung.wunsch2 && { id: anmeldung.wunsch2.id, name: `2. Wunsch: ${anmeldung.wunsch2.name}` },
@@ -189,6 +265,15 @@ Ihr Praktikumsteam`;
}
async function handleReject(event: CustomEvent<{id: number}>) {
const anmeldung = anmeldungen.find(a => a.id === event.detail.id);
// Prüfen ob bereits bearbeitet wird
if (anmeldung?.status === 'processing') {
if (!confirm('Diese Anmeldung wird bereits bearbeitet. Trotzdem ablehnen?')) {
return;
}
}
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
try {
@@ -231,6 +316,10 @@ Ihr Praktikumsteam`;
function closeDialog() {
showDialog = false;
selectedAnmeldungId = null;
// Status zurücksetzen falls Dialog abgebrochen wird
if (selectedAnmeldungId) {
// Hier könnten Sie den Status zurück auf "pending" setzen
}
}
onMount(() => {
@@ -250,8 +339,25 @@ Ihr Praktikumsteam`;
/>
<main class="max-w-7xl mx-auto px-4 py-6">
<!-- E-Mail Konfiguration Button -->
<div class="mb-6 flex justify-end">
<!-- Filter und E-Mail Konfiguration -->
<div class="mb-6 flex justify-between items-center">
<!-- Status Filter -->
<div class="flex items-center space-x-4">
<label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label>
<select
id="status-filter"
bind:value={statusFilter}
class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">Alle ({anmeldungen.length})</option>
<option value="pending">Offen ({anmeldungen.filter(a => (a.status || 'pending') === 'pending').length})</option>
<option value="processing">In Bearbeitung ({anmeldungen.filter(a => a.status === 'processing').length})</option>
<option value="accepted">Angenommen ({anmeldungen.filter(a => a.status === 'accepted').length})</option>
<option value="rejected">Abgelehnt ({anmeldungen.filter(a => a.status === 'rejected').length})</option>
</select>
</div>
<!-- E-Mail Konfiguration Button -->
<button
on:click={() => showEmailConfig = !showEmailConfig}
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
@@ -268,6 +374,81 @@ Ihr Praktikumsteam`;
</button>
</div>
<!-- Status Übersicht -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Offen</p>
<p class="text-2xl font-semibold text-gray-900">
{anmeldungen.filter(a => (a.status || 'pending') === 'pending').length}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">In Bearbeitung</p>
<p class="text-2xl font-semibold text-gray-900">
{anmeldungen.filter(a => a.status === 'processing').length}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Angenommen</p>
<p class="text-2xl font-semibold text-gray-900">
{anmeldungen.filter(a => a.status === 'accepted').length}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Abgelehnt</p>
<p class="text-2xl font-semibold text-gray-900">
{anmeldungen.filter(a => a.status === 'rejected').length}
</p>
</div>
</div>
</div>
</div>
<!-- E-Mail Konfiguration Panel -->
{#if showEmailConfig}
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
@@ -349,18 +530,26 @@ Ihr Praktikumsteam`;
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-3 text-gray-600">Lade Anmeldungen...</span>
</div>
{:else if anmeldungen.length === 0}
{:else if filteredAnmeldungen.length === 0}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Anmeldungen</h3>
<p class="mt-1 text-sm text-gray-500">Es sind noch keine Praktikumsanmeldungen eingegangen.</p>
<h3 class="mt-2 text-sm font-medium text-gray-900">
{statusFilter === 'all' ? 'Keine Anmeldungen' : `Keine ${getStatusText(statusFilter).toLowerCase()}en Anmeldungen`}
</h3>
<p class="mt-1 text-sm text-gray-500">
{statusFilter === 'all'
? 'Es sind noch keine Praktikumsanmeldungen eingegangen.'
: `Es gibt keine Anmeldungen mit dem Status "${getStatusText(statusFilter).toLowerCase()}".`}
</p>
</div>
{:else}
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<AnmeldungenTable
{anmeldungen}
anmeldungen={filteredAnmeldungen}
{getStatusColor}
{getStatusText}
on:accept={handleAccept}
on:reject={handleReject}
on:delete={handleDelete}

View File

@@ -1,214 +1,222 @@
// src/routes/api/admin/anmeldungen/+server.ts
import { PrismaClient, Status } from '@prisma/client';
// src/routes/api/admin/anmeldungen/+server.js
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import fs from 'fs/promises';
import path from 'path';
import { PrismaClient } from '@prisma/client';
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-auth') === 'authenticated';
}
export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) {
return new Response(
JSON.stringify({ error: 'Nicht autorisiert' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
}
export async function GET() {
try {
const anmeldungen = await prisma.anmeldung.findMany({
include: {
wunsch1: true,
wunsch2: true,
wunsch3: true,
zugewiesen: true,
praktikum: true,
pdfs: true
},
orderBy: { timestamp: 'desc' }
orderBy: [
{
status: 'asc' // BEARBEITUNG zuerst, dann OFFEN, etc.
},
{
timestamp: 'desc'
}
]
});
return new Response(JSON.stringify(anmeldungen), {
headers: { 'Content-Type': 'application/json' }
});
// Daten für Frontend formatieren
const formattedAnmeldungen = anmeldungen.map(anmeldung => ({
id: anmeldung.id,
anrede: anmeldung.anrede,
vorname: anmeldung.vorname,
nachname: anmeldung.nachname,
email: anmeldung.email,
noteDeutsch: anmeldung.noteDeutsch?.toString(),
noteMathe: anmeldung.noteMathe?.toString(),
sozialverhalten: anmeldung.sozialverhalten,
// Status-Mapping für Frontend
status: mapPrismaStatusToFrontend(anmeldung.status),
processedBy: anmeldung.processedBy,
processedAt: anmeldung.processedAt?.getTime(),
// Wünsche
wunsch1: anmeldung.wunsch1 ? {
id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name
} : null,
wunsch2: anmeldung.wunsch2 ? {
id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name
} : null,
wunsch3: anmeldung.wunsch3 ? {
id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name
} : null,
// Zugewiesene Dienststelle
assignedDienststelle: anmeldung.zugewiesen ? {
id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name
} : null,
timestamp: anmeldung.timestamp.getTime(),
pdfs: anmeldung.pdfs
}));
return json(formattedAnmeldungen);
} 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, 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 });
}
export async function POST({ request, url }) {
try {
// Prüfe ob eine spezifische Dienststelle zugewiesen werden soll
const body = await request.json().catch(() => ({}));
const dienststelleId = body.dienststelleId;
const id = parseInt(url.searchParams.get('id') || '0');
const { dienststelleId } = await request.json();
const anmeldung = await prisma.anmeldung.findUnique({
where: { id },
include: {
wunsch1: true,
wunsch2: true,
wunsch3: true
}
if (!id || !dienststelleId) {
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
}
// Prüfen ob Anmeldung existiert und bearbeitet werden kann
const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id }
});
if (!anmeldung) {
if (!existingAnmeldung) {
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}` });
if (existingAnmeldung.status === 'ANGENOMMEN') {
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
}
// Fallback: Automatische Zuweisung nach Wunschreihenfolge
const wuensche = [anmeldung.wunsch1, anmeldung.wunsch2, anmeldung.wunsch3];
for (const wunsch of wuensche) {
if (wunsch && wunsch.plaetze > 0) {
await prisma.$transaction([
prisma.anmeldung.update({
where: { id },
data: {
status: Status.ANGENOMMEN,
zugewiesenId: wunsch.id
}
}),
prisma.dienststelle.update({
where: { id: wunsch.id },
data: {
plaetze: { decrement: 1 }
}
})
]);
return json({ success: true, message: `Zugewiesen an: ${wunsch.name}` });
// Anmeldung als angenommen markieren
await prisma.anmeldung.update({
where: { id },
data: {
status: 'ANGENOMMEN',
zugewiesenId: dienststelleId,
processedBy: 'current_user', // TODO: Echten Benutzer verwenden
processedAt: new Date()
}
}
});
return json({ error: 'Keine verfügbaren Plätze bei Wunsch-Dienststellen' }, { status: 409 });
} catch (err) {
console.error('Fehler beim Annehmen der Anmeldung:', err);
return json({ error: 'Interner Serverfehler' }, { status: 500 });
return json({ success: true });
} catch (error) {
console.error('Fehler beim Annehmen der Anmeldung:', error);
return json({ error: 'Fehler beim Annehmen der Anmeldung' }, { 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 });
}
export async function PATCH({ request, url }) {
try {
const body = await request.json().catch(() => ({}));
if (body.action === 'reject') {
await prisma.anmeldung.update({
where: { id },
data: {
status: Status.ABGELEHNT
const id = parseInt(url.searchParams.get('id') || '0');
const { action, processedBy } = await request.json();
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
let updateData = {};
switch (action) {
case 'reject':
updateData = {
status: 'ABGELEHNT',
processedBy: 'current_user', // TODO: Echten Benutzer verwenden
processedAt: new Date()
};
break;
case 'set_processing':
// Nur setzen wenn noch OFFEN
const anmeldung = await prisma.anmeldung.findUnique({
where: { id }
});
if (!anmeldung) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
}
if (anmeldung.status !== 'OFFEN') {
return json({ error: 'Anmeldung kann nicht mehr bearbeitet werden' }, { status: 409 });
}
});
return json({ success: true, message: 'Anmeldung abgelehnt' });
updateData = {
status: 'BEARBEITUNG',
processedBy: processedBy || 'current_user',
processedAt: new Date()
};
break;
case 'reset_processing':
updateData = {
status: 'OFFEN',
processedBy: null,
processedAt: null
};
break;
default:
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
}
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
} catch (err) {
console.error('Fehler beim Ablehnen der Anmeldung:', err);
return json({ error: 'Interner Serverfehler' }, { status: 500 });
}
};
const result = await prisma.anmeldung.update({
where: { id },
data: updateData
});
export const DELETE: RequestHandler = async ({ cookies, url }) => {
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 });
return json({ success: true });
} catch (error) {
console.error('Fehler beim Aktualisieren der Anmeldung:', error);
return json({ error: 'Fehler beim Aktualisieren der Anmeldung' }, { status: 500 });
}
}
export async function DELETE({ url }) {
try {
// 1. Alle PDF-Einträge zur Anmeldung laden
const pdfs = await prisma.pdfDatei.findMany({
where: { anmeldungId: id }
});
const id = parseInt(url.searchParams.get('id') || '0');
// 2. Dateien vom Dateisystem löschen
for (const pdf of pdfs) {
const filePath = path.resolve('static', pdf.pfad.replace(/^\/+/, ''));
try {
await fs.unlink(filePath);
} catch (err) {
console.warn(
`Datei konnte nicht gelöscht werden: ${filePath}`,
err instanceof Error ? err.message : String(err)
);
// Fehler ignorieren, Datei evtl. manuell entfernt
}
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// 3. PDF-Datensätze aus DB löschen
await prisma.pdfDatei.deleteMany({
where: { anmeldungId: id }
});
// 4. Anmeldung löschen
await prisma.anmeldung.delete({
where: { id }
});
return json({ success: true, message: 'Anmeldung erfolgreich gelöscht' });
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error);
return json({ error: 'Löschen fehlgeschlagen' }, { status: 500 });
return json({ error: 'Fehler beim Löschen der Anmeldung' }, { status: 500 });
}
};
}
// Hilfsfunktion: Prisma Status zu Frontend Status
function mapPrismaStatusToFrontend(prismaStatus) {
const statusMap = {
'OFFEN': 'pending',
'BEARBEITUNG': 'processing',
'ANGENOMMEN': 'accepted',
'ABGELEHNT': 'rejected'
};
return statusMap[prismaStatus] || 'pending';
}
// Hilfsfunktion: Frontend Status zu Prisma Status
function mapFrontendStatusToPrisma(frontendStatus) {
const statusMap = {
'pending': 'OFFEN',
'processing': 'BEARBEITUNG',
'accepted': 'ANGENOMMEN',
'rejected': 'ABGELEHNT'
};
return statusMap[frontendStatus] || 'OFFEN';
}