From a99ddf6fa9158e965677eb9e348622eb2c7fce52 Mon Sep 17 00:00:00 2001 From: titver968 Date: Mon, 29 Dec 2025 12:09:55 +0100 Subject: [PATCH] praktikum, Notfallkontakt und ein Platz pro Dienstelle bei neuer Zeitraum --- prisma/prisma/praktika.db | Bin 94208 -> 94208 bytes prisma/schema.prisma | 70 +-- src/lib/components/AnmeldungenTable.svelte | 429 ++++++++-------- src/routes/+page.svelte | 149 +++--- src/routes/admin/anmeldungen/+page.svelte | 255 +++++----- src/routes/api/admin/anmeldungen/+server.ts | 513 +++++++++++--------- src/routes/api/admin/export/+server.ts | 339 +++++++------ src/routes/api/admin/zeitraeume/+server.ts | 216 +++++---- src/routes/api/anmelden/+server.ts | 148 +++--- 9 files changed, 1123 insertions(+), 996 deletions(-) diff --git a/prisma/prisma/praktika.db b/prisma/prisma/praktika.db index 35bc336afcb106153cf9ed9b79f9f755867212ef..1777b31a8a26f1f2ff6877e4a1b97fe5af040a90 100644 GIT binary patch delta 1096 zcma)*O>7%Q6vuaV?U2~PVB#7vk5OWNfd`0urapoId+oiSI zg4bx%h4y~l7v1_;Z%~9A0=x}3{8;Kec=S=}6!e(u5wGSOHoh+2f z#bTkvJM-zCPCyg`!T#?De;2D1VWMhkThk4_Nb9ttXLtqiboyBANb->m}ZH#xOomjSaQ0+BEvtqiWrsFDBw#>MXU-cpSJV*OYo+ z&w~+oxI8;sCD#){?OyrS8r zrsTPc6a#54#mr;`<35)$(hqRd|MSD)SW>znNH^pic`mXM86EC~x5H0|J`bJbk782_ zy?$=!q2%~Dcw67xIxQ7anful(@$wtA&N8V=;dCYU?|y~VnCmtEyZyY^I>_$6vODVM z0#`*p{>gUm%2$DBq66@<0KbR7!kh3iybeF=MN=TRHIo8OcvzOw;NZ%$LsAm#cRAC$ zJ_(-S=#B($!EfMi@DBVA?)0Llk0S6IjD{rc)P0X+1CjtTy<5{j;l!Q*@4_G8@9;19 Y3*5U;%z$+m4oU!s_ma2nmO&!*4@%oTkpKVy delta 458 zcmZp8z}oPDb%L~DJp%)SI1q~gF%u9AOw=)EtlyZhgrAL(KbL_&ce9{EGXLaU`RPFZ z4F>)jn*|Nl@=sO}P~3b&f2%yB;^aPm7e>X+m;D>;89671^eX|$^8Q`{7TzERzMcG8 zeB8W2j8hq>ZWfuaiF^8mY(|Og_o5j~7&iya-NeMh!uOJaUy=U={{+65lNA;yY&KZn z#aGYH!jR9XUs{lppO{jtZ)}zb1eTV%siww>x+Vr`NxBx6Nv66<$!2LLX(pxyNl6xZ z1u1FlW^5?>3@uDj4a`%MbrX}44RuXSOpSCCjT23D&C`++Q!G*}(+mxgK>FCs6jAgg zZvMHBM^Kzan1S;Q$7BvU_G-3QY`(0mtfDN_ScEqV3VdhY?0#w|>&CSvY@05y1n{u% zpJd>F#Q%~1GXKfx3JQ!0+YJ;L8~7zTIhn;7{oUML{W#z(b`Wd&Nqt6ZptkGG{MY$E z@jvIkGhM*|q|JeGIsf$Q_Kdnf$=3}0ulc|6|K|Sym)yY5&A`ac!py+P1|+t>_Gc7T F006FefY<;4 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`;