excel export und Plätze vorhanden Prüfung
This commit is contained in:
949
package-lock.json
generated
949
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.19.0",
|
"@prisma/client": "^6.19.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.13",
|
"@sveltejs/adapter-node": "^5.2.13",
|
||||||
"bcryptjs": "^3.0.2"
|
"bcryptjs": "^3.0.2",
|
||||||
|
"exceljs": "^4.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -23,7 +23,8 @@
|
|||||||
|
|
||||||
let fehler = '';
|
let fehler = '';
|
||||||
let success = false;
|
let success = false;
|
||||||
let dienststellen: any[];
|
let dienststellen: any[] = [];
|
||||||
|
let isLoadingDienststellen = false;
|
||||||
|
|
||||||
let fileInputKey = 0;
|
let fileInputKey = 0;
|
||||||
let noteDeutsch = '';
|
let noteDeutsch = '';
|
||||||
@@ -122,6 +123,34 @@
|
|||||||
// Prüfen ob Formular gültig ist
|
// Prüfen ob Formular gültig ist
|
||||||
$: formHatFehler = alterFehler !== '' || notenFehler !== '' || sozialverhaltenFehler !== '';
|
$: formHatFehler = alterFehler !== '' || notenFehler !== '' || sozialverhaltenFehler !== '';
|
||||||
|
|
||||||
|
// Dienststellen laden wenn Zeitraum sich ändert
|
||||||
|
async function ladeDienststellen(zeitraumId: string) {
|
||||||
|
if (!zeitraumId) {
|
||||||
|
dienststellen = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingDienststellen = true;
|
||||||
|
const res = await fetch(`/api/dienststellen?zeitraumId=${zeitraumId}`);
|
||||||
|
dienststellen = await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden der Dienststellen:', err);
|
||||||
|
dienststellen = [];
|
||||||
|
} finally {
|
||||||
|
isLoadingDienststellen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reaktiv: Wenn Zeitraum sich ändert, Dienststellen neu laden und Wünsche zurücksetzen
|
||||||
|
$: if (zeitraum) {
|
||||||
|
ladeDienststellen(zeitraum);
|
||||||
|
wunsch1Id = '';
|
||||||
|
wunsch2Id = '';
|
||||||
|
wunsch3Id = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: Nur Dienststellen mit freien Plätzen und Alterscheck für PK Mitte
|
||||||
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
|
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
|
||||||
if (d.plaetze <= 0) return false;
|
if (d.plaetze <= 0) return false;
|
||||||
|
|
||||||
@@ -144,9 +173,6 @@
|
|||||||
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const resDienstelle = await fetch('/api/dienststellen');
|
|
||||||
dienststellen = await resDienstelle.json();
|
|
||||||
|
|
||||||
const resZeitraeume = await fetch('/api/zeitraeume');
|
const resZeitraeume = await fetch('/api/zeitraeume');
|
||||||
zeitraeume = await resZeitraeume.json();
|
zeitraeume = await resZeitraeume.json();
|
||||||
});
|
});
|
||||||
@@ -188,6 +214,7 @@
|
|||||||
alterFehler = '';
|
alterFehler = '';
|
||||||
notenFehler = '';
|
notenFehler = '';
|
||||||
sozialverhaltenFehler = '';
|
sozialverhaltenFehler = '';
|
||||||
|
dienststellen = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function anmelden() {
|
async function anmelden() {
|
||||||
@@ -352,6 +379,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Zeitraum-Auswahl -->
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<select bind:value={zeitraum} required class="input">
|
<select bind:value={zeitraum} required class="input">
|
||||||
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
<option value="" disabled selected hidden>Wunschzeitraum fürs Praktikum</option>
|
||||||
@@ -373,28 +401,54 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Wunschdienststellen -->
|
<!-- Wunschdienststellen - erst auswählbar wenn Zeitraum gewählt -->
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<select bind:value={wunsch1Id} required>
|
{#if isLoadingDienststellen}
|
||||||
<option value="" disabled selected>1. Wunschdienststelle</option>
|
<div class="flex items-center justify-center py-4 text-gray-500">
|
||||||
{#each filteredDienststellen as d}
|
<svg class="animate-spin h-5 w-5 mr-2" viewBox="0 0 24 24">
|
||||||
<option value={d.id}>{d.name}</option>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
|
||||||
{/each}
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</select>
|
</svg>
|
||||||
|
Lade verfügbare Dienststellen...
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<select bind:value={wunsch1Id} required disabled={!zeitraum} class="input" class:opacity-50={!zeitraum}>
|
||||||
|
<option value="" disabled selected>
|
||||||
|
{zeitraum ? '1. Wunschdienststelle' : 'Bitte zuerst Zeitraum wählen'}
|
||||||
|
</option>
|
||||||
|
{#each filteredDienststellen as d}
|
||||||
|
<option value={d.id}>{d.name} ({d.plaetze} {d.plaetze === 1 ? 'Platz' : 'Plätze'} frei)</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
<select bind:value={wunsch2Id} required>
|
<select bind:value={wunsch2Id} required disabled={!zeitraum || !wunsch1Id} class="input" class:opacity-50={!zeitraum || !wunsch1Id}>
|
||||||
<option value="" disabled selected>2. Wunschdienststelle</option>
|
<option value="" disabled selected>2. Wunschdienststelle</option>
|
||||||
{#each filteredDienststellen.filter(d => d.id !== wunsch1Id) as d}
|
{#each filteredDienststellen.filter(d => d.id != wunsch1Id) as d}
|
||||||
<option value={d.id}>{d.name}</option>
|
<option value={d.id}>{d.name} ({d.plaetze} {d.plaetze === 1 ? 'Platz' : 'Plätze'} frei)</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select bind:value={wunsch3Id} required>
|
<select bind:value={wunsch3Id} required disabled={!zeitraum || !wunsch2Id} class="input" class:opacity-50={!zeitraum || !wunsch2Id}>
|
||||||
<option value="" disabled selected>3. Wunschdienststelle</option>
|
<option value="" disabled selected>3. Wunschdienststelle</option>
|
||||||
{#each filteredDienststellen.filter(d => d.id !== wunsch1Id && d.id !== wunsch2Id) as d}
|
{#each filteredDienststellen.filter(d => d.id != wunsch1Id && d.id != wunsch2Id) as d}
|
||||||
<option value={d.id}>{d.name}</option>
|
<option value={d.id}>{d.name} ({d.plaetze} {d.plaetze === 1 ? 'Platz' : 'Plätze'} frei)</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Hinweis wenn keine Dienststellen verfügbar -->
|
||||||
|
{#if zeitraum && !isLoadingDienststellen && filteredDienststellen.length === 0}
|
||||||
|
<div class="p-3 bg-yellow-50 border border-yellow-200 rounded-xl">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-yellow-500 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm text-yellow-700">
|
||||||
|
Für den gewählten Zeitraum sind leider keine Praktikumsplätze mehr verfügbar. Bitte wähle einen anderen Zeitraum.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Motivation -->
|
<!-- Motivation -->
|
||||||
@@ -431,16 +485,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Button - deaktiviert bei Validierungsfehlern -->
|
<!-- Button - deaktiviert bei Validierungsfehlern oder fehlenden Pflichtfeldern -->
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={formHatFehler}
|
disabled={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||||
class="w-full py-3 rounded-xl transition-all"
|
class="w-full py-3 rounded-xl transition-all"
|
||||||
class:bg-blue-600={!formHatFehler}
|
class:bg-blue-600={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||||
class:hover:bg-blue-700={!formHatFehler}
|
class:hover:bg-blue-700={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||||
class:text-white={!formHatFehler}
|
class:text-white={!formHatFehler && zeitraum && wunsch1Id && wunsch2Id && wunsch3Id}
|
||||||
class:bg-gray-400={formHatFehler}
|
class:bg-gray-400={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||||
class:cursor-not-allowed={formHatFehler}
|
class:cursor-not-allowed={formHatFehler || !zeitraum || !wunsch1Id || !wunsch2Id || !wunsch3Id}
|
||||||
>
|
>
|
||||||
Jetzt anmelden
|
Jetzt anmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -483,6 +537,10 @@
|
|||||||
box-shadow: 0 0 0 2px #3b82f6;
|
box-shadow: 0 0 0 2px #3b82f6;
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
.input:disabled {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.input-error {
|
.input-error {
|
||||||
border-color: #dc2626;
|
border-color: #dc2626;
|
||||||
background-color: #fef2f2;
|
background-color: #fef2f2;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
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 };
|
||||||
|
zeitraum?: { id: number; bezeichnung: string };
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
id: number;
|
id: number;
|
||||||
status?: 'pending' | 'accepted' | 'rejected';
|
status?: 'pending' | 'accepted' | 'rejected';
|
||||||
@@ -26,12 +27,20 @@
|
|||||||
processedAt?: number;
|
processedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Zeitraum {
|
||||||
|
id: number;
|
||||||
|
bezeichnung: string;
|
||||||
|
startDatum: string;
|
||||||
|
endDatum: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface EmailConfig {
|
interface EmailConfig {
|
||||||
subject: string;
|
subject: string;
|
||||||
template: string;
|
template: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let anmeldungen: Anmeldung[] = [];
|
let anmeldungen: Anmeldung[] = [];
|
||||||
|
let zeitraeume: Zeitraum[] = [];
|
||||||
let isLoading = true;
|
let isLoading = true;
|
||||||
let error = '';
|
let error = '';
|
||||||
|
|
||||||
@@ -72,6 +81,11 @@ Ihr Praktikumsteam`;
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
let emailCopied = false;
|
let emailCopied = false;
|
||||||
|
|
||||||
|
// Export Modal State
|
||||||
|
let showExportModal = false;
|
||||||
|
let selectedExportZeitraum = '';
|
||||||
|
let isExporting = false;
|
||||||
|
|
||||||
// Status-Badge Funktionen
|
// Status-Badge Funktionen
|
||||||
function getStatusColor(status: string): string {
|
function getStatusColor(status: string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -104,6 +118,14 @@ Ihr Praktikumsteam`;
|
|||||||
anmeldungen, statusFilter, filterAnmeldungen();
|
anmeldungen, statusFilter, filterAnmeldungen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zähle angenommene Anmeldungen pro Zeitraum
|
||||||
|
function getAcceptedCountForZeitraum(zeitraumId: number): number {
|
||||||
|
return anmeldungen.filter(a =>
|
||||||
|
a.status === 'accepted' &&
|
||||||
|
a.zeitraum?.id === zeitraumId
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAnmeldungen() {
|
async function loadAnmeldungen() {
|
||||||
try {
|
try {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -136,6 +158,17 @@ Ihr Praktikumsteam`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadZeitraeume() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/zeitraeume');
|
||||||
|
if (res.ok) {
|
||||||
|
zeitraeume = await res.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden der Zeiträume:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadEmailConfig() {
|
async function loadEmailConfig() {
|
||||||
try {
|
try {
|
||||||
isLoadingEmailConfig = true;
|
isLoadingEmailConfig = true;
|
||||||
@@ -209,8 +242,8 @@ Ihr Praktikumsteam`;
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorText = await res.text();
|
const errorData = await res.json();
|
||||||
throw new Error(`Fehler beim Annehmen (${res.status}): ${errorText}`);
|
throw new Error(errorData.error || `Fehler beim Annehmen (${res.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDialog = false;
|
showDialog = false;
|
||||||
@@ -282,6 +315,64 @@ Ihr Praktikumsteam`;
|
|||||||
emailCopied = false;
|
emailCopied = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export-Funktionen
|
||||||
|
function openExportModal() {
|
||||||
|
selectedExportZeitraum = '';
|
||||||
|
showExportModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeExportModal() {
|
||||||
|
showExportModal = false;
|
||||||
|
selectedExportZeitraum = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportToExcel() {
|
||||||
|
if (!selectedExportZeitraum) {
|
||||||
|
error = 'Bitte wählen Sie einen Zeitraum aus.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isExporting = true;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
const res = await fetch(`/api/admin/export?zeitraumId=${selectedExportZeitraum}`);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
throw new Error(errorData.error || 'Fehler beim Exportieren');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download auslösen
|
||||||
|
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) {
|
||||||
|
filename = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
closeExportModal();
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Fehler beim Exportieren';
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
isExporting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleReject(event: CustomEvent<{id: number}>) {
|
async function handleReject(event: CustomEvent<{id: number}>) {
|
||||||
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
|
if (!confirm('Diese Anmeldung wirklich ablehnen?')) return;
|
||||||
|
|
||||||
@@ -329,6 +420,7 @@ Ihr Praktikumsteam`;
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadAnmeldungen();
|
loadAnmeldungen();
|
||||||
|
loadZeitraeume();
|
||||||
loadEmailConfig();
|
loadEmailConfig();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -344,8 +436,8 @@ Ihr Praktikumsteam`;
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||||
<!-- Filter und E-Mail Konfiguration -->
|
<!-- Filter, Export und E-Mail Konfiguration -->
|
||||||
<div class="mb-6 flex justify-between items-center">
|
<div class="mb-6 flex flex-wrap justify-between items-center gap-4">
|
||||||
<!-- Status Filter -->
|
<!-- Status Filter -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label>
|
<label for="status-filter" class="text-sm font-medium text-gray-700">Filter:</label>
|
||||||
@@ -361,21 +453,36 @@ Ihr Praktikumsteam`;
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- E-Mail Konfiguration Button -->
|
<!-- Buttons -->
|
||||||
<button
|
<div class="flex items-center space-x-3">
|
||||||
on:click={() => showEmailConfig = !showEmailConfig}
|
<!-- Excel Export Button -->
|
||||||
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center"
|
<button
|
||||||
disabled={isLoadingEmailConfig}
|
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"
|
||||||
{#if isLoadingEmailConfig}
|
disabled={anmeldungen.filter(a => a.status === 'accepted').length === 0}
|
||||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
>
|
||||||
{:else}
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
<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" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
Excel-Export
|
||||||
E-Mail-Vorlage konfigurieren
|
</button>
|
||||||
</button>
|
|
||||||
|
<!-- 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}
|
||||||
|
>
|
||||||
|
{#if isLoadingEmailConfig}
|
||||||
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
{:else}
|
||||||
|
<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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
E-Mail-Vorlage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Übersicht -->
|
<!-- Status Übersicht -->
|
||||||
@@ -507,6 +614,14 @@ Ihr Praktikumsteam`;
|
|||||||
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
|
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
|
||||||
<p class="mt-1 text-sm text-red-700">{error}</p>
|
<p class="mt-1 text-sm text-red-700">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -555,6 +670,97 @@ Ihr Praktikumsteam`;
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Export Modal -->
|
||||||
|
{#if showExportModal}
|
||||||
|
<div class="fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<div class="flex min-h-screen items-center justify-center p-4">
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 text-green-600" 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" />
|
||||||
|
</svg>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
<option value="" disabled>Zeitraum auswählen...</option>
|
||||||
|
{#each zeitraeume as z}
|
||||||
|
{@const count = getAcceptedCountForZeitraum(z.id)}
|
||||||
|
<option value={z.id} disabled={count === 0}>
|
||||||
|
{z.bezeichnung} ({count} angenommen)
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</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}
|
||||||
|
>
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{#if isExporting}
|
||||||
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Exportiere...
|
||||||
|
{:else}
|
||||||
|
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
|
Exportieren
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- E-Mail Vorschau Modal -->
|
<!-- E-Mail Vorschau Modal -->
|
||||||
{#if showEmailPreview && emailPreviewData}
|
{#if showEmailPreview && emailPreviewData}
|
||||||
<div class="fixed inset-0 z-50 overflow-y-auto">
|
<div class="fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export async function GET() {
|
|||||||
nachname: anmeldung.nachname,
|
nachname: anmeldung.nachname,
|
||||||
email: anmeldung.email,
|
email: anmeldung.email,
|
||||||
|
|
||||||
// === FEHLENDE FELDER HINZUGEFÜGT ===
|
|
||||||
geburtsdatum: anmeldung.geburtsdatum,
|
geburtsdatum: anmeldung.geburtsdatum,
|
||||||
strasse: anmeldung.strasse,
|
strasse: anmeldung.strasse,
|
||||||
hausnummer: anmeldung.hausnummer,
|
hausnummer: anmeldung.hausnummer,
|
||||||
@@ -41,18 +40,14 @@ export async function GET() {
|
|||||||
schulklasse: anmeldung.schulklasse,
|
schulklasse: anmeldung.schulklasse,
|
||||||
motivation: anmeldung.motivation,
|
motivation: anmeldung.motivation,
|
||||||
alter: anmeldung.alter,
|
alter: anmeldung.alter,
|
||||||
// === ENDE FEHLENDE FELDER ===
|
|
||||||
|
|
||||||
// Noten als String konvertieren (falls sie als Int gespeichert sind)
|
|
||||||
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
||||||
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
||||||
sozialverhalten: anmeldung.sozialverhalten,
|
sozialverhalten: anmeldung.sozialverhalten,
|
||||||
|
|
||||||
// Status-Mapping für Frontend
|
|
||||||
status: mapPrismaStatusToFrontend(anmeldung.status),
|
status: mapPrismaStatusToFrontend(anmeldung.status),
|
||||||
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
||||||
|
|
||||||
// Wünsche - sicherstellen dass sie existieren
|
|
||||||
wunsch1: anmeldung.wunsch1 ? {
|
wunsch1: anmeldung.wunsch1 ? {
|
||||||
id: anmeldung.wunsch1.id,
|
id: anmeldung.wunsch1.id,
|
||||||
name: anmeldung.wunsch1.name
|
name: anmeldung.wunsch1.name
|
||||||
@@ -66,13 +61,11 @@ export async function GET() {
|
|||||||
name: anmeldung.wunsch3.name
|
name: anmeldung.wunsch3.name
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|
||||||
// Zugewiesene Dienststelle
|
|
||||||
assignedDienststelle: anmeldung.zugewiesen ? {
|
assignedDienststelle: anmeldung.zugewiesen ? {
|
||||||
id: anmeldung.zugewiesen.id,
|
id: anmeldung.zugewiesen.id,
|
||||||
name: anmeldung.zugewiesen.name
|
name: anmeldung.zugewiesen.name
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|
||||||
// Praktikumszeitraum
|
|
||||||
zeitraum: anmeldung.praktikum ? {
|
zeitraum: anmeldung.praktikum ? {
|
||||||
id: anmeldung.praktikum.id,
|
id: anmeldung.praktikum.id,
|
||||||
bezeichnung: anmeldung.praktikum.bezeichnung,
|
bezeichnung: anmeldung.praktikum.bezeichnung,
|
||||||
@@ -80,17 +73,15 @@ export async function GET() {
|
|||||||
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|
||||||
// Timestamp
|
|
||||||
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
||||||
|
|
||||||
// PDFs
|
|
||||||
pdfs: anmeldung.pdfs || []
|
pdfs: anmeldung.pdfs || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
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.message }, { status: 500 });
|
return json({ error: 'Fehler beim Laden der Anmeldungen', details: (error as Error).message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +116,7 @@ export async function POST({ request, url }) {
|
|||||||
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
|
// Prüfen ob ZeitraumPlaetze Eintrag existiert und freie Plätze vorhanden sind
|
||||||
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
||||||
where: {
|
where: {
|
||||||
zeitraumId_dienststelleId: {
|
zeitraumId_dienststelleId: {
|
||||||
@@ -141,26 +132,42 @@ export async function POST({ request, url }) {
|
|||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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'
|
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
|
||||||
await prisma.$transaction([
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
// Anmeldung aktualisieren
|
// Anmeldung aktualisieren
|
||||||
prisma.anmeldung.update({
|
await tx.anmeldung.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
status: 'ANGENOMMEN',
|
status: 'ANGENOMMEN',
|
||||||
zugewiesenId: dienststelleId,
|
zugewiesenId: dienststelleId,
|
||||||
processedAt: new Date()
|
processedAt: new Date()
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
|
|
||||||
// Plätze in ZeitraumPlaetze reduzieren
|
// Plätze in ZeitraumPlaetze reduzieren
|
||||||
prisma.zeitraumPlaetze.update({
|
await tx.zeitraumPlaetze.update({
|
||||||
where: {
|
where: {
|
||||||
zeitraumId_dienststelleId: {
|
zeitraumId_dienststelleId: {
|
||||||
zeitraumId: zeitraumId,
|
zeitraumId: zeitraumId,
|
||||||
@@ -172,13 +179,21 @@ export async function POST({ request, url }) {
|
|||||||
decrement: 1
|
decrement: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
]);
|
});
|
||||||
|
|
||||||
return json({ success: true });
|
return json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Annehmen der Anmeldung:', error);
|
console.error('Fehler beim Annehmen der Anmeldung:', error);
|
||||||
return json({ error: 'Fehler beim Annehmen der Anmeldung', details: error.message }, { status: 500 });
|
|
||||||
|
// 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +250,7 @@ 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.message }, { status: 500 });
|
return json({ error: 'Fehler beim Aktualisieren der Anmeldung', details: (error as Error).message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +262,7 @@ export async function DELETE({ url }) {
|
|||||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Einfach löschen - Plätze werden NICHT zurückgegeben (gewolltes Verhalten)
|
||||||
await prisma.anmeldung.delete({
|
await prisma.anmeldung.delete({
|
||||||
where: { id }
|
where: { id }
|
||||||
});
|
});
|
||||||
@@ -254,13 +270,13 @@ 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.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) {
|
function mapPrismaStatusToFrontend(prismaStatus: string): string {
|
||||||
const statusMap = {
|
const statusMap: Record<string, string> = {
|
||||||
'OFFEN': 'pending',
|
'OFFEN': 'pending',
|
||||||
'BEARBEITUNG': 'processing',
|
'BEARBEITUNG': 'processing',
|
||||||
'ANGENOMMEN': 'accepted',
|
'ANGENOMMEN': 'accepted',
|
||||||
|
|||||||
183
src/routes/api/admin/export/+server.ts
Normal file
183
src/routes/api/admin/export/+server.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// src/routes/api/admin/export/+server.ts
|
||||||
|
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function GET({ url }) {
|
||||||
|
try {
|
||||||
|
const zeitraumId = url.searchParams.get('zeitraumId');
|
||||||
|
|
||||||
|
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) }
|
||||||
|
});
|
||||||
|
|
||||||
|
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' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Excel-Datei erstellen
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
workbook.creator = 'Praktikumsverwaltung';
|
||||||
|
workbook.created = new Date();
|
||||||
|
|
||||||
|
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 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
: '-'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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' } }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter aktivieren
|
||||||
|
worksheet.autoFilter = {
|
||||||
|
from: 'A1',
|
||||||
|
to: `R${anmeldungen.length + 1}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zusammenfassung am Ende
|
||||||
|
const summaryRow = worksheet.addRow([]);
|
||||||
|
const totalRow = worksheet.addRow([
|
||||||
|
`Gesamt: ${anmeldungen.length} angenommene Anmeldungen`,
|
||||||
|
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''
|
||||||
|
]);
|
||||||
|
totalRow.font = { bold: true };
|
||||||
|
|
||||||
|
// Excel-Datei als Buffer generieren
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// src/routes/api/dienststellen/+server.ts
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
@@ -10,8 +11,60 @@ async function getPrismaClient() {
|
|||||||
return prismaPromise;
|
return prismaPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET: RequestHandler = async () => {
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
const prisma = await getPrismaClient(); // Hier Prisma Client holen
|
const prisma = await getPrismaClient();
|
||||||
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
|
const zeitraumId = url.searchParams.get('zeitraumId');
|
||||||
|
|
||||||
|
if (zeitraumId) {
|
||||||
|
// Dienststellen mit freien Plätzen für einen bestimmten Zeitraum
|
||||||
|
const zeitraumIdInt = parseInt(zeitraumId);
|
||||||
|
|
||||||
|
const dienststellen = await prisma.dienststelle.findMany({
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
include: {
|
||||||
|
zeitraumPlaetze: {
|
||||||
|
where: { zeitraumId: zeitraumIdInt }
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
zugewiesene: {
|
||||||
|
where: {
|
||||||
|
praktikumId: zeitraumIdInt,
|
||||||
|
status: 'ANGENOMMEN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Berechne freie Plätze pro Dienststelle
|
||||||
|
const result = dienststellen.map(d => {
|
||||||
|
const gesamtPlaetze = d.zeitraumPlaetze[0]?.plaetze ?? 0;
|
||||||
|
const belegtePlaetze = d._count.zugewiesene;
|
||||||
|
const freiePlaetze = Math.max(0, gesamtPlaetze - belegtePlaetze);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
plaetze: freiePlaetze,
|
||||||
|
gesamtPlaetze: gesamtPlaetze,
|
||||||
|
belegtePlaetze: belegtePlaetze
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Alle Dienststellen ohne Zeitraum-Filter (für Admin-Bereich etc.)
|
||||||
|
const dienststellen = await prisma.dienststelle.findMany({
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
plaetze: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return json(dienststellen);
|
return json(dienststellen);
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user