praktikum, Notfallkontakt und ein Platz pro Dienstelle bei neuer Zeitraum

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

Binary file not shown.

View File

@@ -84,6 +84,12 @@ model Anmeldung {
sozialverhalten String? sozialverhalten String?
motivation String? motivation String?
alter Int? alter Int?
// Notfallkontakt
notfallVorname String?
notfallNachname String?
notfallTelefon String?
status Status @default(OFFEN) status Status @default(OFFEN)
processedAt DateTime? processedAt DateTime?

View File

@@ -167,6 +167,21 @@
<span class="font-medium text-gray-700">E-Mail:</span><br> <span class="font-medium text-gray-700">E-Mail:</span><br>
<a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a> <a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a>
</div> </div>
<!-- Notfallkontakt -->
{#if anmeldung.notfallVorname || anmeldung.notfallNachname || anmeldung.notfallTelefon}
<div class="mt-2 pt-2 border-t border-gray-100">
<span class="font-medium text-orange-700">Notfallkontakt:</span>
<div class="text-gray-600">
{anmeldung.notfallVorname || ''} {anmeldung.notfallNachname || ''}
</div>
{#if anmeldung.notfallTelefon}
<a href="tel:{anmeldung.notfallTelefon}" class="text-blue-600 hover:text-blue-800">
{anmeldung.notfallTelefon}
</a>
{/if}
</div>
{/if}
</div> </div>
</td> </td>

View File

@@ -16,6 +16,11 @@
let zeitraum = ''; let zeitraum = '';
let motivation = ''; let motivation = '';
// Notfallkontakt
let notfallVorname = '';
let notfallNachname = '';
let notfallTelefon = '';
let wunsch1Id = ''; let wunsch1Id = '';
let wunsch2Id = ''; let wunsch2Id = '';
let wunsch3Id = ''; let wunsch3Id = '';
@@ -208,6 +213,9 @@
noteMathe = ''; noteMathe = '';
sozialverhalten = ''; sozialverhalten = '';
schulklasse = ''; schulklasse = '';
notfallVorname = '';
notfallNachname = '';
notfallTelefon = '';
pdfDateien = []; pdfDateien = [];
fileInputKey += 1; fileInputKey += 1;
success = false; success = false;
@@ -246,6 +254,9 @@
data.append('sozialverhalten', sozialverhalten); data.append('sozialverhalten', sozialverhalten);
data.append('schulklasse', schulklasse); data.append('schulklasse', schulklasse);
data.append('alter', alter); data.append('alter', alter);
data.append('notfallVorname', notfallVorname);
data.append('notfallNachname', notfallNachname);
data.append('notfallTelefon', notfallTelefon);
for (const pdf of pdfDateien) { for (const pdf of pdfDateien) {
data.append('pdfs', pdf); data.append('pdfs', pdf);
@@ -471,6 +482,16 @@
</div> </div>
{/key} {/key}
<!-- Notfallkontakt -->
<div class="border-t pt-4 mt-4">
<h2 class="text-lg font-semibold text-gray-700 mb-3">Notfallkontakt</h2>
<div class="grid grid-cols-2 gap-4">
<input bind:value={notfallVorname} placeholder="Vorname Notfallkontakt" required class="input" />
<input bind:value={notfallNachname} placeholder="Nachname Notfallkontakt" required class="input" />
<input bind:value={notfallTelefon} type="tel" placeholder="Mobilnummer Notfallkontakt" required class="input col-span-2" />
</div>
</div>
{#if showAblehnungModal} {#if showAblehnungModal}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full"> <div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">

View File

@@ -15,6 +15,9 @@
noteDeutsch?: string; noteDeutsch?: string;
noteMathe?: string; noteMathe?: string;
sozialverhalten?: string; sozialverhalten?: string;
notfallVorname?: string;
notfallNachname?: string;
notfallTelefon?: string;
wunsch1?: { id: number; name: string }; wunsch1?: { id: number; name: string };
wunsch2?: { id: number; name: string }; wunsch2?: { id: number; name: string };
wunsch3?: { id: number; name: string }; wunsch3?: { id: number; name: string };

View File

@@ -23,7 +23,7 @@ export async function GET() {
] ]
}); });
const formattedAnmeldungen = anmeldungen.map(anmeldung => ({ const formattedAnmeldungen = anmeldungen.map((anmeldung) => ({
id: anmeldung.id, id: anmeldung.id,
anrede: anmeldung.anrede, anrede: anmeldung.anrede,
vorname: anmeldung.vorname, vorname: anmeldung.vorname,
@@ -45,33 +45,48 @@ export async function GET() {
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined, noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
sozialverhalten: anmeldung.sozialverhalten, sozialverhalten: anmeldung.sozialverhalten,
// Notfallkontakt
notfallVorname: anmeldung.notfallVorname,
notfallNachname: anmeldung.notfallNachname,
notfallTelefon: anmeldung.notfallTelefon,
status: mapPrismaStatusToFrontend(anmeldung.status), status: mapPrismaStatusToFrontend(anmeldung.status),
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined, processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
wunsch1: anmeldung.wunsch1 ? { wunsch1: anmeldung.wunsch1
? {
id: anmeldung.wunsch1.id, id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name name: anmeldung.wunsch1.name
} : undefined, }
wunsch2: anmeldung.wunsch2 ? { : undefined,
wunsch2: anmeldung.wunsch2
? {
id: anmeldung.wunsch2.id, id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name name: anmeldung.wunsch2.name
} : undefined, }
wunsch3: anmeldung.wunsch3 ? { : undefined,
wunsch3: anmeldung.wunsch3
? {
id: anmeldung.wunsch3.id, id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name name: anmeldung.wunsch3.name
} : undefined, }
: undefined,
assignedDienststelle: anmeldung.zugewiesen ? { assignedDienststelle: anmeldung.zugewiesen
? {
id: anmeldung.zugewiesen.id, id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name name: anmeldung.zugewiesen.name
} : undefined, }
: undefined,
zeitraum: anmeldung.praktikum ? { zeitraum: anmeldung.praktikum
? {
id: anmeldung.praktikum.id, id: anmeldung.praktikum.id,
bezeichnung: anmeldung.praktikum.bezeichnung, bezeichnung: anmeldung.praktikum.bezeichnung,
startDatum: anmeldung.praktikum.startDatum.toISOString(), startDatum: anmeldung.praktikum.startDatum.toISOString(),
endDatum: anmeldung.praktikum.endDatum.toISOString() endDatum: anmeldung.praktikum.endDatum.toISOString()
} : undefined, }
: undefined,
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(), timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
@@ -81,7 +96,10 @@ export async function GET() {
return json(formattedAnmeldungen); return json(formattedAnmeldungen);
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der Anmeldungen:', error); console.error('Fehler beim Laden der Anmeldungen:', error);
return json({ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message }, { status: 500 }); return json(
{ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message },
{ status: 500 }
);
} }
} }
@@ -113,7 +131,10 @@ export async function POST({ request, url }) {
const zeitraumId = existingAnmeldung.praktikumId; const zeitraumId = existingAnmeldung.praktikumId;
if (!zeitraumId) { if (!zeitraumId) {
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 }); return json(
{ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' },
{ status: 400 }
);
} }
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind // Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
@@ -127,16 +148,22 @@ export async function POST({ request, url }) {
}); });
if (!zeitraumPlaetze) { if (!zeitraumPlaetze) {
return json({ return json(
{
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert' error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
}, { status: 400 }); },
{ status: 400 }
);
} }
// Prüfen ob noch Plätze frei sind (plaetze > 0) // Prüfen ob noch Plätze frei sind (plaetze > 0)
if (zeitraumPlaetze.plaetze <= 0) { if (zeitraumPlaetze.plaetze <= 0) {
return json({ return json(
{
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.' error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
}, { status: 400 }); },
{ status: 400 }
);
} }
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren // Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
@@ -188,12 +215,18 @@ export async function POST({ request, url }) {
// Spezifische Fehlermeldung für "keine Plätze" // Spezifische Fehlermeldung für "keine Plätze"
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') { if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
return json({ return json(
{
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.' error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
}, { status: 400 }); },
{ status: 400 }
);
} }
return json({ error: 'Fehler beim Annehmen der Anmeldung', details: (error as Error).message }, { status: 500 }); return json(
{ error: 'Fehler beim Annehmen der Anmeldung', details: (error as Error).message },
{ status: 500 }
);
} }
} }
@@ -250,7 +283,10 @@ export async function PATCH({ request, url }) {
return json({ success: true }); return json({ success: true });
} catch (error) { } catch (error) {
console.error('Fehler beim Aktualisieren der Anmeldung:', error); console.error('Fehler beim Aktualisieren der Anmeldung:', error);
return json({ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message }, { status: 500 }); return json(
{ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message },
{ status: 500 }
);
} }
} }
@@ -270,17 +306,20 @@ export async function DELETE({ url }) {
return json({ success: true }); return json({ success: true });
} catch (error) { } catch (error) {
console.error('Fehler beim Löschen der Anmeldung:', error); console.error('Fehler beim Löschen der Anmeldung:', error);
return json({ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message }, { status: 500 }); return json(
{ error: 'Fehler beim Löschen der Anmeldung', details: (error as Error).message },
{ status: 500 }
);
} }
} }
// Hilfsfunktion: Prisma Status zu Frontend Status // Hilfsfunktion: Prisma Status zu Frontend Status
function mapPrismaStatusToFrontend(prismaStatus: string): string { function mapPrismaStatusToFrontend(prismaStatus: string): string {
const statusMap: Record<string, string> = { const statusMap: Record<string, string> = {
'OFFEN': 'pending', OFFEN: 'pending',
'BEARBEITUNG': 'processing', BEARBEITUNG: 'processing',
'ANGENOMMEN': 'accepted', ANGENOMMEN: 'accepted',
'ABGELEHNT': 'rejected' ABGELEHNT: 'rejected'
}; };
return statusMap[prismaStatus] || 'pending'; return statusMap[prismaStatus] || 'pending';

View File

@@ -33,10 +33,7 @@ export async function GET({ url }) {
zugewiesen: true, zugewiesen: true,
praktikum: true praktikum: true
}, },
orderBy: [ orderBy: [{ zugewiesen: { name: 'asc' } }, { nachname: 'asc' }]
{ zugewiesen: { name: 'asc' } },
{ nachname: 'asc' }
]
}); });
// Excel-Datei erstellen // Excel-Datei erstellen
@@ -46,7 +43,7 @@ export async function GET({ url }) {
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen'); const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
// Spalten definieren // Spalten definieren (inkl. Notfallkontakt)
worksheet.columns = [ worksheet.columns = [
{ header: 'Dienststelle', key: 'dienststelle', width: 30 }, { header: 'Dienststelle', key: 'dienststelle', width: 30 },
{ header: 'Anrede', key: 'anrede', width: 10 }, { header: 'Anrede', key: 'anrede', width: 10 },
@@ -65,6 +62,9 @@ export async function GET({ url }) {
{ header: 'Note Deutsch', key: 'noteDeutsch', width: 12 }, { header: 'Note Deutsch', key: 'noteDeutsch', width: 12 },
{ header: 'Note Mathe', key: 'noteMathe', width: 12 }, { header: 'Note Mathe', key: 'noteMathe', width: 12 },
{ header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 }, { header: 'Sozialverhalten', key: 'sozialverhalten', width: 35 },
{ header: '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: 'Angenommen am', key: 'processedAt', width: 15 }
]; ];
@@ -79,8 +79,17 @@ export async function GET({ url }) {
headerRow.alignment = { vertical: 'middle', horizontal: 'center' }; headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
headerRow.height = 25; headerRow.height = 25;
// Notfallkontakt-Spalten orange hervorheben
['R1', 'S1', 'T1'].forEach((cell) => {
worksheet.getCell(cell).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFED7D31' }
};
});
// Daten einfügen // Daten einfügen
anmeldungen.forEach(anmeldung => { anmeldungen.forEach((anmeldung) => {
worksheet.addRow({ worksheet.addRow({
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen', dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
anrede: anmeldung.anrede, anrede: anmeldung.anrede,
@@ -99,6 +108,9 @@ export async function GET({ url }) {
noteDeutsch: anmeldung.noteDeutsch, noteDeutsch: anmeldung.noteDeutsch,
noteMathe: anmeldung.noteMathe, noteMathe: anmeldung.noteMathe,
sozialverhalten: anmeldung.sozialverhalten || '-', sozialverhalten: anmeldung.sozialverhalten || '-',
notfallVorname: anmeldung.notfallVorname || '-',
notfallNachname: anmeldung.notfallNachname || '-',
notfallTelefon: anmeldung.notfallTelefon || '-',
processedAt: anmeldung.processedAt processedAt: anmeldung.processedAt
? new Date(anmeldung.processedAt).toLocaleDateString('de-DE') ? new Date(anmeldung.processedAt).toLocaleDateString('de-DE')
: '-' : '-'
@@ -132,17 +144,36 @@ export async function GET({ url }) {
}); });
}); });
// Filter aktivieren // Filter aktivieren (angepasst auf neue Spaltenanzahl: A bis U = 21 Spalten)
worksheet.autoFilter = { worksheet.autoFilter = {
from: 'A1', from: 'A1',
to: `R${anmeldungen.length + 1}` to: `U${anmeldungen.length + 1}`
}; };
// Zusammenfassung am Ende // Zusammenfassung am Ende
const summaryRow = worksheet.addRow([]); const summaryRow = worksheet.addRow([]);
const totalRow = worksheet.addRow([ const totalRow = worksheet.addRow([
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`, `Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' '',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
''
]); ]);
totalRow.font = { bold: true }; totalRow.font = { bold: true };
@@ -161,22 +192,24 @@ export async function GET({ url }) {
'Content-Length': buffer.byteLength.toString() 'Content-Length': buffer.byteLength.toString()
} }
}); });
} catch (error) { } catch (error) {
console.error('Fehler beim Exportieren:', error); console.error('Fehler beim Exportieren:', error);
return json({ error: 'Fehler beim Exportieren', details: (error as Error).message }, { status: 500 }); return json(
{ error: 'Fehler beim Exportieren', details: (error as Error).message },
{ status: 500 }
);
} }
} }
// Hilfsfunktion: Schulart formatieren // Hilfsfunktion: Schulart formatieren
function formatSchulart(schulart: string): string { function formatSchulart(schulart: string): string {
const schulartMap: Record<string, string> = { const schulartMap: Record<string, string> = {
'Gymnasium': 'Gymnasium', Gymnasium: 'Gymnasium',
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig', KGS_Gymnasialzweig: 'KGS Gymnasialzweig',
'Fachoberschule': 'Fachoberschule', Fachoberschule: 'Fachoberschule',
'Realschule': 'Realschule', Realschule: 'Realschule',
'KGSR': 'KGS Realschulzweig', KGSR: 'KGS Realschulzweig',
'IGSR': 'IGS Realschulzweig' IGSR: 'IGS Realschulzweig'
}; };
return schulartMap[schulart] || schulart; return schulartMap[schulart] || schulart;

View File

@@ -7,13 +7,13 @@ import { prisma } from '$lib/prisma';
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) { async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
const dienststellen = await prisma.dienststelle.findMany(); const dienststellen = await prisma.dienststelle.findMany();
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen // Erstelle für jede existierende Dienststelle einen Eintrag mit 1 Platz
for (const dienststelle of dienststellen) { for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.create({ await prisma.zeitraumPlaetze.create({
data: { data: {
zeitraumId: zeitraumId, zeitraumId: zeitraumId,
dienststelleId: dienststelle.id, dienststelleId: dienststelle.id,
plaetze: 0 // Standardwert: 0 Plätze plaetze: 1 // Standardwert: 1 Platz pro Dienststelle
} }
}); });
} }
@@ -64,14 +64,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
} }
}); });
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen // Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen (mit 1 Platz)
await createZeitraumPlaetzeForZeitraum(zeitraum.id); await createZeitraumPlaetzeForZeitraum(zeitraum.id);
return json(zeitraum); return json(zeitraum);
} catch (error: any) { } catch (error: any) {
console.error('Fehler beim Erstellen des Zeitraums:', error); console.error('Fehler beim Erstellen des Zeitraums:', error);
if (error.code === 'P2002') { if (error.code === 'P2002') {
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 }); return json(
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
{ status: 400 }
);
} }
return json({ error: 'Serverfehler' }, { status: 500 }); return json({ error: 'Serverfehler' }, { status: 500 });
} }
@@ -110,7 +113,10 @@ export const PATCH: RequestHandler = async ({ request, cookies }) => {
} catch (error: any) { } catch (error: any) {
console.error('Fehler beim Aktualisieren des Zeitraums:', error); console.error('Fehler beim Aktualisieren des Zeitraums:', error);
if (error.code === 'P2002') { if (error.code === 'P2002') {
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 }); return json(
{ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
{ status: 400 }
);
} }
return json({ error: 'Serverfehler' }, { status: 500 }); return json({ error: 'Serverfehler' }, { status: 500 });
} }

View File

@@ -57,16 +57,20 @@ export async function POST({ request }: RequestEvent) {
schulklasse: formData.get('schulklasse') as string, schulklasse: formData.get('schulklasse') as string,
noteDeutsch: parseInt(formData.get('noteDeutsch') as string), noteDeutsch: parseInt(formData.get('noteDeutsch') as string),
noteMathe: parseInt(formData.get('noteMathe') as string), noteMathe: parseInt(formData.get('noteMathe') as string),
sozialverhalten: formData.get('sozialverhalten') as string || null, sozialverhalten: (formData.get('sozialverhalten') as string) || null,
// Praktikum // Praktikum
praktikumId: parseInt(formData.get('zeitraum') as string), praktikumId: parseInt(formData.get('zeitraum') as string),
motivation: formData.get('motivation') as string || '', motivation: (formData.get('motivation') as string) || '',
// Wünsche // Wünsche
wunsch1Id: parseInt(formData.get('wunsch1Id') as string), wunsch1Id: parseInt(formData.get('wunsch1Id') as string),
wunsch2Id: parseInt(formData.get('wunsch2Id') as string), wunsch2Id: parseInt(formData.get('wunsch2Id') as string),
wunsch3Id: parseInt(formData.get('wunsch3Id') as string), wunsch3Id: parseInt(formData.get('wunsch3Id') as string),
// Alter (falls vom Frontend gesendet) // Alter (falls vom Frontend gesendet)
alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null, alter: formData.get('alter') ? parseInt(formData.get('alter') as string) : null,
// Notfallkontakt
notfallVorname: (formData.get('notfallVorname') as string) || null,
notfallNachname: (formData.get('notfallNachname') as string) || null,
notfallTelefon: (formData.get('notfallTelefon') as string) || null,
// System // System
zugewiesenId: null, zugewiesenId: null,
// timestamp wird automatisch durch @default(now()) gesetzt // timestamp wird automatisch durch @default(now()) gesetzt