praktikum, Notfallkontakt und ein Platz pro Dienstelle bei neuer Zeitraum
This commit is contained in:
Binary file not shown.
@@ -30,7 +30,7 @@ model Dienststelle {
|
|||||||
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
|
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
|
||||||
zugewiesene Anmeldung[] @relation("Zugewiesen")
|
zugewiesene Anmeldung[] @relation("Zugewiesen")
|
||||||
|
|
||||||
zeitraumPlaetze ZeitraumPlaetze[]
|
zeitraumPlaetze ZeitraumPlaetze[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Praktikumszeitraum {
|
model Praktikumszeitraum {
|
||||||
@@ -44,13 +44,13 @@ model Praktikumszeitraum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ZeitraumPlaetze {
|
model ZeitraumPlaetze {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
zeitraumId Int
|
zeitraumId Int
|
||||||
dienststelleId Int
|
dienststelleId Int
|
||||||
plaetze Int @default(0)
|
plaetze Int @default(0)
|
||||||
|
|
||||||
zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade)
|
zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade)
|
||||||
dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade)
|
dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([zeitraumId, dienststelleId])
|
@@unique([zeitraumId, dienststelleId])
|
||||||
@@index([zeitraumId])
|
@@index([zeitraumId])
|
||||||
@@ -66,7 +66,7 @@ enum Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Anmeldung {
|
model Anmeldung {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
anrede String
|
anrede String
|
||||||
vorname String
|
vorname String
|
||||||
nachname String
|
nachname String
|
||||||
@@ -84,24 +84,30 @@ model Anmeldung {
|
|||||||
sozialverhalten String?
|
sozialverhalten String?
|
||||||
motivation String?
|
motivation String?
|
||||||
alter Int?
|
alter Int?
|
||||||
status Status @default(OFFEN)
|
|
||||||
|
|
||||||
processedAt DateTime?
|
// Notfallkontakt
|
||||||
|
notfallVorname String?
|
||||||
|
notfallNachname String?
|
||||||
|
notfallTelefon String?
|
||||||
|
|
||||||
praktikumId Int?
|
status Status @default(OFFEN)
|
||||||
praktikum Praktikumszeitraum? @relation("PraktikumszeitraumAnmeldungen", fields: [praktikumId], references: [id])
|
|
||||||
|
|
||||||
zugewiesenId Int?
|
processedAt DateTime?
|
||||||
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())
|
praktikumId Int?
|
||||||
pdfs PdfDatei[]
|
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([status])
|
||||||
@@index([processedAt])
|
@@index([processedAt])
|
||||||
|
|||||||
@@ -92,240 +92,255 @@
|
|||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<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-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Persönliche Daten
|
Persönliche Daten
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Kontakt & Adresse
|
Kontakt & Adresse
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Schule & Noten
|
Schule & Noten
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Praktikum & Wünsche
|
Praktikum & Wünsche
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Dokumente
|
Dokumente
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-3 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 align-top">
|
<tr class="hover:bg-gray-50 align-top">
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<td class="px-3 py-4">
|
<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')}">
|
<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>
|
||||||
<div class="text-xs text-gray-400 mt-2">
|
<div class="text-xs text-gray-400 mt-2">
|
||||||
Eingang:<br>
|
Eingang:<br>
|
||||||
{formatDate(anmeldung.timestamp)}
|
{formatDate(anmeldung.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Persönliche Daten -->
|
<!-- Persönliche Daten -->
|
||||||
<td class="px-3 py-4">
|
<td class="px-3 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 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>
|
||||||
<div class="text-xs text-gray-500 mt-1">
|
{/if}
|
||||||
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
|
</td>
|
||||||
</div>
|
|
||||||
{#if anmeldung.alter}
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Kontakt & Adresse -->
|
<!-- Kontakt & Adresse -->
|
||||||
<td class="px-3 py-4 text-sm">
|
<td class="px-3 py-4 text-sm">
|
||||||
<div class="text-xs space-y-1">
|
<div class="text-xs space-y-1">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-700">Adresse:</span><br>
|
<span class="font-medium text-gray-700">Adresse:</span><br>
|
||||||
<span class="text-gray-600">
|
<span class="text-gray-600">
|
||||||
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
|
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
|
||||||
{anmeldung.plz || ''} {anmeldung.ort || ''}
|
{anmeldung.plz || ''} {anmeldung.ort || ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-700">Tel:</span>
|
<span class="font-medium text-gray-700">Tel:</span>
|
||||||
{#if anmeldung.telefon}
|
{#if anmeldung.telefon}
|
||||||
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
|
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-gray-400">-</span>
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{/if}
|
||||||
<span class="font-medium text-gray-700">E-Mail:</span><br>
|
</div>
|
||||||
<a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a>
|
</td>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Schule & Noten -->
|
<!-- Schule & Noten -->
|
||||||
<td class="px-3 py-4 text-sm">
|
<td class="px-3 py-4 text-sm">
|
||||||
<div class="text-xs space-y-1">
|
<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>
|
<div>
|
||||||
<span class="font-medium text-gray-700">Schulart:</span><br>
|
<span class="font-medium text-gray-700">Klasse:</span>
|
||||||
<span class="text-gray-900">{formatSchulart(anmeldung.schulart)}</span>
|
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
|
||||||
</div>
|
</div>
|
||||||
{#if anmeldung.schulklasse}
|
{/if}
|
||||||
<div>
|
<div class="pt-1 border-t border-gray-100">
|
||||||
<span class="font-medium text-gray-700">Klasse:</span>
|
<span class="font-medium text-gray-700">Noten:</span>
|
||||||
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
|
<div class="flex gap-2 mt-1">
|
||||||
</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">
|
|
||||||
<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">
|
||||||
D: <span class="font-bold ml-1">{anmeldung.noteDeutsch || '-'}</span>
|
D: <span class="font-bold ml-1">{anmeldung.noteDeutsch || '-'}</span>
|
||||||
</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>
|
M: <span class="font-bold ml-1">{anmeldung.noteMathe || '-'}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</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>
|
</div>
|
||||||
</td>
|
{#if anmeldung.sozialverhalten}
|
||||||
|
|
||||||
<!-- 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 -->
|
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-700">Wünsche:</span>
|
<span class="font-medium text-gray-700">Sozialverh.:</span>
|
||||||
<div class="space-y-1 mt-1">
|
<span class="text-gray-900 text-xs">{formatSozialverhalten(anmeldung.sozialverhalten)}</span>
|
||||||
{#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>
|
</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}
|
{/if}
|
||||||
</td>
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- Aktionen -->
|
<!-- Praktikum & Wünsche -->
|
||||||
<td class="px-3 py-4">
|
<td class="px-3 py-4 text-sm">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="text-xs space-y-2">
|
||||||
{#if canBeAccepted(anmeldung.status || 'pending')}
|
<!-- Zeitraum -->
|
||||||
<button
|
{#if anmeldung.zeitraum}
|
||||||
on:click={() => dispatch('accept', { id: anmeldung.id })}
|
<div>
|
||||||
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"
|
<span class="font-medium text-gray-700">Zeitraum:</span><br>
|
||||||
>
|
<span class="text-gray-900">{formatZeitraum(anmeldung.zeitraum)}</span>
|
||||||
✓ Annehmen
|
</div>
|
||||||
</button>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if canBeRejected(anmeldung.status || 'pending')}
|
<!-- Zugewiesene Dienststelle -->
|
||||||
<button
|
{#if anmeldung.assignedDienststelle}
|
||||||
on:click={() => dispatch('reject', { id: anmeldung.id })}
|
<div class="p-2 bg-green-50 border border-green-200 rounded">
|
||||||
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"
|
<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">
|
||||||
✗ Ablehnen
|
<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" />
|
||||||
</button>
|
</svg>
|
||||||
{/if}
|
<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
|
<!-- Alle 3 Wünsche -->
|
||||||
on:click={() => dispatch('delete', { id: anmeldung.id })}
|
<div>
|
||||||
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"
|
<span class="font-medium text-gray-700">Wünsche:</span>
|
||||||
>
|
<div class="space-y-1 mt-1">
|
||||||
🗑 Löschen
|
{#if anmeldung.wunsch1}
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
<!-- Motivation -->
|
||||||
{/each}
|
{#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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
let zeitraum = '';
|
let zeitraum = '';
|
||||||
let motivation = '';
|
let motivation = '';
|
||||||
|
|
||||||
|
// Notfallkontakt
|
||||||
|
let notfallVorname = '';
|
||||||
|
let notfallNachname = '';
|
||||||
|
let notfallTelefon = '';
|
||||||
|
|
||||||
let wunsch1Id = '';
|
let wunsch1Id = '';
|
||||||
let wunsch2Id = '';
|
let wunsch2Id = '';
|
||||||
let wunsch3Id = '';
|
let wunsch3Id = '';
|
||||||
@@ -42,9 +47,9 @@
|
|||||||
|
|
||||||
// Hinweis für IGS/KGS mit Lernentwicklungsbericht
|
// Hinweis für IGS/KGS mit Lernentwicklungsbericht
|
||||||
$: zeigeIgsKgsHinweis =
|
$: zeigeIgsKgsHinweis =
|
||||||
['IGSR', 'KGSR'].includes(schulart) &&
|
['IGSR', 'KGSR'].includes(schulart) &&
|
||||||
schulklasse &&
|
schulklasse &&
|
||||||
parseInt(schulklasse) < 10;
|
parseInt(schulklasse) < 10;
|
||||||
|
|
||||||
// Berechnung des Alters
|
// Berechnung des Alters
|
||||||
$: {
|
$: {
|
||||||
@@ -169,8 +174,8 @@
|
|||||||
let startDatum = '';
|
let startDatum = '';
|
||||||
|
|
||||||
$: hideSozialVerhalten =
|
$: hideSozialVerhalten =
|
||||||
Number(schulklasse) >= 11 &&
|
Number(schulklasse) >= 11 &&
|
||||||
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const resZeitraeume = await fetch('/api/zeitraeume');
|
const resZeitraeume = await fetch('/api/zeitraeume');
|
||||||
@@ -208,6 +213,9 @@
|
|||||||
noteMathe = '';
|
noteMathe = '';
|
||||||
sozialverhalten = '';
|
sozialverhalten = '';
|
||||||
schulklasse = '';
|
schulklasse = '';
|
||||||
|
notfallVorname = '';
|
||||||
|
notfallNachname = '';
|
||||||
|
notfallTelefon = '';
|
||||||
pdfDateien = [];
|
pdfDateien = [];
|
||||||
fileInputKey += 1;
|
fileInputKey += 1;
|
||||||
success = false;
|
success = false;
|
||||||
@@ -246,6 +254,9 @@
|
|||||||
data.append('sozialverhalten', sozialverhalten);
|
data.append('sozialverhalten', sozialverhalten);
|
||||||
data.append('schulklasse', schulklasse);
|
data.append('schulklasse', schulklasse);
|
||||||
data.append('alter', alter);
|
data.append('alter', alter);
|
||||||
|
data.append('notfallVorname', notfallVorname);
|
||||||
|
data.append('notfallNachname', notfallNachname);
|
||||||
|
data.append('notfallTelefon', notfallTelefon);
|
||||||
|
|
||||||
for (const pdf of pdfDateien) {
|
for (const pdf of pdfDateien) {
|
||||||
data.append('pdfs', pdf);
|
data.append('pdfs', pdf);
|
||||||
@@ -269,7 +280,7 @@
|
|||||||
|
|
||||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-6">
|
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-6">
|
||||||
<form on:submit|preventDefault={anmelden}
|
<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>
|
<h1 class="text-2xl font-bold text-gray-800 mb-4 text-center">Praktikumsanmeldung</h1>
|
||||||
|
|
||||||
@@ -319,10 +330,10 @@
|
|||||||
{#if !hideSozialVerhalten}
|
{#if !hideSozialVerhalten}
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<select
|
<select
|
||||||
bind:value={sozialverhalten}
|
bind:value={sozialverhalten}
|
||||||
required
|
required
|
||||||
class="input"
|
class="input"
|
||||||
class:input-error={sozialverhaltenFehler}
|
class:input-error={sozialverhaltenFehler}
|
||||||
>
|
>
|
||||||
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
|
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
|
||||||
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
|
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
|
||||||
@@ -353,24 +364,24 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<input
|
<input
|
||||||
bind:value={noteDeutsch}
|
bind:value={noteDeutsch}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="6"
|
max="6"
|
||||||
placeholder="Note in Deutsch"
|
placeholder="Note in Deutsch"
|
||||||
required
|
required
|
||||||
class="input"
|
class="input"
|
||||||
class:input-error={notenFehler}
|
class:input-error={notenFehler}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
bind:value={noteMathe}
|
bind:value={noteMathe}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="6"
|
max="6"
|
||||||
placeholder="Note in Mathe"
|
placeholder="Note in Mathe"
|
||||||
required
|
required
|
||||||
class="input"
|
class="input"
|
||||||
class:input-error={notenFehler}
|
class:input-error={notenFehler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if notenFehler}
|
{#if notenFehler}
|
||||||
@@ -385,7 +396,7 @@
|
|||||||
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
||||||
{#each filteredZeitraeume as d}
|
{#each filteredZeitraeume as d}
|
||||||
<option
|
<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' })})
|
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>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@@ -453,7 +464,7 @@
|
|||||||
|
|
||||||
<!-- Motivation -->
|
<!-- Motivation -->
|
||||||
<textarea bind:value={motivation} placeholder="Motivation (optional)"
|
<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>
|
</textarea>
|
||||||
|
|
||||||
<!-- Mehrere PDF Upload -->
|
<!-- Mehrere PDF Upload -->
|
||||||
@@ -461,23 +472,33 @@
|
|||||||
<div>
|
<div>
|
||||||
<label for="pdf-upload" class="block text-gray-700 font-medium mb-1">Zeugnis hochladen:</label>
|
<label for="pdf-upload" class="block text-gray-700 font-medium mb-1">Zeugnis hochladen:</label>
|
||||||
<input
|
<input
|
||||||
id="pdf-upload"
|
id="pdf-upload"
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/pdf"
|
accept="application/pdf"
|
||||||
multiple
|
multiple
|
||||||
on:change={(e) => pdfDateien = Array.from((e.target as HTMLInputElement).files || [])}
|
on:change={(e) => pdfDateien = Array.from((e.target as HTMLInputElement).files || [])}
|
||||||
class="input"
|
class="input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/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}
|
{#if showAblehnungModal}
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<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">
|
<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>
|
<p class="text-red-600 font-semibold">{ablehnungHinweis}</p>
|
||||||
<button
|
<button
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
on:click={() => { resetForm(); showAblehnungModal = false; }}
|
on:click={() => { resetForm(); showAblehnungModal = false; }}
|
||||||
>
|
>
|
||||||
OK
|
OK
|
||||||
</button>
|
</button>
|
||||||
@@ -487,14 +508,14 @@
|
|||||||
|
|
||||||
<!-- Button - deaktiviert bei Validierungsfehlern oder fehlenden Pflichtfeldern -->
|
<!-- Button - deaktiviert bei Validierungsfehlern oder fehlenden Pflichtfeldern -->
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||||
class="w-full py-3 rounded-xl transition-all"
|
class="w-full py-3 rounded-xl transition-all"
|
||||||
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||||
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||||
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||||
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||||
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||||
>
|
>
|
||||||
Jetzt anmelden
|
Jetzt anmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -508,8 +529,8 @@
|
|||||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
<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>
|
<p class="text-green-600 font-semibold">Anmeldung erfolgreich gesendet!</p>
|
||||||
<button
|
<button
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
on:click={resetForm}
|
on:click={resetForm}
|
||||||
>
|
>
|
||||||
OK
|
OK
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
noteDeutsch?: string;
|
noteDeutsch?: string;
|
||||||
noteMathe?: string;
|
noteMathe?: string;
|
||||||
sozialverhalten?: string;
|
sozialverhalten?: string;
|
||||||
|
notfallVorname?: string;
|
||||||
|
notfallNachname?: string;
|
||||||
|
notfallTelefon?: string;
|
||||||
wunsch1?: { id: number; name: string };
|
wunsch1?: { id: number; name: string };
|
||||||
wunsch2?: { id: number; name: string };
|
wunsch2?: { id: number; name: string };
|
||||||
wunsch3?: { id: number; name: string };
|
wunsch3?: { id: number; name: string };
|
||||||
@@ -121,8 +124,8 @@ Ihr Praktikumsteam`;
|
|||||||
// Zähle angenommene Anmeldungen pro Zeitraum
|
// Zähle angenommene Anmeldungen pro Zeitraum
|
||||||
function getAcceptedCountForZeitraum(zeitraumId: number): number {
|
function getAcceptedCountForZeitraum(zeitraumId: number): number {
|
||||||
return anmeldungen.filter(a =>
|
return anmeldungen.filter(a =>
|
||||||
a.status === 'accepted' &&
|
a.status === 'accepted' &&
|
||||||
a.zeitraum?.id === zeitraumId
|
a.zeitraum?.id === zeitraumId
|
||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,15 +269,15 @@ Ihr Praktikumsteam`;
|
|||||||
// Dienststelle finden
|
// Dienststelle finden
|
||||||
const dienststelle = availableWishes.find(w => w.id === dienststelleId);
|
const dienststelle = availableWishes.find(w => w.id === dienststelleId);
|
||||||
const dienststelleName = dienststelle
|
const dienststelleName = dienststelle
|
||||||
? dienststelle.name.replace(/^\d+\.\s*Wunsch:\s*/, '')
|
? dienststelle.name.replace(/^\d+\.\s*Wunsch:\s*/, '')
|
||||||
: 'Unbekannte Dienststelle';
|
: 'Unbekannte Dienststelle';
|
||||||
|
|
||||||
// E-Mail Text personalisieren
|
// E-Mail Text personalisieren
|
||||||
const personalizedEmail = emailTemplate
|
const personalizedEmail = emailTemplate
|
||||||
.replace(/\{anrede\}/g, anmeldung.anrede)
|
.replace(/\{anrede\}/g, anmeldung.anrede)
|
||||||
.replace(/\{vorname\}/g, anmeldung.vorname)
|
.replace(/\{vorname\}/g, anmeldung.vorname)
|
||||||
.replace(/\{nachname\}/g, anmeldung.nachname)
|
.replace(/\{nachname\}/g, anmeldung.nachname)
|
||||||
.replace(/\{dienststelle\}/g, dienststelleName);
|
.replace(/\{dienststelle\}/g, dienststelleName);
|
||||||
|
|
||||||
// Modal mit Vorschau öffnen
|
// Modal mit Vorschau öffnen
|
||||||
emailPreviewData = {
|
emailPreviewData = {
|
||||||
@@ -431,8 +434,8 @@ Ihr Praktikumsteam`;
|
|||||||
|
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<AdminHeader
|
<AdminHeader
|
||||||
title="Anmeldungen verwalten"
|
title="Anmeldungen verwalten"
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
<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">
|
<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
|
||||||
id="status-filter"
|
id="status-filter"
|
||||||
bind:value={statusFilter}
|
bind:value={statusFilter}
|
||||||
class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
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="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>
|
||||||
@@ -457,9 +460,9 @@ Ihr Praktikumsteam`;
|
|||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<!-- Excel Export Button -->
|
<!-- Excel Export Button -->
|
||||||
<button
|
<button
|
||||||
on:click={openExportModal}
|
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"
|
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}
|
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">
|
<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" />
|
<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 -->
|
<!-- E-Mail Konfiguration Button -->
|
||||||
<button
|
<button
|
||||||
on:click={() => showEmailConfig = !showEmailConfig}
|
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"
|
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}
|
disabled={isLoadingEmailConfig}
|
||||||
>
|
>
|
||||||
{#if isLoadingEmailConfig}
|
{#if isLoadingEmailConfig}
|
||||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
@@ -553,11 +556,11 @@ Ihr Praktikumsteam`;
|
|||||||
E-Mail Betreff
|
E-Mail Betreff
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="email-subject"
|
id="email-subject"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={emailSubject}
|
bind:value={emailSubject}
|
||||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
disabled={isSavingEmailConfig}
|
disabled={isSavingEmailConfig}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -566,11 +569,11 @@ Ihr Praktikumsteam`;
|
|||||||
E-Mail Text
|
E-Mail Text
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="email-template"
|
id="email-template"
|
||||||
bind:value={emailTemplate}
|
bind:value={emailTemplate}
|
||||||
rows="10"
|
rows="10"
|
||||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
disabled={isSavingEmailConfig}
|
disabled={isSavingEmailConfig}
|
||||||
></textarea>
|
></textarea>
|
||||||
<p class="mt-2 text-sm text-gray-500">
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
Verfügbare Platzhalter: {anrede}, {vorname}, {nachname}, {dienststelle}
|
Verfügbare Platzhalter: {anrede}, {vorname}, {nachname}, {dienststelle}
|
||||||
@@ -579,16 +582,16 @@ Ihr Praktikumsteam`;
|
|||||||
|
|
||||||
<div class="flex justify-end space-x-3">
|
<div class="flex justify-end space-x-3">
|
||||||
<button
|
<button
|
||||||
on:click={() => showEmailConfig = false}
|
on:click={() => showEmailConfig = false}
|
||||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||||
disabled={isSavingEmailConfig}
|
disabled={isSavingEmailConfig}
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={saveEmailTemplate}
|
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"
|
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}
|
disabled={isSavingEmailConfig}
|
||||||
>
|
>
|
||||||
{#if isSavingEmailConfig}
|
{#if isSavingEmailConfig}
|
||||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
@@ -615,8 +618,8 @@ Ihr Praktikumsteam`;
|
|||||||
<p class="mt-1 text-sm text-red-700">{error}</p>
|
<p class="mt-1 text-sm text-red-700">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
on:click={() => error = ''}
|
on:click={() => error = ''}
|
||||||
class="ml-auto text-red-400 hover:text-red-600"
|
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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
@@ -641,19 +644,19 @@ Ihr Praktikumsteam`;
|
|||||||
</h3>
|
</h3>
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
{statusFilter === 'all'
|
{statusFilter === 'all'
|
||||||
? 'Es sind noch keine Praktikumsanmeldungen eingegangen.'
|
? 'Es sind noch keine Praktikumsanmeldungen eingegangen.'
|
||||||
: `Es gibt keine Anmeldungen mit dem Status "${getStatusText(statusFilter).toLowerCase()}".`}
|
: `Es gibt keine Anmeldungen mit dem Status "${getStatusText(statusFilter).toLowerCase()}".`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
<AnmeldungenTable
|
<AnmeldungenTable
|
||||||
anmeldungen={filteredAnmeldungen}
|
anmeldungen={filteredAnmeldungen}
|
||||||
{getStatusColor}
|
{getStatusColor}
|
||||||
{getStatusText}
|
{getStatusText}
|
||||||
on:accept={handleAccept}
|
on:accept={handleAccept}
|
||||||
on:reject={handleReject}
|
on:reject={handleReject}
|
||||||
on:delete={handleDelete}
|
on:delete={handleDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -663,10 +666,10 @@ Ihr Praktikumsteam`;
|
|||||||
<!-- Dienststellen Dialog -->
|
<!-- Dienststellen Dialog -->
|
||||||
{#if showDialog}
|
{#if showDialog}
|
||||||
<DienststellenDialog
|
<DienststellenDialog
|
||||||
wishes={availableWishes}
|
wishes={availableWishes}
|
||||||
selectedId={selectedDienststelleId}
|
selectedId={selectedDienststelleId}
|
||||||
on:confirm={handleConfirmAccept}
|
on:confirm={handleConfirmAccept}
|
||||||
on:cancel={closeDialog}
|
on:cancel={closeDialog}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -678,8 +681,8 @@ Ihr Praktikumsteam`;
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||||
on:click={closeExportModal}
|
on:click={closeExportModal}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
@@ -695,8 +698,8 @@ Ihr Praktikumsteam`;
|
|||||||
<h3 class="text-lg font-medium text-gray-900">Excel-Export</h3>
|
<h3 class="text-lg font-medium text-gray-900">Excel-Export</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
on:click={closeExportModal}
|
on:click={closeExportModal}
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
@@ -715,10 +718,10 @@ Ihr Praktikumsteam`;
|
|||||||
Praktikumszeitraum
|
Praktikumszeitraum
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="export-zeitraum"
|
id="export-zeitraum"
|
||||||
bind:value={selectedExportZeitraum}
|
bind:value={selectedExportZeitraum}
|
||||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500"
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500"
|
||||||
disabled={isExporting}
|
disabled={isExporting}
|
||||||
>
|
>
|
||||||
<option value="" disabled>Zeitraum auswählen...</option>
|
<option value="" disabled>Zeitraum auswählen...</option>
|
||||||
{#each zeitraeume as z}
|
{#each zeitraeume as z}
|
||||||
@@ -734,16 +737,16 @@ Ihr Praktikumsteam`;
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end space-x-3">
|
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end space-x-3">
|
||||||
<button
|
<button
|
||||||
on:click={closeExportModal}
|
on:click={closeExportModal}
|
||||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||||
disabled={isExporting}
|
disabled={isExporting}
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={exportToExcel}
|
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"
|
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}
|
disabled={!selectedExportZeitraum || isExporting}
|
||||||
>
|
>
|
||||||
{#if isExporting}
|
{#if isExporting}
|
||||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
@@ -769,8 +772,8 @@ Ihr Praktikumsteam`;
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||||
on:click={closeEmailPreview}
|
on:click={closeEmailPreview}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
@@ -786,8 +789,8 @@ Ihr Praktikumsteam`;
|
|||||||
<h3 class="text-lg font-medium text-gray-900">E-Mail Vorschau</h3>
|
<h3 class="text-lg font-medium text-gray-900">E-Mail Vorschau</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
on:click={closeEmailPreview}
|
on:click={closeEmailPreview}
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
@@ -817,11 +820,11 @@ Ihr Praktikumsteam`;
|
|||||||
<span class="font-normal text-gray-500">(kann bearbeitet werden)</span>
|
<span class="font-normal text-gray-500">(kann bearbeitet werden)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="email-body"
|
id="email-body"
|
||||||
bind:value={emailPreviewData.body}
|
bind:value={emailPreviewData.body}
|
||||||
rows="14"
|
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"
|
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}
|
disabled={emailCopied}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -853,14 +856,14 @@ Ihr Praktikumsteam`;
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-3 w-full sm:w-auto">
|
<div class="flex space-x-3 w-full sm:w-auto">
|
||||||
<button
|
<button
|
||||||
on:click={closeEmailPreview}
|
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"
|
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
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={copyAndOpenMail}
|
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"
|
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">
|
<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" />
|
<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" />
|
||||||
|
|||||||
@@ -6,282 +6,321 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const anmeldungen = await prisma.anmeldung.findMany({
|
const anmeldungen = await prisma.anmeldung.findMany({
|
||||||
include: {
|
include: {
|
||||||
wunsch1: true,
|
wunsch1: true,
|
||||||
wunsch2: true,
|
wunsch2: true,
|
||||||
wunsch3: true,
|
wunsch3: true,
|
||||||
zugewiesen: true,
|
zugewiesen: true,
|
||||||
praktikum: true,
|
praktikum: true,
|
||||||
pdfs: true
|
pdfs: true
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
timestamp: 'desc'
|
timestamp: 'desc'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
geburtsdatum: anmeldung.geburtsdatum,
|
geburtsdatum: anmeldung.geburtsdatum,
|
||||||
strasse: anmeldung.strasse,
|
strasse: anmeldung.strasse,
|
||||||
hausnummer: anmeldung.hausnummer,
|
hausnummer: anmeldung.hausnummer,
|
||||||
ort: anmeldung.ort,
|
ort: anmeldung.ort,
|
||||||
plz: anmeldung.plz,
|
plz: anmeldung.plz,
|
||||||
telefon: anmeldung.telefon,
|
telefon: anmeldung.telefon,
|
||||||
schulart: anmeldung.schulart,
|
schulart: anmeldung.schulart,
|
||||||
schulklasse: anmeldung.schulklasse,
|
schulklasse: anmeldung.schulklasse,
|
||||||
motivation: anmeldung.motivation,
|
motivation: anmeldung.motivation,
|
||||||
alter: anmeldung.alter,
|
alter: anmeldung.alter,
|
||||||
|
|
||||||
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
||||||
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
||||||
sozialverhalten: anmeldung.sozialverhalten,
|
sozialverhalten: anmeldung.sozialverhalten,
|
||||||
|
|
||||||
status: mapPrismaStatusToFrontend(anmeldung.status),
|
// Notfallkontakt
|
||||||
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
notfallVorname: anmeldung.notfallVorname,
|
||||||
|
notfallNachname: anmeldung.notfallNachname,
|
||||||
|
notfallTelefon: anmeldung.notfallTelefon,
|
||||||
|
|
||||||
wunsch1: anmeldung.wunsch1 ? {
|
status: mapPrismaStatusToFrontend(anmeldung.status),
|
||||||
id: anmeldung.wunsch1.id,
|
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
||||||
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 ? {
|
wunsch1: anmeldung.wunsch1
|
||||||
id: anmeldung.zugewiesen.id,
|
? {
|
||||||
name: anmeldung.zugewiesen.name
|
id: anmeldung.wunsch1.id,
|
||||||
} : undefined,
|
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,
|
||||||
|
|
||||||
zeitraum: anmeldung.praktikum ? {
|
assignedDienststelle: anmeldung.zugewiesen
|
||||||
id: anmeldung.praktikum.id,
|
? {
|
||||||
bezeichnung: anmeldung.praktikum.bezeichnung,
|
id: anmeldung.zugewiesen.id,
|
||||||
startDatum: anmeldung.praktikum.startDatum.toISOString(),
|
name: anmeldung.zugewiesen.name
|
||||||
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
}
|
||||||
} : undefined,
|
: undefined,
|
||||||
|
|
||||||
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
zeitraum: anmeldung.praktikum
|
||||||
|
? {
|
||||||
|
id: anmeldung.praktikum.id,
|
||||||
|
bezeichnung: anmeldung.praktikum.bezeichnung,
|
||||||
|
startDatum: anmeldung.praktikum.startDatum.toISOString(),
|
||||||
|
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
pdfs: anmeldung.pdfs || []
|
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
||||||
}));
|
|
||||||
|
|
||||||
return json(formattedAnmeldungen);
|
pdfs: anmeldung.pdfs || []
|
||||||
} 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 });
|
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 }) {
|
export async function POST({ request, url }) {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(url.searchParams.get('id') || '0');
|
const id = parseInt(url.searchParams.get('id') || '0');
|
||||||
const { dienststelleId } = await request.json();
|
const { dienststelleId } = await request.json();
|
||||||
|
|
||||||
if (!id || !dienststelleId) {
|
if (!id || !dienststelleId) {
|
||||||
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
|
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
|
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
|
||||||
const existingAnmeldung = await prisma.anmeldung.findUnique({
|
const existingAnmeldung = await prisma.anmeldung.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
praktikum: true
|
praktikum: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingAnmeldung) {
|
if (!existingAnmeldung) {
|
||||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingAnmeldung.status === 'ANGENOMMEN') {
|
if (existingAnmeldung.status === 'ANGENOMMEN') {
|
||||||
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const zeitraumId = existingAnmeldung.praktikumId;
|
const zeitraumId = existingAnmeldung.praktikumId;
|
||||||
|
|
||||||
if (!zeitraumId) {
|
if (!zeitraumId) {
|
||||||
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 });
|
return json(
|
||||||
}
|
{ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
|
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
|
||||||
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
||||||
where: {
|
where: {
|
||||||
zeitraumId_dienststelleId: {
|
zeitraumId_dienststelleId: {
|
||||||
zeitraumId: zeitraumId,
|
zeitraumId: zeitraumId,
|
||||||
dienststelleId: dienststelleId
|
dienststelleId: dienststelleId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!zeitraumPlaetze) {
|
if (!zeitraumPlaetze) {
|
||||||
return json({
|
return json(
|
||||||
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
|
{
|
||||||
}, { status: 400 });
|
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
|
||||||
}
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfen ob noch Plätze frei sind (plaetze > 0)
|
// Prüfen ob noch Plätze frei sind (plaetze > 0)
|
||||||
if (zeitraumPlaetze.plaetze <= 0) {
|
if (zeitraumPlaetze.plaetze <= 0) {
|
||||||
return json({
|
return json(
|
||||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
|
{
|
||||||
}, { status: 400 });
|
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
|
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
|
||||||
// Verwendung von $transaction für Atomarität (Race Condition vermeiden)
|
// Verwendung von $transaction für Atomarität (Race Condition vermeiden)
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// Nochmal prüfen innerhalb der Transaktion
|
// Nochmal prüfen innerhalb der Transaktion
|
||||||
const aktuellerStand = await tx.zeitraumPlaetze.findUnique({
|
const aktuellerStand = await tx.zeitraumPlaetze.findUnique({
|
||||||
where: {
|
where: {
|
||||||
zeitraumId_dienststelleId: {
|
zeitraumId_dienststelleId: {
|
||||||
zeitraumId: zeitraumId,
|
zeitraumId: zeitraumId,
|
||||||
dienststelleId: dienststelleId
|
dienststelleId: dienststelleId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!aktuellerStand || aktuellerStand.plaetze <= 0) {
|
if (!aktuellerStand || aktuellerStand.plaetze <= 0) {
|
||||||
throw new Error('Keine freien Plätze mehr verfügbar');
|
throw new Error('Keine freien Plätze mehr verfügbar');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anmeldung aktualisieren
|
// Anmeldung aktualisieren
|
||||||
await tx.anmeldung.update({
|
await tx.anmeldung.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
status: 'ANGENOMMEN',
|
status: 'ANGENOMMEN',
|
||||||
zugewiesenId: dienststelleId,
|
zugewiesenId: dienststelleId,
|
||||||
processedAt: new Date()
|
processedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Plätze in ZeitraumPlaetze reduzieren
|
// Plätze in ZeitraumPlaetze reduzieren
|
||||||
await tx.zeitraumPlaetze.update({
|
await tx.zeitraumPlaetze.update({
|
||||||
where: {
|
where: {
|
||||||
zeitraumId_dienststelleId: {
|
zeitraumId_dienststelleId: {
|
||||||
zeitraumId: zeitraumId,
|
zeitraumId: zeitraumId,
|
||||||
dienststelleId: dienststelleId
|
dienststelleId: dienststelleId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
plaetze: {
|
plaetze: {
|
||||||
decrement: 1
|
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);
|
||||||
|
|
||||||
// Spezifische Fehlermeldung für "keine Plätze"
|
// Spezifische Fehlermeldung für "keine Plätze"
|
||||||
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
|
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
|
||||||
return json({
|
return json(
|
||||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
|
{
|
||||||
}, { status: 400 });
|
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(
|
||||||
}
|
{ error: 'Fehler beim Annehmen der Anmeldung', details: (error as 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 } = 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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let updateData = {};
|
let updateData = {};
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'reject':
|
case 'reject':
|
||||||
updateData = {
|
updateData = {
|
||||||
status: 'ABGELEHNT',
|
status: 'ABGELEHNT',
|
||||||
processedAt: new Date()
|
processedAt: new Date()
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'set_processing':
|
case 'set_processing':
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateData = {
|
updateData = {
|
||||||
status: 'BEARBEITUNG',
|
status: 'BEARBEITUNG',
|
||||||
processedAt: new Date()
|
processedAt: new Date()
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'reset_processing':
|
case 'reset_processing':
|
||||||
updateData = {
|
updateData = {
|
||||||
status: 'OFFEN',
|
status: 'OFFEN',
|
||||||
processedAt: null
|
processedAt: null
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
|
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.anmeldung.update({
|
await prisma.anmeldung.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateData
|
data: updateData
|
||||||
});
|
});
|
||||||
|
|
||||||
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', details: (error as Error).message }, { status: 500 });
|
return json(
|
||||||
}
|
{ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE({ url }) {
|
export async function DELETE({ url }) {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(url.searchParams.get('id') || '0');
|
const id = parseInt(url.searchParams.get('id') || '0');
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
|
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
|
||||||
await prisma.anmeldung.delete({
|
await prisma.anmeldung.delete({
|
||||||
where: { id }
|
where: { id }
|
||||||
});
|
});
|
||||||
|
|
||||||
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', details: (error as Error).message }, { status: 500 });
|
return json(
|
||||||
}
|
{ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: Prisma Status zu Frontend Status
|
// Hilfsfunktion: Prisma Status zu Frontend Status
|
||||||
function mapPrismaStatusToFrontend(prismaStatus: string): string {
|
function mapPrismaStatusToFrontend(prismaStatus: string): string {
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
'OFFEN': 'pending',
|
OFFEN: 'pending',
|
||||||
'BEARBEITUNG': 'processing',
|
BEARBEITUNG: 'processing',
|
||||||
'ANGENOMMEN': 'accepted',
|
ANGENOMMEN: 'accepted',
|
||||||
'ABGELEHNT': 'rejected'
|
ABGELEHNT: 'rejected'
|
||||||
};
|
};
|
||||||
|
|
||||||
return statusMap[prismaStatus] || 'pending';
|
return statusMap[prismaStatus] || 'pending';
|
||||||
}
|
}
|
||||||
@@ -7,177 +7,210 @@ import ExcelJS from 'exceljs';
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export async function GET({ url }) {
|
export async function GET({ url }) {
|
||||||
try {
|
try {
|
||||||
const zeitraumId = url.searchParams.get('zeitraumId');
|
const zeitraumId = url.searchParams.get('zeitraumId');
|
||||||
|
|
||||||
if (!zeitraumId) {
|
if (!zeitraumId) {
|
||||||
return json({ error: 'Zeitraum-ID erforderlich' }, { status: 400 });
|
return json({ error: 'Zeitraum-ID erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeitraum laden für Dateinamen
|
// Zeitraum laden für Dateinamen
|
||||||
const zeitraum = await prisma.praktikumszeitraum.findUnique({
|
const zeitraum = await prisma.praktikumszeitraum.findUnique({
|
||||||
where: { id: parseInt(zeitraumId) }
|
where: { id: parseInt(zeitraumId) }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!zeitraum) {
|
if (!zeitraum) {
|
||||||
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
|
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Angenommene Anmeldungen für diesen Zeitraum laden
|
// Angenommene Anmeldungen für diesen Zeitraum laden
|
||||||
const anmeldungen = await prisma.anmeldung.findMany({
|
const anmeldungen = await prisma.anmeldung.findMany({
|
||||||
where: {
|
where: {
|
||||||
praktikumId: parseInt(zeitraumId),
|
praktikumId: parseInt(zeitraumId),
|
||||||
status: 'ANGENOMMEN'
|
status: 'ANGENOMMEN'
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
zugewiesen: true,
|
zugewiesen: true,
|
||||||
praktikum: true
|
praktikum: true
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [{ zugewiesen: { name: 'asc' } }, { nachname: 'asc' }]
|
||||||
{ zugewiesen: { name: 'asc' } },
|
});
|
||||||
{ nachname: 'asc' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Excel-Datei erstellen
|
// Excel-Datei erstellen
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
workbook.creator = 'Praktikumsverwaltung';
|
workbook.creator = 'Praktikumsverwaltung';
|
||||||
workbook.created = new Date();
|
workbook.created = new Date();
|
||||||
|
|
||||||
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
|
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
|
||||||
|
|
||||||
// Spalten definieren
|
// Spalten definieren (inkl. Notfallkontakt)
|
||||||
worksheet.columns = [
|
worksheet.columns = [
|
||||||
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
|
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
|
||||||
{ header: 'Anrede', key: 'anrede', width: 10 },
|
{ header: 'Anrede', key: 'anrede', width: 10 },
|
||||||
{ header: 'Vorname', key: 'vorname', width: 15 },
|
{ header: 'Vorname', key: 'vorname', width: 15 },
|
||||||
{ header: 'Nachname', key: 'nachname', width: 15 },
|
{ header: 'Nachname', key: 'nachname', width: 15 },
|
||||||
{ header: 'Geburtsdatum', key: 'geburtsdatum', width: 12 },
|
{ header: 'Geburtsdatum', key: 'geburtsdatum', width: 12 },
|
||||||
{ header: 'Alter', key: 'alter', width: 8 },
|
{ header: 'Alter', key: 'alter', width: 8 },
|
||||||
{ header: 'Straße', key: 'strasse', width: 20 },
|
{ header: 'Straße', key: 'strasse', width: 20 },
|
||||||
{ header: 'Hausnr.', key: 'hausnummer', width: 10 },
|
{ header: 'Hausnr.', key: 'hausnummer', width: 10 },
|
||||||
{ header: 'PLZ', key: 'plz', width: 8 },
|
{ header: 'PLZ', key: 'plz', width: 8 },
|
||||||
{ header: 'Ort', key: 'ort', width: 15 },
|
{ header: 'Ort', key: 'ort', width: 15 },
|
||||||
{ header: 'Telefon', key: 'telefon', width: 18 },
|
{ header: 'Telefon', key: 'telefon', width: 18 },
|
||||||
{ header: 'E-Mail', key: 'email', width: 30 },
|
{ header: 'E-Mail', key: 'email', width: 30 },
|
||||||
{ header: 'Schulart', key: 'schulart', width: 15 },
|
{ header: 'Schulart', key: 'schulart', width: 15 },
|
||||||
{ header: 'Klasse', key: 'schulklasse', width: 8 },
|
{ header: 'Klasse', key: 'schulklasse', width: 8 },
|
||||||
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
|
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
|
||||||
{ header: 'Note Mathe', key: 'noteMathe', width: 12 },
|
{ header: 'Note Mathe', key: 'noteMathe', width: 12 },
|
||||||
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
|
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
|
||||||
{ header: 'Angenommen am', key: 'processedAt', width: 15 }
|
{ 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
|
// Header-Zeile formatieren
|
||||||
const headerRow = worksheet.getRow(1);
|
const headerRow = worksheet.getRow(1);
|
||||||
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
||||||
headerRow.fill = {
|
headerRow.fill = {
|
||||||
type: 'pattern',
|
type: 'pattern',
|
||||||
pattern: 'solid',
|
pattern: 'solid',
|
||||||
fgColor: { argb: 'FF4472C4' }
|
fgColor: { argb: 'FF4472C4' }
|
||||||
};
|
};
|
||||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||||
headerRow.height = 25;
|
headerRow.height = 25;
|
||||||
|
|
||||||
// Daten einfügen
|
// Notfallkontakt-Spalten orange hervorheben
|
||||||
anmeldungen.forEach(anmeldung => {
|
['R1', 'S1', 'T1'].forEach((cell) => {
|
||||||
worksheet.addRow({
|
worksheet.getCell(cell).fill = {
|
||||||
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
|
type: 'pattern',
|
||||||
anrede: anmeldung.anrede,
|
pattern: 'solid',
|
||||||
vorname: anmeldung.vorname,
|
fgColor: { argb: 'FFED7D31' }
|
||||||
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')
|
|
||||||
: '-'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Datenzeilen formatieren
|
// Daten einfügen
|
||||||
for (let i = 2; i <= anmeldungen.length + 1; i++) {
|
anmeldungen.forEach((anmeldung) => {
|
||||||
const row = worksheet.getRow(i);
|
worksheet.addRow({
|
||||||
row.alignment = { vertical: 'middle' };
|
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')
|
||||||
|
: '-'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Abwechselnde Zeilenfarben
|
// Datenzeilen formatieren
|
||||||
if (i % 2 === 0) {
|
for (let i = 2; i <= anmeldungen.length + 1; i++) {
|
||||||
row.fill = {
|
const row = worksheet.getRow(i);
|
||||||
type: 'pattern',
|
row.alignment = { vertical: 'middle' };
|
||||||
pattern: 'solid',
|
|
||||||
fgColor: { argb: 'FFF2F2F2' }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rahmen für alle Zellen
|
// Abwechselnde Zeilenfarben
|
||||||
worksheet.eachRow((row, rowNumber) => {
|
if (i % 2 === 0) {
|
||||||
row.eachCell((cell) => {
|
row.fill = {
|
||||||
cell.border = {
|
type: 'pattern',
|
||||||
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
pattern: 'solid',
|
||||||
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
fgColor: { argb: 'FFF2F2F2' }
|
||||||
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
};
|
||||||
right: { style: 'thin', color: { argb: 'FFD0D0D0' } }
|
}
|
||||||
};
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter aktivieren
|
// Rahmen für alle Zellen
|
||||||
worksheet.autoFilter = {
|
worksheet.eachRow((row, rowNumber) => {
|
||||||
from: 'A1',
|
row.eachCell((cell) => {
|
||||||
to: `R${anmeldungen.length + 1}`
|
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' } }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Zusammenfassung am Ende
|
// Filter aktivieren (angepasst auf neue Spaltenanzahl: A bis U = 21 Spalten)
|
||||||
const summaryRow = worksheet.addRow([]);
|
worksheet.autoFilter = {
|
||||||
const totalRow = worksheet.addRow([
|
from: 'A1',
|
||||||
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
|
to: `U${anmeldungen.length + 1}`
|
||||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''
|
};
|
||||||
]);
|
|
||||||
totalRow.font = { bold: true };
|
|
||||||
|
|
||||||
// Excel-Datei als Buffer generieren
|
// Zusammenfassung am Ende
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const summaryRow = worksheet.addRow([]);
|
||||||
|
const totalRow = worksheet.addRow([
|
||||||
|
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
]);
|
||||||
|
totalRow.font = { bold: true };
|
||||||
|
|
||||||
// Dateiname generieren
|
// Excel-Datei als Buffer generieren
|
||||||
const dateiname = `Praktikanten_${zeitraum.bezeichnung.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
|
||||||
// Als Download zurückgeben
|
// Dateiname generieren
|
||||||
return new Response(buffer, {
|
const dateiname = `Praktikanten_${zeitraum.bezeichnung.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
'Content-Disposition': `attachment; filename="${dateiname}"`,
|
|
||||||
'Content-Length': buffer.byteLength.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
// Als Download zurückgeben
|
||||||
console.error('Fehler beim Exportieren:', error);
|
return new Response(buffer, {
|
||||||
return json({ error: 'Fehler beim Exportieren', details: (error as Error).message }, { status: 500 });
|
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
|
// Hilfsfunktion: Schulart formatieren
|
||||||
function formatSchulart(schulart: string): string {
|
function formatSchulart(schulart: string): string {
|
||||||
const schulartMap: Record<string, string> = {
|
const schulartMap: Record<string, string> = {
|
||||||
'Gymnasium': 'Gymnasium',
|
Gymnasium: 'Gymnasium',
|
||||||
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig',
|
KGS_Gymnasialzweig: 'KGS Gymnasialzweig',
|
||||||
'Fachoberschule': 'Fachoberschule',
|
Fachoberschule: 'Fachoberschule',
|
||||||
'Realschule': 'Realschule',
|
Realschule: 'Realschule',
|
||||||
'KGSR': 'KGS Realschulzweig',
|
KGSR: 'KGS Realschulzweig',
|
||||||
'IGSR': 'IGS Realschulzweig'
|
IGSR: 'IGS Realschulzweig'
|
||||||
};
|
};
|
||||||
|
|
||||||
return schulartMap[schulart] || schulart;
|
return schulartMap[schulart] || schulart;
|
||||||
}
|
}
|
||||||
@@ -5,137 +5,143 @@ import { prisma } from '$lib/prisma';
|
|||||||
|
|
||||||
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
|
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
|
||||||
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
|
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
|
||||||
const dienststellen = await prisma.dienststelle.findMany();
|
const dienststellen = await prisma.dienststelle.findMany();
|
||||||
|
|
||||||
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
|
// Erstelle für jede existierende Dienststelle einen Eintrag mit 1 Platz
|
||||||
for (const dienststelle of dienststellen) {
|
for (const dienststelle of dienststellen) {
|
||||||
await prisma.zeitraumPlaetze.create({
|
await prisma.zeitraumPlaetze.create({
|
||||||
data: {
|
data: {
|
||||||
zeitraumId: zeitraumId,
|
zeitraumId: zeitraumId,
|
||||||
dienststelleId: dienststelle.id,
|
dienststelleId: dienststelle.id,
|
||||||
plaetze: 0 // Standardwert: 0 Plätze
|
plaetze: 1 // Standardwert: 1 Platz pro Dienststelle
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ cookies }) => {
|
export const GET: RequestHandler = async ({ cookies }) => {
|
||||||
const adminAuth = cookies.get('admin-auth');
|
const adminAuth = cookies.get('admin-auth');
|
||||||
if (adminAuth !== 'authenticated') {
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const zeitraeume = await prisma.praktikumszeitraum.findMany({
|
const zeitraeume = await prisma.praktikumszeitraum.findMany({
|
||||||
orderBy: { startDatum: 'desc' }
|
orderBy: { startDatum: 'desc' }
|
||||||
});
|
});
|
||||||
return json(zeitraeume);
|
return json(zeitraeume);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Zeiträume:', error);
|
console.error('Fehler beim Laden der Zeiträume:', error);
|
||||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||||
const adminAuth = cookies.get('admin-auth');
|
const adminAuth = cookies.get('admin-auth');
|
||||||
if (adminAuth !== 'authenticated') {
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { bezeichnung, startDatum, endDatum } = await request.json();
|
const { bezeichnung, startDatum, endDatum } = await request.json();
|
||||||
|
|
||||||
if (!bezeichnung || !startDatum || !endDatum) {
|
if (!bezeichnung || !startDatum || !endDatum) {
|
||||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = new Date(startDatum);
|
const start = new Date(startDatum);
|
||||||
const end = new Date(endDatum);
|
const end = new Date(endDatum);
|
||||||
|
|
||||||
if (end <= start) {
|
if (end <= start) {
|
||||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const zeitraum = await prisma.praktikumszeitraum.create({
|
const zeitraum = await prisma.praktikumszeitraum.create({
|
||||||
data: {
|
data: {
|
||||||
bezeichnung,
|
bezeichnung,
|
||||||
startDatum: start,
|
startDatum: start,
|
||||||
endDatum: end
|
endDatum: end
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
|
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen (mit 1 Platz)
|
||||||
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
|
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
|
||||||
|
|
||||||
return json(zeitraum);
|
return json(zeitraum);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Fehler beim Erstellen des Zeitraums:', error);
|
console.error('Fehler beim Erstellen des Zeitraums:', error);
|
||||||
if (error.code === 'P2002') {
|
if (error.code === 'P2002') {
|
||||||
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
return json(
|
||||||
}
|
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
|
||||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
{ status: 400 }
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PATCH: RequestHandler = async ({ request, cookies }) => {
|
export const PATCH: RequestHandler = async ({ request, cookies }) => {
|
||||||
const adminAuth = cookies.get('admin-auth');
|
const adminAuth = cookies.get('admin-auth');
|
||||||
if (adminAuth !== 'authenticated') {
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
||||||
|
|
||||||
if (!id || !bezeichnung || !startDatum || !endDatum) {
|
if (!id || !bezeichnung || !startDatum || !endDatum) {
|
||||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = new Date(startDatum);
|
const start = new Date(startDatum);
|
||||||
const end = new Date(endDatum);
|
const end = new Date(endDatum);
|
||||||
|
|
||||||
if (end <= start) {
|
if (end <= start) {
|
||||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const zeitraum = await prisma.praktikumszeitraum.update({
|
const zeitraum = await prisma.praktikumszeitraum.update({
|
||||||
where: { id: parseInt(id) },
|
where: { id: parseInt(id) },
|
||||||
data: {
|
data: {
|
||||||
bezeichnung,
|
bezeichnung,
|
||||||
startDatum: start,
|
startDatum: start,
|
||||||
endDatum: end
|
endDatum: end
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(zeitraum);
|
return json(zeitraum);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
|
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
|
||||||
if (error.code === 'P2002') {
|
if (error.code === 'P2002') {
|
||||||
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
return json(
|
||||||
}
|
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
|
||||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
{ status: 400 }
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||||
const adminAuth = cookies.get('admin-auth');
|
const adminAuth = cookies.get('admin-auth');
|
||||||
if (adminAuth !== 'authenticated') {
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = url.searchParams.get('id');
|
const id = url.searchParams.get('id');
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||||
await prisma.praktikumszeitraum.delete({
|
await prisma.praktikumszeitraum.delete({
|
||||||
where: { id: parseInt(id) }
|
where: { id: parseInt(id) }
|
||||||
});
|
});
|
||||||
|
|
||||||
return json({ success: true });
|
return json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Löschen des Zeitraums:', error);
|
console.error('Fehler beim Löschen des Zeitraums:', error);
|
||||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -6,83 +6,87 @@ import { json } from '@sveltejs/kit';
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export async function POST({ request }: RequestEvent) {
|
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 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');
|
const hasValidPdf = pdfFiles.some((file) => file.size > 0 && file.type === 'application/pdf');
|
||||||
if (!hasValidPdf) {
|
if (!hasValidPdf) {
|
||||||
return json({ error: 'Bitte lade das Zeugnis hoch in PDF Format.' }, { status: 400 });
|
return json({ error: 'Bitte lade das Zeugnis hoch in PDF Format.' }, { status: 400 });
|
||||||
}
|
}
|
||||||
const pdfData = [];
|
const pdfData = [];
|
||||||
|
|
||||||
// const gespeichertePfade: string[] = [];
|
// const gespeichertePfade: string[] = [];
|
||||||
const noteDeutsch = Number(get('noteDeutsch'));
|
const noteDeutsch = Number(get('noteDeutsch'));
|
||||||
const noteMathe = Number(get('noteMathe'));
|
const noteMathe = Number(get('noteMathe'));
|
||||||
|
|
||||||
if (isNaN(noteDeutsch) || isNaN(noteMathe)) {
|
if (isNaN(noteDeutsch) || isNaN(noteMathe)) {
|
||||||
return json({ error: 'Bitte gib gültige Noten an.' }, { status: 400 });
|
return json({ error: 'Bitte gib gültige Noten an.' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of pdfFiles) {
|
for (const file of pdfFiles) {
|
||||||
if (file.size > 0 && file.type === 'application/pdf') {
|
if (file.size > 0 && file.type === 'application/pdf') {
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
const filename = `${randomUUID()}.pdf`;
|
const filename = `${randomUUID()}.pdf`;
|
||||||
const uploadPath = `/uploads/${filename}`;
|
const uploadPath = `/uploads/${filename}`;
|
||||||
await writeFile(`static${uploadPath}`, buffer);
|
await writeFile(`static${uploadPath}`, buffer);
|
||||||
pdfData.push({ pfad: uploadPath });
|
pdfData.push({ pfad: uploadPath });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const anmeldung = await prisma.anmeldung.create({
|
const anmeldung = await prisma.anmeldung.create({
|
||||||
data: {
|
data: {
|
||||||
// Persönliche Daten
|
// Persönliche Daten
|
||||||
anrede: formData.get('anrede') as string,
|
anrede: formData.get('anrede') as string,
|
||||||
vorname: formData.get('vorname') as string,
|
vorname: formData.get('vorname') as string,
|
||||||
nachname: formData.get('nachname') as string,
|
nachname: formData.get('nachname') as string,
|
||||||
geburtsdatum: formData.get('geburtsdatum') as string,
|
geburtsdatum: formData.get('geburtsdatum') as string,
|
||||||
// Adresse
|
// Adresse
|
||||||
strasse: formData.get('strasse') as string,
|
strasse: formData.get('strasse') as string,
|
||||||
hausnummer: formData.get('hausnummer') as string,
|
hausnummer: formData.get('hausnummer') as string,
|
||||||
ort: formData.get('ort') as string,
|
ort: formData.get('ort') as string,
|
||||||
plz: formData.get('plz') as string,
|
plz: formData.get('plz') as string,
|
||||||
// Kontakt
|
// Kontakt
|
||||||
telefon: formData.get('telefon') as string,
|
telefon: formData.get('telefon') as string,
|
||||||
email: formData.get('email') as string,
|
email: formData.get('email') as string,
|
||||||
// Schule
|
// Schule
|
||||||
schulart: formData.get('schulart') as string,
|
schulart: formData.get('schulart') as string,
|
||||||
schulklasse: formData.get('schulklasse') as string,
|
schulklasse: formData.get('schulklasse') as string,
|
||||||
noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
|
noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
|
||||||
noteMathe: parseInt(formData.get('noteMathe') as string),
|
noteMathe: parseInt(formData.get('noteMathe') as string),
|
||||||
sozialverhalten: formData.get('sozialverhalten') as string || null,
|
sozialverhalten: (formData.get('sozialverhalten') as string) || null,
|
||||||
// Praktikum
|
// Praktikum
|
||||||
praktikumId: parseInt(formData.get('zeitraum') as string),
|
praktikumId: parseInt(formData.get('zeitraum') as string),
|
||||||
motivation: formData.get('motivation') as string || '',
|
motivation: (formData.get('motivation') as string) || '',
|
||||||
// Wünsche
|
// Wünsche
|
||||||
wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
|
wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
|
||||||
wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
|
wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
|
||||||
wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
|
wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
|
||||||
// Alter (falls vom Frontend gesendet)
|
// Alter (falls vom Frontend gesendet)
|
||||||
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
|
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
|
||||||
// System
|
// Notfallkontakt
|
||||||
zugewiesenId: null,
|
notfallVorname: (formData.get('notfallVorname') as string) || null,
|
||||||
// timestamp wird automatisch durch @default(now()) gesetzt
|
notfallNachname: (formData.get('notfallNachname') as string) || null,
|
||||||
pdfs: {
|
notfallTelefon: (formData.get('notfallTelefon') as string) || null,
|
||||||
create: pdfData
|
// System
|
||||||
}
|
zugewiesenId: null,
|
||||||
}
|
// timestamp wird automatisch durch @default(now()) gesetzt
|
||||||
});
|
pdfs: {
|
||||||
|
create: pdfData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return json({ success: true, anmeldung });
|
return json({ success: true, anmeldung });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error && (err as { code?: string }).code === 'P2002') {
|
if (err instanceof Error && (err as { code?: string }).code === 'P2002') {
|
||||||
return json({ error: 'Diese E-Mail wurde bereits verwendet.' }, { status: 400 });
|
return json({ error: 'Diese E-Mail wurde bereits verwendet.' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user