praktikum, Notfallkontakt und ein Platz pro Dienstelle bei neuer Zeitraum

This commit is contained in:
titver968
2025-12-29 12:09:55 +01:00
parent 07824f2b6a
commit a99ddf6fa9
9 changed files with 1123 additions and 996 deletions

Binary file not shown.

View File

@@ -17,7 +17,7 @@ model EmailConfig {
id Int @id @default(1)
subject String @default("Praktikumsplatz-Zusage")
template String @default("Sehr geehrte/r {anrede} {nachname},\n\nwir freuen uns, Ihnen mitteilen zu können, dass Ihre Bewerbung für ein Praktikum erfolgreich war.\n\nSie wurden für das Praktikum bei folgender Dienststelle angenommen:\n{dienststelle}\n\nWeitere Informationen erhalten Sie in den kommenden Tagen.\n\nMit freundlichen Grüßen\nIhr Praktikumsteam")
@@map("email_config")
}
@@ -29,8 +29,8 @@ model Dienststelle {
anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2")
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
zugewiesene Anmeldung[] @relation("Zugewiesen")
zeitraumPlaetze ZeitraumPlaetze[]
zeitraumPlaetze ZeitraumPlaetze[]
}
model Praktikumszeitraum {
@@ -39,19 +39,19 @@ model Praktikumszeitraum {
startDatum DateTime
endDatum DateTime
anmeldungen Anmeldung[] @relation("PraktikumszeitraumAnmeldungen")
zeitraumPlaetze ZeitraumPlaetze[]
}
model ZeitraumPlaetze {
id Int @id @default(autoincrement())
zeitraumId Int
dienststelleId Int
plaetze Int @default(0)
zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade)
dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
zeitraumId Int
dienststelleId Int
plaetze Int @default(0)
zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade)
dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade)
@@unique([zeitraumId, dienststelleId])
@@index([zeitraumId])
@@index([dienststelleId])
@@ -66,7 +66,7 @@ enum Status {
}
model Anmeldung {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
anrede String
vorname String
nachname String
@@ -84,25 +84,31 @@ model Anmeldung {
sozialverhalten String?
motivation String?
alter Int?
status Status @default(OFFEN)
processedAt DateTime?
praktikumId Int?
praktikum Praktikumszeitraum? @relation("PraktikumszeitraumAnmeldungen", fields: [praktikumId], references: [id])
zugewiesenId Int?
zugewiesen Dienststelle? @relation("Zugewiesen", fields: [zugewiesenId], references: [id])
wunsch1Id Int?
wunsch1 Dienststelle? @relation("Wunsch1", fields: [wunsch1Id], references: [id])
wunsch2Id Int?
wunsch2 Dienststelle? @relation("Wunsch2", fields: [wunsch2Id], references: [id])
wunsch3Id Int?
wunsch3 Dienststelle? @relation("Wunsch3", fields: [wunsch3Id], references: [id])
timestamp DateTime @default(now())
pdfs PdfDatei[]
// Notfallkontakt
notfallVorname String?
notfallNachname String?
notfallTelefon String?
status Status @default(OFFEN)
processedAt DateTime?
praktikumId Int?
praktikum Praktikumszeitraum? @relation("PraktikumszeitraumAnmeldungen", fields: [praktikumId], references: [id])
zugewiesenId Int?
zugewiesen Dienststelle? @relation("Zugewiesen", fields: [zugewiesenId], references: [id])
wunsch1Id Int?
wunsch1 Dienststelle? @relation("Wunsch1", fields: [wunsch1Id], references: [id])
wunsch2Id Int?
wunsch2 Dienststelle? @relation("Wunsch2", fields: [wunsch2Id], references: [id])
wunsch3Id Int?
wunsch3 Dienststelle? @relation("Wunsch3", fields: [wunsch3Id], references: [id])
timestamp DateTime @default(now())
pdfs PdfDatei[]
@@index([status])
@@index([processedAt])
@@index([zugewiesenId])

View File

@@ -1,11 +1,11 @@
<!-- src/lib/components/AnmeldungenTable.svelte -->
<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 };
@@ -92,240 +92,255 @@
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Persönliche Daten
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kontakt & Adresse
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Schule & Noten
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Praktikum & Wünsche
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dokumente
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
</tr>
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Persönliche Daten
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kontakt & Adresse
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Schule & Noten
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Praktikum & Wünsche
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dokumente
</th>
<th class="px-3 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 align-top">
<!-- Status -->
<td class="px-3 py-4">
{#each anmeldungen as anmeldung (anmeldung.id)}
<tr class="hover:bg-gray-50 align-top">
<!-- Status -->
<td class="px-3 py-4">
<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>
<div class="text-xs text-gray-400 mt-2">
Eingang:<br>
{formatDate(anmeldung.timestamp)}
</div>
</td>
<div class="text-xs text-gray-400 mt-2">
Eingang:<br>
{formatDate(anmeldung.timestamp)}
</div>
</td>
<!-- Persönliche Daten -->
<td class="px-3 py-4">
<div class="text-sm font-medium text-gray-900">
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
<!-- Persönliche Daten -->
<td class="px-3 py-4">
<div class="text-sm font-medium text-gray-900">
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
</div>
<div class="text-xs text-gray-500 mt-1">
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
</div>
{#if anmeldung.alter}
<div class="text-xs text-gray-500">
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
</div>
<div class="text-xs text-gray-500 mt-1">
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
</div>
{#if anmeldung.alter}
<div class="text-xs text-gray-500">
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
</div>
{/if}
</td>
{/if}
</td>
<!-- Kontakt & Adresse -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Adresse:</span><br>
<span class="text-gray-600">
<!-- Kontakt & Adresse -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Adresse:</span><br>
<span class="text-gray-600">
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
{anmeldung.plz || ''} {anmeldung.ort || ''}
{anmeldung.plz || ''} {anmeldung.ort || ''}
</span>
</div>
<div>
<span class="font-medium text-gray-700">Tel:</span>
{#if anmeldung.telefon}
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
{:else}
<span class="text-gray-400">-</span>
</div>
<div>
<span class="font-medium text-gray-700">Tel:</span>
{#if anmeldung.telefon}
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
{:else}
<span class="text-gray-400">-</span>
{/if}
</div>
<div>
<span class="font-medium text-gray-700">E-Mail:</span><br>
<a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a>
</div>
<!-- Notfallkontakt -->
{#if anmeldung.notfallVorname || anmeldung.notfallNachname || anmeldung.notfallTelefon}
<div class="mt-2 pt-2 border-t border-gray-100">
<span class="font-medium text-orange-700">Notfallkontakt:</span>
<div class="text-gray-600">
{anmeldung.notfallVorname || ''} {anmeldung.notfallNachname || ''}
</div>
{#if anmeldung.notfallTelefon}
<a href="tel:{anmeldung.notfallTelefon}" class="text-blue-600 hover:text-blue-800">
{anmeldung.notfallTelefon}
</a>
{/if}
</div>
<div>
<span class="font-medium text-gray-700">E-Mail:</span><br>
<a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a>
</div>
</div>
</td>
{/if}
</div>
</td>
<!-- Schule & Noten -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<!-- Schule & Noten -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Schulart:</span><br>
<span class="text-gray-900">{formatSchulart(anmeldung.schulart)}</span>
</div>
{#if anmeldung.schulklasse}
<div>
<span class="font-medium text-gray-700">Schulart:</span><br>
<span class="text-gray-900">{formatSchulart(anmeldung.schulart)}</span>
<span class="font-medium text-gray-700">Klasse:</span>
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
</div>
{#if anmeldung.schulklasse}
<div>
<span class="font-medium text-gray-700">Klasse:</span>
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
</div>
{/if}
<div class="pt-1 border-t border-gray-100">
<span class="font-medium text-gray-700">Noten:</span>
<div class="flex gap-2 mt-1">
{/if}
<div class="pt-1 border-t border-gray-100">
<span class="font-medium text-gray-700">Noten:</span>
<div class="flex gap-2 mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
D: <span class="font-bold ml-1">{anmeldung.noteDeutsch || '-'}</span>
</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
M: <span class="font-bold ml-1">{anmeldung.noteMathe || '-'}</span>
</span>
</div>
</div>
{#if anmeldung.sozialverhalten}
<div>
<span class="font-medium text-gray-700">Sozialverh.:</span>
<span class="text-gray-900 text-xs">{formatSozialverhalten(anmeldung.sozialverhalten)}</span>
</div>
{/if}
</div>
</td>
<!-- Praktikum & Wünsche -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-2">
<!-- Zeitraum -->
{#if anmeldung.zeitraum}
<div>
<span class="font-medium text-gray-700">Zeitraum:</span><br>
<span class="text-gray-900">{formatZeitraum(anmeldung.zeitraum)}</span>
</div>
{/if}
<!-- Zugewiesene Dienststelle -->
{#if anmeldung.assignedDienststelle}
<div class="p-2 bg-green-50 border border-green-200 rounded">
<div class="flex items-center text-green-700">
<svg class="w-4 h-4 mr-1 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-semibold">Zugewiesen:</span>
</div>
<div class="text-green-800 mt-1">{anmeldung.assignedDienststelle.name}</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1">
{formatProcessedDate(anmeldung.processedAt)}
</div>
{/if}
</div>
{/if}
<!-- Alle 3 Wünsche -->
{#if anmeldung.sozialverhalten}
<div>
<span class="font-medium text-gray-700">Wünsche:</span>
<div class="space-y-1 mt-1">
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-600 rounded-full mr-1 flex-shrink-0">1</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full mr-1 flex-shrink-0">2</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-400 rounded-full mr-1 flex-shrink-0">3</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400">Keine Wünsche</span>
{/if}
</div>
<span class="font-medium text-gray-700">Sozialverh.:</span>
<span class="text-gray-900 text-xs">{formatSozialverhalten(anmeldung.sozialverhalten)}</span>
</div>
<!-- Motivation -->
{#if anmeldung.motivation}
<div>
<span class="font-medium text-gray-700">Motivation:</span>
<p class="text-gray-600 mt-1 text-xs whitespace-pre-wrap line-clamp-3">{anmeldung.motivation}</p>
</div>
{/if}
</div>
</td>
<!-- Dokumente / PDFs -->
<td class="px-3 py-4">
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
<div class="space-y-1">
{#each anmeldung.pdfs as pdf, index}
<a
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
target="_blank"
class="flex items-center text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-1 rounded"
>
<svg class="w-4 h-4 mr-1 text-red-500 flex-shrink-0" 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 {index + 1}
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
{/each}
</div>
{:else}
<span class="text-xs text-gray-400">Keine Dokumente</span>
{/if}
</td>
</div>
</td>
<!-- Aktionen -->
<td class="px-3 py-4">
<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-3 py-1.5 rounded text-xs font-medium border border-green-300 hover:bg-green-50 text-center whitespace-nowrap"
>
✓ Annehmen
</button>
{/if}
<!-- Praktikum & Wünsche -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-2">
<!-- Zeitraum -->
{#if anmeldung.zeitraum}
<div>
<span class="font-medium text-gray-700">Zeitraum:</span><br>
<span class="text-gray-900">{formatZeitraum(anmeldung.zeitraum)}</span>
</div>
{/if}
{#if canBeRejected(anmeldung.status || 'pending')}
<button
on:click={() => dispatch('reject', { id: anmeldung.id })}
class="text-red-600 hover:text-red-900 px-3 py-1.5 rounded text-xs font-medium border border-red-300 hover:bg-red-50 text-center whitespace-nowrap"
>
✗ Ablehnen
</button>
{/if}
<!-- Zugewiesene Dienststelle -->
{#if anmeldung.assignedDienststelle}
<div class="p-2 bg-green-50 border border-green-200 rounded">
<div class="flex items-center text-green-700">
<svg class="w-4 h-4 mr-1 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-semibold">Zugewiesen:</span>
</div>
<div class="text-green-800 mt-1">{anmeldung.assignedDienststelle.name}</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1">
{formatProcessedDate(anmeldung.processedAt)}
</div>
{/if}
</div>
{/if}
<button
on:click={() => dispatch('delete', { id: anmeldung.id })}
class="text-gray-600 hover:text-gray-900 px-3 py-1.5 rounded text-xs font-medium border border-gray-300 hover:bg-gray-50 text-center whitespace-nowrap"
>
🗑 Löschen
</button>
<!-- Alle 3 Wünsche -->
<div>
<span class="font-medium text-gray-700">Wünsche:</span>
<div class="space-y-1 mt-1">
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-600 rounded-full mr-1 flex-shrink-0">1</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full mr-1 flex-shrink-0">2</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-400 rounded-full mr-1 flex-shrink-0">3</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400">Keine Wünsche</span>
{/if}
</div>
</div>
</td>
</tr>
{/each}
<!-- Motivation -->
{#if anmeldung.motivation}
<div>
<span class="font-medium text-gray-700">Motivation:</span>
<p class="text-gray-600 mt-1 text-xs whitespace-pre-wrap line-clamp-3">{anmeldung.motivation}</p>
</div>
{/if}
</div>
</td>
<!-- Dokumente / PDFs -->
<td class="px-3 py-4">
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
<div class="space-y-1">
{#each anmeldung.pdfs as pdf, index}
<a
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
target="_blank"
class="flex items-center text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-1 rounded"
>
<svg class="w-4 h-4 mr-1 text-red-500 flex-shrink-0" 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 {index + 1}
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
{/each}
</div>
{:else}
<span class="text-xs text-gray-400">Keine Dokumente</span>
{/if}
</td>
<!-- Aktionen -->
<td class="px-3 py-4">
<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-3 py-1.5 rounded text-xs font-medium border border-green-300 hover:bg-green-50 text-center whitespace-nowrap"
>
✓ 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-3 py-1.5 rounded text-xs font-medium border border-red-300 hover:bg-red-50 text-center whitespace-nowrap"
>
✗ Ablehnen
</button>
{/if}
<button
on:click={() => dispatch('delete', { id: anmeldung.id })}
class="text-gray-600 hover:text-gray-900 px-3 py-1.5 rounded text-xs font-medium border border-gray-300 hover:bg-gray-50 text-center whitespace-nowrap"
>
🗑 Löschen
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>

View File

@@ -16,6 +16,11 @@
let zeitraum = '';
let motivation = '';
// Notfallkontakt
let notfallVorname = '';
let notfallNachname = '';
let notfallTelefon = '';
let wunsch1Id = '';
let wunsch2Id = '';
let wunsch3Id = '';
@@ -41,10 +46,10 @@
let sozialverhaltenFehler = '';
// Hinweis für IGS/KGS mit Lernentwicklungsbericht
$: zeigeIgsKgsHinweis =
['IGSR', 'KGSR'].includes(schulart) &&
schulklasse &&
parseInt(schulklasse) < 10;
$: zeigeIgsKgsHinweis =
['IGSR', 'KGSR'].includes(schulart) &&
schulklasse &&
parseInt(schulklasse) < 10;
// Berechnung des Alters
$: {
@@ -53,10 +58,10 @@
if (gewaehlterZeitraum) {
const geburt = new Date(geburtsdatum);
const praktikumStart = new Date(gewaehlterZeitraum.startDatum);
let altersberechnung = praktikumStart.getFullYear() - geburt.getFullYear();
const monthDiff = praktikumStart.getMonth() - geburt.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && praktikumStart.getDate() < geburt.getDate())) {
altersberechnung--;
}
@@ -80,7 +85,7 @@
const deutsch = parseInt(noteDeutsch);
const mathe = parseInt(noteMathe);
const klasse = parseInt(schulklasse);
if (noteDeutsch && noteMathe && schulart) {
// Gymnasium oder KGS_Gymnasialzweig: mindestens 4 in Deutsch UND Mathe
if (['Gymnasium', 'KGS_Gymnasialzweig'].includes(schulart)) {
@@ -129,7 +134,7 @@
dienststellen = [];
return;
}
try {
isLoadingDienststellen = true;
const res = await fetch(`/api/dienststellen?zeitraumId=${zeitraumId}`);
@@ -153,7 +158,7 @@
// Filter: Nur Dienststellen mit freien Plätzen und Alterscheck für PK Mitte
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
if (d.plaetze <= 0) return false;
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
return parseInt(alter) >= 18;
}
@@ -168,9 +173,9 @@
let startDatum = '';
$: hideSozialVerhalten =
Number(schulklasse) >= 11 &&
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
$: hideSozialVerhalten =
Number(schulklasse) >= 11 &&
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
onMount(async () => {
const resZeitraeume = await fetch('/api/zeitraeume');
@@ -208,6 +213,9 @@
noteMathe = '';
sozialverhalten = '';
schulklasse = '';
notfallVorname = '';
notfallNachname = '';
notfallTelefon = '';
pdfDateien = [];
fileInputKey += 1;
success = false;
@@ -246,6 +254,9 @@
data.append('sozialverhalten', sozialverhalten);
data.append('schulklasse', schulklasse);
data.append('alter', alter);
data.append('notfallVorname', notfallVorname);
data.append('notfallNachname', notfallNachname);
data.append('notfallTelefon', notfallTelefon);
for (const pdf of pdfDateien) {
data.append('pdfs', pdf);
@@ -254,7 +265,7 @@
const res = await fetch('/api/anmelden', {
method: 'POST',
body: data
});
});
const result = await res.json();
if (!res.ok) {
@@ -263,13 +274,13 @@
} else {
fehler = '';
success = true;
}
}
}
</script>
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<form on:submit|preventDefault={anmelden}
class="bg-white shadow-md rounded-2xl p-8 max-w-2xl w-full space-y-4">
class="bg-white shadow-md rounded-2xl p-8 max-w-2xl w-full space-y-4">
<h1 class="text-2xl font-bold text-gray-800 mb-4 text-center">Praktikumsanmeldung</h1>
@@ -302,7 +313,7 @@
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
</select>
<!-- Schulklasse -->
<select bind:value={schulklasse} required class="input">
<option value="" disabled selected hidden>Schulklasse</option>
@@ -314,15 +325,15 @@
<option value="12">12. Klasse</option>
<option value="13">13. Klasse</option>
</select>
<!-- Sozialverhalten mit Echtzeit-Validierung -->
{#if !hideSozialVerhalten}
{#if !hideSozialVerhalten}
<div class="col-span-2">
<select
bind:value={sozialverhalten}
required
class="input"
class:input-error={sozialverhaltenFehler}
<select
bind:value={sozialverhalten}
required
class="input"
class:input-error={sozialverhaltenFehler}
>
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
@@ -350,27 +361,27 @@
</div>
</div>
{/if}
<div class="grid grid-cols-2 gap-4">
<input
bind:value={noteDeutsch}
type="number"
min="1"
max="6"
placeholder="Note in Deutsch"
required
class="input"
class:input-error={notenFehler}
<input
bind:value={noteDeutsch}
type="number"
min="1"
max="6"
placeholder="Note in Deutsch"
required
class="input"
class:input-error={notenFehler}
/>
<input
bind:value={noteMathe}
type="number"
min="1"
max="6"
placeholder="Note in Mathe"
required
class="input"
class:input-error={notenFehler}
<input
bind:value={noteMathe}
type="number"
min="1"
max="6"
placeholder="Note in Mathe"
required
class="input"
class:input-error={notenFehler}
/>
</div>
{#if notenFehler}
@@ -384,8 +395,8 @@
<select bind:value={zeitraum} required class="input">
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
{#each filteredZeitraeume as d}
<option
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
<option
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
</option>
{/each}
</select>
@@ -453,7 +464,7 @@
<!-- Motivation -->
<textarea bind:value={motivation} placeholder="Motivation (optional)"
class="w-full border border-gray-300 rounded-xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 h-32 resize-none" >
class="w-full border border-gray-300 rounded-xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 h-32 resize-none" >
</textarea>
<!-- Mehrere PDF Upload -->
@@ -461,23 +472,33 @@
<div>
<label for="pdf-upload" class="block text-gray-700 font-medium mb-1">Zeugnis hochladen:</label>
<input
id="pdf-upload"
type="file"
accept="application/pdf"
multiple
on:change={(e) => pdfDateien = Array.from((e.target as HTMLInputElement).files || [])}
class="input"
id="pdf-upload"
type="file"
accept="application/pdf"
multiple
on:change={(e) => pdfDateien = Array.from((e.target as HTMLInputElement).files || [])}
class="input"
/>
</div>
{/key}
<!-- Notfallkontakt -->
<div class="border-t pt-4 mt-4">
<h2 class="text-lg font-semibold text-gray-700 mb-3">Notfallkontakt</h2>
<div class="grid grid-cols-2 gap-4">
<input bind:value={notfallVorname} placeholder="Vorname Notfallkontakt" required class="input" />
<input bind:value={notfallNachname} placeholder="Nachname Notfallkontakt" required class="input" />
<input bind:value={notfallTelefon} type="tel" placeholder="Mobilnummer Notfallkontakt" required class="input col-span-2" />
</div>
</div>
{#if showAblehnungModal}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
<p class="text-red-600 font-semibold">{ablehnungHinweis}</p>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
on:click={() => { resetForm(); showAblehnungModal = false; }}
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
on:click={() => { resetForm(); showAblehnungModal = false; }}
>
OK
</button>
@@ -486,15 +507,15 @@
{/if}
<!-- Button - deaktiviert bei Validierungsfehlern oder fehlenden Pflichtfeldern -->
<button
type="submit"
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
class="w-full py-3 rounded-xl transition-all"
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
<button
type="submit"
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
class="w-full py-3 rounded-xl transition-all"
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
>
Jetzt anmelden
</button>
@@ -508,8 +529,8 @@
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
<p class="text-green-600 font-semibold">Anmeldung erfolgreich gesendet!</p>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
on:click={resetForm}
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
on:click={resetForm}
>
OK
</button>

View File

@@ -5,7 +5,7 @@
import AnmeldungenTable from '$lib/components/AnmeldungenTable.svelte';
import DienststellenDialog from '$lib/components/DienststellenDialog.svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
interface Anmeldung {
pdfs: { pfad: string }[];
anrede: string;
@@ -15,6 +15,9 @@
noteDeutsch?: string;
noteMathe?: string;
sozialverhalten?: string;
notfallVorname?: string;
notfallNachname?: string;
notfallTelefon?: string;
wunsch1?: { id: number; name: string };
wunsch2?: { id: number; name: string };
wunsch3?: { id: number; name: string };
@@ -43,11 +46,11 @@
let zeitraeume: Zeitraum[] = [];
let isLoading = true;
let error = '';
// Filter für Status
let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' = 'all';
let filteredAnmeldungen: Anmeldung[] = [];
// Dialog state
let showDialog = false;
let selectedAnmeldungId: number | null = null;
@@ -120,9 +123,9 @@ Ihr Praktikumsteam`;
// Zähle angenommene Anmeldungen pro Zeitraum
function getAcceptedCountForZeitraum(zeitraumId: number): number {
return anmeldungen.filter(a =>
a.status === 'accepted' &&
a.zeitraum?.id === zeitraumId
return anmeldungen.filter(a =>
a.status === 'accepted' &&
a.zeitraum?.id === zeitraumId
).length;
}
@@ -130,26 +133,26 @@ Ihr Praktikumsteam`;
try {
isLoading = true;
error = '';
const res = await fetch('/api/admin/anmeldungen');
if (!res.ok) {
const errorText = await res.text();
console.error('❌ API Fehler:', res.status, errorText);
throw new Error(`Fehler beim Laden: ${res.status} - ${errorText}`);
}
const data = await res.json();
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) {
error = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('❌ Frontend Fehler beim Laden der Anmeldungen:', err);
@@ -172,13 +175,13 @@ Ihr Praktikumsteam`;
async function loadEmailConfig() {
try {
isLoadingEmailConfig = true;
const res = await fetch('/api/admin/email-config');
if (!res.ok) {
throw new Error(`Fehler beim Laden der E-Mail-Konfiguration: ${res.status}`);
}
const config: EmailConfig = await res.json();
emailSubject = config.subject;
emailTemplate = config.template;
@@ -192,7 +195,7 @@ Ihr Praktikumsteam`;
async function saveEmailTemplate() {
try {
isSavingEmailConfig = true;
const res = await fetch('/api/admin/email-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -247,10 +250,10 @@ Ihr Praktikumsteam`;
}
showDialog = false;
// E-Mail Vorschau öffnen nach erfolgreichem Annehmen
openEmailPreview(selectedAnmeldungId, event.detail.dienststelleId);
selectedAnmeldungId = null;
await loadAnmeldungen();
} catch (err) {
@@ -265,16 +268,16 @@ Ihr Praktikumsteam`;
// Dienststelle finden
const dienststelle = availableWishes.find(w => w.id === dienststelleId);
const dienststelleName = dienststelle
? dienststelle.name.replace(/^\d+\.\s*Wunsch:\s*/, '')
: 'Unbekannte Dienststelle';
const dienststelleName = dienststelle
? dienststelle.name.replace(/^\d+\.\s*Wunsch:\s*/, '')
: 'Unbekannte Dienststelle';
// E-Mail Text personalisieren
const personalizedEmail = emailTemplate
.replace(/\{anrede\}/g, anmeldung.anrede)
.replace(/\{vorname\}/g, anmeldung.vorname)
.replace(/\{nachname\}/g, anmeldung.nachname)
.replace(/\{dienststelle\}/g, dienststelleName);
.replace(/\{anrede\}/g, anmeldung.anrede)
.replace(/\{vorname\}/g, anmeldung.vorname)
.replace(/\{nachname\}/g, anmeldung.nachname)
.replace(/\{dienststelle\}/g, dienststelleName);
// Modal mit Vorschau öffnen
emailPreviewData = {
@@ -288,11 +291,11 @@ Ihr Praktikumsteam`;
async function copyAndOpenMail() {
if (!emailPreviewData) return;
try {
await navigator.clipboard.writeText(emailPreviewData.body);
emailCopied = true;
// Kurz warten, dann Mail öffnen
setTimeout(() => {
const mailtoLink = `mailto:${emailPreviewData!.to}?subject=${encodeURIComponent(emailPreviewData!.subject)}`;
@@ -347,7 +350,7 @@ Ihr Praktikumsteam`;
const blob = await res.blob();
const contentDisposition = res.headers.get('Content-Disposition');
let filename = 'export.xlsx';
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+)"/);
if (match) {
@@ -375,18 +378,18 @@ Ihr Praktikumsteam`;
async function handleReject(event: CustomEvent<{id: number}>) {
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'reject' })
});
if (!res.ok) {
throw new Error(`Fehler beim Ablehnen: ${res.status}`);
}
await loadAnmeldungen();
} catch (err) {
error = err instanceof Error ? err.message : 'Fehler beim Ablehnen';
@@ -396,16 +399,16 @@ Ihr Praktikumsteam`;
async function handleDelete(event: CustomEvent<{id: number}>) {
if (!confirm('Diese Anmeldung wirklich löschen?')) return;
try {
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
method: 'DELETE'
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
method: 'DELETE'
});
if (!res.ok) {
throw new Error(`Fehler beim Löschen: ${res.status}`);
}
await loadAnmeldungen();
} catch (err) {
error = err instanceof Error ? err.message : 'Fehler beim Löschen';
@@ -430,9 +433,9 @@ Ihr Praktikumsteam`;
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Anmeldungen verwalten"
showBackButton={true}
<AdminHeader
title="Anmeldungen verwalten"
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
@@ -442,9 +445,9 @@ Ihr Praktikumsteam`;
<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"
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>
@@ -457,9 +460,9 @@ Ihr Praktikumsteam`;
<div class="flex items-center space-x-3">
<!-- Excel Export Button -->
<button
on:click={openExportModal}
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
disabled={anmeldungen.filter(a => a.status === 'accepted').length === 0}
on:click={openExportModal}
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
disabled={anmeldungen.filter(a => a.status === 'accepted').length === 0}
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
@@ -469,9 +472,9 @@ Ihr Praktikumsteam`;
<!-- E-Mail Konfiguration Button -->
<button
on:click={() => showEmailConfig = !showEmailConfig}
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
disabled={isLoadingEmailConfig}
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"
disabled={isLoadingEmailConfig}
>
{#if isLoadingEmailConfig}
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
@@ -546,49 +549,49 @@ Ihr Praktikumsteam`;
{#if showEmailConfig}
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">E-Mail-Vorlage konfigurieren</h3>
<div class="space-y-4">
<div>
<label for="email-subject" class="block text-sm font-medium text-gray-700 mb-2">
E-Mail Betreff
</label>
<input
id="email-subject"
type="text"
bind:value={emailSubject}
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isSavingEmailConfig}
id="email-subject"
type="text"
bind:value={emailSubject}
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isSavingEmailConfig}
/>
</div>
<div>
<label for="email-template" class="block text-sm font-medium text-gray-700 mb-2">
E-Mail Text
</label>
<textarea
id="email-template"
bind:value={emailTemplate}
rows="10"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isSavingEmailConfig}
id="email-template"
bind:value={emailTemplate}
rows="10"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isSavingEmailConfig}
></textarea>
<p class="mt-2 text-sm text-gray-500">
Verfügbare Platzhalter: &#123;anrede&#125;, &#123;vorname&#125;, &#123;nachname&#125;, &#123;dienststelle&#125;
</p>
</div>
<div class="flex justify-end space-x-3">
<button
on:click={() => showEmailConfig = false}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
disabled={isSavingEmailConfig}
on:click={() => showEmailConfig = false}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
disabled={isSavingEmailConfig}
>
Abbrechen
</button>
<button
on:click={saveEmailTemplate}
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
disabled={isSavingEmailConfig}
on:click={saveEmailTemplate}
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
disabled={isSavingEmailConfig}
>
{#if isSavingEmailConfig}
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
@@ -614,9 +617,9 @@ Ihr Praktikumsteam`;
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
<p class="mt-1 text-sm text-red-700">{error}</p>
</div>
<button
on:click={() => error = ''}
class="ml-auto text-red-400 hover:text-red-600"
<button
on:click={() => error = ''}
class="ml-auto text-red-400 hover:text-red-600"
>
<svg class="w-5 h-5" 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" />
@@ -640,20 +643,20 @@ Ihr Praktikumsteam`;
{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()}".`}
{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={filteredAnmeldungen}
{getStatusColor}
{getStatusText}
on:accept={handleAccept}
on:reject={handleReject}
on:delete={handleDelete}
<AnmeldungenTable
anmeldungen={filteredAnmeldungen}
{getStatusColor}
{getStatusText}
on:accept={handleAccept}
on:reject={handleReject}
on:delete={handleDelete}
/>
</div>
{/if}
@@ -663,10 +666,10 @@ Ihr Praktikumsteam`;
<!-- Dienststellen Dialog -->
{#if showDialog}
<DienststellenDialog
wishes={availableWishes}
selectedId={selectedDienststelleId}
on:confirm={handleConfirmAccept}
on:cancel={closeDialog}
wishes={availableWishes}
selectedId={selectedDienststelleId}
on:confirm={handleConfirmAccept}
on:cancel={closeDialog}
/>
{/if}
@@ -677,11 +680,11 @@ Ihr Praktikumsteam`;
<!-- Backdrop -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
on:click={closeExportModal}
<div
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
on:click={closeExportModal}
></div>
<!-- Modal -->
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
<!-- Header -->
@@ -694,31 +697,31 @@ Ihr Praktikumsteam`;
</div>
<h3 class="text-lg font-medium text-gray-900">Excel-Export</h3>
</div>
<button
on:click={closeExportModal}
class="text-gray-400 hover:text-gray-600 transition-colors"
<button
on:click={closeExportModal}
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg class="w-6 h-6" 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>
</button>
</div>
<!-- Content -->
<div class="px-6 py-4">
<p class="text-sm text-gray-600 mb-4">
Wählen Sie einen Praktikumszeitraum aus, um alle angenommenen Anmeldungen als Excel-Datei zu exportieren.
</p>
<div>
<label for="export-zeitraum" class="block text-sm font-medium text-gray-700 mb-2">
Praktikumszeitraum
</label>
<select
id="export-zeitraum"
bind:value={selectedExportZeitraum}
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500"
disabled={isExporting}
id="export-zeitraum"
bind:value={selectedExportZeitraum}
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500"
disabled={isExporting}
>
<option value="" disabled>Zeitraum auswählen...</option>
{#each zeitraeume as z}
@@ -730,20 +733,20 @@ Ihr Praktikumsteam`;
</select>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end space-x-3">
<button
on:click={closeExportModal}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
disabled={isExporting}
on:click={closeExportModal}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
disabled={isExporting}
>
Abbrechen
</button>
<button
on:click={exportToExcel}
class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-md text-sm font-medium inline-flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedExportZeitraum || isExporting}
on:click={exportToExcel}
class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-md text-sm font-medium inline-flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedExportZeitraum || isExporting}
>
{#if isExporting}
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
@@ -768,11 +771,11 @@ Ihr Praktikumsteam`;
<!-- Backdrop -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
on:click={closeEmailPreview}
<div
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
on:click={closeEmailPreview}
></div>
<!-- Modal -->
<div class="relative bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
<!-- Header -->
@@ -785,16 +788,16 @@ Ihr Praktikumsteam`;
</div>
<h3 class="text-lg font-medium text-gray-900">E-Mail Vorschau</h3>
</div>
<button
on:click={closeEmailPreview}
class="text-gray-400 hover:text-gray-600 transition-colors"
<button
on:click={closeEmailPreview}
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg class="w-6 h-6" 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>
</button>
</div>
<!-- Content -->
<div class="px-6 py-4 overflow-y-auto max-h-[60vh]">
<div class="space-y-4">
@@ -803,30 +806,30 @@ Ihr Praktikumsteam`;
<span class="text-sm font-medium text-gray-500 w-16">An:</span>
<span class="text-sm text-gray-900">{emailPreviewData.to}</span>
</div>
<!-- Betreff -->
<div class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<span class="text-sm font-medium text-gray-500 w-16">Betreff:</span>
<span class="text-sm text-gray-900">{emailPreviewData.subject}</span>
</div>
<!-- Nachricht (editierbar) -->
<div>
<label for="email-body" class="block text-sm font-medium text-gray-700 mb-2">
Nachricht
Nachricht
<span class="font-normal text-gray-500">(kann bearbeitet werden)</span>
</label>
<textarea
id="email-body"
bind:value={emailPreviewData.body}
rows="14"
class="w-full border border-gray-300 rounded-lg px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
disabled={emailCopied}
id="email-body"
bind:value={emailPreviewData.body}
rows="14"
class="w-full border border-gray-300 rounded-lg px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
disabled={emailCopied}
></textarea>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
{#if emailCopied}
@@ -853,14 +856,14 @@ Ihr Praktikumsteam`;
</div>
<div class="flex space-x-3 w-full sm:w-auto">
<button
on:click={closeEmailPreview}
class="flex-1 sm:flex-none px-4 py-2.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
on:click={closeEmailPreview}
class="flex-1 sm:flex-none px-4 py-2.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
on:click={copyAndOpenMail}
class="flex-1 sm:flex-none bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg text-sm font-medium inline-flex items-center justify-center transition-colors"
on:click={copyAndOpenMail}
class="flex-1 sm:flex-none bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg text-sm font-medium inline-flex items-center justify-center transition-colors"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />

View File

@@ -6,282 +6,321 @@ import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
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'
}
]
});
try {
const anmeldungen = await prisma.anmeldung.findMany({
include: {
wunsch1: true,
wunsch2: true,
wunsch3: true,
zugewiesen: true,
praktikum: true,
pdfs: true
},
orderBy: [
{
timestamp: 'desc'
}
]
});
const formattedAnmeldungen = anmeldungen.map(anmeldung => ({
id: anmeldung.id,
anrede: anmeldung.anrede,
vorname: anmeldung.vorname,
nachname: anmeldung.nachname,
email: anmeldung.email,
geburtsdatum: anmeldung.geburtsdatum,
strasse: anmeldung.strasse,
hausnummer: anmeldung.hausnummer,
ort: anmeldung.ort,
plz: anmeldung.plz,
telefon: anmeldung.telefon,
schulart: anmeldung.schulart,
schulklasse: anmeldung.schulklasse,
motivation: anmeldung.motivation,
alter: anmeldung.alter,
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
sozialverhalten: anmeldung.sozialverhalten,
status: mapPrismaStatusToFrontend(anmeldung.status),
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
wunsch1: anmeldung.wunsch1 ? {
id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name
} : undefined,
wunsch2: anmeldung.wunsch2 ? {
id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name
} : undefined,
wunsch3: anmeldung.wunsch3 ? {
id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name
} : undefined,
assignedDienststelle: anmeldung.zugewiesen ? {
id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name
} : undefined,
zeitraum: anmeldung.praktikum ? {
id: anmeldung.praktikum.id,
bezeichnung: anmeldung.praktikum.bezeichnung,
startDatum: anmeldung.praktikum.startDatum.toISOString(),
endDatum: anmeldung.praktikum.endDatum.toISOString()
} : undefined,
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
pdfs: anmeldung.pdfs || []
}));
const formattedAnmeldungen = anmeldungen.map((anmeldung) => ({
id: anmeldung.id,
anrede: anmeldung.anrede,
vorname: anmeldung.vorname,
nachname: anmeldung.nachname,
email: anmeldung.email,
return json(formattedAnmeldungen);
} catch (error) {
console.error('Fehler beim Laden der Anmeldungen:', error);
return json({ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message }, { status: 500 });
}
geburtsdatum: anmeldung.geburtsdatum,
strasse: anmeldung.strasse,
hausnummer: anmeldung.hausnummer,
ort: anmeldung.ort,
plz: anmeldung.plz,
telefon: anmeldung.telefon,
schulart: anmeldung.schulart,
schulklasse: anmeldung.schulklasse,
motivation: anmeldung.motivation,
alter: anmeldung.alter,
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
sozialverhalten: anmeldung.sozialverhalten,
// Notfallkontakt
notfallVorname: anmeldung.notfallVorname,
notfallNachname: anmeldung.notfallNachname,
notfallTelefon: anmeldung.notfallTelefon,
status: mapPrismaStatusToFrontend(anmeldung.status),
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
wunsch1: anmeldung.wunsch1
? {
id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name
}
: undefined,
wunsch2: anmeldung.wunsch2
? {
id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name
}
: undefined,
wunsch3: anmeldung.wunsch3
? {
id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name
}
: undefined,
assignedDienststelle: anmeldung.zugewiesen
? {
id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name
}
: undefined,
zeitraum: anmeldung.praktikum
? {
id: anmeldung.praktikum.id,
bezeichnung: anmeldung.praktikum.bezeichnung,
startDatum: anmeldung.praktikum.startDatum.toISOString(),
endDatum: anmeldung.praktikum.endDatum.toISOString()
}
: undefined,
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
pdfs: anmeldung.pdfs || []
}));
return json(formattedAnmeldungen);
} catch (error) {
console.error('Fehler beim Laden der Anmeldungen:', error);
return json(
{ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message },
{ status: 500 }
);
}
}
export async function POST({ request, url }) {
try {
const id = parseInt(url.searchParams.get('id') || '0');
const { dienststelleId } = await request.json();
try {
const id = parseInt(url.searchParams.get('id') || '0');
const { dienststelleId } = await request.json();
if (!id || !dienststelleId) {
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
}
if (!id || !dienststelleId) {
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
}
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id },
include: {
praktikum: true
}
});
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id },
include: {
praktikum: true
}
});
if (!existingAnmeldung) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
}
if (!existingAnmeldung) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
}
if (existingAnmeldung.status === 'ANGENOMMEN') {
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
}
if (existingAnmeldung.status === 'ANGENOMMEN') {
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
}
const zeitraumId = existingAnmeldung.praktikumId;
const zeitraumId = existingAnmeldung.praktikumId;
if (!zeitraumId) {
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 });
}
if (!zeitraumId) {
return json(
{ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' },
{ status: 400 }
);
}
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
});
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
});
if (!zeitraumPlaetze) {
return json({
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
}, { status: 400 });
}
if (!zeitraumPlaetze) {
return json(
{
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
},
{ status: 400 }
);
}
// Prüfen ob noch Plätze frei sind (plaetze > 0)
if (zeitraumPlaetze.plaetze <= 0) {
return json({
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
}, { status: 400 });
}
// Prüfen ob noch Plätze frei sind (plaetze > 0)
if (zeitraumPlaetze.plaetze <= 0) {
return json(
{
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
},
{ status: 400 }
);
}
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
// Verwendung von $transaction für Atomarität (Race Condition vermeiden)
await prisma.$transaction(async (tx) => {
// Nochmal prüfen innerhalb der Transaktion
const aktuellerStand = await tx.zeitraumPlaetze.findUnique({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
});
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
// Verwendung von $transaction für Atomarität (Race Condition vermeiden)
await prisma.$transaction(async (tx) => {
// Nochmal prüfen innerhalb der Transaktion
const aktuellerStand = await tx.zeitraumPlaetze.findUnique({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
});
if (!aktuellerStand || aktuellerStand.plaetze <= 0) {
throw new Error('Keine freien Plätze mehr verfügbar');
}
if (!aktuellerStand || aktuellerStand.plaetze <= 0) {
throw new Error('Keine freien Plätze mehr verfügbar');
}
// Anmeldung aktualisieren
await tx.anmeldung.update({
where: { id },
data: {
status: 'ANGENOMMEN',
zugewiesenId: dienststelleId,
processedAt: new Date()
}
});
// Anmeldung aktualisieren
await tx.anmeldung.update({
where: { id },
data: {
status: 'ANGENOMMEN',
zugewiesenId: dienststelleId,
processedAt: new Date()
}
});
// Plätze in ZeitraumPlaetze reduzieren
await tx.zeitraumPlaetze.update({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
},
data: {
plaetze: {
decrement: 1
}
}
});
});
// Plätze in ZeitraumPlaetze reduzieren
await tx.zeitraumPlaetze.update({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
},
data: {
plaetze: {
decrement: 1
}
}
});
});
return json({ success: true });
} catch (error) {
console.error('Fehler beim Annehmen der Anmeldung:', error);
// Spezifische Fehlermeldung für "keine Plätze"
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
return json({
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
}, { status: 400 });
}
return json({ error: 'Fehler beim Annehmen der Anmeldung', details: (error as Error).message }, { status: 500 });
}
return json({ success: true });
} catch (error) {
console.error('Fehler beim Annehmen der Anmeldung:', error);
// Spezifische Fehlermeldung für "keine Plätze"
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
return json(
{
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
},
{ status: 400 }
);
}
return json(
{ error: 'Fehler beim Annehmen der Anmeldung', details: (error as Error).message },
{ status: 500 }
);
}
}
export async function PATCH({ request, url }) {
try {
const id = parseInt(url.searchParams.get('id') || '0');
const { action } = await request.json();
try {
const id = parseInt(url.searchParams.get('id') || '0');
const { action } = await request.json();
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
let updateData = {};
let updateData = {};
switch (action) {
case 'reject':
updateData = {
status: 'ABGELEHNT',
processedAt: new Date()
};
break;
switch (action) {
case 'reject':
updateData = {
status: 'ABGELEHNT',
processedAt: new Date()
};
break;
case 'set_processing':
const anmeldung = await prisma.anmeldung.findUnique({
where: { id }
});
case 'set_processing':
const anmeldung = await prisma.anmeldung.findUnique({
where: { id }
});
if (!anmeldung) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
}
if (!anmeldung) {
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
}
updateData = {
status: 'BEARBEITUNG',
processedAt: new Date()
};
break;
updateData = {
status: 'BEARBEITUNG',
processedAt: new Date()
};
break;
case 'reset_processing':
updateData = {
status: 'OFFEN',
processedAt: null
};
break;
case 'reset_processing':
updateData = {
status: 'OFFEN',
processedAt: null
};
break;
default:
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
}
default:
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
}
await prisma.anmeldung.update({
where: { id },
data: updateData
});
await prisma.anmeldung.update({
where: { id },
data: updateData
});
return json({ success: true });
} catch (error) {
console.error('Fehler beim Aktualisieren der Anmeldung:', error);
return json({ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message }, { status: 500 });
}
return json({ success: true });
} catch (error) {
console.error('Fehler beim Aktualisieren der Anmeldung:', error);
return json(
{ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message },
{ status: 500 }
);
}
}
export async function DELETE({ url }) {
try {
const id = parseInt(url.searchParams.get('id') || '0');
try {
const id = parseInt(url.searchParams.get('id') || '0');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
await prisma.anmeldung.delete({
where: { id }
});
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
await prisma.anmeldung.delete({
where: { id }
});
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error);
return json({ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message }, { status: 500 });
}
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error);
return json(
{ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message },
{ status: 500 }
);
}
}
// Hilfsfunktion: Prisma Status zu Frontend Status
function mapPrismaStatusToFrontend(prismaStatus: string): string {
const statusMap: Record<string, string> = {
'OFFEN': 'pending',
'BEARBEITUNG': 'processing',
'ANGENOMMEN': 'accepted',
'ABGELEHNT': 'rejected'
};
return statusMap[prismaStatus] || 'pending';
}
const statusMap: Record<string, string> = {
OFFEN: 'pending',
BEARBEITUNG: 'processing',
ANGENOMMEN: 'accepted',
ABGELEHNT: 'rejected'
};
return statusMap[prismaStatus] || 'pending';
}

View File

@@ -7,177 +7,210 @@ import ExcelJS from 'exceljs';
const prisma = new PrismaClient();
export async function GET({ url }) {
try {
const zeitraumId = url.searchParams.get('zeitraumId');
try {
const zeitraumId = url.searchParams.get('zeitraumId');
if (!zeitraumId) {
return json({ error: 'Zeitraum-ID erforderlich' }, { status: 400 });
}
if (!zeitraumId) {
return json({ error: 'Zeitraum-ID erforderlich' }, { status: 400 });
}
// Zeitraum laden für Dateinamen
const zeitraum = await prisma.praktikumszeitraum.findUnique({
where: { id: parseInt(zeitraumId) }
});
// Zeitraum laden für Dateinamen
const zeitraum = await prisma.praktikumszeitraum.findUnique({
where: { id: parseInt(zeitraumId) }
});
if (!zeitraum) {
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
}
if (!zeitraum) {
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
}
// Angenommene Anmeldungen für diesen Zeitraum laden
const anmeldungen = await prisma.anmeldung.findMany({
where: {
praktikumId: parseInt(zeitraumId),
status: 'ANGENOMMEN'
},
include: {
zugewiesen: true,
praktikum: true
},
orderBy: [
{ zugewiesen: { name: 'asc' } },
{ nachname: 'asc' }
]
});
// Angenommene Anmeldungen für diesen Zeitraum laden
const anmeldungen = await prisma.anmeldung.findMany({
where: {
praktikumId: parseInt(zeitraumId),
status: 'ANGENOMMEN'
},
include: {
zugewiesen: true,
praktikum: true
},
orderBy: [{ zugewiesen: { name: 'asc' } }, { nachname: 'asc' }]
});
// Excel-Datei erstellen
const workbook = new ExcelJS.Workbook();
workbook.creator = 'Praktikumsverwaltung';
workbook.created = new Date();
// Excel-Datei erstellen
const workbook = new ExcelJS.Workbook();
workbook.creator = 'Praktikumsverwaltung';
workbook.created = new Date();
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
// Spalten definieren
worksheet.columns = [
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
{ header: 'Anrede', key: 'anrede', width: 10 },
{ header: 'Vorname', key: 'vorname', width: 15 },
{ header: 'Nachname', key: 'nachname', width: 15 },
{ header: 'Geburtsdatum', key: 'geburtsdatum', width: 12 },
{ header: 'Alter', key: 'alter', width: 8 },
{ header: 'Straße', key: 'strasse', width: 20 },
{ header: 'Hausnr.', key: 'hausnummer', width: 10 },
{ header: 'PLZ', key: 'plz', width: 8 },
{ header: 'Ort', key: 'ort', width: 15 },
{ header: 'Telefon', key: 'telefon', width: 18 },
{ header: 'E-Mail', key: 'email', width: 30 },
{ header: 'Schulart', key: 'schulart', width: 15 },
{ header: 'Klasse', key: 'schulklasse', width: 8 },
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
{ header: 'Note Mathe', key: 'noteMathe', width: 12 },
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
{ header: 'Angenommen am', key: 'processedAt', width: 15 }
];
// Spalten definieren (inkl. Notfallkontakt)
worksheet.columns = [
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
{ header: 'Anrede', key: 'anrede', width: 10 },
{ header: 'Vorname', key: 'vorname', width: 15 },
{ header: 'Nachname', key: 'nachname', width: 15 },
{ header: 'Geburtsdatum', key: 'geburtsdatum', width: 12 },
{ header: 'Alter', key: 'alter', width: 8 },
{ header: 'Straße', key: 'strasse', width: 20 },
{ header: 'Hausnr.', key: 'hausnummer', width: 10 },
{ header: 'PLZ', key: 'plz', width: 8 },
{ header: 'Ort', key: 'ort', width: 15 },
{ header: 'Telefon', key: 'telefon', width: 18 },
{ header: 'E-Mail', key: 'email', width: 30 },
{ header: 'Schulart', key: 'schulart', width: 15 },
{ header: 'Klasse', key: 'schulklasse', width: 8 },
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
{ header: 'Note Mathe', key: 'noteMathe', width: 12 },
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
{ header: 'Notfall Vorname', key: 'notfallVorname', width: 15 },
{ header: 'Notfall Nachname', key: 'notfallNachname', width: 15 },
{ header: 'Notfall Telefon', key: 'notfallTelefon', width: 18 },
{ header: 'Angenommen am', key: 'processedAt', width: 15 }
];
// Header-Zeile formatieren
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' }
};
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
headerRow.height = 25;
// Header-Zeile formatieren
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' }
};
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
headerRow.height = 25;
// Daten einfügen
anmeldungen.forEach(anmeldung => {
worksheet.addRow({
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
anrede: anmeldung.anrede,
vorname: anmeldung.vorname,
nachname: anmeldung.nachname,
geburtsdatum: anmeldung.geburtsdatum,
alter: anmeldung.alter,
strasse: anmeldung.strasse,
hausnummer: anmeldung.hausnummer,
plz: anmeldung.plz,
ort: anmeldung.ort,
telefon: anmeldung.telefon,
email: anmeldung.email,
schulart: formatSchulart(anmeldung.schulart),
schulklasse: anmeldung.schulklasse,
noteDeutsch: anmeldung.noteDeutsch,
noteMathe: anmeldung.noteMathe,
sozialverhalten: anmeldung.sozialverhalten || '-',
processedAt: anmeldung.processedAt
? new Date(anmeldung.processedAt).toLocaleDateString('de-DE')
: '-'
});
});
// Notfallkontakt-Spalten orange hervorheben
['R1', 'S1', 'T1'].forEach((cell) => {
worksheet.getCell(cell).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFED7D31' }
};
});
// Datenzeilen formatieren
for (let i = 2; i <= anmeldungen.length + 1; i++) {
const row = worksheet.getRow(i);
row.alignment = { vertical: 'middle' };
// Abwechselnde Zeilenfarben
if (i % 2 === 0) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF2F2F2' }
};
}
}
// Daten einfügen
anmeldungen.forEach((anmeldung) => {
worksheet.addRow({
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
anrede: anmeldung.anrede,
vorname: anmeldung.vorname,
nachname: anmeldung.nachname,
geburtsdatum: anmeldung.geburtsdatum,
alter: anmeldung.alter,
strasse: anmeldung.strasse,
hausnummer: anmeldung.hausnummer,
plz: anmeldung.plz,
ort: anmeldung.ort,
telefon: anmeldung.telefon,
email: anmeldung.email,
schulart: formatSchulart(anmeldung.schulart),
schulklasse: anmeldung.schulklasse,
noteDeutsch: anmeldung.noteDeutsch,
noteMathe: anmeldung.noteMathe,
sozialverhalten: anmeldung.sozialverhalten || '-',
notfallVorname: anmeldung.notfallVorname || '-',
notfallNachname: anmeldung.notfallNachname || '-',
notfallTelefon: anmeldung.notfallTelefon || '-',
processedAt: anmeldung.processedAt
? new Date(anmeldung.processedAt).toLocaleDateString('de-DE')
: '-'
});
});
// Rahmen für alle Zellen
worksheet.eachRow((row, rowNumber) => {
row.eachCell((cell) => {
cell.border = {
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
right: { style: 'thin', color: { argb: 'FFD0D0D0' } }
};
});
});
// Datenzeilen formatieren
for (let i = 2; i <= anmeldungen.length + 1; i++) {
const row = worksheet.getRow(i);
row.alignment = { vertical: 'middle' };
// Filter aktivieren
worksheet.autoFilter = {
from: 'A1',
to: `R${anmeldungen.length + 1}`
};
// Abwechselnde Zeilenfarben
if (i % 2 === 0) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF2F2F2' }
};
}
}
// Zusammenfassung am Ende
const summaryRow = worksheet.addRow([]);
const totalRow = worksheet.addRow([
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''
]);
totalRow.font = { bold: true };
// Rahmen für alle Zellen
worksheet.eachRow((row, rowNumber) => {
row.eachCell((cell) => {
cell.border = {
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
right: { style: 'thin', color: { argb: 'FFD0D0D0' } }
};
});
});
// Excel-Datei als Buffer generieren
const buffer = await workbook.xlsx.writeBuffer();
// Filter aktivieren (angepasst auf neue Spaltenanzahl: A bis U = 21 Spalten)
worksheet.autoFilter = {
from: 'A1',
to: `U${anmeldungen.length + 1}`
};
// Dateiname generieren
const dateiname = `Praktikanten_${zeitraum.bezeichnung.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
// Zusammenfassung am Ende
const summaryRow = worksheet.addRow([]);
const totalRow = worksheet.addRow([
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
''
]);
totalRow.font = { bold: true };
// Als Download zurückgeben
return new Response(buffer, {
status: 200,
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': `attachment; filename="${dateiname}"`,
'Content-Length': buffer.byteLength.toString()
}
});
// Excel-Datei als Buffer generieren
const buffer = await workbook.xlsx.writeBuffer();
} catch (error) {
console.error('Fehler beim Exportieren:', error);
return json({ error: 'Fehler beim Exportieren', details: (error as Error).message }, { status: 500 });
}
// Dateiname generieren
const dateiname = `Praktikanten_${zeitraum.bezeichnung.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
// Als Download zurückgeben
return new Response(buffer, {
status: 200,
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': `attachment; filename="${dateiname}"`,
'Content-Length': buffer.byteLength.toString()
}
});
} catch (error) {
console.error('Fehler beim Exportieren:', error);
return json(
{ error: 'Fehler beim Exportieren', details: (error as Error).message },
{ status: 500 }
);
}
}
// Hilfsfunktion: Schulart formatieren
function formatSchulart(schulart: string): string {
const schulartMap: Record<string, string> = {
'Gymnasium': 'Gymnasium',
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig',
'Fachoberschule': 'Fachoberschule',
'Realschule': 'Realschule',
'KGSR': 'KGS Realschulzweig',
'IGSR': 'IGS Realschulzweig'
};
return schulartMap[schulart] || schulart;
}
const schulartMap: Record<string, string> = {
Gymnasium: 'Gymnasium',
KGS_Gymnasialzweig: 'KGS Gymnasialzweig',
Fachoberschule: 'Fachoberschule',
Realschule: 'Realschule',
KGSR: 'KGS Realschulzweig',
IGSR: 'IGS Realschulzweig'
};
return schulartMap[schulart] || schulart;
}

View File

@@ -5,137 +5,143 @@ import { prisma } from '$lib/prisma';
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
const dienststellen = await prisma.dienststelle.findMany();
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraumId,
dienststelleId: dienststelle.id,
plaetze: 0 // Standardwert: 0 Plätze
}
});
}
const dienststellen = await prisma.dienststelle.findMany();
// Erstelle für jede existierende Dienststelle einen Eintrag mit 1 Platz
for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraumId,
dienststelleId: dienststelle.id,
plaetze: 1 // Standardwert: 1 Platz pro Dienststelle
}
});
}
}
export const GET: RequestHandler = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const zeitraeume = await prisma.praktikumszeitraum.findMany({
orderBy: { startDatum: 'desc' }
});
return json(zeitraeume);
} catch (error) {
console.error('Fehler beim Laden der Zeiträume:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
try {
const zeitraeume = await prisma.praktikumszeitraum.findMany({
orderBy: { startDatum: 'desc' }
});
return json(zeitraeume);
} catch (error) {
console.error('Fehler beim Laden der Zeiträume:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { bezeichnung, startDatum, endDatum } = await request.json();
try {
const { bezeichnung, startDatum, endDatum } = await request.json();
if (!bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
if (!bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
const start = new Date(startDatum);
const end = new Date(endDatum);
const start = new Date(startDatum);
const end = new Date(endDatum);
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
const zeitraum = await prisma.praktikumszeitraum.create({
data: {
bezeichnung,
startDatum: start,
endDatum: end
}
});
const zeitraum = await prisma.praktikumszeitraum.create({
data: {
bezeichnung,
startDatum: start,
endDatum: end
}
});
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen (mit 1 Platz)
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
return json(zeitraum);
} catch (error: any) {
console.error('Fehler beim Erstellen des Zeitraums:', error);
if (error.code === 'P2002') {
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
return json(zeitraum);
} catch (error: any) {
console.error('Fehler beim Erstellen des Zeitraums:', error);
if (error.code === 'P2002') {
return json(
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
{ status: 400 }
);
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { id, bezeichnung, startDatum, endDatum } = await request.json();
try {
const { id, bezeichnung, startDatum, endDatum } = await request.json();
if (!id || !bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
if (!id || !bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
const start = new Date(startDatum);
const end = new Date(endDatum);
const start = new Date(startDatum);
const end = new Date(endDatum);
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
const zeitraum = await prisma.praktikumszeitraum.update({
where: { id: parseInt(id) },
data: {
bezeichnung,
startDatum: start,
endDatum: end
}
});
const zeitraum = await prisma.praktikumszeitraum.update({
where: { id: parseInt(id) },
data: {
bezeichnung,
startDatum: start,
endDatum: end
}
});
return json(zeitraum);
} catch (error: any) {
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
if (error.code === 'P2002') {
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
return json(zeitraum);
} catch (error: any) {
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
if (error.code === 'P2002') {
return json(
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
{ status: 400 }
);
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const id = url.searchParams.get('id');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
try {
const id = url.searchParams.get('id');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
await prisma.praktikumszeitraum.delete({
where: { id: parseInt(id) }
});
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
await prisma.praktikumszeitraum.delete({
where: { id: parseInt(id) }
});
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen des Zeitraums:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen des Zeitraums:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -6,83 +6,87 @@ import { json } from '@sveltejs/kit';
const prisma = new PrismaClient();
export async function POST({ request }: RequestEvent) {
const formData = await request.formData();
const formData = await request.formData();
const get = (key: string) => formData.get(key)?.toString() ?? '';
const get = (key: string) => formData.get(key)?.toString() ?? '';
// 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 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[] = [];
const noteDeutsch = Number(get('noteDeutsch'));
const noteMathe = Number(get('noteMathe'));
// const gespeichertePfade: string[] = [];
const noteDeutsch = Number(get('noteDeutsch'));
const noteMathe = Number(get('noteMathe'));
if (isNaN(noteDeutsch) || isNaN(noteMathe)) {
return json({ error: 'Bitte gib gültige Noten an.' }, { status: 400 });
}
if (isNaN(noteDeutsch) || isNaN(noteMathe)) {
return json({ error: 'Bitte gib gültige Noten an.' }, { status: 400 });
}
for (const file of pdfFiles) {
if (file.size > 0 && file.type === 'application/pdf') {
const buffer = Buffer.from(await file.arrayBuffer());
const filename = `${randomUUID()}.pdf`;
const uploadPath = `/uploads/${filename}`;
await writeFile(`static${uploadPath}`, buffer);
pdfData.push({ pfad: uploadPath });
}
}
for (const file of pdfFiles) {
if (file.size > 0 && file.type === 'application/pdf') {
const buffer = Buffer.from(await file.arrayBuffer());
const filename = `${randomUUID()}.pdf`;
const uploadPath = `/uploads/${filename}`;
await writeFile(`static${uploadPath}`, buffer);
pdfData.push({ pfad: uploadPath });
}
}
try {
const anmeldung = await prisma.anmeldung.create({
data: {
// Persönliche Daten
anrede: formData.get('anrede') as string,
vorname: formData.get('vorname') as string,
nachname: formData.get('nachname') as string,
geburtsdatum: formData.get('geburtsdatum') as string,
// Adresse
strasse: formData.get('strasse') as string,
hausnummer: formData.get('hausnummer') as string,
ort: formData.get('ort') as string,
plz: formData.get('plz') as string,
// Kontakt
telefon: formData.get('telefon') as string,
email: formData.get('email') as string,
// Schule
schulart: formData.get('schulart') as string,
schulklasse: formData.get('schulklasse') as string,
noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
noteMathe: parseInt(formData.get('noteMathe') as string),
sozialverhalten: formData.get('sozialverhalten') as string || null,
// Praktikum
praktikumId: parseInt(formData.get('zeitraum') as string),
motivation: formData.get('motivation') as string || '',
// Wünsche
wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
// Alter (falls vom Frontend gesendet)
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
// System
zugewiesenId: null,
// timestamp wird automatisch durch @default(now()) gesetzt
pdfs: {
create: pdfData
}
}
});
try {
const anmeldung = await prisma.anmeldung.create({
data: {
// Persönliche Daten
anrede: formData.get('anrede') as string,
vorname: formData.get('vorname') as string,
nachname: formData.get('nachname') as string,
geburtsdatum: formData.get('geburtsdatum') as string,
// Adresse
strasse: formData.get('strasse') as string,
hausnummer: formData.get('hausnummer') as string,
ort: formData.get('ort') as string,
plz: formData.get('plz') as string,
// Kontakt
telefon: formData.get('telefon') as string,
email: formData.get('email') as string,
// Schule
schulart: formData.get('schulart') as string,
schulklasse: formData.get('schulklasse') as string,
noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
noteMathe: parseInt(formData.get('noteMathe') as string),
sozialverhalten: (formData.get('sozialverhalten') as string) || null,
// Praktikum
praktikumId: parseInt(formData.get('zeitraum') as string),
motivation: (formData.get('motivation') as string) || '',
// Wünsche
wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
// Alter (falls vom Frontend gesendet)
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
// Notfallkontakt
notfallVorname: (formData.get('notfallVorname') as string) || null,
notfallNachname: (formData.get('notfallNachname') as string) || null,
notfallTelefon: (formData.get('notfallTelefon') as string) || null,
// System
zugewiesenId: null,
// timestamp wird automatisch durch @default(now()) gesetzt
pdfs: {
create: pdfData
}
}
});
return json({ success: true, anmeldung });
} catch (err: unknown) {
if (err instanceof Error && (err as { code?: string }).code === 'P2002') {
return json({ error: 'Diese E-Mail wurde bereits verwendet.' }, { status: 400 });
}
return json({ success: true, anmeldung });
} catch (err: unknown) {
if (err instanceof Error && (err as { code?: string }).code === 'P2002') {
return json({ error: 'Diese E-Mail wurde bereits verwendet.' }, { status: 400 });
}
console.error(err);
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
}
}
console.error(err);
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
}
}