praktikum, Notfallkontakt und ein Platz pro Dienstelle bei neuer Zeitraum
This commit is contained in:
@@ -16,6 +16,11 @@
|
||||
let zeitraum = '';
|
||||
let motivation = '';
|
||||
|
||||
// Notfallkontakt
|
||||
let notfallVorname = '';
|
||||
let notfallNachname = '';
|
||||
let notfallTelefon = '';
|
||||
|
||||
let wunsch1Id = '';
|
||||
let wunsch2Id = '';
|
||||
let wunsch3Id = '';
|
||||
@@ -41,10 +46,10 @@
|
||||
let sozialverhaltenFehler = '';
|
||||
|
||||
// Hinweis für IGS/KGS mit Lernentwicklungsbericht
|
||||
$: zeigeIgsKgsHinweis =
|
||||
['IGSR', 'KGSR'].includes(schulart) &&
|
||||
schulklasse &&
|
||||
parseInt(schulklasse) < 10;
|
||||
$: zeigeIgsKgsHinweis =
|
||||
['IGSR', 'KGSR'].includes(schulart) &&
|
||||
schulklasse &&
|
||||
parseInt(schulklasse) < 10;
|
||||
|
||||
// Berechnung des Alters
|
||||
$: {
|
||||
@@ -53,10 +58,10 @@
|
||||
if (gewaehlterZeitraum) {
|
||||
const geburt = new Date(geburtsdatum);
|
||||
const praktikumStart = new Date(gewaehlterZeitraum.startDatum);
|
||||
|
||||
|
||||
let altersberechnung = praktikumStart.getFullYear() - geburt.getFullYear();
|
||||
const monthDiff = praktikumStart.getMonth() - geburt.getMonth();
|
||||
|
||||
|
||||
if (monthDiff < 0 || (monthDiff === 0 && praktikumStart.getDate() < geburt.getDate())) {
|
||||
altersberechnung--;
|
||||
}
|
||||
@@ -80,7 +85,7 @@
|
||||
const deutsch = parseInt(noteDeutsch);
|
||||
const mathe = parseInt(noteMathe);
|
||||
const klasse = parseInt(schulklasse);
|
||||
|
||||
|
||||
if (noteDeutsch && noteMathe && schulart) {
|
||||
// Gymnasium oder KGS_Gymnasialzweig: mindestens 4 in Deutsch UND Mathe
|
||||
if (['Gymnasium', 'KGS_Gymnasialzweig'].includes(schulart)) {
|
||||
@@ -129,7 +134,7 @@
|
||||
dienststellen = [];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
isLoadingDienststellen = true;
|
||||
const res = await fetch(`/api/dienststellen?zeitraumId=${zeitraumId}`);
|
||||
@@ -153,7 +158,7 @@
|
||||
// Filter: Nur Dienststellen mit freien Plätzen und Alterscheck für PK Mitte
|
||||
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
|
||||
if (d.plaetze <= 0) return false;
|
||||
|
||||
|
||||
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
|
||||
return parseInt(alter) >= 18;
|
||||
}
|
||||
@@ -168,9 +173,9 @@
|
||||
|
||||
let startDatum = '';
|
||||
|
||||
$: hideSozialVerhalten =
|
||||
Number(schulklasse) >= 11 &&
|
||||
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
||||
$: hideSozialVerhalten =
|
||||
Number(schulklasse) >= 11 &&
|
||||
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
||||
|
||||
onMount(async () => {
|
||||
const resZeitraeume = await fetch('/api/zeitraeume');
|
||||
@@ -208,6 +213,9 @@
|
||||
noteMathe = '';
|
||||
sozialverhalten = '';
|
||||
schulklasse = '';
|
||||
notfallVorname = '';
|
||||
notfallNachname = '';
|
||||
notfallTelefon = '';
|
||||
pdfDateien = [];
|
||||
fileInputKey += 1;
|
||||
success = false;
|
||||
@@ -246,6 +254,9 @@
|
||||
data.append('sozialverhalten', sozialverhalten);
|
||||
data.append('schulklasse', schulklasse);
|
||||
data.append('alter', alter);
|
||||
data.append('notfallVorname', notfallVorname);
|
||||
data.append('notfallNachname', notfallNachname);
|
||||
data.append('notfallTelefon', notfallTelefon);
|
||||
|
||||
for (const pdf of pdfDateien) {
|
||||
data.append('pdfs', pdf);
|
||||
@@ -254,7 +265,7 @@
|
||||
const res = await fetch('/api/anmelden', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
@@ -263,13 +274,13 @@
|
||||
} else {
|
||||
fehler = '';
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-6">
|
||||
<form on:submit|preventDefault={anmelden}
|
||||
class="bg-white shadow-md rounded-2xl p-8 max-w-2xl w-full space-y-4">
|
||||
class="bg-white shadow-md rounded-2xl p-8 max-w-2xl w-full space-y-4">
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-4 text-center">Praktikumsanmeldung</h1>
|
||||
|
||||
@@ -302,7 +313,7 @@
|
||||
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
|
||||
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
|
||||
</select>
|
||||
|
||||
|
||||
<!-- Schulklasse -->
|
||||
<select bind:value={schulklasse} required class="input">
|
||||
<option value="" disabled selected hidden>Schulklasse</option>
|
||||
@@ -314,15 +325,15 @@
|
||||
<option value="12">12. Klasse</option>
|
||||
<option value="13">13. Klasse</option>
|
||||
</select>
|
||||
|
||||
|
||||
<!-- Sozialverhalten mit Echtzeit-Validierung -->
|
||||
{#if !hideSozialVerhalten}
|
||||
{#if !hideSozialVerhalten}
|
||||
<div class="col-span-2">
|
||||
<select
|
||||
bind:value={sozialverhalten}
|
||||
required
|
||||
class="input"
|
||||
class:input-error={sozialverhaltenFehler}
|
||||
<select
|
||||
bind:value={sozialverhalten}
|
||||
required
|
||||
class="input"
|
||||
class:input-error={sozialverhaltenFehler}
|
||||
>
|
||||
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
|
||||
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
|
||||
@@ -350,27 +361,27 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<input
|
||||
bind:value={noteDeutsch}
|
||||
type="number"
|
||||
min="1"
|
||||
max="6"
|
||||
placeholder="Note in Deutsch"
|
||||
required
|
||||
class="input"
|
||||
class:input-error={notenFehler}
|
||||
<input
|
||||
bind:value={noteDeutsch}
|
||||
type="number"
|
||||
min="1"
|
||||
max="6"
|
||||
placeholder="Note in Deutsch"
|
||||
required
|
||||
class="input"
|
||||
class:input-error={notenFehler}
|
||||
/>
|
||||
<input
|
||||
bind:value={noteMathe}
|
||||
type="number"
|
||||
min="1"
|
||||
max="6"
|
||||
placeholder="Note in Mathe"
|
||||
required
|
||||
class="input"
|
||||
class:input-error={notenFehler}
|
||||
<input
|
||||
bind:value={noteMathe}
|
||||
type="number"
|
||||
min="1"
|
||||
max="6"
|
||||
placeholder="Note in Mathe"
|
||||
required
|
||||
class="input"
|
||||
class:input-error={notenFehler}
|
||||
/>
|
||||
</div>
|
||||
{#if notenFehler}
|
||||
@@ -384,8 +395,8 @@
|
||||
<select bind:value={zeitraum} required class="input">
|
||||
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
||||
{#each filteredZeitraeume as d}
|
||||
<option
|
||||
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
|
||||
<option
|
||||
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -453,7 +464,7 @@
|
||||
|
||||
<!-- Motivation -->
|
||||
<textarea bind:value={motivation} placeholder="Motivation (optional)"
|
||||
class="w-full border border-gray-300 rounded-xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 h-32 resize-none" >
|
||||
class="w-full border border-gray-300 rounded-xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 h-32 resize-none" >
|
||||
</textarea>
|
||||
|
||||
<!-- Mehrere PDF Upload -->
|
||||
@@ -461,23 +472,33 @@
|
||||
<div>
|
||||
<label for="pdf-upload" class="block text-gray-700 font-medium mb-1">Zeugnis hochladen:</label>
|
||||
<input
|
||||
id="pdf-upload"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
multiple
|
||||
on:change={(e) => pdfDateien = Array.from((e.target as HTMLInputElement).files || [])}
|
||||
class="input"
|
||||
id="pdf-upload"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
multiple
|
||||
on:change={(e) => pdfDateien = Array.from((e.target as HTMLInputElement).files || [])}
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<!-- Notfallkontakt -->
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<h2 class="text-lg font-semibold text-gray-700 mb-3">Notfallkontakt</h2>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<input bind:value={notfallVorname} placeholder="Vorname Notfallkontakt" required class="input" />
|
||||
<input bind:value={notfallNachname} placeholder="Nachname Notfallkontakt" required class="input" />
|
||||
<input bind:value={notfallTelefon} type="tel" placeholder="Mobilnummer Notfallkontakt" required class="input col-span-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showAblehnungModal}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
||||
<p class="text-red-600 font-semibold">{ablehnungHinweis}</p>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
on:click={() => { resetForm(); showAblehnungModal = false; }}
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
on:click={() => { resetForm(); showAblehnungModal = false; }}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
@@ -486,15 +507,15 @@
|
||||
{/if}
|
||||
|
||||
<!-- Button - deaktiviert bei Validierungsfehlern oder fehlenden Pflichtfeldern -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||
class="w-full py-3 rounded-xl transition-all"
|
||||
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||
class="w-full py-3 rounded-xl transition-all"
|
||||
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||
>
|
||||
Jetzt anmelden
|
||||
</button>
|
||||
@@ -508,8 +529,8 @@
|
||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
||||
<p class="text-green-600 font-semibold">Anmeldung erfolgreich gesendet!</p>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
on:click={resetForm}
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
on:click={resetForm}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import AnmeldungenTable from '$lib/components/AnmeldungenTable.svelte';
|
||||
import DienststellenDialog from '$lib/components/DienststellenDialog.svelte';
|
||||
import AdminHeader from '$lib/components/AdminHeader.svelte';
|
||||
|
||||
|
||||
interface Anmeldung {
|
||||
pdfs: { pfad: string }[];
|
||||
anrede: string;
|
||||
@@ -15,6 +15,9 @@
|
||||
noteDeutsch?: string;
|
||||
noteMathe?: string;
|
||||
sozialverhalten?: string;
|
||||
notfallVorname?: string;
|
||||
notfallNachname?: string;
|
||||
notfallTelefon?: string;
|
||||
wunsch1?: { id: number; name: string };
|
||||
wunsch2?: { id: number; name: string };
|
||||
wunsch3?: { id: number; name: string };
|
||||
@@ -43,11 +46,11 @@
|
||||
let zeitraeume: Zeitraum[] = [];
|
||||
let isLoading = true;
|
||||
let error = '';
|
||||
|
||||
|
||||
// Filter für Status
|
||||
let statusFilter: 'all' | 'pending' | 'accepted' | 'rejected' = 'all';
|
||||
let filteredAnmeldungen: Anmeldung[] = [];
|
||||
|
||||
|
||||
// Dialog state
|
||||
let showDialog = false;
|
||||
let selectedAnmeldungId: number | null = null;
|
||||
@@ -120,9 +123,9 @@ Ihr Praktikumsteam`;
|
||||
|
||||
// Zähle angenommene Anmeldungen pro Zeitraum
|
||||
function getAcceptedCountForZeitraum(zeitraumId: number): number {
|
||||
return anmeldungen.filter(a =>
|
||||
a.status === 'accepted' &&
|
||||
a.zeitraum?.id === zeitraumId
|
||||
return anmeldungen.filter(a =>
|
||||
a.status === 'accepted' &&
|
||||
a.zeitraum?.id === zeitraumId
|
||||
).length;
|
||||
}
|
||||
|
||||
@@ -130,26 +133,26 @@ Ihr Praktikumsteam`;
|
||||
try {
|
||||
isLoading = true;
|
||||
error = '';
|
||||
|
||||
|
||||
const res = await fetch('/api/admin/anmeldungen');
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
console.error('❌ API Fehler:', res.status, errorText);
|
||||
throw new Error(`Fehler beim Laden: ${res.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('❌ Antwort ist kein Array:', data);
|
||||
throw new Error('Antwort vom Server ist kein Array');
|
||||
}
|
||||
|
||||
|
||||
anmeldungen = data.map(a => {
|
||||
return { ...a, status: a.status || 'pending' };
|
||||
});
|
||||
|
||||
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||
console.error('❌ Frontend Fehler beim Laden der Anmeldungen:', err);
|
||||
@@ -172,13 +175,13 @@ Ihr Praktikumsteam`;
|
||||
async function loadEmailConfig() {
|
||||
try {
|
||||
isLoadingEmailConfig = true;
|
||||
|
||||
|
||||
const res = await fetch('/api/admin/email-config');
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Fehler beim Laden der E-Mail-Konfiguration: ${res.status}`);
|
||||
}
|
||||
|
||||
|
||||
const config: EmailConfig = await res.json();
|
||||
emailSubject = config.subject;
|
||||
emailTemplate = config.template;
|
||||
@@ -192,7 +195,7 @@ Ihr Praktikumsteam`;
|
||||
async function saveEmailTemplate() {
|
||||
try {
|
||||
isSavingEmailConfig = true;
|
||||
|
||||
|
||||
const res = await fetch('/api/admin/email-config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -247,10 +250,10 @@ Ihr Praktikumsteam`;
|
||||
}
|
||||
|
||||
showDialog = false;
|
||||
|
||||
|
||||
// E-Mail Vorschau öffnen nach erfolgreichem Annehmen
|
||||
openEmailPreview(selectedAnmeldungId, event.detail.dienststelleId);
|
||||
|
||||
|
||||
selectedAnmeldungId = null;
|
||||
await loadAnmeldungen();
|
||||
} catch (err) {
|
||||
@@ -265,16 +268,16 @@ Ihr Praktikumsteam`;
|
||||
|
||||
// Dienststelle finden
|
||||
const dienststelle = availableWishes.find(w => w.id === dienststelleId);
|
||||
const dienststelleName = dienststelle
|
||||
? dienststelle.name.replace(/^\d+\.\s*Wunsch:\s*/, '')
|
||||
: 'Unbekannte Dienststelle';
|
||||
const dienststelleName = dienststelle
|
||||
? dienststelle.name.replace(/^\d+\.\s*Wunsch:\s*/, '')
|
||||
: 'Unbekannte Dienststelle';
|
||||
|
||||
// E-Mail Text personalisieren
|
||||
const personalizedEmail = emailTemplate
|
||||
.replace(/\{anrede\}/g, anmeldung.anrede)
|
||||
.replace(/\{vorname\}/g, anmeldung.vorname)
|
||||
.replace(/\{nachname\}/g, anmeldung.nachname)
|
||||
.replace(/\{dienststelle\}/g, dienststelleName);
|
||||
.replace(/\{anrede\}/g, anmeldung.anrede)
|
||||
.replace(/\{vorname\}/g, anmeldung.vorname)
|
||||
.replace(/\{nachname\}/g, anmeldung.nachname)
|
||||
.replace(/\{dienststelle\}/g, dienststelleName);
|
||||
|
||||
// Modal mit Vorschau öffnen
|
||||
emailPreviewData = {
|
||||
@@ -288,11 +291,11 @@ Ihr Praktikumsteam`;
|
||||
|
||||
async function copyAndOpenMail() {
|
||||
if (!emailPreviewData) return;
|
||||
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(emailPreviewData.body);
|
||||
emailCopied = true;
|
||||
|
||||
|
||||
// Kurz warten, dann Mail öffnen
|
||||
setTimeout(() => {
|
||||
const mailtoLink = `mailto:${emailPreviewData!.to}?subject=${encodeURIComponent(emailPreviewData!.subject)}`;
|
||||
@@ -347,7 +350,7 @@ Ihr Praktikumsteam`;
|
||||
const blob = await res.blob();
|
||||
const contentDisposition = res.headers.get('Content-Disposition');
|
||||
let filename = 'export.xlsx';
|
||||
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="(.+)"/);
|
||||
if (match) {
|
||||
@@ -375,18 +378,18 @@ Ihr Praktikumsteam`;
|
||||
|
||||
async function handleReject(event: CustomEvent<{id: number}>) {
|
||||
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
|
||||
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
|
||||
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'reject' })
|
||||
});
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Fehler beim Ablehnen: ${res.status}`);
|
||||
}
|
||||
|
||||
|
||||
await loadAnmeldungen();
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Fehler beim Ablehnen';
|
||||
@@ -396,16 +399,16 @@ Ihr Praktikumsteam`;
|
||||
|
||||
async function handleDelete(event: CustomEvent<{id: number}>) {
|
||||
if (!confirm('Diese Anmeldung wirklich löschen?')) return;
|
||||
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
|
||||
method: 'DELETE'
|
||||
const res = await fetch(`/api/admin/anmeldungen?id=${event.detail.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Fehler beim Löschen: ${res.status}`);
|
||||
}
|
||||
|
||||
|
||||
await loadAnmeldungen();
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Fehler beim Löschen';
|
||||
@@ -430,9 +433,9 @@ Ihr Praktikumsteam`;
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<AdminHeader
|
||||
title="Anmeldungen verwalten"
|
||||
showBackButton={true}
|
||||
<AdminHeader
|
||||
title="Anmeldungen verwalten"
|
||||
showBackButton={true}
|
||||
/>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||
@@ -442,9 +445,9 @@ Ihr Praktikumsteam`;
|
||||
<div class="flex items-center space-x-4">
|
||||
<label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label>
|
||||
<select
|
||||
id="status-filter"
|
||||
bind:value={statusFilter}
|
||||
class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
id="status-filter"
|
||||
bind:value={statusFilter}
|
||||
class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="all">Alle ({anmeldungen.length})</option>
|
||||
<option value="pending">Offen ({anmeldungen.filter(a => (a.status || 'pending') === 'pending').length})</option>
|
||||
@@ -457,9 +460,9 @@ Ihr Praktikumsteam`;
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Excel Export Button -->
|
||||
<button
|
||||
on:click={openExportModal}
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
||||
disabled={anmeldungen.filter(a => a.status === 'accepted').length === 0}
|
||||
on:click={openExportModal}
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
||||
disabled={anmeldungen.filter(a => a.status === 'accepted').length === 0}
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
@@ -469,9 +472,9 @@ Ihr Praktikumsteam`;
|
||||
|
||||
<!-- E-Mail Konfiguration Button -->
|
||||
<button
|
||||
on:click={() => showEmailConfig = !showEmailConfig}
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
||||
disabled={isLoadingEmailConfig}
|
||||
on:click={() => showEmailConfig = !showEmailConfig}
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
||||
disabled={isLoadingEmailConfig}
|
||||
>
|
||||
{#if isLoadingEmailConfig}
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
@@ -546,49 +549,49 @@ Ihr Praktikumsteam`;
|
||||
{#if showEmailConfig}
|
||||
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">E-Mail-Vorlage konfigurieren</h3>
|
||||
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email-subject" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
E-Mail Betreff
|
||||
</label>
|
||||
<input
|
||||
id="email-subject"
|
||||
type="text"
|
||||
bind:value={emailSubject}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={isSavingEmailConfig}
|
||||
id="email-subject"
|
||||
type="text"
|
||||
bind:value={emailSubject}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={isSavingEmailConfig}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="email-template" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
E-Mail Text
|
||||
</label>
|
||||
<textarea
|
||||
id="email-template"
|
||||
bind:value={emailTemplate}
|
||||
rows="10"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={isSavingEmailConfig}
|
||||
id="email-template"
|
||||
bind:value={emailTemplate}
|
||||
rows="10"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={isSavingEmailConfig}
|
||||
></textarea>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
Verfügbare Platzhalter: {anrede}, {vorname}, {nachname}, {dienststelle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
on:click={() => showEmailConfig = false}
|
||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||
disabled={isSavingEmailConfig}
|
||||
on:click={() => showEmailConfig = false}
|
||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||
disabled={isSavingEmailConfig}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
on:click={saveEmailTemplate}
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
||||
disabled={isSavingEmailConfig}
|
||||
on:click={saveEmailTemplate}
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
||||
disabled={isSavingEmailConfig}
|
||||
>
|
||||
{#if isSavingEmailConfig}
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
@@ -614,9 +617,9 @@ Ihr Praktikumsteam`;
|
||||
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
|
||||
<p class="mt-1 text-sm text-red-700">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
on:click={() => error = ''}
|
||||
class="ml-auto text-red-400 hover:text-red-600"
|
||||
<button
|
||||
on:click={() => error = ''}
|
||||
class="ml-auto text-red-400 hover:text-red-600"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -640,20 +643,20 @@ Ihr Praktikumsteam`;
|
||||
{statusFilter === 'all' ? 'Keine Anmeldungen' : `Keine ${getStatusText(statusFilter).toLowerCase()}en Anmeldungen`}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{statusFilter === 'all'
|
||||
? 'Es sind noch keine Praktikumsanmeldungen eingegangen.'
|
||||
: `Es gibt keine Anmeldungen mit dem Status "${getStatusText(statusFilter).toLowerCase()}".`}
|
||||
{statusFilter === 'all'
|
||||
? 'Es sind noch keine Praktikumsanmeldungen eingegangen.'
|
||||
: `Es gibt keine Anmeldungen mit dem Status "${getStatusText(statusFilter).toLowerCase()}".`}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<AnmeldungenTable
|
||||
anmeldungen={filteredAnmeldungen}
|
||||
{getStatusColor}
|
||||
{getStatusText}
|
||||
on:accept={handleAccept}
|
||||
on:reject={handleReject}
|
||||
on:delete={handleDelete}
|
||||
<AnmeldungenTable
|
||||
anmeldungen={filteredAnmeldungen}
|
||||
{getStatusColor}
|
||||
{getStatusText}
|
||||
on:accept={handleAccept}
|
||||
on:reject={handleReject}
|
||||
on:delete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -663,10 +666,10 @@ Ihr Praktikumsteam`;
|
||||
<!-- Dienststellen Dialog -->
|
||||
{#if showDialog}
|
||||
<DienststellenDialog
|
||||
wishes={availableWishes}
|
||||
selectedId={selectedDienststelleId}
|
||||
on:confirm={handleConfirmAccept}
|
||||
on:cancel={closeDialog}
|
||||
wishes={availableWishes}
|
||||
selectedId={selectedDienststelleId}
|
||||
on:confirm={handleConfirmAccept}
|
||||
on:cancel={closeDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -677,11 +680,11 @@ Ihr Praktikumsteam`;
|
||||
<!-- Backdrop -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
on:click={closeExportModal}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
on:click={closeExportModal}
|
||||
></div>
|
||||
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||
<!-- Header -->
|
||||
@@ -694,31 +697,31 @@ Ihr Praktikumsteam`;
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Excel-Export</h3>
|
||||
</div>
|
||||
<button
|
||||
on:click={closeExportModal}
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
<button
|
||||
on:click={closeExportModal}
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Content -->
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Wählen Sie einen Praktikumszeitraum aus, um alle angenommenen Anmeldungen als Excel-Datei zu exportieren.
|
||||
</p>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="export-zeitraum" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Praktikumszeitraum
|
||||
</label>
|
||||
<select
|
||||
id="export-zeitraum"
|
||||
bind:value={selectedExportZeitraum}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500"
|
||||
disabled={isExporting}
|
||||
id="export-zeitraum"
|
||||
bind:value={selectedExportZeitraum}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500"
|
||||
disabled={isExporting}
|
||||
>
|
||||
<option value="" disabled>Zeitraum auswählen...</option>
|
||||
{#each zeitraeume as z}
|
||||
@@ -730,20 +733,20 @@ Ihr Praktikumsteam`;
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end space-x-3">
|
||||
<button
|
||||
on:click={closeExportModal}
|
||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||
disabled={isExporting}
|
||||
on:click={closeExportModal}
|
||||
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||
disabled={isExporting}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
on:click={exportToExcel}
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-md text-sm font-medium inline-flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!selectedExportZeitraum || isExporting}
|
||||
on:click={exportToExcel}
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-md text-sm font-medium inline-flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!selectedExportZeitraum || isExporting}
|
||||
>
|
||||
{#if isExporting}
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
@@ -768,11 +771,11 @@ Ihr Praktikumsteam`;
|
||||
<!-- Backdrop -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
on:click={closeEmailPreview}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
on:click={closeEmailPreview}
|
||||
></div>
|
||||
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
|
||||
<!-- Header -->
|
||||
@@ -785,16 +788,16 @@ Ihr Praktikumsteam`;
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900">E-Mail Vorschau</h3>
|
||||
</div>
|
||||
<button
|
||||
on:click={closeEmailPreview}
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
<button
|
||||
on:click={closeEmailPreview}
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Content -->
|
||||
<div class="px-6 py-4 overflow-y-auto max-h-[60vh]">
|
||||
<div class="space-y-4">
|
||||
@@ -803,30 +806,30 @@ Ihr Praktikumsteam`;
|
||||
<span class="text-sm font-medium text-gray-500 w-16">An:</span>
|
||||
<span class="text-sm text-gray-900">{emailPreviewData.to}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Betreff -->
|
||||
<div class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<span class="text-sm font-medium text-gray-500 w-16">Betreff:</span>
|
||||
<span class="text-sm text-gray-900">{emailPreviewData.subject}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Nachricht (editierbar) -->
|
||||
<div>
|
||||
<label for="email-body" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nachricht
|
||||
Nachricht
|
||||
<span class="font-normal text-gray-500">(kann bearbeitet werden)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="email-body"
|
||||
bind:value={emailPreviewData.body}
|
||||
rows="14"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
||||
disabled={emailCopied}
|
||||
id="email-body"
|
||||
bind:value={emailPreviewData.body}
|
||||
rows="14"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
||||
disabled={emailCopied}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||
{#if emailCopied}
|
||||
@@ -853,14 +856,14 @@ Ihr Praktikumsteam`;
|
||||
</div>
|
||||
<div class="flex space-x-3 w-full sm:w-auto">
|
||||
<button
|
||||
on:click={closeEmailPreview}
|
||||
class="flex-1 sm:flex-none px-4 py-2.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
on:click={closeEmailPreview}
|
||||
class="flex-1 sm:flex-none px-4 py-2.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
on:click={copyAndOpenMail}
|
||||
class="flex-1 sm:flex-none bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg text-sm font-medium inline-flex items-center justify-center transition-colors"
|
||||
on:click={copyAndOpenMail}
|
||||
class="flex-1 sm:flex-none bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg text-sm font-medium inline-flex items-center justify-center transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
|
||||
@@ -6,282 +6,321 @@ import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const anmeldungen = await prisma.anmeldung.findMany({
|
||||
include: {
|
||||
wunsch1: true,
|
||||
wunsch2: true,
|
||||
wunsch3: true,
|
||||
zugewiesen: true,
|
||||
praktikum: true,
|
||||
pdfs: true
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
timestamp: 'desc'
|
||||
}
|
||||
]
|
||||
});
|
||||
try {
|
||||
const anmeldungen = await prisma.anmeldung.findMany({
|
||||
include: {
|
||||
wunsch1: true,
|
||||
wunsch2: true,
|
||||
wunsch3: true,
|
||||
zugewiesen: true,
|
||||
praktikum: true,
|
||||
pdfs: true
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
timestamp: 'desc'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const formattedAnmeldungen = anmeldungen.map(anmeldung => ({
|
||||
id: anmeldung.id,
|
||||
anrede: anmeldung.anrede,
|
||||
vorname: anmeldung.vorname,
|
||||
nachname: anmeldung.nachname,
|
||||
email: anmeldung.email,
|
||||
|
||||
geburtsdatum: anmeldung.geburtsdatum,
|
||||
strasse: anmeldung.strasse,
|
||||
hausnummer: anmeldung.hausnummer,
|
||||
ort: anmeldung.ort,
|
||||
plz: anmeldung.plz,
|
||||
telefon: anmeldung.telefon,
|
||||
schulart: anmeldung.schulart,
|
||||
schulklasse: anmeldung.schulklasse,
|
||||
motivation: anmeldung.motivation,
|
||||
alter: anmeldung.alter,
|
||||
|
||||
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
||||
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
||||
sozialverhalten: anmeldung.sozialverhalten,
|
||||
|
||||
status: mapPrismaStatusToFrontend(anmeldung.status),
|
||||
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
||||
|
||||
wunsch1: anmeldung.wunsch1 ? {
|
||||
id: anmeldung.wunsch1.id,
|
||||
name: anmeldung.wunsch1.name
|
||||
} : undefined,
|
||||
wunsch2: anmeldung.wunsch2 ? {
|
||||
id: anmeldung.wunsch2.id,
|
||||
name: anmeldung.wunsch2.name
|
||||
} : undefined,
|
||||
wunsch3: anmeldung.wunsch3 ? {
|
||||
id: anmeldung.wunsch3.id,
|
||||
name: anmeldung.wunsch3.name
|
||||
} : undefined,
|
||||
|
||||
assignedDienststelle: anmeldung.zugewiesen ? {
|
||||
id: anmeldung.zugewiesen.id,
|
||||
name: anmeldung.zugewiesen.name
|
||||
} : undefined,
|
||||
|
||||
zeitraum: anmeldung.praktikum ? {
|
||||
id: anmeldung.praktikum.id,
|
||||
bezeichnung: anmeldung.praktikum.bezeichnung,
|
||||
startDatum: anmeldung.praktikum.startDatum.toISOString(),
|
||||
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
||||
} : undefined,
|
||||
|
||||
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
||||
|
||||
pdfs: anmeldung.pdfs || []
|
||||
}));
|
||||
const formattedAnmeldungen = anmeldungen.map((anmeldung) => ({
|
||||
id: anmeldung.id,
|
||||
anrede: anmeldung.anrede,
|
||||
vorname: anmeldung.vorname,
|
||||
nachname: anmeldung.nachname,
|
||||
email: anmeldung.email,
|
||||
|
||||
return json(formattedAnmeldungen);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anmeldungen:', error);
|
||||
return json({ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
geburtsdatum: anmeldung.geburtsdatum,
|
||||
strasse: anmeldung.strasse,
|
||||
hausnummer: anmeldung.hausnummer,
|
||||
ort: anmeldung.ort,
|
||||
plz: anmeldung.plz,
|
||||
telefon: anmeldung.telefon,
|
||||
schulart: anmeldung.schulart,
|
||||
schulklasse: anmeldung.schulklasse,
|
||||
motivation: anmeldung.motivation,
|
||||
alter: anmeldung.alter,
|
||||
|
||||
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
||||
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
||||
sozialverhalten: anmeldung.sozialverhalten,
|
||||
|
||||
// Notfallkontakt
|
||||
notfallVorname: anmeldung.notfallVorname,
|
||||
notfallNachname: anmeldung.notfallNachname,
|
||||
notfallTelefon: anmeldung.notfallTelefon,
|
||||
|
||||
status: mapPrismaStatusToFrontend(anmeldung.status),
|
||||
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
||||
|
||||
wunsch1: anmeldung.wunsch1
|
||||
? {
|
||||
id: anmeldung.wunsch1.id,
|
||||
name: anmeldung.wunsch1.name
|
||||
}
|
||||
: undefined,
|
||||
wunsch2: anmeldung.wunsch2
|
||||
? {
|
||||
id: anmeldung.wunsch2.id,
|
||||
name: anmeldung.wunsch2.name
|
||||
}
|
||||
: undefined,
|
||||
wunsch3: anmeldung.wunsch3
|
||||
? {
|
||||
id: anmeldung.wunsch3.id,
|
||||
name: anmeldung.wunsch3.name
|
||||
}
|
||||
: undefined,
|
||||
|
||||
assignedDienststelle: anmeldung.zugewiesen
|
||||
? {
|
||||
id: anmeldung.zugewiesen.id,
|
||||
name: anmeldung.zugewiesen.name
|
||||
}
|
||||
: undefined,
|
||||
|
||||
zeitraum: anmeldung.praktikum
|
||||
? {
|
||||
id: anmeldung.praktikum.id,
|
||||
bezeichnung: anmeldung.praktikum.bezeichnung,
|
||||
startDatum: anmeldung.praktikum.startDatum.toISOString(),
|
||||
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
||||
}
|
||||
: undefined,
|
||||
|
||||
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
||||
|
||||
pdfs: anmeldung.pdfs || []
|
||||
}));
|
||||
|
||||
return json(formattedAnmeldungen);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anmeldungen:', error);
|
||||
return json(
|
||||
{ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST({ request, url }) {
|
||||
try {
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
const { dienststelleId } = await request.json();
|
||||
try {
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
const { dienststelleId } = await request.json();
|
||||
|
||||
if (!id || !dienststelleId) {
|
||||
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
|
||||
}
|
||||
if (!id || !dienststelleId) {
|
||||
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
|
||||
const existingAnmeldung = await prisma.anmeldung.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
praktikum: true
|
||||
}
|
||||
});
|
||||
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
|
||||
const existingAnmeldung = await prisma.anmeldung.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
praktikum: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingAnmeldung) {
|
||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
if (!existingAnmeldung) {
|
||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (existingAnmeldung.status === 'ANGENOMMEN') {
|
||||
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
||||
}
|
||||
if (existingAnmeldung.status === 'ANGENOMMEN') {
|
||||
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
||||
}
|
||||
|
||||
const zeitraumId = existingAnmeldung.praktikumId;
|
||||
const zeitraumId = existingAnmeldung.praktikumId;
|
||||
|
||||
if (!zeitraumId) {
|
||||
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 });
|
||||
}
|
||||
if (!zeitraumId) {
|
||||
return json(
|
||||
{ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
|
||||
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
}
|
||||
});
|
||||
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
|
||||
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!zeitraumPlaetze) {
|
||||
return json({
|
||||
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
|
||||
}, { status: 400 });
|
||||
}
|
||||
if (!zeitraumPlaetze) {
|
||||
return json(
|
||||
{
|
||||
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Prüfen ob noch Plätze frei sind (plaetze > 0)
|
||||
if (zeitraumPlaetze.plaetze <= 0) {
|
||||
return json({
|
||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
|
||||
}, { status: 400 });
|
||||
}
|
||||
// Prüfen ob noch Plätze frei sind (plaetze > 0)
|
||||
if (zeitraumPlaetze.plaetze <= 0) {
|
||||
return json(
|
||||
{
|
||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
|
||||
// Verwendung von $transaction für Atomarität (Race Condition vermeiden)
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Nochmal prüfen innerhalb der Transaktion
|
||||
const aktuellerStand = await tx.zeitraumPlaetze.findUnique({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
}
|
||||
});
|
||||
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
|
||||
// Verwendung von $transaction für Atomarität (Race Condition vermeiden)
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Nochmal prüfen innerhalb der Transaktion
|
||||
const aktuellerStand = await tx.zeitraumPlaetze.findUnique({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!aktuellerStand || aktuellerStand.plaetze <= 0) {
|
||||
throw new Error('Keine freien Plätze mehr verfügbar');
|
||||
}
|
||||
if (!aktuellerStand || aktuellerStand.plaetze <= 0) {
|
||||
throw new Error('Keine freien Plätze mehr verfügbar');
|
||||
}
|
||||
|
||||
// Anmeldung aktualisieren
|
||||
await tx.anmeldung.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'ANGENOMMEN',
|
||||
zugewiesenId: dienststelleId,
|
||||
processedAt: new Date()
|
||||
}
|
||||
});
|
||||
// Anmeldung aktualisieren
|
||||
await tx.anmeldung.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'ANGENOMMEN',
|
||||
zugewiesenId: dienststelleId,
|
||||
processedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Plätze in ZeitraumPlaetze reduzieren
|
||||
await tx.zeitraumPlaetze.update({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
},
|
||||
data: {
|
||||
plaetze: {
|
||||
decrement: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Plätze in ZeitraumPlaetze reduzieren
|
||||
await tx.zeitraumPlaetze.update({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
},
|
||||
data: {
|
||||
plaetze: {
|
||||
decrement: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Annehmen der Anmeldung:', error);
|
||||
|
||||
// Spezifische Fehlermeldung für "keine Plätze"
|
||||
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
|
||||
return json({
|
||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: 'Fehler beim Annehmen der Anmeldung', details: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Annehmen der Anmeldung:', error);
|
||||
|
||||
// Spezifische Fehlermeldung für "keine Plätze"
|
||||
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
|
||||
return json(
|
||||
{
|
||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return json(
|
||||
{ error: 'Fehler beim Annehmen der Anmeldung', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH({ request, url }) {
|
||||
try {
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
const { action } = await request.json();
|
||||
try {
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
const { action } = await request.json();
|
||||
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
let updateData = {};
|
||||
let updateData = {};
|
||||
|
||||
switch (action) {
|
||||
case 'reject':
|
||||
updateData = {
|
||||
status: 'ABGELEHNT',
|
||||
processedAt: new Date()
|
||||
};
|
||||
break;
|
||||
switch (action) {
|
||||
case 'reject':
|
||||
updateData = {
|
||||
status: 'ABGELEHNT',
|
||||
processedAt: new Date()
|
||||
};
|
||||
break;
|
||||
|
||||
case 'set_processing':
|
||||
const anmeldung = await prisma.anmeldung.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
case 'set_processing':
|
||||
const anmeldung = await prisma.anmeldung.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!anmeldung) {
|
||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
if (!anmeldung) {
|
||||
return json({ error: 'Anmeldung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
updateData = {
|
||||
status: 'BEARBEITUNG',
|
||||
processedAt: new Date()
|
||||
};
|
||||
break;
|
||||
updateData = {
|
||||
status: 'BEARBEITUNG',
|
||||
processedAt: new Date()
|
||||
};
|
||||
break;
|
||||
|
||||
case 'reset_processing':
|
||||
updateData = {
|
||||
status: 'OFFEN',
|
||||
processedAt: null
|
||||
};
|
||||
break;
|
||||
case 'reset_processing':
|
||||
updateData = {
|
||||
status: 'OFFEN',
|
||||
processedAt: null
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
|
||||
}
|
||||
default:
|
||||
return json({ error: 'Unbekannte Aktion' }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.anmeldung.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
});
|
||||
await prisma.anmeldung.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Anmeldung:', error);
|
||||
return json({ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Anmeldung:', error);
|
||||
return json(
|
||||
{ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE({ url }) {
|
||||
try {
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
try {
|
||||
const id = parseInt(url.searchParams.get('id') || '0');
|
||||
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
|
||||
await prisma.anmeldung.delete({
|
||||
where: { id }
|
||||
});
|
||||
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
|
||||
await prisma.anmeldung.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Anmeldung:', error);
|
||||
return json({ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Anmeldung:', error);
|
||||
return json(
|
||||
{ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion: Prisma Status zu Frontend Status
|
||||
function mapPrismaStatusToFrontend(prismaStatus: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
'OFFEN': 'pending',
|
||||
'BEARBEITUNG': 'processing',
|
||||
'ANGENOMMEN': 'accepted',
|
||||
'ABGELEHNT': 'rejected'
|
||||
};
|
||||
|
||||
return statusMap[prismaStatus] || 'pending';
|
||||
}
|
||||
const statusMap: Record<string, string> = {
|
||||
OFFEN: 'pending',
|
||||
BEARBEITUNG: 'processing',
|
||||
ANGENOMMEN: 'accepted',
|
||||
ABGELEHNT: 'rejected'
|
||||
};
|
||||
|
||||
return statusMap[prismaStatus] || 'pending';
|
||||
}
|
||||
|
||||
@@ -7,177 +7,210 @@ import ExcelJS from 'exceljs';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET({ url }) {
|
||||
try {
|
||||
const zeitraumId = url.searchParams.get('zeitraumId');
|
||||
try {
|
||||
const zeitraumId = url.searchParams.get('zeitraumId');
|
||||
|
||||
if (!zeitraumId) {
|
||||
return json({ error: 'Zeitraum-ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
if (!zeitraumId) {
|
||||
return json({ error: 'Zeitraum-ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Zeitraum laden für Dateinamen
|
||||
const zeitraum = await prisma.praktikumszeitraum.findUnique({
|
||||
where: { id: parseInt(zeitraumId) }
|
||||
});
|
||||
// Zeitraum laden für Dateinamen
|
||||
const zeitraum = await prisma.praktikumszeitraum.findUnique({
|
||||
where: { id: parseInt(zeitraumId) }
|
||||
});
|
||||
|
||||
if (!zeitraum) {
|
||||
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
if (!zeitraum) {
|
||||
return json({ error: 'Zeitraum nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Angenommene Anmeldungen für diesen Zeitraum laden
|
||||
const anmeldungen = await prisma.anmeldung.findMany({
|
||||
where: {
|
||||
praktikumId: parseInt(zeitraumId),
|
||||
status: 'ANGENOMMEN'
|
||||
},
|
||||
include: {
|
||||
zugewiesen: true,
|
||||
praktikum: true
|
||||
},
|
||||
orderBy: [
|
||||
{ zugewiesen: { name: 'asc' } },
|
||||
{ nachname: 'asc' }
|
||||
]
|
||||
});
|
||||
// Angenommene Anmeldungen für diesen Zeitraum laden
|
||||
const anmeldungen = await prisma.anmeldung.findMany({
|
||||
where: {
|
||||
praktikumId: parseInt(zeitraumId),
|
||||
status: 'ANGENOMMEN'
|
||||
},
|
||||
include: {
|
||||
zugewiesen: true,
|
||||
praktikum: true
|
||||
},
|
||||
orderBy: [{ zugewiesen: { name: 'asc' } }, { nachname: 'asc' }]
|
||||
});
|
||||
|
||||
// Excel-Datei erstellen
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.creator = 'Praktikumsverwaltung';
|
||||
workbook.created = new Date();
|
||||
// Excel-Datei erstellen
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.creator = 'Praktikumsverwaltung';
|
||||
workbook.created = new Date();
|
||||
|
||||
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
|
||||
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
|
||||
|
||||
// Spalten definieren
|
||||
worksheet.columns = [
|
||||
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
|
||||
{ header: 'Anrede', key: 'anrede', width: 10 },
|
||||
{ header: 'Vorname', key: 'vorname', width: 15 },
|
||||
{ header: 'Nachname', key: 'nachname', width: 15 },
|
||||
{ header: 'Geburtsdatum', key: 'geburtsdatum', width: 12 },
|
||||
{ header: 'Alter', key: 'alter', width: 8 },
|
||||
{ header: 'Straße', key: 'strasse', width: 20 },
|
||||
{ header: 'Hausnr.', key: 'hausnummer', width: 10 },
|
||||
{ header: 'PLZ', key: 'plz', width: 8 },
|
||||
{ header: 'Ort', key: 'ort', width: 15 },
|
||||
{ header: 'Telefon', key: 'telefon', width: 18 },
|
||||
{ header: 'E-Mail', key: 'email', width: 30 },
|
||||
{ header: 'Schulart', key: 'schulart', width: 15 },
|
||||
{ header: 'Klasse', key: 'schulklasse', width: 8 },
|
||||
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
|
||||
{ header: 'Note Mathe', key: 'noteMathe', width: 12 },
|
||||
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
|
||||
{ header: 'Angenommen am', key: 'processedAt', width: 15 }
|
||||
];
|
||||
// Spalten definieren (inkl. Notfallkontakt)
|
||||
worksheet.columns = [
|
||||
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
|
||||
{ header: 'Anrede', key: 'anrede', width: 10 },
|
||||
{ header: 'Vorname', key: 'vorname', width: 15 },
|
||||
{ header: 'Nachname', key: 'nachname', width: 15 },
|
||||
{ header: 'Geburtsdatum', key: 'geburtsdatum', width: 12 },
|
||||
{ header: 'Alter', key: 'alter', width: 8 },
|
||||
{ header: 'Straße', key: 'strasse', width: 20 },
|
||||
{ header: 'Hausnr.', key: 'hausnummer', width: 10 },
|
||||
{ header: 'PLZ', key: 'plz', width: 8 },
|
||||
{ header: 'Ort', key: 'ort', width: 15 },
|
||||
{ header: 'Telefon', key: 'telefon', width: 18 },
|
||||
{ header: 'E-Mail', key: 'email', width: 30 },
|
||||
{ header: 'Schulart', key: 'schulart', width: 15 },
|
||||
{ header: 'Klasse', key: 'schulklasse', width: 8 },
|
||||
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
|
||||
{ header: 'Note Mathe', key: 'noteMathe', width: 12 },
|
||||
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
|
||||
{ header: 'Notfall Vorname', key: 'notfallVorname', width: 15 },
|
||||
{ header: 'Notfall Nachname', key: 'notfallNachname', width: 15 },
|
||||
{ header: 'Notfall Telefon', key: 'notfallTelefon', width: 18 },
|
||||
{ header: 'Angenommen am', key: 'processedAt', width: 15 }
|
||||
];
|
||||
|
||||
// Header-Zeile formatieren
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
||||
headerRow.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FF4472C4' }
|
||||
};
|
||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
headerRow.height = 25;
|
||||
// Header-Zeile formatieren
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
||||
headerRow.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FF4472C4' }
|
||||
};
|
||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
headerRow.height = 25;
|
||||
|
||||
// Daten einfügen
|
||||
anmeldungen.forEach(anmeldung => {
|
||||
worksheet.addRow({
|
||||
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
|
||||
anrede: anmeldung.anrede,
|
||||
vorname: anmeldung.vorname,
|
||||
nachname: anmeldung.nachname,
|
||||
geburtsdatum: anmeldung.geburtsdatum,
|
||||
alter: anmeldung.alter,
|
||||
strasse: anmeldung.strasse,
|
||||
hausnummer: anmeldung.hausnummer,
|
||||
plz: anmeldung.plz,
|
||||
ort: anmeldung.ort,
|
||||
telefon: anmeldung.telefon,
|
||||
email: anmeldung.email,
|
||||
schulart: formatSchulart(anmeldung.schulart),
|
||||
schulklasse: anmeldung.schulklasse,
|
||||
noteDeutsch: anmeldung.noteDeutsch,
|
||||
noteMathe: anmeldung.noteMathe,
|
||||
sozialverhalten: anmeldung.sozialverhalten || '-',
|
||||
processedAt: anmeldung.processedAt
|
||||
? new Date(anmeldung.processedAt).toLocaleDateString('de-DE')
|
||||
: '-'
|
||||
});
|
||||
});
|
||||
// Notfallkontakt-Spalten orange hervorheben
|
||||
['R1', 'S1', 'T1'].forEach((cell) => {
|
||||
worksheet.getCell(cell).fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFED7D31' }
|
||||
};
|
||||
});
|
||||
|
||||
// Datenzeilen formatieren
|
||||
for (let i = 2; i <= anmeldungen.length + 1; i++) {
|
||||
const row = worksheet.getRow(i);
|
||||
row.alignment = { vertical: 'middle' };
|
||||
|
||||
// Abwechselnde Zeilenfarben
|
||||
if (i % 2 === 0) {
|
||||
row.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFF2F2F2' }
|
||||
};
|
||||
}
|
||||
}
|
||||
// Daten einfügen
|
||||
anmeldungen.forEach((anmeldung) => {
|
||||
worksheet.addRow({
|
||||
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
|
||||
anrede: anmeldung.anrede,
|
||||
vorname: anmeldung.vorname,
|
||||
nachname: anmeldung.nachname,
|
||||
geburtsdatum: anmeldung.geburtsdatum,
|
||||
alter: anmeldung.alter,
|
||||
strasse: anmeldung.strasse,
|
||||
hausnummer: anmeldung.hausnummer,
|
||||
plz: anmeldung.plz,
|
||||
ort: anmeldung.ort,
|
||||
telefon: anmeldung.telefon,
|
||||
email: anmeldung.email,
|
||||
schulart: formatSchulart(anmeldung.schulart),
|
||||
schulklasse: anmeldung.schulklasse,
|
||||
noteDeutsch: anmeldung.noteDeutsch,
|
||||
noteMathe: anmeldung.noteMathe,
|
||||
sozialverhalten: anmeldung.sozialverhalten || '-',
|
||||
notfallVorname: anmeldung.notfallVorname || '-',
|
||||
notfallNachname: anmeldung.notfallNachname || '-',
|
||||
notfallTelefon: anmeldung.notfallTelefon || '-',
|
||||
processedAt: anmeldung.processedAt
|
||||
? new Date(anmeldung.processedAt).toLocaleDateString('de-DE')
|
||||
: '-'
|
||||
});
|
||||
});
|
||||
|
||||
// Rahmen für alle Zellen
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
row.eachCell((cell) => {
|
||||
cell.border = {
|
||||
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
right: { style: 'thin', color: { argb: 'FFD0D0D0' } }
|
||||
};
|
||||
});
|
||||
});
|
||||
// Datenzeilen formatieren
|
||||
for (let i = 2; i <= anmeldungen.length + 1; i++) {
|
||||
const row = worksheet.getRow(i);
|
||||
row.alignment = { vertical: 'middle' };
|
||||
|
||||
// Filter aktivieren
|
||||
worksheet.autoFilter = {
|
||||
from: 'A1',
|
||||
to: `R${anmeldungen.length + 1}`
|
||||
};
|
||||
// Abwechselnde Zeilenfarben
|
||||
if (i % 2 === 0) {
|
||||
row.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFF2F2F2' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Zusammenfassung am Ende
|
||||
const summaryRow = worksheet.addRow([]);
|
||||
const totalRow = worksheet.addRow([
|
||||
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''
|
||||
]);
|
||||
totalRow.font = { bold: true };
|
||||
// Rahmen für alle Zellen
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
row.eachCell((cell) => {
|
||||
cell.border = {
|
||||
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
right: { style: 'thin', color: { argb: 'FFD0D0D0' } }
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Excel-Datei als Buffer generieren
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
// Filter aktivieren (angepasst auf neue Spaltenanzahl: A bis U = 21 Spalten)
|
||||
worksheet.autoFilter = {
|
||||
from: 'A1',
|
||||
to: `U${anmeldungen.length + 1}`
|
||||
};
|
||||
|
||||
// Dateiname generieren
|
||||
const dateiname = `Praktikanten_${zeitraum.bezeichnung.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
// Zusammenfassung am Ende
|
||||
const summaryRow = worksheet.addRow([]);
|
||||
const totalRow = worksheet.addRow([
|
||||
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
]);
|
||||
totalRow.font = { bold: true };
|
||||
|
||||
// Als Download zurückgeben
|
||||
return new Response(buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition': `attachment; filename="${dateiname}"`,
|
||||
'Content-Length': buffer.byteLength.toString()
|
||||
}
|
||||
});
|
||||
// Excel-Datei als Buffer generieren
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Exportieren:', error);
|
||||
return json({ error: 'Fehler beim Exportieren', details: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
// Dateiname generieren
|
||||
const dateiname = `Praktikanten_${zeitraum.bezeichnung.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
|
||||
// Als Download zurückgeben
|
||||
return new Response(buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition': `attachment; filename="${dateiname}"`,
|
||||
'Content-Length': buffer.byteLength.toString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Exportieren:', error);
|
||||
return json(
|
||||
{ error: 'Fehler beim Exportieren', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion: Schulart formatieren
|
||||
function formatSchulart(schulart: string): string {
|
||||
const schulartMap: Record<string, string> = {
|
||||
'Gymnasium': 'Gymnasium',
|
||||
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig',
|
||||
'Fachoberschule': 'Fachoberschule',
|
||||
'Realschule': 'Realschule',
|
||||
'KGSR': 'KGS Realschulzweig',
|
||||
'IGSR': 'IGS Realschulzweig'
|
||||
};
|
||||
|
||||
return schulartMap[schulart] || schulart;
|
||||
}
|
||||
const schulartMap: Record<string, string> = {
|
||||
Gymnasium: 'Gymnasium',
|
||||
KGS_Gymnasialzweig: 'KGS Gymnasialzweig',
|
||||
Fachoberschule: 'Fachoberschule',
|
||||
Realschule: 'Realschule',
|
||||
KGSR: 'KGS Realschulzweig',
|
||||
IGSR: 'IGS Realschulzweig'
|
||||
};
|
||||
|
||||
return schulartMap[schulart] || schulart;
|
||||
}
|
||||
|
||||
@@ -5,137 +5,143 @@ import { prisma } from '$lib/prisma';
|
||||
|
||||
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
|
||||
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
|
||||
const dienststellen = await prisma.dienststelle.findMany();
|
||||
|
||||
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
|
||||
for (const dienststelle of dienststellen) {
|
||||
await prisma.zeitraumPlaetze.create({
|
||||
data: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelle.id,
|
||||
plaetze: 0 // Standardwert: 0 Plätze
|
||||
}
|
||||
});
|
||||
}
|
||||
const dienststellen = await prisma.dienststelle.findMany();
|
||||
|
||||
// Erstelle für jede existierende Dienststelle einen Eintrag mit 1 Platz
|
||||
for (const dienststelle of dienststellen) {
|
||||
await prisma.zeitraumPlaetze.create({
|
||||
data: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelle.id,
|
||||
plaetze: 1 // Standardwert: 1 Platz pro Dienststelle
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const zeitraeume = await prisma.praktikumszeitraum.findMany({
|
||||
orderBy: { startDatum: 'desc' }
|
||||
});
|
||||
return json(zeitraeume);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Zeiträume:', error);
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
try {
|
||||
const zeitraeume = await prisma.praktikumszeitraum.findMany({
|
||||
orderBy: { startDatum: 'desc' }
|
||||
});
|
||||
return json(zeitraeume);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Zeiträume:', error);
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { bezeichnung, startDatum, endDatum } = await request.json();
|
||||
try {
|
||||
const { bezeichnung, startDatum, endDatum } = await request.json();
|
||||
|
||||
if (!bezeichnung || !startDatum || !endDatum) {
|
||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
if (!bezeichnung || !startDatum || !endDatum) {
|
||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
const start = new Date(startDatum);
|
||||
const end = new Date(endDatum);
|
||||
const start = new Date(startDatum);
|
||||
const end = new Date(endDatum);
|
||||
|
||||
if (end <= start) {
|
||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||
}
|
||||
if (end <= start) {
|
||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||
}
|
||||
|
||||
const zeitraum = await prisma.praktikumszeitraum.create({
|
||||
data: {
|
||||
bezeichnung,
|
||||
startDatum: start,
|
||||
endDatum: end
|
||||
}
|
||||
});
|
||||
const zeitraum = await prisma.praktikumszeitraum.create({
|
||||
data: {
|
||||
bezeichnung,
|
||||
startDatum: start,
|
||||
endDatum: end
|
||||
}
|
||||
});
|
||||
|
||||
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
|
||||
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
|
||||
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen (mit 1 Platz)
|
||||
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
|
||||
|
||||
return json(zeitraum);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Erstellen des Zeitraums:', error);
|
||||
if (error.code === 'P2002') {
|
||||
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
||||
}
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
return json(zeitraum);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Erstellen des Zeitraums:', error);
|
||||
if (error.code === 'P2002') {
|
||||
return json(
|
||||
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ request, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
||||
try {
|
||||
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
||||
|
||||
if (!id || !bezeichnung || !startDatum || !endDatum) {
|
||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
if (!id || !bezeichnung || !startDatum || !endDatum) {
|
||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
const start = new Date(startDatum);
|
||||
const end = new Date(endDatum);
|
||||
const start = new Date(startDatum);
|
||||
const end = new Date(endDatum);
|
||||
|
||||
if (end <= start) {
|
||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||
}
|
||||
if (end <= start) {
|
||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||
}
|
||||
|
||||
const zeitraum = await prisma.praktikumszeitraum.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: {
|
||||
bezeichnung,
|
||||
startDatum: start,
|
||||
endDatum: end
|
||||
}
|
||||
});
|
||||
const zeitraum = await prisma.praktikumszeitraum.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: {
|
||||
bezeichnung,
|
||||
startDatum: start,
|
||||
endDatum: end
|
||||
}
|
||||
});
|
||||
|
||||
return json(zeitraum);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
|
||||
if (error.code === 'P2002') {
|
||||
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
||||
}
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
return json(zeitraum);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
|
||||
if (error.code === 'P2002') {
|
||||
return json(
|
||||
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||
await prisma.praktikumszeitraum.delete({
|
||||
where: { id: parseInt(id) }
|
||||
});
|
||||
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||
await prisma.praktikumszeitraum.delete({
|
||||
where: { id: parseInt(id) }
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Zeitraums:', error);
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Zeitraums:', error);
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,83 +6,87 @@ import { json } from '@sveltejs/kit';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST({ request }: RequestEvent) {
|
||||
const formData = await request.formData();
|
||||
const formData = await request.formData();
|
||||
|
||||
const get = (key: string) => formData.get(key)?.toString() ?? '';
|
||||
const get = (key: string) => formData.get(key)?.toString() ?? '';
|
||||
|
||||
// const pdfs = formData.getAll('pdfs') as File[];
|
||||
const pdfFiles = formData.getAll('pdfs') as File[];
|
||||
const hasValidPdf = pdfFiles.some((file) => file.size > 0 && file.type === 'application/pdf');
|
||||
if (!hasValidPdf) {
|
||||
return json({ error: 'Bitte lade das Zeugnis hoch in PDF Format.' }, { status: 400 });
|
||||
}
|
||||
const pdfData = [];
|
||||
// const pdfs = formData.getAll('pdfs') as File[];
|
||||
const pdfFiles = formData.getAll('pdfs') as File[];
|
||||
const hasValidPdf = pdfFiles.some((file) => file.size > 0 && file.type === 'application/pdf');
|
||||
if (!hasValidPdf) {
|
||||
return json({ error: 'Bitte lade das Zeugnis hoch in PDF Format.' }, { status: 400 });
|
||||
}
|
||||
const pdfData = [];
|
||||
|
||||
// const gespeichertePfade: string[] = [];
|
||||
const noteDeutsch = Number(get('noteDeutsch'));
|
||||
const noteMathe = Number(get('noteMathe'));
|
||||
// const gespeichertePfade: string[] = [];
|
||||
const noteDeutsch = Number(get('noteDeutsch'));
|
||||
const noteMathe = Number(get('noteMathe'));
|
||||
|
||||
if (isNaN(noteDeutsch) || isNaN(noteMathe)) {
|
||||
return json({ error: 'Bitte gib gültige Noten an.' }, { status: 400 });
|
||||
}
|
||||
if (isNaN(noteDeutsch) || isNaN(noteMathe)) {
|
||||
return json({ error: 'Bitte gib gültige Noten an.' }, { status: 400 });
|
||||
}
|
||||
|
||||
for (const file of pdfFiles) {
|
||||
if (file.size > 0 && file.type === 'application/pdf') {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const filename = `${randomUUID()}.pdf`;
|
||||
const uploadPath = `/uploads/${filename}`;
|
||||
await writeFile(`static${uploadPath}`, buffer);
|
||||
pdfData.push({ pfad: uploadPath });
|
||||
}
|
||||
}
|
||||
for (const file of pdfFiles) {
|
||||
if (file.size > 0 && file.type === 'application/pdf') {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const filename = `${randomUUID()}.pdf`;
|
||||
const uploadPath = `/uploads/${filename}`;
|
||||
await writeFile(`static${uploadPath}`, buffer);
|
||||
pdfData.push({ pfad: uploadPath });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const anmeldung = await prisma.anmeldung.create({
|
||||
data: {
|
||||
// Persönliche Daten
|
||||
anrede: formData.get('anrede') as string,
|
||||
vorname: formData.get('vorname') as string,
|
||||
nachname: formData.get('nachname') as string,
|
||||
geburtsdatum: formData.get('geburtsdatum') as string,
|
||||
// Adresse
|
||||
strasse: formData.get('strasse') as string,
|
||||
hausnummer: formData.get('hausnummer') as string,
|
||||
ort: formData.get('ort') as string,
|
||||
plz: formData.get('plz') as string,
|
||||
// Kontakt
|
||||
telefon: formData.get('telefon') as string,
|
||||
email: formData.get('email') as string,
|
||||
// Schule
|
||||
schulart: formData.get('schulart') as string,
|
||||
schulklasse: formData.get('schulklasse') as string,
|
||||
noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
|
||||
noteMathe: parseInt(formData.get('noteMathe') as string),
|
||||
sozialverhalten: formData.get('sozialverhalten') as string || null,
|
||||
// Praktikum
|
||||
praktikumId: parseInt(formData.get('zeitraum') as string),
|
||||
motivation: formData.get('motivation') as string || '',
|
||||
// Wünsche
|
||||
wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
|
||||
wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
|
||||
wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
|
||||
// Alter (falls vom Frontend gesendet)
|
||||
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
|
||||
// System
|
||||
zugewiesenId: null,
|
||||
// timestamp wird automatisch durch @default(now()) gesetzt
|
||||
pdfs: {
|
||||
create: pdfData
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
const anmeldung = await prisma.anmeldung.create({
|
||||
data: {
|
||||
// Persönliche Daten
|
||||
anrede: formData.get('anrede') as string,
|
||||
vorname: formData.get('vorname') as string,
|
||||
nachname: formData.get('nachname') as string,
|
||||
geburtsdatum: formData.get('geburtsdatum') as string,
|
||||
// Adresse
|
||||
strasse: formData.get('strasse') as string,
|
||||
hausnummer: formData.get('hausnummer') as string,
|
||||
ort: formData.get('ort') as string,
|
||||
plz: formData.get('plz') as string,
|
||||
// Kontakt
|
||||
telefon: formData.get('telefon') as string,
|
||||
email: formData.get('email') as string,
|
||||
// Schule
|
||||
schulart: formData.get('schulart') as string,
|
||||
schulklasse: formData.get('schulklasse') as string,
|
||||
noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
|
||||
noteMathe: parseInt(formData.get('noteMathe') as string),
|
||||
sozialverhalten: (formData.get('sozialverhalten') as string) || null,
|
||||
// Praktikum
|
||||
praktikumId: parseInt(formData.get('zeitraum') as string),
|
||||
motivation: (formData.get('motivation') as string) || '',
|
||||
// Wünsche
|
||||
wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
|
||||
wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
|
||||
wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
|
||||
// Alter (falls vom Frontend gesendet)
|
||||
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
|
||||
// Notfallkontakt
|
||||
notfallVorname: (formData.get('notfallVorname') as string) || null,
|
||||
notfallNachname: (formData.get('notfallNachname') as string) || null,
|
||||
notfallTelefon: (formData.get('notfallTelefon') as string) || null,
|
||||
// System
|
||||
zugewiesenId: null,
|
||||
// timestamp wird automatisch durch @default(now()) gesetzt
|
||||
pdfs: {
|
||||
create: pdfData
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return json({ success: true, anmeldung });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && (err as { code?: string }).code === 'P2002') {
|
||||
return json({ error: 'Diese E-Mail wurde bereits verwendet.' }, { status: 400 });
|
||||
}
|
||||
return json({ success: true, anmeldung });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && (err as { code?: string }).code === 'P2002') {
|
||||
return json({ error: 'Diese E-Mail wurde bereits verwendet.' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
console.error(err);
|
||||
return json({ error: 'Fehler beim Speichern.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user