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

View File

@@ -33,12 +33,13 @@
}); });
} }
// Vereinfachte Logik ohne processing Status
function canBeAccepted(status: string): boolean { function canBeAccepted(status: string): boolean {
return status === 'pending' || status === 'processing'; return status === 'pending';
} }
function canBeRejected(status: string): boolean { function canBeRejected(status: string): boolean {
return status === 'pending' || status === 'processing'; return status === 'pending';
} }
</script> </script>
@@ -46,62 +47,51 @@
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <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 Status
</th> </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 Bewerber/in
</th> </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 Noten
</th> </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/3 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Wünsche Wünsche / Zuweisung
</th> </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">
Zugewiesen
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Eingegangen Eingegangen
</th> </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">
Bearbeitet
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen Aktionen
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
{#each anmeldungen as anmeldung (anmeldung.id)} {#each anmeldungen as anmeldung (anmeldung.id)}
<tr class="hover:bg-gray-50" class:bg-blue-50={anmeldung.status === 'processing'}> <tr class="hover:bg-gray-50">
<!-- Status --> <!-- Status (processing Styling entfernt) -->
<td class="px-6 py-4 whitespace-nowrap"> <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')}"> <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')} {getStatusText(anmeldung.status || 'pending')}
</span> </span>
{#if anmeldung.status === 'processing' && anmeldung.processedBy}
<div class="text-xs text-gray-500 mt-1">
von {anmeldung.processedBy}
</div>
{/if}
</td> </td>
<!-- Bewerber/in --> <!-- Bewerber/in -->
<td class="px-6 py-4"> <td class="px-4 py-4">
<div class="text-sm font-medium text-gray-900"> <div class="text-sm font-medium text-gray-900">
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname} {anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
</div> </div>
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500 break-all">
{anmeldung.email} {anmeldung.email}
</div> </div>
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0} {#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
<div class="mt-1"> <div class="mt-2">
{#each anmeldung.pdfs as pdf} {#each anmeldung.pdfs as pdf}
<a <a
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}" href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
target="_blank" 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"> <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" /> <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> </td>
<!-- Noten --> <!-- 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} {#if anmeldung.noteDeutsch || anmeldung.noteMathe}
<div class="space-y-1"> <div class="space-y-1">
{#if anmeldung.noteDeutsch} {#if anmeldung.noteDeutsch}
<div>D: {anmeldung.noteDeutsch}</div> <div class="text-xs">D: {anmeldung.noteDeutsch}</div>
{/if} {/if}
{#if anmeldung.noteMathe} {#if anmeldung.noteMathe}
<div>M: {anmeldung.noteMathe}</div> <div class="text-xs">M: {anmeldung.noteMathe}</div>
{/if} {/if}
</div> </div>
{:else} {:else}
@@ -134,77 +124,79 @@
{/if} {/if}
</td> </td>
<!-- Wünsche --> <!-- Wünsche / Zuweisung -->
<td class="px-6 py-4 text-sm text-gray-900"> <td class="px-4 py-4 text-sm text-gray-900">
<div class="space-y-1"> <!-- Zugewiesene Dienststelle (falls vorhanden) -->
{#if anmeldung.wunsch1} {#if anmeldung.assignedDienststelle}
<div class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
<div class="flex items-center"> <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> <svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
{anmeldung.wunsch1.name} <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> </div>
{/if} {/if}
{#if anmeldung.wunsch2} {#if anmeldung.wunsch2}
<div class="flex items-center"> <div class="flex items-start">
<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> <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>
{anmeldung.wunsch2.name} <span class="text-sm leading-5">{anmeldung.wunsch2.name}</span>
</div> </div>
{/if} {/if}
{#if anmeldung.wunsch3} {#if anmeldung.wunsch3}
<div class="flex items-center"> <div class="flex items-start">
<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> <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>
{anmeldung.wunsch3.name} <span class="text-sm leading-5">{anmeldung.wunsch3.name}</span>
</div> </div>
{/if} {/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400 text-sm">Keine Wünsche angegeben</span>
{/if}
</div> </div>
</td> </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 --> <!-- Eingegangen -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(anmeldung.timestamp)} <div class="text-sm">{formatDate(anmeldung.timestamp)}</div>
</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> </td>
<!-- Aktionen --> <!-- Aktionen -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium"> <td class="px-4 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2"> <div class="flex flex-col space-y-2">
{#if canBeAccepted(anmeldung.status || 'pending')} {#if canBeAccepted(anmeldung.status || 'pending')}
<button <button
on:click={() => dispatch('accept', { id: anmeldung.id })} 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="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"
class:opacity-50={anmeldung.status === 'processing'}
class:cursor-not-allowed={anmeldung.status === 'processing'}
> >
{anmeldung.status === 'processing' ? 'Wird bearbeitet' : 'Annehmen'} Annehmen
</button> </button>
{/if} {/if}
{#if canBeRejected(anmeldung.status || 'pending')} {#if canBeRejected(anmeldung.status || 'pending')}
<button <button
on:click={() => dispatch('reject', { id: anmeldung.id })} 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 Ablehnen
</button> </button>
@@ -212,21 +204,11 @@
<button <button
on:click={() => dispatch('delete', { id: anmeldung.id })} 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 Löschen
</button> </button>
</div> </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> </td>
</tr> </tr>
{/each} {/each}

View File

@@ -20,10 +20,10 @@
wunsch3?: { id: number; name: string }; wunsch3?: { id: number; name: string };
timestamp: number; timestamp: number;
id: number; id: number;
status?: 'pending' | 'accepted' | 'rejected' | 'processing'; // Neuer Status status?: 'pending' | 'accepted' | 'rejected'; // processing entfernt
assignedDienststelle?: { id: number; name: string }; // Zugewiesene Dienststelle assignedDienststelle?: { id: number; name: string };
processedBy?: string; // Wer die Anmeldung bearbeitet processedBy?: string;
processedAt?: number; // Wann bearbeitet processedAt?: number;
} }
interface EmailConfig { interface EmailConfig {
@@ -35,8 +35,8 @@
let isLoading = true; let isLoading = true;
let error = ''; let error = '';
// Filter für Status // Filter für Status (processing entfernt)
let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' | 'processing' = 'all'; let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' = 'all';
let filteredAnmeldungen: Anmeldung[] = []; let filteredAnmeldungen: Anmeldung[] = [];
// Dialog state // Dialog state
@@ -63,11 +63,10 @@ Ihr Praktikumsteam`;
let isLoadingEmailConfig = false; let isLoadingEmailConfig = false;
let isSavingEmailConfig = false; let isSavingEmailConfig = false;
// Status-Badge Funktionen // Status-Badge Funktionen (processing entfernt)
function getStatusColor(status: string): string { function getStatusColor(status: string): string {
switch (status) { switch (status) {
case 'pending': return 'bg-yellow-100 text-yellow-800'; 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 'accepted': return 'bg-green-100 text-green-800';
case 'rejected': return 'bg-red-100 text-red-800'; case 'rejected': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800'; default: return 'bg-gray-100 text-gray-800';
@@ -77,7 +76,6 @@ Ihr Praktikumsteam`;
function getStatusText(status: string): string { function getStatusText(status: string): string {
switch (status) { switch (status) {
case 'pending': return 'Offen'; case 'pending': return 'Offen';
case 'processing': return 'In Bearbeitung';
case 'accepted': return 'Angenommen'; case 'accepted': return 'Angenommen';
case 'rejected': return 'Abgelehnt'; case 'rejected': return 'Abgelehnt';
default: return 'Unbekannt'; default: return 'Unbekannt';
@@ -94,7 +92,7 @@ Ihr Praktikumsteam`;
} }
$: { $: {
filterAnmeldungen(); anmeldungen, statusFilter, filterAnmeldungen();
} }
async function loadAnmeldungen() { async function loadAnmeldungen() {
@@ -105,42 +103,31 @@ Ihr Praktikumsteam`;
const res = await fetch('/api/admin/anmeldungen'); const res = await fetch('/api/admin/anmeldungen');
if (!res.ok) { 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(); const data = await res.json();
// Standardstatus setzen falls nicht vorhanden
anmeldungen = anmeldungen.map(a => ({ ...a, status: a.status || 'pending' })); // 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' };
});
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : 'Unbekannter Fehler'; error = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Anmeldungen:', err); console.error('❌ Frontend Fehler beim Laden der Anmeldungen:', err);
} finally { } finally {
isLoading = false; isLoading = false;
} }
} }
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() { async function loadEmailConfig() {
try { try {
isLoadingEmailConfig = true; isLoadingEmailConfig = true;
@@ -193,16 +180,6 @@ Ihr Praktikumsteam`;
const anmeldung = anmeldungen.find(a => a.id === event.detail.id); const anmeldung = anmeldungen.find(a => a.id === event.detail.id);
if (!anmeldung) return; 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 = [ availableWishes = [
anmeldung.wunsch1 && { id: anmeldung.wunsch1.id, name: `1. Wunsch: ${anmeldung.wunsch1.name}` }, anmeldung.wunsch1 && { id: anmeldung.wunsch1.id, name: `1. Wunsch: ${anmeldung.wunsch1.name}` },
anmeldung.wunsch2 && { id: anmeldung.wunsch2.id, name: `2. Wunsch: ${anmeldung.wunsch2.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}>) { 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; if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
try { try {
@@ -316,10 +284,6 @@ Ihr Praktikumsteam`;
function closeDialog() { function closeDialog() {
showDialog = false; showDialog = false;
selectedAnmeldungId = null; selectedAnmeldungId = null;
// Status zurücksetzen falls Dialog abgebrochen wird
if (selectedAnmeldungId) {
// Hier könnten Sie den Status zurück auf "pending" setzen
}
} }
onMount(() => { onMount(() => {
@@ -341,7 +305,7 @@ Ihr Praktikumsteam`;
<main class="max-w-7xl mx-auto px-4 py-6"> <main class="max-w-7xl mx-auto px-4 py-6">
<!-- Filter und E-Mail Konfiguration --> <!-- Filter und E-Mail Konfiguration -->
<div class="mb-6 flex justify-between items-center"> <div class="mb-6 flex justify-between items-center">
<!-- Status Filter --> <!-- Status Filter (processing entfernt) -->
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label> <label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label>
<select <select
@@ -351,7 +315,6 @@ Ihr Praktikumsteam`;
> >
<option value="all">Alle ({anmeldungen.length})</option> <option value="all">Alle ({anmeldungen.length})</option>
<option value="pending">Offen ({anmeldungen.filter(a => (a.status || 'pending') === 'pending').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="accepted">Angenommen ({anmeldungen.filter(a => a.status === 'accepted').length})</option>
<option value="rejected">Abgelehnt ({anmeldungen.filter(a => a.status === 'rejected').length})</option> <option value="rejected">Abgelehnt ({anmeldungen.filter(a => a.status === 'rejected').length})</option>
</select> </select>
@@ -374,8 +337,8 @@ Ihr Praktikumsteam`;
</button> </button>
</div> </div>
<!-- Status Übersicht --> <!-- Status Übersicht (processing entfernt) -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <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="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
@@ -394,24 +357,6 @@ Ihr Praktikumsteam`;
</div> </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="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <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 { json } from '@sveltejs/kit';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
@@ -17,59 +17,60 @@ export async function GET() {
pdfs: true pdfs: true
}, },
orderBy: [ orderBy: [
{
status: 'asc' // BEARBEITUNG zuerst, dann OFFEN, etc.
},
{ {
timestamp: 'desc' timestamp: 'desc'
} }
] ]
}); });
// Daten für Frontend formatieren
const formattedAnmeldungen = anmeldungen.map(anmeldung => ({ const formattedAnmeldungen = anmeldungen.map(anmeldung => ({
id: anmeldung.id, id: anmeldung.id,
anrede: anmeldung.anrede, anrede: anmeldung.anrede,
vorname: anmeldung.vorname, vorname: anmeldung.vorname,
nachname: anmeldung.nachname, nachname: anmeldung.nachname,
email: anmeldung.email, 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, sozialverhalten: anmeldung.sozialverhalten,
// Status-Mapping für Frontend // Status-Mapping für Frontend
status: mapPrismaStatusToFrontend(anmeldung.status), status: mapPrismaStatusToFrontend(anmeldung.status),
processedBy: anmeldung.processedBy, // processedBy: anmeldung.processedBy,
processedAt: anmeldung.processedAt?.getTime(), processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
// Wünsche // Wünsche - sicherstellen dass sie existieren
wunsch1: anmeldung.wunsch1 ? { wunsch1: anmeldung.wunsch1 ? {
id: anmeldung.wunsch1.id, id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name name: anmeldung.wunsch1.name
} : null, } : undefined,
wunsch2: anmeldung.wunsch2 ? { wunsch2: anmeldung.wunsch2 ? {
id: anmeldung.wunsch2.id, id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name name: anmeldung.wunsch2.name
} : null, } : undefined,
wunsch3: anmeldung.wunsch3 ? { wunsch3: anmeldung.wunsch3 ? {
id: anmeldung.wunsch3.id, id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name name: anmeldung.wunsch3.name
} : null, } : undefined,
// Zugewiesene Dienststelle // Zugewiesene Dienststelle
assignedDienststelle: anmeldung.zugewiesen ? { assignedDienststelle: anmeldung.zugewiesen ? {
id: anmeldung.zugewiesen.id, id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name name: anmeldung.zugewiesen.name
} : null, } : undefined,
timestamp: anmeldung.timestamp.getTime(), // Timestamp
pdfs: anmeldung.pdfs timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
// PDFs
pdfs: anmeldung.pdfs || []
})); }));
return json(formattedAnmeldungen); return json(formattedAnmeldungen);
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der Anmeldungen:', 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 }); 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({ const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id } where: { id }
}); });
@@ -96,27 +97,37 @@ export async function POST({ request, url }) {
} }
// Anmeldung als angenommen markieren // Anmeldung als angenommen markieren
await prisma.anmeldung.update({ await prisma.$transaction([
where: { id }, prisma.anmeldung.update({
data: { where: { id },
status: 'ANGENOMMEN', data: {
zugewiesenId: dienststelleId, status: 'ANGENOMMEN',
processedBy: 'current_user', // TODO: Echten Benutzer verwenden zugewiesenId: dienststelleId,
processedAt: new Date() processedAt: new Date()
}
}),
prisma.dienststelle.update({
where: { id: dienststelleId },
data: {
plaetze: {
decrement: 1
}
} }
}); })
]);
return json({ success: true }); return json({ success: true });
} catch (error) { } catch (error) {
console.error('Fehler beim Annehmen der Anmeldung:', 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 }) { export async function PATCH({ request, url }) {
try { try {
const id = parseInt(url.searchParams.get('id') || '0'); const id = parseInt(url.searchParams.get('id') || '0');
const { action, processedBy } = await request.json(); const { action } = await request.json();
if (!id) { if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 }); return json({ error: 'ID erforderlich' }, { status: 400 });
@@ -128,28 +139,21 @@ export async function PATCH({ request, url }) {
case 'reject': case 'reject':
updateData = { updateData = {
status: 'ABGELEHNT', status: 'ABGELEHNT',
processedBy: 'current_user', // TODO: Echten Benutzer verwenden
processedAt: new Date() processedAt: new Date()
}; };
break; break;
case 'set_processing': case 'set_processing':
// Nur setzen wenn noch OFFEN
const anmeldung = await prisma.anmeldung.findUnique({ const anmeldung = await prisma.anmeldung.findUnique({
where: { id } where: { id }
}); });
if (!anmeldung) { if (!anmeldung) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 }); return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
} }
if (anmeldung.status !== 'OFFEN') {
return json({ error: 'Anmeldung kann nicht mehr bearbeitet werden' }, { status: 409 });
}
updateData = { updateData = {
status: 'BEARBEITUNG', status: 'BEARBEITUNG',
processedBy: processedBy || 'current_user',
processedAt: new Date() processedAt: new Date()
}; };
break; break;
@@ -157,7 +161,6 @@ export async function PATCH({ request, url }) {
case 'reset_processing': case 'reset_processing':
updateData = { updateData = {
status: 'OFFEN', status: 'OFFEN',
processedBy: null,
processedAt: null processedAt: null
}; };
break; break;
@@ -166,7 +169,7 @@ export async function PATCH({ request, url }) {
return json({ error: 'Unbekannte Aktion' }, { status: 400 }); return json({ error: 'Unbekannte Aktion' }, { status: 400 });
} }
const result = await prisma.anmeldung.update({ await prisma.anmeldung.update({
where: { id }, where: { id },
data: updateData data: updateData
}); });
@@ -174,7 +177,7 @@ export async function PATCH({ request, url }) {
return json({ success: true }); return json({ success: true });
} catch (error) { } catch (error) {
console.error('Fehler beim Aktualisieren der Anmeldung:', 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 }); return json({ success: true });
} catch (error) { } catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error); console.error('Fehler beim Löschen der Anmeldung:', error);
return json({ error: 'Fehler beim Löschen der Anmeldung' }, { status: 500 }); return json({ error: 'Fehler beim Löschen der Anmeldung', details: error.message }, { status: 500 });
} }
} }
@@ -207,16 +210,4 @@ function mapPrismaStatusToFrontend(prismaStatus) {
}; };
return statusMap[prismaStatus] || 'pending'; 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 pdfs = formData.getAll('pdfs') as File[];
const pdfFiles = 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 pdfData = [];
// const gespeichertePfade: string[] = []; // const gespeichertePfade: string[] = [];