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?
motivation String?
alter Int?
// Notfallkontakt
notfallVorname String?
notfallNachname String?
notfallTelefon String?
status Status @default(OFFEN)
processedAt DateTime?

View File

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

View File

@@ -16,6 +16,11 @@
let zeitraum = '';
let motivation = '';
// Notfallkontakt
let notfallVorname = '';
let notfallNachname = '';
let notfallTelefon = '';
let wunsch1Id = '';
let wunsch2Id = '';
let wunsch3Id = '';
@@ -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);
@@ -471,6 +482,16 @@
</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">

View File

@@ -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 };

View File

@@ -23,7 +23,7 @@ export async function GET() {
]
});
const formattedAnmeldungen = anmeldungen.map(anmeldung => ({
const formattedAnmeldungen = anmeldungen.map((anmeldung) => ({
id: anmeldung.id,
anrede: anmeldung.anrede,
vorname: anmeldung.vorname,
@@ -45,33 +45,48 @@ export async function GET() {
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 ? {
wunsch1: anmeldung.wunsch1
? {
id: anmeldung.wunsch1.id,
name: anmeldung.wunsch1.name
} : undefined,
wunsch2: anmeldung.wunsch2 ? {
}
: undefined,
wunsch2: anmeldung.wunsch2
? {
id: anmeldung.wunsch2.id,
name: anmeldung.wunsch2.name
} : undefined,
wunsch3: anmeldung.wunsch3 ? {
}
: undefined,
wunsch3: anmeldung.wunsch3
? {
id: anmeldung.wunsch3.id,
name: anmeldung.wunsch3.name
} : undefined,
}
: undefined,
assignedDienststelle: anmeldung.zugewiesen ? {
assignedDienststelle: anmeldung.zugewiesen
? {
id: anmeldung.zugewiesen.id,
name: anmeldung.zugewiesen.name
} : undefined,
}
: undefined,
zeitraum: anmeldung.praktikum ? {
zeitraum: anmeldung.praktikum
? {
id: anmeldung.praktikum.id,
bezeichnung: anmeldung.praktikum.bezeichnung,
startDatum: anmeldung.praktikum.startDatum.toISOString(),
endDatum: anmeldung.praktikum.endDatum.toISOString()
} : undefined,
}
: undefined,
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
@@ -81,7 +96,10 @@ export async function GET() {
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 });
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;
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
@@ -127,16 +148,22 @@ export async function POST({ request, url }) {
});
if (!zeitraumPlaetze) {
return json({
return json(
{
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)
if (zeitraumPlaetze.plaetze <= 0) {
return json({
return json(
{
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
}, { status: 400 });
},
{ status: 400 }
);
}
// 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"
if ((error as Error).message === 'Keine freien Plätze mehr verfügbar') {
return json({
return json(
{
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum verfügbar.'
}, { status: 400 });
},
{ 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 });
} 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(
{ 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 });
} 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(
{ 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'
OFFEN: 'pending',
BEARBEITUNG: 'processing',
ANGENOMMEN: 'accepted',
ABGELEHNT: 'rejected'
};
return statusMap[prismaStatus] || 'pending';

View File

@@ -33,10 +33,7 @@ export async function GET({ url }) {
zugewiesen: true,
praktikum: true
},
orderBy: [
{ zugewiesen: { name: 'asc' } },
{ nachname: 'asc' }
]
orderBy: [{ zugewiesen: { name: 'asc' } }, { nachname: 'asc' }]
});
// Excel-Datei erstellen
@@ -46,7 +43,7 @@ export async function GET({ url }) {
const worksheet = workbook.addWorksheet('Angenommene Anmeldungen');
// Spalten definieren
// Spalten definieren (inkl. Notfallkontakt)
worksheet.columns = [
{ header: 'Dienststelle', key: 'dienststelle', width: 30 },
{ header: 'Anrede', key: 'anrede', width: 10 },
@@ -65,6 +62,9 @@ export async function GET({ url }) {
{ 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 }
];
@@ -79,8 +79,17 @@ export async function GET({ url }) {
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
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
anmeldungen.forEach(anmeldung => {
anmeldungen.forEach((anmeldung) => {
worksheet.addRow({
dienststelle: anmeldung.zugewiesen?.name || 'Nicht zugewiesen',
anrede: anmeldung.anrede,
@@ -99,6 +108,9 @@ export async function GET({ url }) {
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')
: '-'
@@ -132,17 +144,36 @@ export async function GET({ url }) {
});
});
// Filter aktivieren
// Filter aktivieren (angepasst auf neue Spaltenanzahl: A bis U = 21 Spalten)
worksheet.autoFilter = {
from: 'A1',
to: `R${anmeldungen.length + 1}`
to: `U${anmeldungen.length + 1}`
};
// Zusammenfassung am Ende
const summaryRow = worksheet.addRow([]);
const totalRow = worksheet.addRow([
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
''
]);
totalRow.font = { bold: true };
@@ -161,22 +192,24 @@ export async function GET({ url }) {
'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 });
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'
Gymnasium: 'Gymnasium',
KGS_Gymnasialzweig: 'KGS Gymnasialzweig',
Fachoberschule: 'Fachoberschule',
Realschule: 'Realschule',
KGSR: 'KGS Realschulzweig',
IGSR: 'IGS Realschulzweig'
};
return schulartMap[schulart] || schulart;

View File

@@ -7,13 +7,13 @@ import { prisma } from '$lib/prisma';
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
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) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraumId,
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);
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: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
{ status: 400 }
);
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
@@ -110,7 +113,10 @@ export const PATCH: RequestHandler = async ({ request, cookies }) => {
} 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: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' },
{ status: 400 }
);
}
return json({ error: 'Serverfehler' }, { status: 500 });
}

View File

@@ -57,16 +57,20 @@ export async function POST({ request }: RequestEvent) {
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,
sozialverhalten: (formData.get('sozialverhalten') as string) || null,
// Praktikum
praktikumId: parseInt(formData.get('zeitraum') as string),
motivation: formData.get('motivation') 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