Anmeldungen Bearbeitung hinzugefuegt, leider die Anzeige ist noch nicht voll funktionsfaehig
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `noteDeutsch` on the `anmeldungen` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`.
|
||||
- You are about to alter the column `noteMathe` on the `anmeldungen` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`.
|
||||
- You are about to alter the column `timestamp` on the `anmeldungen` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `DateTime`.
|
||||
- Added the required column `geburtsdatum` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `hausnummer` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `ort` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `plz` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `schulart` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `strasse` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `telefon` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
|
||||
- Made the column `noteDeutsch` on table `anmeldungen` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `noteMathe` on table `anmeldungen` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_anmeldungen" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"anrede" TEXT NOT NULL,
|
||||
"vorname" TEXT NOT NULL,
|
||||
"nachname" TEXT NOT NULL,
|
||||
"geburtsdatum" TEXT NOT NULL,
|
||||
"strasse" TEXT NOT NULL,
|
||||
"hausnummer" TEXT NOT NULL,
|
||||
"ort" TEXT NOT NULL,
|
||||
"plz" TEXT NOT NULL,
|
||||
"telefon" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"schulart" TEXT NOT NULL,
|
||||
"schulklasse" TEXT,
|
||||
"noteDeutsch" INTEGER NOT NULL,
|
||||
"noteMathe" INTEGER NOT NULL,
|
||||
"sozialverhalten" TEXT,
|
||||
"motivation" TEXT,
|
||||
"alter" INTEGER,
|
||||
"status" TEXT NOT NULL DEFAULT 'OFFEN',
|
||||
"processedBy" TEXT,
|
||||
"processedAt" DATETIME,
|
||||
"praktikumId" INTEGER,
|
||||
"zugewiesenId" INTEGER,
|
||||
"wunsch1Id" INTEGER,
|
||||
"wunsch2Id" INTEGER,
|
||||
"wunsch3Id" INTEGER,
|
||||
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "anmeldungen_praktikumId_fkey" FOREIGN KEY ("praktikumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "anmeldungen_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "anmeldungen_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "anmeldungen_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "anmeldungen_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_anmeldungen" ("anrede", "email", "id", "nachname", "noteDeutsch", "noteMathe", "sozialverhalten", "status", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId") SELECT "anrede", "email", "id", "nachname", "noteDeutsch", "noteMathe", "sozialverhalten", "status", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId" FROM "anmeldungen";
|
||||
DROP TABLE "anmeldungen";
|
||||
ALTER TABLE "new_anmeldungen" RENAME TO "anmeldungen";
|
||||
CREATE INDEX "anmeldungen_status_idx" ON "anmeldungen"("status");
|
||||
CREATE INDEX "anmeldungen_processedAt_idx" ON "anmeldungen"("processedAt");
|
||||
CREATE INDEX "anmeldungen_zugewiesenId_idx" ON "anmeldungen"("zugewiesenId");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -40,10 +40,12 @@ model Praktikumszeitraum {
|
||||
anmeldungen Anmeldung[] @relation("PraktikumszeitraumAnmeldungen")
|
||||
}
|
||||
|
||||
// Erweiterte Status-Enum für bessere Nachverfolgung
|
||||
enum Status {
|
||||
OFFEN
|
||||
ANGENOMMEN
|
||||
ABGELEHNT
|
||||
OFFEN // pending - neu eingegangen
|
||||
BEARBEITUNG // processing - wird gerade bearbeitet
|
||||
ANGENOMMEN // accepted - wurde angenommen
|
||||
ABGELEHNT // rejected - wurde abgelehnt
|
||||
}
|
||||
|
||||
model Anmeldung {
|
||||
@@ -67,6 +69,10 @@ model Anmeldung {
|
||||
alter Int? // Neu hinzugefügt für Altersvalidierung
|
||||
status Status @default(OFFEN)
|
||||
|
||||
// Neue Felder für Status-Tracking
|
||||
processedBy String? // Wer bearbeitet die Anmeldung
|
||||
processedAt DateTime? // Wann wurde sie bearbeitet
|
||||
|
||||
// Praktikumszeitraum Relation
|
||||
praktikumId Int?
|
||||
praktikum Praktikumszeitraum? @relation("PraktikumszeitraumAnmeldungen", fields: [praktikumId], references: [id])
|
||||
@@ -84,6 +90,10 @@ model Anmeldung {
|
||||
timestamp DateTime @default(now())
|
||||
pdfs PdfDatei[]
|
||||
|
||||
// Indizes für bessere Performance
|
||||
@@index([status])
|
||||
@@index([processedAt])
|
||||
@@index([zugewiesenId])
|
||||
@@map("anmeldungen")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,16 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let anmeldungen: any[];
|
||||
export let getStatusColor: (status: string) => string;
|
||||
export let getStatusText: (status: string) => string;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
accept: { id: number };
|
||||
reject: { id: number };
|
||||
delete: { id: number };
|
||||
}>();
|
||||
|
||||
interface Anmeldung {
|
||||
pdfs: { pfad: string }[];
|
||||
anrede: string;
|
||||
vorname: string;
|
||||
nachname: string;
|
||||
email: string;
|
||||
noteDeutsch?: string;
|
||||
noteMathe?: string;
|
||||
sozialverhalten?: string;
|
||||
wunsch1?: { id: number; name: string };
|
||||
wunsch2?: { id: number; name: string };
|
||||
wunsch3?: { id: number; name: string };
|
||||
timestamp: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export let anmeldungen: Anmeldung[];
|
||||
|
||||
function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
@@ -36,16 +22,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleAccept(id: number) {
|
||||
dispatch('accept', { id });
|
||||
function formatProcessedDate(timestamp: number | undefined): string {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function handleReject(id: number) {
|
||||
dispatch('reject', { id });
|
||||
function canBeAccepted(status: string): boolean {
|
||||
return status === 'pending' || status === 'processing';
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
dispatch('delete', { id });
|
||||
function canBeRejected(status: string): boolean {
|
||||
return status === 'pending' || status === 'processing';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -54,7 +47,10 @@
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Bewerber
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Bewerber/in
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Noten
|
||||
@@ -63,129 +59,174 @@
|
||||
Wünsche
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Dokumente
|
||||
Zugewiesen
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Anmeldung
|
||||
Eingegangen
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Bearbeitet
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each anmeldungen as anmeldung (anmeldung.id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<!-- Bewerber Info -->
|
||||
<tr class="hover:bg-gray-50" class:bg-blue-50={anmeldung.status === 'processing'}>
|
||||
<!-- Status -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {getStatusColor(anmeldung.status || 'pending')}">
|
||||
{getStatusText(anmeldung.status || 'pending')}
|
||||
</span>
|
||||
{#if anmeldung.status === 'processing' && anmeldung.processedBy}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
von {anmeldung.processedBy}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Bewerber/in -->
|
||||
<td class="px-6 py-4">
|
||||
<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>
|
||||
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
|
||||
<div class="mt-1">
|
||||
{#each anmeldung.pdfs as pdf}
|
||||
<a
|
||||
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
|
||||
target="_blank"
|
||||
class="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mr-2"
|
||||
>
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
PDF ansehen
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- 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 class="text-xs text-gray-600 mt-2">
|
||||
<span class="font-medium">Sozialverhalten:</span><br>
|
||||
{anmeldung.sozialverhalten}
|
||||
</div>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{#if anmeldung.noteDeutsch || anmeldung.noteMathe}
|
||||
<div class="space-y-1">
|
||||
{#if anmeldung.noteDeutsch}
|
||||
<div>D: {anmeldung.noteDeutsch}</div>
|
||||
{/if}
|
||||
{#if anmeldung.noteMathe}
|
||||
<div>M: {anmeldung.noteMathe}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-gray-400">-</span>
|
||||
{/if}
|
||||
{#if anmeldung.sozialverhalten}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
SV: {anmeldung.sozialverhalten}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Wünsche -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="space-y-2 text-sm">
|
||||
<td class="px-6 py-4 text-sm text-gray-900">
|
||||
<div class="space-y-1">
|
||||
{#if anmeldung.wunsch1}
|
||||
<div class="flex items-center">
|
||||
<span class="inline-flex items-center 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>
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-medium text-white bg-blue-600 rounded-full mr-2">1</span>
|
||||
{anmeldung.wunsch1.name}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if anmeldung.wunsch2}
|
||||
<div class="flex items-center">
|
||||
<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>
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-medium text-white bg-blue-500 rounded-full mr-2">2</span>
|
||||
{anmeldung.wunsch2.name}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if anmeldung.wunsch3}
|
||||
<div class="flex items-center">
|
||||
<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>
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-medium text-white bg-blue-400 rounded-full mr-2">3</span>
|
||||
{anmeldung.wunsch3.name}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 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="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" />
|
||||
<!-- Zugewiesen -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{#if anmeldung.assignedDienststelle}
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2" 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>
|
||||
PDF {index + 1}
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
<span class="font-medium">{anmeldung.assignedDienststelle.name}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-gray-400">-</span>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Anmeldedatum -->
|
||||
<!-- Eingegangen -->
|
||||
<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 flex-col space-y-2">
|
||||
<button
|
||||
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="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>
|
||||
<!-- Bearbeitet -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<div>{formatProcessedDate(anmeldung.processedAt)}</div>
|
||||
{#if anmeldung.processedBy && anmeldung.processedAt}
|
||||
<div class="text-xs text-gray-400">
|
||||
von {anmeldung.processedBy}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
{#if canBeAccepted(anmeldung.status || 'pending')}
|
||||
<button
|
||||
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"
|
||||
on:click={() => dispatch('accept', { id: anmeldung.id })}
|
||||
class="text-green-600 hover:text-green-900 px-2 py-1 rounded text-xs font-medium border border-green-200 hover:bg-green-50"
|
||||
class:opacity-50={anmeldung.status === 'processing'}
|
||||
class:cursor-not-allowed={anmeldung.status === 'processing'}
|
||||
>
|
||||
{anmeldung.status === 'processing' ? 'Wird bearbeitet' : 'Annehmen'}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if canBeRejected(anmeldung.status || 'pending')}
|
||||
<button
|
||||
on:click={() => dispatch('reject', { id: anmeldung.id })}
|
||||
class="text-red-600 hover:text-red-900 px-2 py-1 rounded text-xs font-medium border border-red-200 hover:bg-red-50"
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
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"
|
||||
on:click={() => dispatch('delete', { id: anmeldung.id })}
|
||||
class="text-gray-600 hover:text-gray-900 px-2 py-1 rounded text-xs font-medium border border-gray-200 hover:bg-gray-50"
|
||||
>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Warnung bei "In Bearbeitung" Status -->
|
||||
{#if anmeldung.status === 'processing'}
|
||||
<div class="mt-2 flex items-center text-xs text-amber-600">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Wird bereits bearbeitet
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -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">
|
||||
<!-- 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 -->
|
||||
<div class="mb-6 flex justify-end">
|
||||
<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}
|
||||
|
||||
@@ -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 (!existingAnmeldung) {
|
||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (existingAnmeldung.status === 'ANGENOMMEN') {
|
||||
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
||||
}
|
||||
|
||||
// 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({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Annehmen der Anmeldung:', error);
|
||||
return json({ error: 'Fehler beim Annehmen der Anmeldung' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH({ request, url }) {
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
|
||||
// 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 (anmeldung.status !== 'OFFEN') {
|
||||
return json({ error: 'Anmeldung kann nicht mehr bearbeitet werden' }, { status: 409 });
|
||||
}
|
||||
|
||||
if (dienststelle.plaetze <= 0) {
|
||||
return json({ error: 'Keine verfügbaren Plätze bei dieser Dienststelle' }, { status: 409 });
|
||||
}
|
||||
updateData = {
|
||||
status: 'BEARBEITUNG',
|
||||
processedBy: processedBy || 'current_user',
|
||||
processedAt: new Date()
|
||||
};
|
||||
break;
|
||||
|
||||
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) {
|
||||
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}` });
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
// 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' });
|
||||
}
|
||||
case 'reset_processing':
|
||||
updateData = {
|
||||
status: 'OFFEN',
|
||||
processedBy: null,
|
||||
processedAt: null
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
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 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({
|
||||
where: { anmeldungId: id }
|
||||
const result = await prisma.anmeldung.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
// 2. Dateien vom Dateisystem löschen
|
||||
for (const pdf of pdfs) {
|
||||
const filePath = path.resolve('static', pdf.pfad.replace(/^\/+/, ''));
|
||||
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 {
|
||||
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
|
||||
}
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user