pdf erforderlich, decrement wenn Dienstelle zugewiesen wurde

This commit is contained in:
titver968
2025-07-26 16:48:46 +02:00
parent 043704d0a4
commit 84f9aab3c0
8 changed files with 223 additions and 223 deletions

View File

@@ -0,0 +1,14 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_PdfDatei" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pfad" TEXT NOT NULL,
"anmeldungId" INTEGER NOT NULL,
CONSTRAINT "PdfDatei_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "anmeldungen" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_PdfDatei" ("anmeldungId", "id", "pfad") SELECT "anmeldungId", "id", "pfad" FROM "PdfDatei";
DROP TABLE "PdfDatei";
ALTER TABLE "new_PdfDatei" RENAME TO "PdfDatei";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,50 @@
/*
Warnings:
- You are about to drop the column `processedBy` on the `anmeldungen` table. All the data in the column will be lost.
*/
-- 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',
"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" ("alter", "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "processedAt", "schulart", "schulklasse", "sozialverhalten", "status", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId") SELECT "alter", "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "processedAt", "schulart", "schulklasse", "sozialverhalten", "status", "strasse", "telefon", "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;

View File

@@ -0,0 +1,14 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Dienststelle" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"plaetze" INTEGER NOT NULL DEFAULT 0
);
INSERT INTO "new_Dienststelle" ("id", "name", "plaetze") SELECT "id", "name", "plaetze" FROM "Dienststelle";
DROP TABLE "Dienststelle";
ALTER TABLE "new_Dienststelle" RENAME TO "Dienststelle";
CREATE UNIQUE INDEX "Dienststelle_name_key" ON "Dienststelle"("name");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -25,7 +25,7 @@ model EmailConfig {
model Dienststelle {
id Int @id @default(autoincrement())
name String @unique
plaetze Int
plaetze Int @default(0)
anmeldungenWunsch1 Anmeldung[] @relation("Wunsch1")
anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2")
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
@@ -70,7 +70,7 @@ model Anmeldung {
status Status @default(OFFEN)
// Neue Felder für Status-Tracking
processedBy String? // Wer bearbeitet die Anmeldung
// processedBy String? // Wer bearbeitet die Anmeldung
processedAt DateTime? // Wann wurde sie bearbeitet
// Praktikumszeitraum Relation
@@ -100,6 +100,6 @@ model Anmeldung {
model PdfDatei {
id Int @id @default(autoincrement())
pfad String
anmeldung Anmeldung @relation(fields: [anmeldungId], references: [id])
anmeldung Anmeldung @relation(fields: [anmeldungId], references: [id], onDelete: Cascade)
anmeldungId Int
}

View File

@@ -33,12 +33,13 @@
});
}
// Vereinfachte Logik ohne processing Status
function canBeAccepted(status: string): boolean {
return status === 'pending' || status === 'processing';
return status === 'pending';
}
function canBeRejected(status: string): boolean {
return status === 'pending' || status === 'processing';
return status === 'pending';
}
</script>
@@ -46,62 +47,51 @@
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="min-w-0 w-1/4 px-4 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">
<th class="w-16 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Noten
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Wünsche
<th class="min-w-0 w-1/3 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Wünsche / Zuweisung
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Zugewiesen
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Eingegangen
</th>
<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">
<th class="w-40 px-4 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" class:bg-blue-50={anmeldung.status === 'processing'}>
<!-- Status -->
<td class="px-6 py-4 whitespace-nowrap">
<tr class="hover:bg-gray-50">
<!-- Status (processing Styling entfernt) -->
<td class="px-4 py-4 whitespace-nowrap">
<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">
<td class="px-4 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">
<div class="text-sm text-gray-500 break-all">
{anmeldung.email}
</div>
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
<div class="mt-1">
<div class="mt-2">
{#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"
class="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mr-2 mb-1"
>
<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" />
@@ -114,14 +104,14 @@
</td>
<!-- Noten -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<td class="px-4 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>
<div class="text-xs">D: {anmeldung.noteDeutsch}</div>
{/if}
{#if anmeldung.noteMathe}
<div>M: {anmeldung.noteMathe}</div>
<div class="text-xs">M: {anmeldung.noteMathe}</div>
{/if}
</div>
{:else}
@@ -134,77 +124,79 @@
{/if}
</td>
<!-- Wünsche -->
<td class="px-6 py-4 text-sm text-gray-900">
<div class="space-y-1">
{#if anmeldung.wunsch1}
<!-- Wünsche / Zuweisung -->
<td class="px-4 py-4 text-sm text-gray-900">
<!-- Zugewiesene Dienststelle (falls vorhanden) -->
{#if anmeldung.assignedDienststelle}
<div class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
<div class="flex items-center">
<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}
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" 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>
<span class="font-medium text-green-800 text-sm">Zugewiesen:</span>
</div>
<div class="text-sm text-green-700 mt-1 ml-6">
{anmeldung.assignedDienststelle.name}
</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1 ml-6">
{formatProcessedDate(anmeldung.processedAt)}
{#if anmeldung.processedBy}
von {anmeldung.processedBy}
{/if}
</div>
{/if}
</div>
{/if}
<!-- Wünsche -->
<div class="space-y-2">
<div class="text-xs font-medium text-gray-500 uppercase tracking-wider">Wünsche:</div>
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-600 rounded-full mr-2 mt-0.5 flex-shrink-0">1</span>
<span class="text-sm leading-5">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-center">
<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 class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-500 rounded-full mr-2 mt-0.5 flex-shrink-0">2</span>
<span class="text-sm leading-5">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-center">
<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 class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-400 rounded-full mr-2 mt-0.5 flex-shrink-0">3</span>
<span class="text-sm leading-5">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400 text-sm">Keine Wünsche angegeben</span>
{/if}
</div>
</td>
<!-- 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>
<span class="font-medium">{anmeldung.assignedDienststelle.name}</span>
</div>
{:else}
<span class="text-gray-400">-</span>
{/if}
</td>
<!-- Eingegangen -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(anmeldung.timestamp)}
</td>
<!-- 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 class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="text-sm">{formatDate(anmeldung.timestamp)}</div>
</td>
<!-- Aktionen -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex flex-col space-y-2">
{#if canBeAccepted(anmeldung.status || 'pending')}
<button
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'}
class="text-green-600 hover:text-green-900 px-3 py-1 rounded text-xs font-medium border border-green-200 hover:bg-green-50 w-full text-center"
>
{anmeldung.status === 'processing' ? 'Wird bearbeitet' : 'Annehmen'}
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"
class="text-red-600 hover:text-red-900 px-3 py-1 rounded text-xs font-medium border border-red-200 hover:bg-red-50 w-full text-center"
>
Ablehnen
</button>
@@ -212,21 +204,11 @@
<button
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"
class="text-gray-600 hover:text-gray-900 px-3 py-1 rounded text-xs font-medium border border-gray-200 hover:bg-gray-50 w-full text-center"
>
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}

View File

@@ -20,10 +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
status?: 'pending' | 'accepted' | 'rejected'; // processing entfernt
assignedDienststelle?: { id: number; name: string };
processedBy?: string;
processedAt?: number;
}
interface EmailConfig {
@@ -35,8 +35,8 @@
let isLoading = true;
let error = '';
// Filter für Status
let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' | 'processing' = 'all';
// Filter für Status (processing entfernt)
let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' = 'all';
let filteredAnmeldungen: Anmeldung[] = [];
// Dialog state
@@ -63,11 +63,10 @@ Ihr Praktikumsteam`;
let isLoadingEmailConfig = false;
let isSavingEmailConfig = false;
// Status-Badge Funktionen
// Status-Badge Funktionen (processing entfernt)
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';
@@ -77,7 +76,6 @@ Ihr Praktikumsteam`;
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';
@@ -94,7 +92,7 @@ Ihr Praktikumsteam`;
}
$: {
filterAnmeldungen();
anmeldungen, statusFilter, filterAnmeldungen();
}
async function loadAnmeldungen() {
@@ -105,39 +103,28 @@ Ihr Praktikumsteam`;
const res = await fetch('/api/admin/anmeldungen');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
const errorText = await res.text();
console.error('❌ API Fehler:', res.status, errorText);
throw new Error(`Fehler beim Laden: ${res.status} - ${errorText}`);
}
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);
} finally {
isLoading = false;
}
}
const data = await res.json();
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
})
// Prüfen ob es ein Array ist
if (!Array.isArray(data)) {
console.error('❌ Antwort ist kein Array:', data);
throw new Error('Antwort vom Server ist kein Array');
}
anmeldungen = data.map(a => {
return { ...a, status: a.status || 'pending' };
});
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);
error = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('❌ Frontend Fehler beim Laden der Anmeldungen:', err);
} finally {
isLoading = false;
}
}
@@ -193,16 +180,6 @@ 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}` },
@@ -265,15 +242,6 @@ 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 {
@@ -316,10 +284,6 @@ 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(() => {
@@ -341,7 +305,7 @@ 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 -->
<!-- Status Filter (processing entfernt) -->
<div class="flex items-center space-x-4">
<label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label>
<select
@@ -351,7 +315,6 @@ Ihr Praktikumsteam`;
>
<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>
@@ -374,8 +337,8 @@ Ihr Praktikumsteam`;
</button>
</div>
<!-- Status Übersicht -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- Status Übersicht (processing entfernt) -->
<div class="grid grid-cols-1 md:grid-cols-3 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">
@@ -394,24 +357,6 @@ Ihr Praktikumsteam`;
</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">

View File

@@ -1,4 +1,4 @@
// src/routes/api/admin/anmeldungen/+server.js
// src/routes/api/admin/anmeldungen/+server.ts
import { json } from '@sveltejs/kit';
import { PrismaClient } from '@prisma/client';
@@ -17,59 +17,60 @@ export async function GET() {
pdfs: true
},
orderBy: [
{
status: 'asc' // BEARBEITUNG zuerst, dann OFFEN, etc.
},
{
timestamp: 'desc'
}
]
});
// 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(),
// Noten als String konvertieren (falls sie als Int gespeichert sind)
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
sozialverhalten: anmeldung.sozialverhalten,
// Status-Mapping für Frontend
status: mapPrismaStatusToFrontend(anmeldung.status),
processedBy: anmeldung.processedBy,
processedAt: anmeldung.processedAt?.getTime(),
// processedBy: anmeldung.processedBy,
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
// Wünsche
// Wünsche - sicherstellen dass sie existieren
wunsch1: anmeldung.wunsch1 ? {
id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name
} : null,
} : undefined,
wunsch2: anmeldung.wunsch2 ? {
id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name
} : null,
} : undefined,
wunsch3: anmeldung.wunsch3 ? {
id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name
} : null,
} : undefined,
// Zugewiesene Dienststelle
assignedDienststelle: anmeldung.zugewiesen ? {
id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name
} : null,
} : undefined,
timestamp: anmeldung.timestamp.getTime(),
pdfs: anmeldung.pdfs
// Timestamp
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
// PDFs
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 });
return json({ error: 'Fehler beim Laden der Anmeldungen', details: error.message }, { status: 500 });
}
}
@@ -82,7 +83,7 @@ export async function POST({ request, url }) {
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
}
// Prüfen ob Anmeldung existiert und bearbeitet werden kann
// Prüfen ob Anmeldung existiert
const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id }
});
@@ -96,27 +97,37 @@ export async function POST({ request, url }) {
}
// 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()
await prisma.$transaction([
prisma.anmeldung.update({
where: { id },
data: {
status: 'ANGENOMMEN',
zugewiesenId: dienststelleId,
processedAt: new Date()
}
}),
prisma.dienststelle.update({
where: { id: dienststelleId },
data: {
plaetze: {
decrement: 1
}
}
});
})
]);
return json({ success: true });
} catch (error) {
console.error('Fehler beim Annehmen der Anmeldung:', error);
return json({ error: 'Fehler beim Annehmen der Anmeldung' }, { status: 500 });
return json({ error: 'Fehler beim Annehmen der Anmeldung', details: error.message }, { status: 500 });
}
}
export async function PATCH({ request, url }) {
try {
const id = parseInt(url.searchParams.get('id') || '0');
const { action, processedBy } = await request.json();
const { action } = await request.json();
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
@@ -128,13 +139,11 @@ export async function PATCH({ request, url }) {
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 }
});
@@ -143,13 +152,8 @@ export async function PATCH({ request, url }) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
}
if (anmeldung.status !== 'OFFEN') {
return json({ error: 'Anmeldung kann nicht mehr bearbeitet werden' }, { status: 409 });
}
updateData = {
status: 'BEARBEITUNG',
processedBy: processedBy || 'current_user',
processedAt: new Date()
};
break;
@@ -157,7 +161,6 @@ export async function PATCH({ request, url }) {
case 'reset_processing':
updateData = {
status: 'OFFEN',
processedBy: null,
processedAt: null
};
break;
@@ -166,7 +169,7 @@ export async function PATCH({ request, url }) {
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
}
const result = await prisma.anmeldung.update({
await prisma.anmeldung.update({
where: { id },
data: updateData
});
@@ -174,7 +177,7 @@ export async function PATCH({ request, url }) {
return json({ success: true });
} catch (error) {
console.error('Fehler beim Aktualisieren der Anmeldung:', error);
return json({ error: 'Fehler beim Aktualisieren der Anmeldung' }, { status: 500 });
return json({ error: 'Fehler beim Aktualisieren der Anmeldung', details: error.message }, { status: 500 });
}
}
@@ -193,7 +196,7 @@ export async function DELETE({ url }) {
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error);
return json({ error: 'Fehler beim Löschen der Anmeldung' }, { status: 500 });
return json({ error: 'Fehler beim Löschen der Anmeldung', details: error.message }, { status: 500 });
}
}
@@ -208,15 +211,3 @@ function mapPrismaStatusToFrontend(prismaStatus) {
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';
}

View File

@@ -12,6 +12,10 @@ export async function POST({ request }: RequestEvent) {
// const pdfs = formData.getAll('pdfs') as File[];
const pdfFiles = formData.getAll('pdfs') as File[];
const hasValidPdf = pdfFiles.some((file) => file.size > 0 && file.type === 'application/pdf');
if (!hasValidPdf) {
return json({ error: 'Bitte lade das Zeugnis hoch in PDF Format.' }, { status: 400 });
}
const pdfData = [];
// const gespeichertePfade: string[] = [];