praktikum refinemend Plaetze pro Dienstelle und Pro Zeitraum

This commit is contained in:
titver968
2025-11-26 15:27:21 +01:00
parent be9228b71d
commit 89bf0298ce
40 changed files with 2932 additions and 1247 deletions

1363
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"private": true,
"version": "0.0.1",
"overrides": {
"cookie": "^0.7.0"
"cookie": "^0.7.0"
},
"type": "module",
"prisma": {
@@ -35,7 +35,7 @@
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prisma": "^6.12.0",
"prisma": "^6.19.0",
"svelte": "^5.36.17",
"svelte-check": "^4.3.0",
"tailwindcss": "^3.4.17",
@@ -46,7 +46,9 @@
"vite-plugin": "^0.0.0"
},
"dependencies": {
"@prisma/client": "^6.12.0",
"@prisma/adapter-better-sqlite3": "^7.0.0",
"@prisma/client": "^6.19.0",
"@prisma/migrate": "^7.0.0",
"@sveltejs/adapter-node": "^5.2.13",
"bcryptjs": "^3.0.2"
}

View File

@@ -1,21 +0,0 @@
-- CreateTable
CREATE TABLE "Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"zeitraum" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"wunsch1" TEXT NOT NULL,
"wunsch2" TEXT NOT NULL,
"wunsch3" TEXT NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[email]` on the table `Anmeldung` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");

View File

@@ -1,52 +0,0 @@
/*
Warnings:
- You are about to drop the column `wunsch1` on the `Anmeldung` table. All the data in the column will be lost.
- You are about to drop the column `wunsch2` on the `Anmeldung` table. All the data in the column will be lost.
- You are about to drop the column `wunsch3` on the `Anmeldung` table. All the data in the column will be lost.
- Added the required column `wunsch1Id` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
- Added the required column `wunsch2Id` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
- Added the required column `wunsch3Id` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
*/
-- CreateTable
CREATE TABLE "Dienststelle" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"zeitraum" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "zeitraum") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "zeitraum" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "Dienststelle_name_key" ON "Dienststelle"("name");

View File

@@ -1,5 +0,0 @@
-- CreateTable
CREATE TABLE "Admin" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
"password" TEXT NOT NULL
);

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Anmeldung" ADD COLUMN "pdfdatei" TEXT;

View File

@@ -1,39 +0,0 @@
/*
Warnings:
- You are about to drop the column `pdfdatei` on the `Anmeldung` table. All the data in the column will be lost.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"zeitraum" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"pdfDatei" TEXT,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zeitraum") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zeitraum" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,46 +0,0 @@
/*
Warnings:
- You are about to drop the column `pdfDatei` on the `Anmeldung` table. All the data in the column will be lost.
*/
-- CreateTable
CREATE TABLE "PdfDatei" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pfad" TEXT NOT NULL,
"anmeldungId" INTEGER NOT NULL,
CONSTRAINT "PdfDatei_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "Anmeldung" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"zeitraum" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zeitraum") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zeitraum" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,20 +0,0 @@
/*
Warnings:
- Added the required column `plaetze` to the `Dienststelle` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Dienststelle" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"plaetze" INTEGER NOT NULL
);
INSERT INTO "new_Dienststelle" ("id", "name") SELECT "id", "name" FROM "Dienststelle";
DROP TABLE "Dienststelle";
ALTER TABLE "new_Dienststelle" RENAME TO "Dienststelle";
CREATE UNIQUE INDEX "Dienststelle_name_key" ON "Dienststelle"("name");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,45 +0,0 @@
-- CreateTable
CREATE TABLE "Praktikumszeitraum" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"bezeichnung" TEXT NOT NULL,
"startDatum" DATETIME NOT NULL,
"endDatum" DATETIME NOT NULL
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"zeitraum" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"praktikumId" INTEGER,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_praktikumId_fkey" FOREIGN KEY ("praktikumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zeitraum") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zeitraum" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "Praktikumszeitraum_bezeichnung_key" ON "Praktikumszeitraum"("bezeichnung");

View File

@@ -1,46 +0,0 @@
/*
Warnings:
- You are about to drop the column `zeitraum` on the `Anmeldung` table. All the data in the column will be lost.
- Added the required column `noteDeutsch` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
- Added the required column `noteMathe` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
- Added the required column `sozialverhalten` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
- Made the column `praktikumId` on table `Anmeldung` required. This step will fail if there are existing NULL values in that column.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"praktikumId" INTEGER NOT NULL,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_praktikumId_fkey" FOREIGN KEY ("praktikumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "praktikumId", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "ort", "plz", "praktikumId", "schulart", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,35 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"praktikumId" INTEGER NOT NULL,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "schulart", "sozialverhalten", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "schulart", "sozialverhalten", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,44 +0,0 @@
/*
Warnings:
- Added the required column `zugewiesenId` to the `Anmeldung` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"praktikumId" INTEGER NOT NULL,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'OFFEN',
"zugewiesenId" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "schulart", "sozialverhalten", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "schulart", "sozialverhalten", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,38 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Anmeldung" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"motivation" TEXT NOT NULL,
"praktikumId" INTEGER NOT NULL,
"wunsch1Id" INTEGER NOT NULL,
"wunsch2Id" INTEGER NOT NULL,
"wunsch3Id" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'OFFEN',
"zugewiesenId" INTEGER,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Anmeldung_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Anmeldung_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Anmeldung" ("anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "schulart", "sozialverhalten", "status", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId") SELECT "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "schulart", "sozialverhalten", "status", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId" FROM "Anmeldung";
DROP TABLE "Anmeldung";
ALTER TABLE "new_Anmeldung" RENAME TO "Anmeldung";
CREATE UNIQUE INDEX "Anmeldung_email_key" ON "Anmeldung"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,50 +0,0 @@
/*
Warnings:
- You are about to drop the `Anmeldung` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropIndex
DROP INDEX "Anmeldung_email_key";
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "Anmeldung";
PRAGMA foreign_keys=on;
-- CreateTable
CREATE TABLE "anmeldungen" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"email" TEXT NOT NULL,
"noteDeutsch" TEXT,
"noteMathe" TEXT,
"sozialverhalten" TEXT,
"status" TEXT NOT NULL DEFAULT 'OFFEN',
"zugewiesenId" INTEGER,
"wunsch1Id" INTEGER,
"wunsch2Id" INTEGER,
"wunsch3Id" INTEGER,
"timestamp" BIGINT NOT NULL,
CONSTRAINT "anmeldungen_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_PdfDatei" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pfad" TEXT NOT NULL,
"anmeldungId" INTEGER NOT NULL,
CONSTRAINT "PdfDatei_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "anmeldungen" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_PdfDatei" ("anmeldungId", "id", "pfad") SELECT "anmeldungId", "id", "pfad" FROM "PdfDatei";
DROP TABLE "PdfDatei";
ALTER TABLE "new_PdfDatei" RENAME TO "PdfDatei";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "email_config" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
"subject" TEXT NOT NULL DEFAULT 'Praktikumsplatz-Zusage',
"template" TEXT NOT NULL DEFAULT 'Sehr geehrte/r {anrede} {nachname},
wir freuen uns, Ihnen mitteilen zu können, dass Ihre Bewerbung für ein Praktikum erfolgreich war.
Sie wurden für das Praktikum bei folgender Dienststelle angenommen:
{dienststelle}
Weitere Informationen erhalten Sie in den kommenden Tagen.
Mit freundlichen Grüßen
Ihr Praktikumsteam'
);

View File

@@ -1,62 +0,0 @@
/*
Warnings:
- You are about to alter the column `noteDeutsch` on the `anmeldungen` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`.
- You are about to alter the column `noteMathe` on the `anmeldungen` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`.
- You are about to alter the column `timestamp` on the `anmeldungen` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `DateTime`.
- Added the required column `geburtsdatum` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Added the required column `hausnummer` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Added the required column `ort` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Added the required column `plz` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Added the required column `schulart` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Added the required column `strasse` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Added the required column `telefon` to the `anmeldungen` table without a default value. This is not possible if the table is not empty.
- Made the column `noteDeutsch` on table `anmeldungen` required. This step will fail if there are existing NULL values in that column.
- Made the column `noteMathe` on table `anmeldungen` required. This step will fail if there are existing NULL values in that column.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_anmeldungen" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"schulklasse" TEXT,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT,
"motivation" TEXT,
"alter" INTEGER,
"status" TEXT NOT NULL DEFAULT 'OFFEN',
"processedBy" TEXT,
"processedAt" DATETIME,
"praktikumId" INTEGER,
"zugewiesenId" INTEGER,
"wunsch1Id" INTEGER,
"wunsch2Id" INTEGER,
"wunsch3Id" INTEGER,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "anmeldungen_praktikumId_fkey" FOREIGN KEY ("praktikumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_anmeldungen" ("anrede", "email", "id", "nachname", "noteDeutsch", "noteMathe", "sozialverhalten", "status", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId") SELECT "anrede", "email", "id", "nachname", "noteDeutsch", "noteMathe", "sozialverhalten", "status", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId" FROM "anmeldungen";
DROP TABLE "anmeldungen";
ALTER TABLE "new_anmeldungen" RENAME TO "anmeldungen";
CREATE INDEX "anmeldungen_status_idx" ON "anmeldungen"("status");
CREATE INDEX "anmeldungen_processedAt_idx" ON "anmeldungen"("processedAt");
CREATE INDEX "anmeldungen_zugewiesenId_idx" ON "anmeldungen"("zugewiesenId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,14 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_PdfDatei" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pfad" TEXT NOT NULL,
"anmeldungId" INTEGER NOT NULL,
CONSTRAINT "PdfDatei_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "anmeldungen" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_PdfDatei" ("anmeldungId", "id", "pfad") SELECT "anmeldungId", "id", "pfad" FROM "PdfDatei";
DROP TABLE "PdfDatei";
ALTER TABLE "new_PdfDatei" RENAME TO "PdfDatei";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,50 +0,0 @@
/*
Warnings:
- You are about to drop the column `processedBy` on the `anmeldungen` table. All the data in the column will be lost.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_anmeldungen" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"schulklasse" TEXT,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT,
"motivation" TEXT,
"alter" INTEGER,
"status" TEXT NOT NULL DEFAULT 'OFFEN',
"processedAt" DATETIME,
"praktikumId" INTEGER,
"zugewiesenId" INTEGER,
"wunsch1Id" INTEGER,
"wunsch2Id" INTEGER,
"wunsch3Id" INTEGER,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "anmeldungen_praktikumId_fkey" FOREIGN KEY ("praktikumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_anmeldungen" ("alter", "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "processedAt", "schulart", "schulklasse", "sozialverhalten", "status", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId") SELECT "alter", "anrede", "email", "geburtsdatum", "hausnummer", "id", "motivation", "nachname", "noteDeutsch", "noteMathe", "ort", "plz", "praktikumId", "processedAt", "schulart", "schulklasse", "sozialverhalten", "status", "strasse", "telefon", "timestamp", "vorname", "wunsch1Id", "wunsch2Id", "wunsch3Id", "zugewiesenId" FROM "anmeldungen";
DROP TABLE "anmeldungen";
ALTER TABLE "new_anmeldungen" RENAME TO "anmeldungen";
CREATE INDEX "anmeldungen_status_idx" ON "anmeldungen"("status");
CREATE INDEX "anmeldungen_processedAt_idx" ON "anmeldungen"("processedAt");
CREATE INDEX "anmeldungen_zugewiesenId_idx" ON "anmeldungen"("zugewiesenId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -1,14 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Dienststelle" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"plaetze" INTEGER NOT NULL DEFAULT 0
);
INSERT INTO "new_Dienststelle" ("id", "name", "plaetze") SELECT "id", "name", "plaetze" FROM "Dienststelle";
DROP TABLE "Dienststelle";
ALTER TABLE "new_Dienststelle" RENAME TO "Dienststelle";
CREATE UNIQUE INDEX "Dienststelle_name_key" ON "Dienststelle"("name");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,114 @@
-- CreateTable
CREATE TABLE "Admin" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
"password" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "email_config" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
"subject" TEXT NOT NULL DEFAULT 'Praktikumsplatz-Zusage',
"template" TEXT NOT NULL DEFAULT 'Sehr geehrte/r {anrede} {nachname},
wir freuen uns, Ihnen mitteilen zu können, dass Ihre Bewerbung für ein Praktikum erfolgreich war.
Sie wurden für das Praktikum bei folgender Dienststelle angenommen:
{dienststelle}
Weitere Informationen erhalten Sie in den kommenden Tagen.
Mit freundlichen Grüßen
Ihr Praktikumsteam'
);
-- CreateTable
CREATE TABLE "Dienststelle" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"plaetze" INTEGER NOT NULL DEFAULT 0
);
-- CreateTable
CREATE TABLE "Praktikumszeitraum" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"bezeichnung" TEXT NOT NULL,
"startDatum" DATETIME NOT NULL,
"endDatum" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "zeitraum_plaetze" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"zeitraumId" INTEGER NOT NULL,
"dienststelleId" INTEGER NOT NULL,
"plaetze" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "zeitraum_plaetze_zeitraumId_fkey" FOREIGN KEY ("zeitraumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "zeitraum_plaetze_dienststelleId_fkey" FOREIGN KEY ("dienststelleId") REFERENCES "Dienststelle" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "anmeldungen" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"anrede" TEXT NOT NULL,
"vorname" TEXT NOT NULL,
"nachname" TEXT NOT NULL,
"geburtsdatum" TEXT NOT NULL,
"strasse" TEXT NOT NULL,
"hausnummer" TEXT NOT NULL,
"ort" TEXT NOT NULL,
"plz" TEXT NOT NULL,
"telefon" TEXT NOT NULL,
"email" TEXT NOT NULL,
"schulart" TEXT NOT NULL,
"schulklasse" TEXT,
"noteDeutsch" INTEGER NOT NULL,
"noteMathe" INTEGER NOT NULL,
"sozialverhalten" TEXT,
"motivation" TEXT,
"alter" INTEGER,
"status" TEXT NOT NULL DEFAULT 'OFFEN',
"processedAt" DATETIME,
"praktikumId" INTEGER,
"zugewiesenId" INTEGER,
"wunsch1Id" INTEGER,
"wunsch2Id" INTEGER,
"wunsch3Id" INTEGER,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "anmeldungen_praktikumId_fkey" FOREIGN KEY ("praktikumId") REFERENCES "Praktikumszeitraum" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_zugewiesenId_fkey" FOREIGN KEY ("zugewiesenId") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch1Id_fkey" FOREIGN KEY ("wunsch1Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch2Id_fkey" FOREIGN KEY ("wunsch2Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "anmeldungen_wunsch3Id_fkey" FOREIGN KEY ("wunsch3Id") REFERENCES "Dienststelle" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "PdfDatei" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pfad" TEXT NOT NULL,
"anmeldungId" INTEGER NOT NULL,
CONSTRAINT "PdfDatei_anmeldungId_fkey" FOREIGN KEY ("anmeldungId") REFERENCES "anmeldungen" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Dienststelle_name_key" ON "Dienststelle"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Praktikumszeitraum_bezeichnung_key" ON "Praktikumszeitraum"("bezeichnung");
-- CreateIndex
CREATE INDEX "zeitraum_plaetze_zeitraumId_idx" ON "zeitraum_plaetze"("zeitraumId");
-- CreateIndex
CREATE INDEX "zeitraum_plaetze_dienststelleId_idx" ON "zeitraum_plaetze"("dienststelleId");
-- CreateIndex
CREATE UNIQUE INDEX "zeitraum_plaetze_zeitraumId_dienststelleId_key" ON "zeitraum_plaetze"("zeitraumId", "dienststelleId");
-- CreateIndex
CREATE INDEX "anmeldungen_status_idx" ON "anmeldungen"("status");
-- CreateIndex
CREATE INDEX "anmeldungen_processedAt_idx" ON "anmeldungen"("processedAt");
-- CreateIndex
CREATE INDEX "anmeldungen_zugewiesenId_idx" ON "anmeldungen"("zugewiesenId");

BIN
prisma/prisma/praktika.db Normal file

Binary file not shown.

View File

@@ -6,7 +6,7 @@ generator client {
datasource db {
provider = "sqlite"
url = "file:./praktika.db"
url = env("DATABASE_URL")
}
model Admin {
@@ -25,11 +25,14 @@ model EmailConfig {
model Dienststelle {
id Int @id @default(autoincrement())
name String @unique
plaetze Int @default(0)
plaetze Int @default(0) // Standard-Plätze, wird nicht mehr direkt verwendet
anmeldungenWunsch1 Anmeldung[] @relation("Wunsch1")
anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2")
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
zugewiesene Anmeldung[] @relation("Zugewiesen")
// Neue Relation zu ZeitraumPlaetze
zeitraumPlaetze ZeitraumPlaetze[]
}
model Praktikumszeitraum {
@@ -38,6 +41,26 @@ model Praktikumszeitraum {
startDatum DateTime
endDatum DateTime
anmeldungen Anmeldung[] @relation("PraktikumszeitraumAnmeldungen")
// Neue Relation zu ZeitraumPlaetze
zeitraumPlaetze ZeitraumPlaetze[]
}
// Neue Zwischentabelle für Plätze pro Zeitraum und Dienststelle
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)
// Unique constraint: Pro Zeitraum und Dienststelle nur ein Eintrag
@@unique([zeitraumId, dienststelleId])
@@index([zeitraumId])
@@index([dienststelleId])
@@map("zeitraum_plaetze")
}
// Erweiterte Status-Enum für bessere Nachverfolgung
@@ -70,7 +93,6 @@ model Anmeldung {
status Status @default(OFFEN)
// Neue Felder für Status-Tracking
// processedBy String? // Wer bearbeitet die Anmeldung
processedAt DateTime? // Wann wurde sie bearbeitet
// Praktikumszeitraum Relation

View File

@@ -30,6 +30,13 @@
description: 'Praktikumszeiträume verwalten',
color: 'bg-purple-600 hover:bg-purple-700'
},
{
href: '/admin/plaetze',
title: 'Plätze verwalten',
icon: '📊',
description: 'Praktikumsplätze pro Zeitraum und Dienststelle festlegen',
color: 'bg-indigo-600 hover:bg-indigo-700'
},
{
href: '/admin/change-password',
title: 'Passwort ändern',

View File

@@ -22,6 +22,15 @@
});
}
function formatGeburtsdatum(dateString: string | undefined): string {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
function formatProcessedDate(timestamp: number | undefined): string {
if (!timestamp) return '-';
return new Date(timestamp).toLocaleDateString('de-DE', {
@@ -33,7 +42,44 @@
});
}
// Vereinfachte Logik ohne processing Status
function formatZeitraum(zeitraum: any): string {
if (!zeitraum) return '-';
const start = new Date(zeitraum.startDatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
const end = new Date(zeitraum.endDatum).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
return `${zeitraum.bezeichnung} (${start} - ${end})`;
}
// Schulart formatieren
function formatSchulart(schulart: string | undefined): string {
if (!schulart) return '-';
const mapping: Record<string, string> = {
'Gymnasium': 'Gymnasium',
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig',
'Fachoberschule': 'Fachoberschule',
'Realschule': 'Realschule',
'KGSR': 'KGS Realschulzweig',
'IGSR': 'IGS Realschulzweig'
};
return mapping[schulart] || schulart;
}
// Sozialverhalten kürzen
function formatSozialverhalten(sv: string | undefined): string {
if (!sv) return '-';
if (sv === 'Entspricht den Erwartungen in vollem Umfang') return 'Voll entspr.';
if (sv === 'Entspricht den Erwartungen') return 'Entsprechend';
if (sv === 'Entspricht den Erwartungen mit Einschränkungen') return 'Mit Einschr.';
return sv;
}
function canBeAccepted(status: string): boolean {
return status === 'pending';
}
@@ -47,166 +93,234 @@
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="min-w-0 w-1/4 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Bewerber/in
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Persönliche Daten
</th>
<th class="w-16 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Noten
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kontakt & Adresse
</th>
<th class="min-w-0 w-1/3 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Wünsche / Zuweisung
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Schule & Noten
</th>
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Eingegangen
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Praktikum & Wünsche
</th>
<th class="w-40 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dokumente
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each anmeldungen as anmeldung (anmeldung.id)}
<tr class="hover:bg-gray-50">
<!-- Status (processing Styling entfernt) -->
<td class="px-4 py-4 whitespace-nowrap">
<tr class="hover:bg-gray-50 align-top">
<!-- Status -->
<td class="px-3 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {getStatusColor(anmeldung.status || 'pending')}">
{getStatusText(anmeldung.status || 'pending')}
</span>
<div class="text-xs text-gray-400 mt-2">
Eingang:<br>
{formatDate(anmeldung.timestamp)}
</div>
</td>
<!-- Bewerber/in -->
<td class="px-4 py-4">
<!-- Persönliche Daten -->
<td class="px-3 py-4">
<div class="text-sm font-medium text-gray-900">
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
</div>
<div class="text-sm text-gray-500 break-all">
{anmeldung.email}
<div class="text-xs text-gray-500 mt-1">
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
</div>
{#if anmeldung.alter}
<div class="text-xs text-gray-500">
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
</div>
{/if}
</td>
<!-- Kontakt & Adresse -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Adresse:</span><br>
<span class="text-gray-600">
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
{anmeldung.plz || ''} {anmeldung.ort || ''}
</span>
</div>
<div>
<span class="font-medium text-gray-700">Tel:</span>
{#if anmeldung.telefon}
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
{:else}
<span class="text-gray-400">-</span>
{/if}
</div>
<div>
<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>
</div>
</td>
<!-- Schule & Noten -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-1">
<div>
<span class="font-medium text-gray-700">Schulart:</span><br>
<span class="text-gray-900">{formatSchulart(anmeldung.schulart)}</span>
</div>
{#if anmeldung.schulklasse}
<div>
<span class="font-medium text-gray-700">Klasse:</span>
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
</div>
{/if}
<div class="pt-1 border-t border-gray-100">
<span class="font-medium text-gray-700">Noten:</span>
<div class="flex gap-2 mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
D: <span class="font-bold ml-1">{anmeldung.noteDeutsch || '-'}</span>
</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
M: <span class="font-bold ml-1">{anmeldung.noteMathe || '-'}</span>
</span>
</div>
</div>
{#if anmeldung.sozialverhalten}
<div>
<span class="font-medium text-gray-700">Sozialverh.:</span>
<span class="text-gray-900 text-xs">{formatSozialverhalten(anmeldung.sozialverhalten)}</span>
</div>
{/if}
</div>
</td>
<!-- Praktikum & Wünsche -->
<td class="px-3 py-4 text-sm">
<div class="text-xs space-y-2">
<!-- Zeitraum -->
{#if anmeldung.zeitraum}
<div>
<span class="font-medium text-gray-700">Zeitraum:</span><br>
<span class="text-gray-900">{formatZeitraum(anmeldung.zeitraum)}</span>
</div>
{/if}
<!-- Zugewiesene Dienststelle -->
{#if anmeldung.assignedDienststelle}
<div class="p-2 bg-green-50 border border-green-200 rounded">
<div class="flex items-center text-green-700">
<svg class="w-4 h-4 mr-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<span class="font-semibold">Zugewiesen:</span>
</div>
<div class="text-green-800 mt-1">{anmeldung.assignedDienststelle.name}</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1">
{formatProcessedDate(anmeldung.processedAt)}
</div>
{/if}
</div>
{/if}
<!-- Alle 3 Wünsche -->
<div>
<span class="font-medium text-gray-700">Wünsche:</span>
<div class="space-y-1 mt-1">
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-600 rounded-full mr-1 flex-shrink-0">1</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full mr-1 flex-shrink-0">2</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-400 rounded-full mr-1 flex-shrink-0">3</span>
<span class="text-gray-900 leading-tight">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400">Keine Wünsche</span>
{/if}
</div>
</div>
<!-- Motivation -->
{#if anmeldung.motivation}
<div>
<span class="font-medium text-gray-700">Motivation:</span>
<p class="text-gray-600 mt-1 text-xs whitespace-pre-wrap line-clamp-3">{anmeldung.motivation}</p>
</div>
{/if}
</div>
</td>
<!-- Dokumente / PDFs -->
<td class="px-3 py-4">
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
<div class="mt-2">
{#each anmeldung.pdfs as pdf}
<div class="space-y-1">
{#each anmeldung.pdfs as pdf, index}
<a
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
target="_blank"
class="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mr-2 mb-1"
class="flex items-center text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-1 rounded"
>
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-4 h-4 mr-1 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
</svg>
PDF ansehen
PDF {index + 1}
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
{/each}
</div>
{/if}
</td>
<!-- Noten -->
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
{#if anmeldung.noteDeutsch || anmeldung.noteMathe}
<div class="space-y-1">
{#if anmeldung.noteDeutsch}
<div class="text-xs">D: {anmeldung.noteDeutsch}</div>
{/if}
{#if anmeldung.noteMathe}
<div class="text-xs">M: {anmeldung.noteMathe}</div>
{/if}
</div>
{:else}
<span class="text-gray-400">-</span>
<span class="text-xs text-gray-400">Keine Dokumente</span>
{/if}
{#if anmeldung.sozialverhalten}
<div class="text-xs text-gray-500 mt-1">
SV: {anmeldung.sozialverhalten}
</div>
{/if}
</td>
<!-- Wünsche / Zuweisung -->
<td class="px-4 py-4 text-sm text-gray-900">
<!-- Zugewiesene Dienststelle (falls vorhanden) -->
{#if anmeldung.assignedDienststelle}
<div class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
<div class="flex items-center">
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<span class="font-medium text-green-800 text-sm">Zugewiesen:</span>
</div>
<div class="text-sm text-green-700 mt-1 ml-6">
{anmeldung.assignedDienststelle.name}
</div>
{#if anmeldung.processedAt}
<div class="text-xs text-green-600 mt-1 ml-6">
{formatProcessedDate(anmeldung.processedAt)}
{#if anmeldung.processedBy}
von {anmeldung.processedBy}
{/if}
</div>
{/if}
</div>
{/if}
<!-- Wünsche -->
<div class="space-y-2">
<div class="text-xs font-medium text-gray-500 uppercase tracking-wider">Wünsche:</div>
{#if anmeldung.wunsch1}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-600 rounded-full mr-2 mt-0.5 flex-shrink-0">1</span>
<span class="text-sm leading-5">{anmeldung.wunsch1.name}</span>
</div>
{/if}
{#if anmeldung.wunsch2}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-500 rounded-full mr-2 mt-0.5 flex-shrink-0">2</span>
<span class="text-sm leading-5">{anmeldung.wunsch2.name}</span>
</div>
{/if}
{#if anmeldung.wunsch3}
<div class="flex items-start">
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-400 rounded-full mr-2 mt-0.5 flex-shrink-0">3</span>
<span class="text-sm leading-5">{anmeldung.wunsch3.name}</span>
</div>
{/if}
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
<span class="text-gray-400 text-sm">Keine Wünsche angegeben</span>
{/if}
</div>
</td>
<!-- Eingegangen -->
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="text-sm">{formatDate(anmeldung.timestamp)}</div>
</td>
<!-- Aktionen -->
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium">
<td class="px-3 py-4">
<div class="flex flex-col space-y-2">
{#if canBeAccepted(anmeldung.status || 'pending')}
<button
on:click={() => dispatch('accept', { id: anmeldung.id })}
class="text-green-600 hover:text-green-900 px-3 py-1 rounded text-xs font-medium border border-green-200 hover:bg-green-50 w-full text-center"
class="text-green-600 hover:text-green-900 px-3 py-1.5 rounded text-xs font-medium border border-green-300 hover:bg-green-50 text-center whitespace-nowrap"
>
Annehmen
Annehmen
</button>
{/if}
{#if canBeRejected(anmeldung.status || 'pending')}
<button
on:click={() => dispatch('reject', { id: anmeldung.id })}
class="text-red-600 hover:text-red-900 px-3 py-1 rounded text-xs font-medium border border-red-200 hover:bg-red-50 w-full text-center"
class="text-red-600 hover:text-red-900 px-3 py-1.5 rounded text-xs font-medium border border-red-300 hover:bg-red-50 text-center whitespace-nowrap"
>
Ablehnen
Ablehnen
</button>
{/if}
<button
on:click={() => dispatch('delete', { id: anmeldung.id })}
class="text-gray-600 hover:text-gray-900 px-3 py-1 rounded text-xs font-medium border border-gray-200 hover:bg-gray-50 w-full text-center"
class="text-gray-600 hover:text-gray-900 px-3 py-1.5 rounded text-xs font-medium border border-gray-300 hover:bg-gray-50 text-center whitespace-nowrap"
>
Löschen
🗑 Löschen
</button>
</div>
</td>
@@ -215,3 +329,12 @@
</tbody>
</table>
</div>
<style>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

15
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV !== 'production' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

View File

@@ -32,8 +32,14 @@
let schulklasse = '';
let ablehnungHinweis = '';
let showAblehnungModal = false;
let showIgsHinweis = false;
let alter = '';
// Validierungsfehler für Echtzeit-Anzeige
let alterFehler = '';
let notenFehler = '';
let sozialverhaltenFehler = '';
// Berechnung des Alters
$: {
if (geburtsdatum && zeitraum && zeitraeume.length > 0) {
const gewaehlterZeitraum = zeitraeume.find(z => z.id == zeitraum);
@@ -47,34 +53,81 @@
if (monthDiff < 0 || (monthDiff === 0 && praktikumStart.getDate() < geburt.getDate())) {
altersberechnung--;
}
alter = altersberechnung.toString();
alter = altersberechnung.toString();
}
}
}
// Überwachung für IGS + Klasse 7 Kombination
// Echtzeit-Validierung: Alter
$: {
if (["KGSR", "IGSR"].includes(schulart) && schulklasse === '7') {
showIgsHinweis = true;
const altersWert = parseInt(alter);
if (alter && !isNaN(altersWert) && altersWert < 14) {
alterFehler = 'Du musst mindestens 14 Jahre alt sein, um ein Praktikum beginnen zu können.';
} else {
alterFehler = '';
}
}
// Echtzeit-Validierung: Noten
$: {
const deutsch = parseInt(noteDeutsch);
const mathe = parseInt(noteMathe);
const klasse = parseInt(schulklasse);
if (noteDeutsch && noteMathe && schulart) {
// Gymnasium oder KGS_Gymnasialzweig: mindestens 4 in Deutsch UND Mathe
if (['Gymnasium', 'KGS_Gymnasialzweig'].includes(schulart)) {
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 4 || mathe > 4)) {
notenFehler = 'Du brauchst mindestens eine 4 in Deutsch und Mathematik.';
} else {
notenFehler = '';
}
}
// Fachoberschule Klasse 11 oder 12: mindestens 4 in Deutsch UND Mathe
else if (schulart === 'Fachoberschule' && (klasse === 11 || klasse === 12)) {
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 4 || mathe > 4)) {
notenFehler = 'Du brauchst mindestens eine 4 in Deutsch und Mathematik.';
} else {
notenFehler = '';
}
}
// Alle anderen: mindestens 3 in Deutsch UND Mathe
else {
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 3 || mathe > 3)) {
notenFehler = 'Du brauchst mindestens eine 3 in Deutsch und Mathematik.';
} else {
notenFehler = '';
}
}
} else {
notenFehler = '';
}
}
// Echtzeit-Validierung: Sozialverhalten
$: {
if (sozialverhalten === 'Entspricht den Erwartungen mit Einschränkungen') {
sozialverhaltenFehler = 'Dein Sozialverhalten muss mindestens den Erwartungen entsprechen.';
} else {
sozialverhaltenFehler = '';
}
}
// Prüfen ob Formular gültig ist
$: formHatFehler = alterFehler !== '' || notenFehler !== '' || sozialverhaltenFehler !== '';
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
if (d.plaetze <= 0) return false;
// PK Mitte nur anzeigen wenn mindestens 18 Jahre alt
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
return parseInt(alter) >= 18;
}
return true;
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
return parseInt(alter) >= 18;
}
);
return true;
});
$: filteredZeitraeume = (zeitraeume ?? []).filter(zeitraum => {
const heute = new Date();
const startDatum = new Date(zeitraum.startDatum);
// Nur Zeiträume anzeigen, die noch nicht gestartet haben
return startDatum > heute;
});
@@ -82,7 +135,7 @@
$: hideSozialVerhalten =
Number(schulklasse) >= 11 &&
["Gymnasium", "KGS_Gymnasialzweig", "IGS_Gymnasialzweig"].includes(schulart);
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
onMount(async () => {
const resDienstelle = await fetch('/api/dienststellen');
@@ -91,6 +144,7 @@
const resZeitraeume = await fetch('/api/zeitraeume');
zeitraeume = await resZeitraeume.json();
});
interface Zeitraum {
id: number;
bezeichnung: string;
@@ -98,9 +152,6 @@
endDatum: string;
}
let zeitraeume: Zeitraum[] = [];
//let neuerBezeichnung = '';
//let neuerstartDatum = '';
//let neuerendDatum = '';
let fehlermeldung = '';
let bearbeiteId: number | null = null;
@@ -124,12 +175,21 @@
noteDeutsch = '';
noteMathe = '';
sozialverhalten = '';
schulklasse = '';
pdfDateien = [];
fileInputKey += 1;
success = false;
alterFehler = '';
notenFehler = '';
sozialverhaltenFehler = '';
}
async function anmelden() {
// Abbrechen wenn Validierungsfehler vorhanden
if (formHatFehler) {
return;
}
const data = new FormData();
data.append('anrede', anrede);
@@ -158,36 +218,6 @@
data.append('pdfs', pdf);
}
const altersWert = parseInt(alter);
if (isNaN(altersWert) || altersWert < 14) {
ablehnungHinweis = 'Du musst mindestens 14 Jahre alt sein, um ein Praktikum beginnen zu können. Bewirb dich gern erneut, wenn du das Mindestalter erreicht hast.';
showAblehnungModal = true;
return;
}
const deutsch = parseInt(noteDeutsch);
const mathe = parseInt(noteMathe);
if (['Gymnasium', 'KGS_Gymnasialzweig', 'IGS_Gymnasialzweig'].includes(schulart) ) {
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 4 && mathe > 4) {
ablehnungHinweis = 'Du brauchst mindestens eine 4 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
return;
}
} else {
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 3 && mathe > 3) {
ablehnungHinweis = 'Du brauchst mindestens eine 3 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
return;
}
}
if (sozialverhalten === 'Entspricht den Erwartungen mit Einschränkungen') {
ablehnungHinweis = 'Dein Sozialverhalten muss mindestens den Erwartungen entsprechen. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
showAblehnungModal = true;
return;
}
const res = await fetch('/api/anmelden', {
method: 'POST',
body: data
@@ -234,17 +264,13 @@
<option value="" disabled selected hidden>Schulart wählen</option>
<option value="Gymnasium">Gymnasium</option>
<option value="KGS_Gymnasialzweig">KGS Gymnasialzweig</option>
<option value="IGS_Gymnasialzweig">IGS Gymniasalzweig Fachoberschule</option>
<option value="Fachoberschule">Fachoberschule</option>
<option value="Realschule">Realschule</option>
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
</select>
<!-- Noten -->
<div class="grid grid-cols-2 gap-4">
<input bind:value={noteDeutsch} type="number" min="1" max="6" placeholder="Note in Deutsch" required class="input" />
<input bind:value={noteMathe} type="number" min="1" max="6" placeholder="Note in Mathe" required class="input" />
</div>
<!-- Schulklasse -->
<select bind:value={schulklasse} required class="input">
<option value="" disabled selected hidden>Schulklasse</option>
<option value="7">7. Klasse</option>
@@ -255,15 +281,55 @@
<option value="12">12. Klasse</option>
<option value="13">13. Klasse</option>
</select>
<!-- Sozialverhalten -->
<!-- Sozialverhalten mit Echtzeit-Validierung -->
{#if !hideSozialVerhalten}
<select bind:value={sozialverhalten} required class="input">
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</option>
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
</select>
<div class="col-span-2">
<select
bind:value={sozialverhalten}
required
class="input"
class:input-error={sozialverhaltenFehler}
>
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</option>
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
</select>
{#if sozialverhaltenFehler}
<p class="text-red-600 text-sm mt-1">{sozialverhaltenFehler}</p>
{/if}
</div>
{/if}
<!-- Noten mit Echtzeit-Validierung -->
<div class="col-span-2">
<div class="grid grid-cols-2 gap-4">
<input
bind:value={noteDeutsch}
type="number"
min="1"
max="6"
placeholder="Note in Deutsch"
required
class="input"
class:input-error={notenFehler}
/>
<input
bind:value={noteMathe}
type="number"
min="1"
max="6"
placeholder="Note in Mathe"
required
class="input"
class:input-error={notenFehler}
/>
</div>
{#if notenFehler}
<p class="text-red-600 text-sm mt-1">{notenFehler}</p>
{/if}
</div>
</div>
<div class="grid grid-cols-1 gap-4">
@@ -273,13 +339,20 @@
<option
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
</option>
startDatum = {new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
{/each}
</select>
<p>Startdatum: {startDatum}</p>
</div>
<!-- Alter-Anzeige mit Echtzeit-Validierung -->
{#if alter}
<div class="text-sm" class:text-gray-600={!alterFehler} class:text-red-600={alterFehler}>
Alter zu Praktikumsbeginn: {alter} Jahre
{#if alterFehler}
<p class="font-semibold mt-1">{alterFehler}</p>
{/if}
</div>
{/if}
<!-- Wunschdienststellen -->
<div class="grid grid-cols-1 gap-4">
<select bind:value={wunsch1Id} required>
@@ -324,24 +397,6 @@
</div>
{/key}
<!-- IGS Klasse 7 Hinweis Modal -->
{#if showIgsHinweis}
<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-md w-full">
<h3 class="text-lg font-semibold text-blue-600">Wichtiger Hinweis</h3>
<p class="text-gray-700">
Bitte keinen Entwicklungsbericht beifügen, sondern Noten von der Schule bescheinigen lassen (zu Bewerbungszwecken).
</p>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
on:click={() => showIgsHinweis = false}
>
Verstanden
</button>
</div>
</div>
{/if}
{#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">
@@ -356,12 +411,24 @@
</div>
{/if}
<!-- Button -->
<button type="submit"
class="w-full bg-blue-600 text-white py-3 rounded-xl hover:bg-blue-700 transition-all">
<!-- Button - deaktiviert bei Validierungsfehlern -->
<button
type="submit"
disabled={formHatFehler}
class="w-full py-3 rounded-xl transition-all"
class:bg-blue-600={!formHatFehler}
class:hover:bg-blue-700={!formHatFehler}
class:text-white={!formHatFehler}
class:bg-gray-400={formHatFehler}
class:cursor-not-allowed={formHatFehler}
>
Jetzt anmelden
</button>
{#if formHatFehler}
<p class="text-red-600 text-sm text-center">Bitte korrigiere die markierten Fehler, um fortzufahren.</p>
{/if}
{#if success}
<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">
@@ -376,17 +443,6 @@
</div>
{/if}
{#if alter}
<div class="text-sm text-gray-600">
Alter zu Praktikumsbeginn: {alter} Jahre
{#if parseInt(alter) < 14}
<span class="text-red-600 font-semibold">
(Mindestalter 14 Jahre erforderlich)
</span>
{/if}
</div>
{/if}
{#if fehler}
<p class="text-red-600">{fehler}</p>
{/if}
@@ -396,15 +452,23 @@
<style>
.input {
width: 100%;
border: 1px solid #d1d5db; /* border-gray-300 */
border-radius: 0.75rem; /* rounded-xl */
padding: 0.75rem; /* p-3 */
border: 1px solid #d1d5db;
border-radius: 0.75rem;
padding: 0.75rem;
outline: none;
transition: box-shadow 0.2s;
transition: box-shadow 0.2s, border-color 0.2s;
}
.input:focus {
outline: none;
box-shadow: 0 0 0 2px #3b82f6; /* focus:ring-2 focus:ring-blue-500 */
box-shadow: 0 0 0 2px #3b82f6;
border-color: #3b82f6;
}
.input-error {
border-color: #dc2626;
background-color: #fef2f2;
}
.input-error:focus {
box-shadow: 0 0 0 2px #dc2626;
border-color: #dc2626;
}
</style>

View File

@@ -3,9 +3,8 @@
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
let dienststellen: { id: number; name: string }[] = [];
let neuerName = '';
let neuePlaetze = 0;
let fehlermeldung = '';
let bearbeiteId: number | null = null;
let isLoading = true;
@@ -30,9 +29,8 @@
}
}
function bearbeiten(d: { id: number; name: string; plaetze: number }) {
function bearbeiten(d: { id: number; name: string }) {
neuerName = d.name;
neuePlaetze = d.plaetze;
bearbeiteId = d.id;
}
@@ -46,8 +44,8 @@
try {
const method = bearbeiteId ? 'PATCH' : 'POST';
const body = bearbeiteId
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
: { name: neuerName, plaetze: neuePlaetze };
? { id: bearbeiteId, name: neuerName }
: { name: neuerName };
const res = await fetch('/api/admin/dienststellen', {
method,
@@ -57,7 +55,6 @@
if (res.ok) {
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
await ladeDienststellen();
} else {
@@ -71,7 +68,7 @@
}
async function loeschen(id: number) {
if (!confirm('Diese Dienststelle wirklich löschen?')) return;
if (!confirm('Diese Dienststelle wirklich löschen? Alle zugehörigen Platzangaben werden ebenfalls gelöscht.')) return;
try {
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
@@ -91,7 +88,6 @@
function resetForm() {
neuerName = '';
neuePlaetze = 0;
bearbeiteId = null;
}
@@ -108,7 +104,7 @@
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
<main class="max-w-5xl mx-auto px-4 py-6">
{#if fehlermeldung}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
@@ -125,14 +121,30 @@
</div>
{/if}
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
Hier werden nur die Dienststellen verwaltet. Die Anzahl der Praktikumsplätze pro Zeitraum können Sie unter
<a href="/admin/plaetze" class="font-medium underline hover:text-blue-800">Plätze verwalten</a> festlegen.
</p>
</div>
</div>
</div>
<!-- Eingabeformular -->
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">
{bearbeiteId !== null ? 'Dienststelle bearbeiten' : 'Neue Dienststelle hinzufügen'}
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<div class="flex gap-4">
<div class="flex-1">
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
Dienststelle
</label>
@@ -145,20 +157,6 @@
/>
</div>
<div>
<label for="plaetze" class="block text-sm font-medium text-gray-700 mb-2">
Anzahl Plätze
</label>
<input
id="plaetze"
type="number"
bind:value={neuePlaetze}
placeholder="0"
min="0"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div class="flex items-end gap-2">
<button
on:click={resetForm}
@@ -192,7 +190,7 @@
{:else}
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen</h3>
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen ({dienststellen.length})</h3>
</div>
<div class="overflow-x-auto">
@@ -202,9 +200,6 @@
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dienststelle
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Plätze
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
@@ -216,9 +211,6 @@
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{d.name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-right">
{d.plaetze}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => bearbeiten(d)}

View File

@@ -0,0 +1,15 @@
// src/routes/admin/plaetze/+page.server.ts
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
throw redirect(303, '/admin');
}
return {
title: 'Plätze verwalten'
};
};

View File

@@ -0,0 +1,259 @@
<!-- src/routes/admin/plaetze/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
type ZeitraumPlaetzeData = {
zeitraumId: number;
zeitraumBezeichnung: string;
dienststellen: {
dienststelleId: number;
dienststelleName: string;
plaetze: number;
id: number; // ZeitraumPlaetze-ID
}[];
};
let data: ZeitraumPlaetzeData[] = [];
let isLoading = true;
let fehlermeldung = '';
let erfolgsmeldung = '';
let editingCell: { zeitraumId: number; dienststelleId: number } | null = null;
let editValue = '';
async function ladeDaten() {
try {
isLoading = true;
fehlermeldung = '';
const res = await fetch('/api/admin/plaetze');
if (!res.ok) {
throw new Error(`Fehler beim Laden: ${res.status}`);
}
data = await res.json();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Unbekannter Fehler';
console.error('Fehler beim Laden der Plätze:', err);
} finally {
isLoading = false;
}
}
function startEdit(zeitraumId: number, dienststelleId: number, currentValue: number) {
editingCell = { zeitraumId, dienststelleId };
editValue = currentValue.toString();
}
async function speicherePlaetze(zeitraumId: number, dienststelleId: number) {
const plaetze = parseInt(editValue);
if (isNaN(plaetze) || plaetze < 0) {
fehlermeldung = 'Bitte geben Sie eine gültige Zahl ein (≥ 0)';
return;
}
try {
fehlermeldung = '';
erfolgsmeldung = '';
const res = await fetch('/api/admin/plaetze', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
zeitraumId,
dienststelleId,
plaetze
})
});
if (!res.ok) {
const err = await res.json();
fehlermeldung = err.error || 'Fehler beim Speichern';
return;
}
editingCell = null;
await ladeDaten();
erfolgsmeldung = 'Plätze erfolgreich aktualisiert';
setTimeout(() => erfolgsmeldung = '', 3000);
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
}
}
function abbrechen() {
editingCell = null;
editValue = '';
}
function handleKeydown(e: KeyboardEvent, zeitraumId: number, dienststelleId: number) {
if (e.key === 'Enter') {
speicherePlaetze(zeitraumId, dienststelleId);
} else if (e.key === 'Escape') {
abbrechen();
}
}
onMount(ladeDaten);
</script>
<svelte:head>
<title>Plätze verwalten - Admin</title>
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Praktikumsplätze verwalten"
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
{#if fehlermeldung}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
<p class="mt-1 text-sm text-red-700">{fehlermeldung}</p>
</div>
</div>
</div>
{/if}
{#if erfolgsmeldung}
<div class="bg-green-50 border border-green-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-green-700">{erfolgsmeldung}</p>
</div>
</div>
</div>
{/if}
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
Klicken Sie auf eine Zahl, um die Anzahl der Praktikumsplätze zu bearbeiten.
Drücken Sie Enter zum Speichern oder Esc zum Abbrechen.
</p>
</div>
</div>
</div>
{#if isLoading}
<div class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-3 text-gray-600">Lade Daten...</span>
</div>
{:else if data.length === 0}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Zeiträume vorhanden</h3>
<p class="mt-1 text-sm text-gray-500">
Erstellen Sie zunächst
<a href="/admin/zeitraeume" class="text-blue-600 hover:underline">Praktikumszeiträume</a>
und
<a href="/admin/dienststellen" class="text-blue-600 hover:underline">Dienststellen</a>.
</p>
</div>
{:else}
{#each data as zeitraum}
<div class="bg-white shadow-sm rounded-lg overflow-hidden mb-6">
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">{zeitraum.zeitraumBezeichnung}</h3>
</div>
{#if zeitraum.dienststellen.length === 0}
<div class="px-6 py-8 text-center text-gray-500 text-sm">
Keine Dienststellen vorhanden.
<a href="/admin/dienststellen" class="text-blue-600 hover:underline">Dienststellen erstellen</a>
</div>
{:else}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dienststelle
</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Anzahl Plätze
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each zeitraum.dienststellen as dienststelle}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{dienststelle.dienststelleName}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
{#if editingCell?.zeitraumId === zeitraum.zeitraumId && editingCell?.dienststelleId === dienststelle.dienststelleId}
<div class="flex items-center justify-center gap-2">
<input
type="number"
bind:value={editValue}
min="0"
on:keydown={(e) => handleKeydown(e, zeitraum.zeitraumId, dienststelle.dienststelleId)}
class="w-20 border border-blue-500 rounded px-2 py-1 text-sm text-center focus:ring-2 focus:ring-blue-500 focus:border-transparent"
autofocus
/>
<button
on:click={() => speicherePlaetze(zeitraum.zeitraumId, dienststelle.dienststelleId)}
class="text-green-600 hover:text-green-800"
title="Speichern"
>
<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="M5 13l4 4L19 7" />
</svg>
</button>
<button
on:click={abbrechen}
class="text-red-600 hover:text-red-800"
title="Abbrechen"
>
<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>
{:else}
<button
on:click={() => startEdit(zeitraum.zeitraumId, dienststelle.dienststelleId, dienststelle.plaetze)}
class="text-blue-600 hover:text-blue-800 hover:underline font-medium text-sm"
>
{dienststelle.plaetze}
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{/each}
{/if}
</main>
</div>

View File

@@ -30,6 +30,19 @@ export async function GET() {
nachname: anmeldung.nachname,
email: anmeldung.email,
// === FEHLENDE FELDER HINZUGEFÜGT ===
geburtsdatum: anmeldung.geburtsdatum,
strasse: anmeldung.strasse,
hausnummer: anmeldung.hausnummer,
ort: anmeldung.ort,
plz: anmeldung.plz,
telefon: anmeldung.telefon,
schulart: anmeldung.schulart,
schulklasse: anmeldung.schulklasse,
motivation: anmeldung.motivation,
alter: anmeldung.alter,
// === ENDE FEHLENDE FELDER ===
// Noten als String konvertieren (falls sie als Int gespeichert sind)
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
@@ -37,7 +50,6 @@ export async function GET() {
// Status-Mapping für Frontend
status: mapPrismaStatusToFrontend(anmeldung.status),
// processedBy: anmeldung.processedBy,
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
// Wünsche - sicherstellen dass sie existieren
@@ -60,6 +72,14 @@ export async function GET() {
name: anmeldung.zugewiesen.name
} : undefined,
// Praktikumszeitraum
zeitraum: anmeldung.praktikum ? {
id: anmeldung.praktikum.id,
bezeichnung: anmeldung.praktikum.bezeichnung,
startDatum: anmeldung.praktikum.startDatum.toISOString(),
endDatum: anmeldung.praktikum.endDatum.toISOString()
} : undefined,
// Timestamp
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
@@ -83,9 +103,12 @@ export async function POST({ request, url }) {
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
}
// Prüfen ob Anmeldung existiert
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
const existingAnmeldung = await prisma.anmeldung.findUnique({
where: { id }
where: { id },
include: {
praktikum: true
}
});
if (!existingAnmeldung) {
@@ -96,8 +119,37 @@ export async function POST({ request, url }) {
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
}
// Anmeldung als angenommen markieren
const zeitraumId = existingAnmeldung.praktikumId;
if (!zeitraumId) {
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 });
}
// Prüfen ob ZeitraumPlaetze Eintrag existiert
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
});
if (!zeitraumPlaetze) {
return json({
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
}, { status: 400 });
}
if (zeitraumPlaetze.plaetze <= 0) {
return json({
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum'
}, { status: 400 });
}
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
await prisma.$transaction([
// Anmeldung aktualisieren
prisma.anmeldung.update({
where: { id },
data: {
@@ -107,13 +159,19 @@ export async function POST({ request, url }) {
}
}),
prisma.dienststelle.update({
where: { id: dienststelleId },
data: {
plaetze: {
decrement: 1
// Plätze in ZeitraumPlaetze reduzieren
prisma.zeitraumPlaetze.update({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraumId,
dienststelleId: dienststelleId
}
}
},
data: {
plaetze: {
decrement: 1
}
}
})
]);

View File

@@ -1,193 +1,124 @@
// src/routes/api/admin/dienststellen/+server.ts
import { PrismaClient } from '@prisma/client';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { prisma } from '$lib/prisma';
const prisma = new PrismaClient();
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für eine neue Dienststelle
async function createZeitraumPlaetzeForDienststelle(dienststelleId: number) {
const zeitraeume = await prisma.praktikumszeitraum.findMany();
import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) {
return cookies.get('admin-auth') === 'authenticated';
// Erstelle für jeden existierenden Zeitraum einen Eintrag mit 0 Plätzen
for (const zeitraum of zeitraeume) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraum.id,
dienststelleId: dienststelleId,
plaetze: 0 // Standardwert: 0 Plätze
}
});
}
}
export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) {
return new Response(
JSON.stringify({ error: 'Nicht autorisiert' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const dienststellen = await prisma.dienststelle.findMany({
orderBy: { name: 'asc' },
/*
include: {
_count: {
select: {
Anmeldung: true // Use the correct relation name as defined in your Prisma schema
}
}
}
*/
orderBy: { name: 'asc' }
});
return json(dienststellen);
} catch (error) {
console.error('Fehler beim Laden der Dienststellen:', error);
return json({ error: 'Fehler beim Laden der Dienststellen' }, { status: 500 });
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { name, plaetze } = await request.json();
const { name } = await request.json();
// Validierung
if (!name || typeof name !== 'string' || name.trim().length === 0) {
if (!name) {
return json({ error: 'Name ist erforderlich' }, { status: 400 });
}
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
}
// Prüfe ob Name bereits existiert
const existing = await prisma.dienststelle.findFirst({
where: { name: name.trim() }
const dienststelle = await prisma.dienststelle.create({
data: {
name,
plaetze: 0 // Wird nicht mehr verwendet, aber bleibt im Schema für Kompatibilität
}
});
if (existing) {
// Automatisch ZeitraumPlaetze für alle existierenden Zeiträume erstellen
await createZeitraumPlaetzeForDienststelle(dienststelle.id);
return json(dienststelle);
} catch (error: any) {
console.error('Fehler beim Erstellen der Dienststelle:', error);
if (error.code === 'P2002') {
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
const created = await prisma.dienststelle.create({
data: {
name: name.trim(),
plaetze,
}
});
return json(created, { status: 201 });
} catch (error) {
console.error('Fehler beim Erstellen der Dienststelle:', error);
return json({ error: 'Fehler beim Erstellen der Dienststelle' }, { status: 500 });
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { id, name, plaetze } = await request.json();
const { id, name } = await request.json();
// Validierung
if (typeof id !== 'number' || isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
if (!id || !name) {
return json({ error: 'ID und Name sind erforderlich' }, { status: 400 });
}
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return json({ error: 'Name ist erforderlich' }, { status: 400 });
}
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
}
// Prüfe ob Dienststelle existiert
const existing = await prisma.dienststelle.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
}
// Prüfe ob neuer Name bereits bei anderer Dienststelle existiert
const nameConflict = await prisma.dienststelle.findFirst({
where: {
name: name.trim(),
NOT: { id },
},
});
if (nameConflict) {
return json({ error: 'Eine andere Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
// Prüfe ob Plätze reduziert werden und ob das möglich ist
const assignedCount = await prisma.anmeldung.count({
where: { zugewiesenId: id }
});
if (plaetze < assignedCount) {
return json({
error: `Plätze können nicht auf ${plaetze} reduziert werden. ${assignedCount} Anmeldungen sind bereits zugewiesen.`
}, { status: 400 });
}
const updated = await prisma.dienststelle.update({
where: { id },
const dienststelle = await prisma.dienststelle.update({
where: { id: parseInt(id) },
data: {
name: name.trim(),
plaetze
},
name
}
});
return json(updated);
} catch (error) {
return json(dienststelle);
} catch (error: any) {
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
return json({ error: 'Fehler beim Aktualisieren der Dienststelle' }, { status: 500 });
if (error.code === 'P2002') {
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
}
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) {
export const DELETE: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id'));
if (isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
}
try {
// Prüfe ob Dienststelle existiert
const existing = await prisma.dienststelle.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
const id = url.searchParams.get('id');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// Prüfe ob noch Anmeldungen zugewiesen sind
const assignedCount = await prisma.anmeldung.count({
where: {
OR: [
{ zugewiesenId: id },
{ wunsch1Id: id },
{ wunsch2Id: id },
{ wunsch3Id: id }
]
}
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
await prisma.dienststelle.delete({
where: { id: parseInt(id) }
});
if (assignedCount > 0) {
return json({
error: 'Dienststelle kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
}, { status: 400 });
}
await prisma.dienststelle.delete({ where: { id } });
return json({ success: true, message: 'Dienststelle erfolgreich gelöscht' });
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen der Dienststelle:', error);
return json({ error: 'Fehler beim Löschen der Dienststelle' }, { status: 500 });
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -1,59 +1,37 @@
// src/routes/api/admin/login/+server.ts
import type { RequestHandler } from './$types';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
//const ADMIN_PASSWORD_HASH = 'your-hashed-password-here'; // Ersetze mit deinem Hash
const adminRecord = await prisma.admin.findUnique({ where: { id: 1 } });
if (!adminRecord || !adminRecord.password) {
throw new Error('Admin password hash not found in database');
} else {
console.log('Admin password hash loaded successfully');
}
const ADMIN_PASSWORD_HASH = adminRecord.password;
import { prisma } from '$lib/prisma';
export const POST: RequestHandler = async ({ request, cookies }) => {
try {
const adminRecord = await prisma.admin.findUnique({ where: { id: 1 } });
if (!adminRecord?.password) {
return new Response(JSON.stringify({ message: 'Admin password not found' }), { status: 500 });
}
const { passwort } = await request.json();
if (!passwort) {
return new Response(
JSON.stringify({ message: 'Passwort erforderlich' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
return new Response(JSON.stringify({ message: 'Passwort erforderlich' }), { status: 400 });
}
// Hier solltest du den Hash aus der Datenbank oder Umgebungsvariable laden
const isValid = await bcrypt.compare(passwort, ADMIN_PASSWORD_HASH);
const isValid = await bcrypt.compare(passwort, adminRecord.password);
if (isValid) {
// Setze konsistenten Cookie-Namen
cookies.set('admin-auth', 'authenticated', {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 // 24 Stunden
maxAge: 60 * 60 * 24,
});
return new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} else {
return new Response(
JSON.stringify({ message: 'Falsches Passwort' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
return new Response(JSON.stringify({ message: 'Falsches Passwort' }), { status: 401 });
} catch (error) {
console.error('Login error:', error);
return new Response(
JSON.stringify({ message: 'Serverfehler' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
return new Response(JSON.stringify({ message: 'Serverfehler' }), { status: 500 });
}
};

View File

@@ -0,0 +1,92 @@
// src/routes/api/admin/plaetze/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { prisma } from '$lib/prisma';
export const GET: RequestHandler = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
// Lade alle Zeiträume mit ihren ZeitraumPlaetze-Einträgen
const zeitraeume = await prisma.praktikumszeitraum.findMany({
include: {
zeitraumPlaetze: {
include: {
dienststelle: true
},
orderBy: {
dienststelle: {
name: 'asc'
}
}
}
},
orderBy: {
startDatum: 'desc'
}
});
// Transformiere die Daten in das gewünschte Format
const result = zeitraeume.map(zeitraum => ({
zeitraumId: zeitraum.id,
zeitraumBezeichnung: zeitraum.bezeichnung,
dienststellen: zeitraum.zeitraumPlaetze.map(zp => ({
id: zp.id,
dienststelleId: zp.dienststelleId,
dienststelleName: zp.dienststelle.name,
plaetze: zp.plaetze
}))
}));
return json(result);
} catch (error) {
console.error('Fehler beim Laden der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { zeitraumId, dienststelleId, plaetze } = await request.json();
if (!zeitraumId || !dienststelleId || plaetze === undefined) {
return json({ error: 'Zeitraum-ID, Dienststellen-ID und Plätze sind erforderlich' }, { status: 400 });
}
const plaetzeInt = parseInt(plaetze);
if (isNaN(plaetzeInt) || plaetzeInt < 0) {
return json({ error: 'Plätze muss eine gültige Zahl ≥ 0 sein' }, { status: 400 });
}
// Aktualisiere oder erstelle den Eintrag
const updated = await prisma.zeitraumPlaetze.upsert({
where: {
zeitraumId_dienststelleId: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId)
}
},
update: {
plaetze: plaetzeInt
},
create: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId),
plaetze: plaetzeInt
}
});
return json(updated);
} catch (error) {
console.error('Fehler beim Aktualisieren der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -1,160 +1,141 @@
import { PrismaClient } from '@prisma/client';
// src/routes/api/admin/zeitraeume/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { prisma } from '$lib/prisma';
const prisma = new PrismaClient();
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
const dienststellen = await prisma.dienststelle.findMany();
import type { Cookies } from '@sveltejs/kit';
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
function checkAuth(cookies: Cookies) {
return cookies.get('admin-auth') === 'authenticated';
}
function isValidDate(date: string | Date) {
const parsed = new Date(date)
return !isNaN(parsed.getTime())
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.create({
data: {
zeitraumId: zeitraumId,
dienststelleId: dienststelle.id,
plaetze: 0 // Standardwert: 0 Plätze
}
});
}
}
export const GET: RequestHandler = async ({ cookies }) => {
if (!checkAuth(cookies)) {
return new Response(
JSON.stringify({ error: 'Nicht autorisiert' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const zeitraeume = await prisma.praktikumszeitraum.findMany();
const zeitraeume = await prisma.praktikumszeitraum.findMany({
orderBy: { startDatum: 'desc' }
});
return json(zeitraeume);
} catch (error) {
console.error('Fehler beim Laden der Praktikumszeiträume:', error);
return json({ error: 'Fehler beim Laden der Praktikumszeiträume' }, { status: 500 });
console.error('Fehler beim Laden der Zeiträume:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
export const POST: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { bezeichnung, startDatum, endDatum } = await request.json();
// Validierung
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
if (!bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültiges Datum' }, { status: 400 });
const start = new Date(startDatum);
const end = new Date(endDatum);
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
const created = await prisma.praktikumszeitraum.create({
const zeitraum = await prisma.praktikumszeitraum.create({
data: {
bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
bezeichnung,
startDatum: start,
endDatum: end
}
});
return json(created, { status: 201 });
} catch (error) {
console.error('Fehler beim Erstellen des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Erstellen des Praktikumszeitraums' }, { status: 500 });
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
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: 'Serverfehler' }, { status: 500 });
}
};
export const PATCH: RequestHandler = async ({ cookies, request }) => {
if (!checkAuth(cookies)) {
export const PATCH: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { id, bezeichnung, startDatum, endDatum } = await request.json();
// Validierung
if (typeof id !== 'number' || isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
if (!id || !bezeichnung || !startDatum || !endDatum) {
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
}
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
const start = new Date(startDatum);
const end = new Date(endDatum);
if (end <= start) {
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
}
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
return json({ error: 'Ungültiges Datum' }, { status: 400 });
}
// Prüfe ob Praktikumszeitraum existiert
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
}
// Prüfe ob neue Bezeichnung bereits bei anderem Zeitraum existiert
const konflikt = await prisma.praktikumszeitraum.findFirst({
where: {
bezeichnung: bezeichnung.trim(),
NOT: { id },
},
});
if (konflikt) {
return json({ error: 'Ein anderer Praktikumszeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
}
const updated = await prisma.praktikumszeitraum.update({
where: { id },
const zeitraum = await prisma.praktikumszeitraum.update({
where: { id: parseInt(id) },
data: {
bezeichnung: bezeichnung.trim(),
startDatum: new Date(startDatum),
endDatum: new Date(endDatum)
},
bezeichnung,
startDatum: start,
endDatum: end
}
});
return json(updated);
} catch (error) {
console.error('Fehler beim Aktualisieren des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Aktualisieren des Praktikumszeitraums' }, { status: 500 });
return json(zeitraum);
} 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: 'Serverfehler' }, { status: 500 });
}
};
export const DELETE: RequestHandler = async ({ cookies, url }) => {
if (!checkAuth(cookies)) {
export const DELETE: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
const id = Number(url.searchParams.get('id'));
if (isNaN(id)) {
return json({ error: 'Ungültige ID' }, { status: 400 });
}
try {
// Prüfe ob Praktikumszeitraum existiert
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
if (!existing) {
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
const id = url.searchParams.get('id');
if (!id) {
return json({ error: 'ID erforderlich' }, { status: 400 });
}
// Hier könntest du prüfen, ob noch Anmeldungen mit diesem Zeitraum verknüpft sind
// const assignedCount = await prisma.anmeldung.count({
// where: { praktikumszeitraumId: id }
// });
//
// if (assignedCount > 0) {
// return json({
// error: 'Praktikumszeitraum kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
// }, { status: 400 });
// }
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
await prisma.praktikumszeitraum.delete({
where: { id: parseInt(id) }
});
await prisma.praktikumszeitraum.delete({ where: { id } });
return json({ success: true, message: 'Praktikumszeitraum erfolgreich gelöscht' });
return json({ success: true });
} catch (error) {
console.error('Fehler beim Löschen des Praktikumszeitraums:', error);
return json({ error: 'Fehler beim Löschen des Praktikumszeitraums' }, { status: 500 });
console.error('Fehler beim Löschen des Zeitraums:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -0,0 +1,295 @@
<!-- src/routes/admin/zeitraum-plaetze/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import AdminHeader from '$lib/components/AdminHeader.svelte';
interface Zeitraum {
id: number;
bezeichnung: string;
startDatum: string;
endDatum: string;
}
interface ZeitraumPlatz {
id: number;
zeitraumId: number;
dienststelleId: number;
plaetze: number;
dienststelle: {
id: number;
name: string;
};
}
let zeitraeume: Zeitraum[] = [];
let selectedZeitraumId: number | null = null;
let plaetze: ZeitraumPlatz[] = [];
let isLoading = true;
let isSaving = false;
let fehlermeldung = '';
let erfolgsmeldung = '';
// Temporäre Änderungen speichern
let aenderungen: Map<number, number> = new Map();
async function ladeZeitraeume() {
try {
const res = await fetch('/api/admin/zeitraeume');
if (!res.ok) throw new Error('Fehler beim Laden');
zeitraeume = await res.json();
if (zeitraeume.length > 0 && !selectedZeitraumId) {
selectedZeitraumId = zeitraeume[0].id;
await ladePlaetze();
}
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Laden';
console.error(err);
}
}
async function ladePlaetze() {
if (!selectedZeitraumId) return;
try {
isLoading = true;
fehlermeldung = '';
erfolgsmeldung = '';
const res = await fetch(`/api/admin/zeitraum-plaetze?zeitraumId=${selectedZeitraumId}`);
if (!res.ok) throw new Error('Fehler beim Laden');
plaetze = await res.json();
aenderungen.clear();
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Laden';
console.error(err);
} finally {
isLoading = false;
}
}
function handlePlaetzeChange(dienststelleId: number, wert: string) {
const plaetzeWert = parseInt(wert) || 0;
aenderungen.set(dienststelleId, plaetzeWert);
aenderungen = aenderungen; // Trigger reactivity
}
function getPlaetzeWert(platz: ZeitraumPlatz): number {
return aenderungen.has(platz.dienststelleId)
? aenderungen.get(platz.dienststelleId)!
: platz.plaetze;
}
async function speichernAlle() {
if (!selectedZeitraumId || aenderungen.size === 0) {
erfolgsmeldung = 'Keine Änderungen zu speichern';
return;
}
try {
isSaving = true;
fehlermeldung = '';
erfolgsmeldung = '';
// Alle Änderungen nacheinander speichern
for (const [dienststelleId, plaetzeWert] of aenderungen) {
const res = await fetch('/api/admin/zeitraum-plaetze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
zeitraumId: selectedZeitraumId,
dienststelleId,
plaetze: plaetzeWert
})
});
if (!res.ok) {
throw new Error(`Fehler beim Speichern für Dienststelle ${dienststelleId}`);
}
}
erfolgsmeldung = 'Alle Änderungen erfolgreich gespeichert';
aenderungen.clear();
await ladePlaetze();
// Erfolgsmeldung nach 3 Sekunden ausblenden
setTimeout(() => { erfolgsmeldung = ''; }, 3000);
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Speichern';
console.error(err);
} finally {
isSaving = false;
}
}
async function syncAlles() {
if (!confirm('Möchten Sie wirklich alle Zeitraum-Dienststellen-Kombinationen synchronisieren?')) {
return;
}
try {
isSaving = true;
fehlermeldung = '';
const res = await fetch('/api/admin/zeitraum-plaetze', {
method: 'PATCH'
});
if (!res.ok) throw new Error('Fehler beim Synchronisieren');
erfolgsmeldung = 'Synchronisierung erfolgreich';
await ladePlaetze();
setTimeout(() => { erfolgsmeldung = ''; }, 3000);
} catch (err) {
fehlermeldung = err instanceof Error ? err.message : 'Fehler beim Synchronisieren';
console.error(err);
} finally {
isSaving = false;
}
}
$: if (selectedZeitraumId) {
ladePlaetze();
}
onMount(ladeZeitraeume);
</script>
<svelte:head>
<title>Plätze pro Zeitraum verwalten - Admin</title>
</svelte:head>
<div class="min-h-screen bg-gray-50">
<AdminHeader
title="Plätze pro Zeitraum verwalten"
showBackButton={true}
/>
<main class="max-w-7xl mx-auto px-4 py-6">
{#if fehlermeldung}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
<p class="mt-1 text-sm text-red-700">{fehlermeldung}</p>
</div>
</div>
</div>
{/if}
{#if erfolgsmeldung}
<div class="bg-green-50 border border-green-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800">{erfolgsmeldung}</p>
</div>
</div>
</div>
{/if}
<!-- Zeitraum Auswahl -->
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
<div class="flex justify-between items-center">
<div class="flex-1">
<label for="zeitraum" class="block text-sm font-medium text-gray-700 mb-2">
Praktikumszeitraum auswählen
</label>
<select
id="zeitraum"
bind:value={selectedZeitraumId}
class="w-full max-w-md border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
>
{#each zeitraeume as z}
<option value={z.id}>
{z.bezeichnung} ({new Date(z.startDatum).toLocaleDateString('de-DE')} - {new Date(z.endDatum).toLocaleDateString('de-DE')})
</option>
{/each}
</select>
</div>
<button
on:click={syncAlles}
disabled={isSaving}
class="px-4 py-2 text-sm text-blue-600 hover:text-blue-800 hover:underline disabled:opacity-50"
>
Alles synchronisieren
</button>
</div>
</div>
{#if isLoading}
<div class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-3 text-gray-600">Lade Plätze...</span>
</div>
{:else if plaetze.length === 0}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Keine Dienststellen</h3>
<p class="mt-1 text-sm text-gray-500">Erstellen Sie zuerst Dienststellen, um Plätze zuzuweisen.</p>
</div>
{:else}
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">
Plätze pro Dienststelle
</h3>
<button
on:click={speichernAlle}
disabled={isSaving || aenderungen.size === 0}
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? 'Speichert...' : `Änderungen speichern ${aenderungen.size > 0 ? `(${aenderungen.size})` : ''}`}
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dienststelle
</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Verfügbare Plätze
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each plaetze as platz}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{platz.dienststelle.name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<input
type="number"
min="0"
value={getPlaetzeWert(platz)}
on:input={(e) => handlePlaetzeChange(platz.dienststelleId, e.currentTarget.value)}
class="w-24 border border-gray-300 rounded-md px-3 py-1 text-center focus:ring-blue-500 focus:border-blue-500 {aenderungen.has(platz.dienststelleId) ? 'bg-yellow-50 border-yellow-300' : ''}"
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</main>
</div>

View File

@@ -0,0 +1,114 @@
// src/routes/api/admin/zeitraum-plaetze/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET: Plätze für einen bestimmten Zeitraum abrufen
export const GET: RequestHandler = async ({ url, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const zeitraumId = url.searchParams.get('zeitraumId');
if (!zeitraumId) {
return json({ error: 'zeitraumId erforderlich' }, { status: 400 });
}
const plaetze = await prisma.zeitraumPlaetze.findMany({
where: { zeitraumId: parseInt(zeitraumId) },
include: {
dienststelle: true
},
orderBy: {
dienststelle: { name: 'asc' }
}
});
return json(plaetze);
} catch (error) {
console.error('Fehler beim Laden der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
// POST: Plätze für einen Zeitraum und Dienststelle aktualisieren
export const POST: RequestHandler = async ({ request, cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
const { zeitraumId, dienststelleId, plaetze } = await request.json();
if (!zeitraumId || !dienststelleId || plaetze === undefined) {
return json({ error: 'Fehlende Parameter' }, { status: 400 });
}
const result = await prisma.zeitraumPlaetze.upsert({
where: {
zeitraumId_dienststelleId: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId)
}
},
update: {
plaetze: parseInt(plaetze)
},
create: {
zeitraumId: parseInt(zeitraumId),
dienststelleId: parseInt(dienststelleId),
plaetze: parseInt(plaetze)
}
});
return json(result);
} catch (error) {
console.error('Fehler beim Speichern der Plätze:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};
// PATCH: Synchronisiert ZeitraumPlaetze wenn neue Dienststellen oder Zeiträume hinzugefügt werden
export const PATCH: RequestHandler = async ({ cookies }) => {
const adminAuth = cookies.get('admin-auth');
if (adminAuth !== 'authenticated') {
return json({ error: 'Nicht autorisiert' }, { status: 401 });
}
try {
// Alle Zeiträume und Dienststellen holen
const zeitraeume = await prisma.praktikumszeitraum.findMany();
const dienststellen = await prisma.dienststelle.findMany();
// Für jede Kombination prüfen ob Eintrag existiert, sonst erstellen
for (const zeitraum of zeitraeume) {
for (const dienststelle of dienststellen) {
await prisma.zeitraumPlaetze.upsert({
where: {
zeitraumId_dienststelleId: {
zeitraumId: zeitraum.id,
dienststelleId: dienststelle.id
}
},
update: {}, // Nichts updaten wenn bereits vorhanden
create: {
zeitraumId: zeitraum.id,
dienststelleId: dienststelle.id,
plaetze: dienststelle.plaetze // Standardwert von Dienststelle übernehmen
}
});
}
}
return json({ success: true, message: 'Synchronisierung abgeschlossen' });
} catch (error) {
console.error('Fehler bei der Synchronisierung:', error);
return json({ error: 'Serverfehler' }, { status: 500 });
}
};

View File

@@ -1,10 +1,17 @@
import { PrismaClient } from '@prisma/client';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
const prisma = new PrismaClient();
let prismaPromise: Promise<any> | null = null;
async function getPrismaClient() {
if (!prismaPromise) {
prismaPromise = import('@prisma/client').then(({ PrismaClient }) => new PrismaClient());
}
return prismaPromise;
}
export const GET: RequestHandler = async () => {
const prisma = await getPrismaClient(); // Hier Prisma Client holen
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
return json(dienststellen);
};

View File

@@ -2,5 +2,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
optimizeDeps: {
exclude: ['@prisma/client']
},
ssr: {
noExternal: process.env.NODE_ENV === 'production' ? ['@prisma/client'] : [],
}
});