diff --git a/prisma/prisma/praktika.db b/prisma/prisma/praktika.db index 35bc336..1777b31 100644 Binary files a/prisma/prisma/praktika.db and b/prisma/prisma/praktika.db differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a1805b4..7e8b8ad 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,7 @@ model EmailConfig { id Int @id @default(1) subject String @default("Praktikumsplatz-Zusage") template String @default("Sehr geehrte/r {anrede} {nachname},\n\nwir freuen uns, Ihnen mitteilen zu können, dass Ihre Bewerbung für ein Praktikum erfolgreich war.\n\nSie wurden für das Praktikum bei folgender Dienststelle angenommen:\n{dienststelle}\n\nWeitere Informationen erhalten Sie in den kommenden Tagen.\n\nMit freundlichen Grüßen\nIhr Praktikumsteam") - + @@map("email_config") } @@ -29,8 +29,8 @@ model Dienststelle { anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2") anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3") zugewiesene Anmeldung[] @relation("Zugewiesen") - - zeitraumPlaetze ZeitraumPlaetze[] + + zeitraumPlaetze ZeitraumPlaetze[] } model Praktikumszeitraum { @@ -39,19 +39,19 @@ model Praktikumszeitraum { startDatum DateTime endDatum DateTime anmeldungen Anmeldung[] @relation("PraktikumszeitraumAnmeldungen") - + zeitraumPlaetze ZeitraumPlaetze[] } model ZeitraumPlaetze { - id Int @id @default(autoincrement()) - zeitraumId Int - dienststelleId Int - plaetze Int @default(0) - - zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade) - dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade) - + id Int @id @default(autoincrement()) + zeitraumId Int + dienststelleId Int + plaetze Int @default(0) + + zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade) + dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade) + @@unique([zeitraumId, dienststelleId]) @@index([zeitraumId]) @@index([dienststelleId]) @@ -66,7 +66,7 @@ enum Status { } model Anmeldung { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) anrede String vorname String nachname String @@ -84,25 +84,31 @@ model Anmeldung { sozialverhalten String? motivation String? alter Int? - status Status @default(OFFEN) - - processedAt DateTime? - - praktikumId Int? - praktikum Praktikumszeitraum? @relation("PraktikumszeitraumAnmeldungen", fields: [praktikumId], references: [id]) - - zugewiesenId Int? - zugewiesen Dienststelle? @relation("Zugewiesen", fields: [zugewiesenId], references: [id]) - wunsch1Id Int? - wunsch1 Dienststelle? @relation("Wunsch1", fields: [wunsch1Id], references: [id]) - wunsch2Id Int? - wunsch2 Dienststelle? @relation("Wunsch2", fields: [wunsch2Id], references: [id]) - wunsch3Id Int? - wunsch3 Dienststelle? @relation("Wunsch3", fields: [wunsch3Id], references: [id]) - - timestamp DateTime @default(now()) - pdfs PdfDatei[] - + + // Notfallkontakt + notfallVorname String? + notfallNachname String? + notfallTelefon String? + + status Status @default(OFFEN) + + processedAt DateTime? + + praktikumId Int? + praktikum Praktikumszeitraum? @relation("PraktikumszeitraumAnmeldungen", fields: [praktikumId], references: [id]) + + zugewiesenId Int? + zugewiesen Dienststelle? @relation("Zugewiesen", fields: [zugewiesenId], references: [id]) + wunsch1Id Int? + wunsch1 Dienststelle? @relation("Wunsch1", fields: [wunsch1Id], references: [id]) + wunsch2Id Int? + wunsch2 Dienststelle? @relation("Wunsch2", fields: [wunsch2Id], references: [id]) + wunsch3Id Int? + wunsch3 Dienststelle? @relation("Wunsch3", fields: [wunsch3Id], references: [id]) + + timestamp DateTime @default(now()) + pdfs PdfDatei[] + @@index([status]) @@index([processedAt]) @@index([zugewiesenId]) diff --git a/src/lib/components/AnmeldungenTable.svelte b/src/lib/components/AnmeldungenTable.svelte index 832fdad..6ae1a07 100644 --- a/src/lib/components/AnmeldungenTable.svelte +++ b/src/lib/components/AnmeldungenTable.svelte @@ -1,11 +1,11 @@
+ class="bg-white shadow-md rounded-2xl p-8 max-w-2xl w-full space-y-4">

Praktikumsanmeldung

@@ -302,7 +313,7 @@ - + - + - {#if !hideSozialVerhalten} + {#if !hideSozialVerhalten}
- -
{#if notenFehler} @@ -384,8 +395,8 @@ @@ -453,7 +464,7 @@ @@ -461,23 +472,33 @@
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" />
{/key} + +
+

Notfallkontakt

+
+ + + +
+
+ {#if showAblehnungModal}

{ablehnungHinweis}

@@ -486,15 +507,15 @@ {/if} - @@ -508,8 +529,8 @@

Anmeldung erfolgreich gesendet!

diff --git a/src/routes/admin/anmeldungen/+page.svelte b/src/routes/admin/anmeldungen/+page.svelte index a6d5569..690612f 100644 --- a/src/routes/admin/anmeldungen/+page.svelte +++ b/src/routes/admin/anmeldungen/+page.svelte @@ -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`;
-
@@ -442,9 +445,9 @@ Ihr Praktikumsteam`;
- +

Verfügbare Platzhalter: {anrede}, {vorname}, {nachname}, {dienststelle}

- +
-
{:else}
-
{/if} @@ -663,10 +666,10 @@ Ihr Praktikumsteam`; {#if showDialog} {/if} @@ -677,11 +680,11 @@ Ihr Praktikumsteam`; -
- +
@@ -694,31 +697,31 @@ Ihr Praktikumsteam`;

Excel-Export

-
- +

Wählen Sie einen Praktikumszeitraum aus, um alle angenommenen Anmeldungen als Excel-Datei zu exportieren.

- +
- +
-
- +
@@ -803,30 +806,30 @@ Ihr Praktikumsteam`; An: {emailPreviewData.to}
- +
Betreff: {emailPreviewData.subject}
- +
- +
{#if emailCopied} @@ -853,14 +856,14 @@ Ihr Praktikumsteam`;