praktikum refinemend Plaetze pro Dienstelle und Pro Zeitraum
This commit is contained in:
1363
package-lock.json
generated
1363
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@
|
|||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prisma": "^6.12.0",
|
"prisma": "^6.19.0",
|
||||||
"svelte": "^5.36.17",
|
"svelte": "^5.36.17",
|
||||||
"svelte-check": "^4.3.0",
|
"svelte-check": "^4.3.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
@@ -46,7 +46,9 @@
|
|||||||
"vite-plugin": "^0.0.0"
|
"vite-plugin": "^0.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@sveltejs/adapter-node": "^5.2.13",
|
||||||
"bcryptjs": "^3.0.2"
|
"bcryptjs": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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");
|
|
||||||
@@ -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");
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Admin" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
|
|
||||||
"password" TEXT NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Anmeldung" ADD COLUMN "pdfdatei" TEXT;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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");
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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'
|
|
||||||
);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
114
prisma/migrations/20251125082147_prisma6/migration.sql
Normal file
114
prisma/migrations/20251125082147_prisma6/migration.sql
Normal 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
BIN
prisma/prisma/praktika.db
Normal file
Binary file not shown.
@@ -6,7 +6,7 @@ generator client {
|
|||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = "file:./praktika.db"
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Admin {
|
model Admin {
|
||||||
@@ -25,11 +25,14 @@ model EmailConfig {
|
|||||||
model Dienststelle {
|
model Dienststelle {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
plaetze Int @default(0)
|
plaetze Int @default(0) // Standard-Plätze, wird nicht mehr direkt verwendet
|
||||||
anmeldungenWunsch1 Anmeldung[] @relation("Wunsch1")
|
anmeldungenWunsch1 Anmeldung[] @relation("Wunsch1")
|
||||||
anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2")
|
anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2")
|
||||||
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
|
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
|
||||||
zugewiesene Anmeldung[] @relation("Zugewiesen")
|
zugewiesene Anmeldung[] @relation("Zugewiesen")
|
||||||
|
|
||||||
|
// Neue Relation zu ZeitraumPlaetze
|
||||||
|
zeitraumPlaetze ZeitraumPlaetze[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Praktikumszeitraum {
|
model Praktikumszeitraum {
|
||||||
@@ -38,6 +41,26 @@ model Praktikumszeitraum {
|
|||||||
startDatum DateTime
|
startDatum DateTime
|
||||||
endDatum DateTime
|
endDatum DateTime
|
||||||
anmeldungen Anmeldung[] @relation("PraktikumszeitraumAnmeldungen")
|
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
|
// Erweiterte Status-Enum für bessere Nachverfolgung
|
||||||
@@ -70,7 +93,6 @@ model Anmeldung {
|
|||||||
status Status @default(OFFEN)
|
status Status @default(OFFEN)
|
||||||
|
|
||||||
// Neue Felder für Status-Tracking
|
// Neue Felder für Status-Tracking
|
||||||
// processedBy String? // Wer bearbeitet die Anmeldung
|
|
||||||
processedAt DateTime? // Wann wurde sie bearbeitet
|
processedAt DateTime? // Wann wurde sie bearbeitet
|
||||||
|
|
||||||
// Praktikumszeitraum Relation
|
// Praktikumszeitraum Relation
|
||||||
|
|||||||
@@ -30,6 +30,13 @@
|
|||||||
description: 'Praktikumszeiträume verwalten',
|
description: 'Praktikumszeiträume verwalten',
|
||||||
color: 'bg-purple-600 hover:bg-purple-700'
|
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',
|
href: '/admin/change-password',
|
||||||
title: 'Passwort ändern',
|
title: 'Passwort ändern',
|
||||||
|
|||||||
@@ -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 {
|
function formatProcessedDate(timestamp: number | undefined): string {
|
||||||
if (!timestamp) return '-';
|
if (!timestamp) return '-';
|
||||||
return new Date(timestamp).toLocaleDateString('de-DE', {
|
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 {
|
function canBeAccepted(status: string): boolean {
|
||||||
return status === 'pending';
|
return status === 'pending';
|
||||||
}
|
}
|
||||||
@@ -47,166 +93,234 @@
|
|||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<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
|
Status
|
||||||
</th>
|
</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">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Bewerber/in
|
Persönliche Daten
|
||||||
</th>
|
</th>
|
||||||
<th class="w-16 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">
|
||||||
Noten
|
Kontakt & Adresse
|
||||||
</th>
|
</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">
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Wünsche / Zuweisung
|
Schule & Noten
|
||||||
</th>
|
</th>
|
||||||
<th class="w-32 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">
|
||||||
Eingegangen
|
Praktikum & Wünsche
|
||||||
</th>
|
</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
|
Aktionen
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
{#each anmeldungen as anmeldung (anmeldung.id)}
|
{#each anmeldungen as anmeldung (anmeldung.id)}
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50 align-top">
|
||||||
<!-- Status (processing Styling entfernt) -->
|
<!-- Status -->
|
||||||
<td class="px-4 py-4 whitespace-nowrap">
|
<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')}">
|
<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')}
|
{getStatusText(anmeldung.status || 'pending')}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="text-xs text-gray-400 mt-2">
|
||||||
|
Eingang:<br>
|
||||||
|
{formatDate(anmeldung.timestamp)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Bewerber/in -->
|
<!-- Persönliche Daten -->
|
||||||
<td class="px-4 py-4">
|
<td class="px-3 py-4">
|
||||||
<div class="text-sm font-medium text-gray-900">
|
<div class="text-sm font-medium text-gray-900">
|
||||||
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
|
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-500 break-all">
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
{anmeldung.email}
|
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
|
||||||
</div>
|
</div>
|
||||||
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
|
{#if anmeldung.alter}
|
||||||
<div class="mt-2">
|
<div class="text-xs text-gray-500">
|
||||||
{#each anmeldung.pdfs as pdf}
|
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3 mr-1" 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
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Noten -->
|
<!-- Kontakt & Adresse -->
|
||||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-3 py-4 text-sm">
|
||||||
{#if anmeldung.noteDeutsch || anmeldung.noteMathe}
|
<div class="text-xs space-y-1">
|
||||||
<div class="space-y-1">
|
<div>
|
||||||
{#if anmeldung.noteDeutsch}
|
<span class="font-medium text-gray-700">Adresse:</span><br>
|
||||||
<div class="text-xs">D: {anmeldung.noteDeutsch}</div>
|
<span class="text-gray-600">
|
||||||
{/if}
|
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
|
||||||
{#if anmeldung.noteMathe}
|
{anmeldung.plz || ''} {anmeldung.ort || ''}
|
||||||
<div class="text-xs">M: {anmeldung.noteMathe}</div>
|
</span>
|
||||||
{/if}
|
|
||||||
</div>
|
</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}
|
{:else}
|
||||||
<span class="text-gray-400">-</span>
|
<span class="text-gray-400">-</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if anmeldung.sozialverhalten}
|
|
||||||
<div class="text-xs text-gray-500 mt-1">
|
|
||||||
SV: {anmeldung.sozialverhalten}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<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>
|
</td>
|
||||||
|
|
||||||
<!-- Wünsche / Zuweisung -->
|
<!-- Schule & Noten -->
|
||||||
<td class="px-4 py-4 text-sm text-gray-900">
|
<td class="px-3 py-4 text-sm">
|
||||||
<!-- Zugewiesene Dienststelle (falls vorhanden) -->
|
<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}
|
{#if anmeldung.assignedDienststelle}
|
||||||
<div class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
|
<div class="p-2 bg-green-50 border border-green-200 rounded">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center text-green-700">
|
||||||
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span class="font-medium text-green-800 text-sm">Zugewiesen:</span>
|
<span class="font-semibold">Zugewiesen:</span>
|
||||||
</div>
|
|
||||||
<div class="text-sm text-green-700 mt-1 ml-6">
|
|
||||||
{anmeldung.assignedDienststelle.name}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-green-800 mt-1">{anmeldung.assignedDienststelle.name}</div>
|
||||||
{#if anmeldung.processedAt}
|
{#if anmeldung.processedAt}
|
||||||
<div class="text-xs text-green-600 mt-1 ml-6">
|
<div class="text-xs text-green-600 mt-1">
|
||||||
{formatProcessedDate(anmeldung.processedAt)}
|
{formatProcessedDate(anmeldung.processedAt)}
|
||||||
{#if anmeldung.processedBy}
|
|
||||||
von {anmeldung.processedBy}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Wünsche -->
|
<!-- Alle 3 Wünsche -->
|
||||||
<div class="space-y-2">
|
<div>
|
||||||
<div class="text-xs font-medium text-gray-500 uppercase tracking-wider">Wünsche:</div>
|
<span class="font-medium text-gray-700">Wünsche:</span>
|
||||||
|
<div class="space-y-1 mt-1">
|
||||||
{#if anmeldung.wunsch1}
|
{#if anmeldung.wunsch1}
|
||||||
<div class="flex items-start">
|
<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="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-sm leading-5">{anmeldung.wunsch1.name}</span>
|
<span class="text-gray-900 leading-tight">{anmeldung.wunsch1.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if anmeldung.wunsch2}
|
{#if anmeldung.wunsch2}
|
||||||
<div class="flex items-start">
|
<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="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-sm leading-5">{anmeldung.wunsch2.name}</span>
|
<span class="text-gray-900 leading-tight">{anmeldung.wunsch2.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if anmeldung.wunsch3}
|
{#if anmeldung.wunsch3}
|
||||||
<div class="flex items-start">
|
<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="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-sm leading-5">{anmeldung.wunsch3.name}</span>
|
<span class="text-gray-900 leading-tight">{anmeldung.wunsch3.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
|
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
|
||||||
<span class="text-gray-400 text-sm">Keine Wünsche angegeben</span>
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Eingegangen -->
|
<!-- Dokumente / PDFs -->
|
||||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td class="px-3 py-4">
|
||||||
<div class="text-sm">{formatDate(anmeldung.timestamp)}</div>
|
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#each anmeldung.pdfs as pdf, index}
|
||||||
|
<a
|
||||||
|
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
|
||||||
|
target="_blank"
|
||||||
|
class="flex items-center text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-1 rounded"
|
||||||
|
>
|
||||||
|
<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 {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>
|
||||||
|
{:else}
|
||||||
|
<span class="text-xs text-gray-400">Keine Dokumente</span>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Aktionen -->
|
<!-- 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">
|
<div class="flex flex-col space-y-2">
|
||||||
{#if canBeAccepted(anmeldung.status || 'pending')}
|
{#if canBeAccepted(anmeldung.status || 'pending')}
|
||||||
<button
|
<button
|
||||||
on:click={() => dispatch('accept', { id: anmeldung.id })}
|
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>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if canBeRejected(anmeldung.status || 'pending')}
|
{#if canBeRejected(anmeldung.status || 'pending')}
|
||||||
<button
|
<button
|
||||||
on:click={() => dispatch('reject', { id: anmeldung.id })}
|
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>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={() => dispatch('delete', { id: anmeldung.id })}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -215,3 +329,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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
15
src/lib/prisma.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -32,8 +32,14 @@
|
|||||||
let schulklasse = '';
|
let schulklasse = '';
|
||||||
let ablehnungHinweis = '';
|
let ablehnungHinweis = '';
|
||||||
let showAblehnungModal = false;
|
let showAblehnungModal = false;
|
||||||
let showIgsHinweis = false;
|
|
||||||
let alter = '';
|
let alter = '';
|
||||||
|
|
||||||
|
// Validierungsfehler für Echtzeit-Anzeige
|
||||||
|
let alterFehler = '';
|
||||||
|
let notenFehler = '';
|
||||||
|
let sozialverhaltenFehler = '';
|
||||||
|
|
||||||
|
// Berechnung des Alters
|
||||||
$: {
|
$: {
|
||||||
if (geburtsdatum && zeitraum && zeitraeume.length > 0) {
|
if (geburtsdatum && zeitraum && zeitraeume.length > 0) {
|
||||||
const gewaehlterZeitraum = zeitraeume.find(z => z.id == zeitraum);
|
const gewaehlterZeitraum = zeitraeume.find(z => z.id == zeitraum);
|
||||||
@@ -52,29 +58,76 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überwachung für IGS + Klasse 7 Kombination
|
// Echtzeit-Validierung: Alter
|
||||||
$: {
|
$: {
|
||||||
if (["KGSR", "IGSR"].includes(schulart) && schulklasse === '7') {
|
const altersWert = parseInt(alter);
|
||||||
showIgsHinweis = true;
|
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 => {
|
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
|
||||||
if (d.plaetze <= 0) return false;
|
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')) {
|
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
|
||||||
return parseInt(alter) >= 18;
|
return parseInt(alter) >= 18;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
$: filteredZeitraeume = (zeitraeume ?? []).filter(zeitraum => {
|
$: filteredZeitraeume = (zeitraeume ?? []).filter(zeitraum => {
|
||||||
const heute = new Date();
|
const heute = new Date();
|
||||||
const startDatum = new Date(zeitraum.startDatum);
|
const startDatum = new Date(zeitraum.startDatum);
|
||||||
|
|
||||||
// Nur Zeiträume anzeigen, die noch nicht gestartet haben
|
|
||||||
return startDatum > heute;
|
return startDatum > heute;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,7 +135,7 @@
|
|||||||
|
|
||||||
$: hideSozialVerhalten =
|
$: hideSozialVerhalten =
|
||||||
Number(schulklasse) >= 11 &&
|
Number(schulklasse) >= 11 &&
|
||||||
["Gymnasium", "KGS_Gymnasialzweig", "IGS_Gymnasialzweig"].includes(schulart);
|
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const resDienstelle = await fetch('/api/dienststellen');
|
const resDienstelle = await fetch('/api/dienststellen');
|
||||||
@@ -91,6 +144,7 @@
|
|||||||
const resZeitraeume = await fetch('/api/zeitraeume');
|
const resZeitraeume = await fetch('/api/zeitraeume');
|
||||||
zeitraeume = await resZeitraeume.json();
|
zeitraeume = await resZeitraeume.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Zeitraum {
|
interface Zeitraum {
|
||||||
id: number;
|
id: number;
|
||||||
bezeichnung: string;
|
bezeichnung: string;
|
||||||
@@ -98,9 +152,6 @@
|
|||||||
endDatum: string;
|
endDatum: string;
|
||||||
}
|
}
|
||||||
let zeitraeume: Zeitraum[] = [];
|
let zeitraeume: Zeitraum[] = [];
|
||||||
//let neuerBezeichnung = '';
|
|
||||||
//let neuerstartDatum = '';
|
|
||||||
//let neuerendDatum = '';
|
|
||||||
let fehlermeldung = '';
|
let fehlermeldung = '';
|
||||||
let bearbeiteId: number | null = null;
|
let bearbeiteId: number | null = null;
|
||||||
|
|
||||||
@@ -124,12 +175,21 @@
|
|||||||
noteDeutsch = '';
|
noteDeutsch = '';
|
||||||
noteMathe = '';
|
noteMathe = '';
|
||||||
sozialverhalten = '';
|
sozialverhalten = '';
|
||||||
|
schulklasse = '';
|
||||||
pdfDateien = [];
|
pdfDateien = [];
|
||||||
fileInputKey += 1;
|
fileInputKey += 1;
|
||||||
success = false;
|
success = false;
|
||||||
|
alterFehler = '';
|
||||||
|
notenFehler = '';
|
||||||
|
sozialverhaltenFehler = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function anmelden() {
|
async function anmelden() {
|
||||||
|
// Abbrechen wenn Validierungsfehler vorhanden
|
||||||
|
if (formHatFehler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
|
|
||||||
data.append('anrede', anrede);
|
data.append('anrede', anrede);
|
||||||
@@ -158,36 +218,6 @@
|
|||||||
data.append('pdfs', pdf);
|
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', {
|
const res = await fetch('/api/anmelden', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data
|
body: data
|
||||||
@@ -234,17 +264,13 @@
|
|||||||
<option value="" disabled selected hidden>Schulart wählen</option>
|
<option value="" disabled selected hidden>Schulart wählen</option>
|
||||||
<option value="Gymnasium">Gymnasium</option>
|
<option value="Gymnasium">Gymnasium</option>
|
||||||
<option value="KGS_Gymnasialzweig">KGS Gymnasialzweig</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="Realschule">Realschule</option>
|
||||||
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
|
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
|
||||||
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
|
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
|
||||||
</select>
|
</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">
|
<select bind:value={schulklasse} required class="input">
|
||||||
<option value="" disabled selected hidden>Schulklasse</option>
|
<option value="" disabled selected hidden>Schulklasse</option>
|
||||||
<option value="7">7. Klasse</option>
|
<option value="7">7. Klasse</option>
|
||||||
@@ -255,16 +281,56 @@
|
|||||||
<option value="12">12. Klasse</option>
|
<option value="12">12. Klasse</option>
|
||||||
<option value="13">13. Klasse</option>
|
<option value="13">13. Klasse</option>
|
||||||
</select>
|
</select>
|
||||||
<!-- Sozialverhalten -->
|
|
||||||
|
<!-- Sozialverhalten mit Echtzeit-Validierung -->
|
||||||
{#if !hideSozialVerhalten}
|
{#if !hideSozialVerhalten}
|
||||||
<select bind:value={sozialverhalten} required class="input">
|
<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="" 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 in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
|
||||||
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</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>
|
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
|
||||||
</select>
|
</select>
|
||||||
|
{#if sozialverhaltenFehler}
|
||||||
|
<p class="text-red-600 text-sm mt-1">{sozialverhaltenFehler}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<select bind:value={zeitraum} required class="input">
|
<select bind:value={zeitraum} required class="input">
|
||||||
@@ -273,13 +339,20 @@
|
|||||||
<option
|
<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' })})
|
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>
|
</option>
|
||||||
startDatum = {new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
<p>Startdatum: {startDatum}</p>
|
|
||||||
</div>
|
</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 -->
|
<!-- Wunschdienststellen -->
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<select bind:value={wunsch1Id} required>
|
<select bind:value={wunsch1Id} required>
|
||||||
@@ -324,24 +397,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/key}
|
{/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}
|
{#if showAblehnungModal}
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
||||||
@@ -356,12 +411,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Button -->
|
<!-- Button - deaktiviert bei Validierungsfehlern -->
|
||||||
<button type="submit"
|
<button
|
||||||
class="w-full bg-blue-600 text-white py-3 rounded-xl hover:bg-blue-700 transition-all">
|
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
|
Jetzt anmelden
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if formHatFehler}
|
||||||
|
<p class="text-red-600 text-sm text-center">Bitte korrigiere die markierten Fehler, um fortzufahren.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if success}
|
{#if success}
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
||||||
@@ -376,17 +443,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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}
|
{#if fehler}
|
||||||
<p class="text-red-600">{fehler}</p>
|
<p class="text-red-600">{fehler}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -396,15 +452,23 @@
|
|||||||
<style>
|
<style>
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #d1d5db; /* border-gray-300 */
|
border: 1px solid #d1d5db;
|
||||||
border-radius: 0.75rem; /* rounded-xl */
|
border-radius: 0.75rem;
|
||||||
padding: 0.75rem; /* p-3 */
|
padding: 0.75rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
.input:focus {
|
.input:focus {
|
||||||
outline: none;
|
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;
|
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>
|
</style>
|
||||||
@@ -3,9 +3,8 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import AdminHeader from '$lib/components/AdminHeader.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 neuerName = '';
|
||||||
let neuePlaetze = 0;
|
|
||||||
let fehlermeldung = '';
|
let fehlermeldung = '';
|
||||||
let bearbeiteId: number | null = null;
|
let bearbeiteId: number | null = null;
|
||||||
let isLoading = true;
|
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;
|
neuerName = d.name;
|
||||||
neuePlaetze = d.plaetze;
|
|
||||||
bearbeiteId = d.id;
|
bearbeiteId = d.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +44,8 @@
|
|||||||
try {
|
try {
|
||||||
const method = bearbeiteId ? 'PATCH' : 'POST';
|
const method = bearbeiteId ? 'PATCH' : 'POST';
|
||||||
const body = bearbeiteId
|
const body = bearbeiteId
|
||||||
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
|
? { id: bearbeiteId, name: neuerName }
|
||||||
: { name: neuerName, plaetze: neuePlaetze };
|
: { name: neuerName };
|
||||||
|
|
||||||
const res = await fetch('/api/admin/dienststellen', {
|
const res = await fetch('/api/admin/dienststellen', {
|
||||||
method,
|
method,
|
||||||
@@ -57,7 +55,6 @@
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
neuerName = '';
|
neuerName = '';
|
||||||
neuePlaetze = 0;
|
|
||||||
bearbeiteId = null;
|
bearbeiteId = null;
|
||||||
await ladeDienststellen();
|
await ladeDienststellen();
|
||||||
} else {
|
} else {
|
||||||
@@ -71,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loeschen(id: number) {
|
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 {
|
try {
|
||||||
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
|
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
|
||||||
@@ -91,7 +88,6 @@
|
|||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
neuerName = '';
|
neuerName = '';
|
||||||
neuePlaetze = 0;
|
|
||||||
bearbeiteId = null;
|
bearbeiteId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +104,7 @@
|
|||||||
showBackButton={true}
|
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}
|
{#if fehlermeldung}
|
||||||
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -125,14 +121,30 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 -->
|
<!-- Eingabeformular -->
|
||||||
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
|
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
|
||||||
<h2 class="text-lg font-medium text-gray-900 mb-4">
|
<h2 class="text-lg font-medium text-gray-900 mb-4">
|
||||||
{bearbeiteId !== null ? 'Dienststelle bearbeiten' : 'Neue Dienststelle hinzufügen'}
|
{bearbeiteId !== null ? 'Dienststelle bearbeiten' : 'Neue Dienststelle hinzufügen'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="flex gap-4">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Dienststelle
|
Dienststelle
|
||||||
</label>
|
</label>
|
||||||
@@ -145,20 +157,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div class="flex items-end gap-2">
|
||||||
<button
|
<button
|
||||||
on:click={resetForm}
|
on:click={resetForm}
|
||||||
@@ -192,7 +190,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
<div class="px-6 py-4 border-b border-gray-200">
|
<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>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<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">
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Dienststelle
|
Dienststelle
|
||||||
</th>
|
</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">
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Aktionen
|
Aktionen
|
||||||
</th>
|
</th>
|
||||||
@@ -216,9 +211,6 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
{d.name}
|
{d.name}
|
||||||
</td>
|
</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">
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
on:click={() => bearbeiten(d)}
|
on:click={() => bearbeiten(d)}
|
||||||
|
|||||||
15
src/routes/admin/plaetze/+page.server.ts
Normal file
15
src/routes/admin/plaetze/+page.server.ts
Normal 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'
|
||||||
|
};
|
||||||
|
};
|
||||||
259
src/routes/admin/plaetze/+page.svelte
Normal file
259
src/routes/admin/plaetze/+page.svelte
Normal 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>
|
||||||
@@ -30,6 +30,19 @@ export async function GET() {
|
|||||||
nachname: anmeldung.nachname,
|
nachname: anmeldung.nachname,
|
||||||
email: anmeldung.email,
|
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)
|
// Noten als String konvertieren (falls sie als Int gespeichert sind)
|
||||||
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
||||||
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
||||||
@@ -37,7 +50,6 @@ export async function GET() {
|
|||||||
|
|
||||||
// Status-Mapping für Frontend
|
// Status-Mapping für Frontend
|
||||||
status: mapPrismaStatusToFrontend(anmeldung.status),
|
status: mapPrismaStatusToFrontend(anmeldung.status),
|
||||||
// processedBy: anmeldung.processedBy,
|
|
||||||
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
||||||
|
|
||||||
// Wünsche - sicherstellen dass sie existieren
|
// Wünsche - sicherstellen dass sie existieren
|
||||||
@@ -60,6 +72,14 @@ export async function GET() {
|
|||||||
name: anmeldung.zugewiesen.name
|
name: anmeldung.zugewiesen.name
|
||||||
} : undefined,
|
} : 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
|
||||||
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
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 });
|
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({
|
const existingAnmeldung = await prisma.anmeldung.findUnique({
|
||||||
where: { id }
|
where: { id },
|
||||||
|
include: {
|
||||||
|
praktikum: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingAnmeldung) {
|
if (!existingAnmeldung) {
|
||||||
@@ -96,8 +119,37 @@ export async function POST({ request, url }) {
|
|||||||
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
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([
|
await prisma.$transaction([
|
||||||
|
// Anmeldung aktualisieren
|
||||||
prisma.anmeldung.update({
|
prisma.anmeldung.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
@@ -107,8 +159,14 @@ export async function POST({ request, url }) {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
prisma.dienststelle.update({
|
// Plätze in ZeitraumPlaetze reduzieren
|
||||||
where: { id: dienststelleId },
|
prisma.zeitraumPlaetze.update({
|
||||||
|
where: {
|
||||||
|
zeitraumId_dienststelleId: {
|
||||||
|
zeitraumId: zeitraumId,
|
||||||
|
dienststelleId: dienststelleId
|
||||||
|
}
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
plaetze: {
|
plaetze: {
|
||||||
decrement: 1
|
decrement: 1
|
||||||
|
|||||||
@@ -1,193 +1,124 @@
|
|||||||
// src/routes/api/admin/dienststellen/+server.ts
|
// src/routes/api/admin/dienststellen/+server.ts
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
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';
|
// Erstelle für jeden existierenden Zeitraum einen Eintrag mit 0 Plätzen
|
||||||
|
for (const zeitraum of zeitraeume) {
|
||||||
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
|
await prisma.zeitraumPlaetze.create({
|
||||||
function checkAuth(cookies: Cookies) {
|
data: {
|
||||||
return cookies.get('admin-auth') === 'authenticated';
|
zeitraumId: zeitraum.id,
|
||||||
|
dienststelleId: dienststelleId,
|
||||||
|
plaetze: 0 // Standardwert: 0 Plätze
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ cookies }) => {
|
export const GET: RequestHandler = async ({ cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
return new Response(
|
if (adminAuth !== 'authenticated') {
|
||||||
JSON.stringify({ error: 'Nicht autorisiert' }),
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dienststellen = await prisma.dienststelle.findMany({
|
const dienststellen = await prisma.dienststelle.findMany({
|
||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' }
|
||||||
/*
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
Anmeldung: true // Use the correct relation name as defined in your Prisma schema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
return json(dienststellen);
|
return json(dienststellen);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Dienststellen:', 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 }) => {
|
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { name, plaetze } = await request.json();
|
const { name } = await request.json();
|
||||||
|
|
||||||
// Validierung
|
if (!name) {
|
||||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
||||||
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
|
const dienststelle = await prisma.dienststelle.create({
|
||||||
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
|
data: {
|
||||||
|
name,
|
||||||
|
plaetze: 0 // Wird nicht mehr verwendet, aber bleibt im Schema für Kompatibilität
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob Name bereits existiert
|
|
||||||
const existing = await prisma.dienststelle.findFirst({
|
|
||||||
where: { name: name.trim() }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 });
|
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PATCH: RequestHandler = async ({ cookies, request }) => {
|
export const PATCH: RequestHandler = async ({ request, cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id, name, plaetze } = await request.json();
|
const { id, name } = await request.json();
|
||||||
|
|
||||||
// Validierung
|
if (!id || !name) {
|
||||||
if (typeof id !== 'number' || isNaN(id)) {
|
return json({ error: 'ID und Name sind erforderlich' }, { status: 400 });
|
||||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
const dienststelle = await prisma.dienststelle.update({
|
||||||
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
where: { id: parseInt(id) },
|
||||||
}
|
|
||||||
|
|
||||||
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 },
|
|
||||||
data: {
|
data: {
|
||||||
name: name.trim(),
|
name
|
||||||
plaetze
|
}
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(updated);
|
return json(dienststelle);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
|
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 }) => {
|
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
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 {
|
try {
|
||||||
// Prüfe ob Dienststelle existiert
|
const id = url.searchParams.get('id');
|
||||||
const existing = await prisma.dienststelle.findUnique({ where: { id } });
|
if (!id) {
|
||||||
if (!existing) {
|
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob noch Anmeldungen zugewiesen sind
|
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||||
const assignedCount = await prisma.anmeldung.count({
|
await prisma.dienststelle.delete({
|
||||||
where: {
|
where: { id: parseInt(id) }
|
||||||
OR: [
|
|
||||||
{ zugewiesenId: id },
|
|
||||||
{ wunsch1Id: id },
|
|
||||||
{ wunsch2Id: id },
|
|
||||||
{ wunsch3Id: id }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (assignedCount > 0) {
|
return json({ success: true });
|
||||||
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' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Löschen der Dienststelle:', 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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,59 +1,37 @@
|
|||||||
// src/routes/api/admin/login/+server.ts
|
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { prisma } from '$lib/prisma';
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||||
try {
|
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();
|
const { passwort } = await request.json();
|
||||||
|
|
||||||
if (!passwort) {
|
if (!passwort) {
|
||||||
return new Response(
|
return new Response(JSON.stringify({ message: 'Passwort erforderlich' }), { status: 400 });
|
||||||
JSON.stringify({ message: 'Passwort erforderlich' }),
|
|
||||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hier solltest du den Hash aus der Datenbank oder Umgebungsvariable laden
|
const isValid = await bcrypt.compare(passwort, adminRecord.password);
|
||||||
const isValid = await bcrypt.compare(passwort, ADMIN_PASSWORD_HASH);
|
|
||||||
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// Setze konsistenten Cookie-Namen
|
|
||||||
cookies.set('admin-auth', 'authenticated', {
|
cookies.set('admin-auth', 'authenticated', {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
maxAge: 60 * 60 * 24 // 24 Stunden
|
maxAge: 60 * 60 * 24,
|
||||||
});
|
});
|
||||||
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
||||||
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({ message: 'Falsches Passwort' }), { status: 401 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
return new Response(
|
return new Response(JSON.stringify({ message: 'Serverfehler' }), { status: 500 });
|
||||||
JSON.stringify({ message: 'Serverfehler' }),
|
|
||||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
92
src/routes/api/admin/plaetze/+server.ts
Normal file
92
src/routes/api/admin/plaetze/+server.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,160 +1,141 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
// src/routes/api/admin/zeitraeume/+server.ts
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
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';
|
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
|
||||||
|
for (const dienststelle of dienststellen) {
|
||||||
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
|
await prisma.zeitraumPlaetze.create({
|
||||||
function checkAuth(cookies: Cookies) {
|
data: {
|
||||||
return cookies.get('admin-auth') === 'authenticated';
|
zeitraumId: zeitraumId,
|
||||||
|
dienststelleId: dienststelle.id,
|
||||||
|
plaetze: 0 // Standardwert: 0 Plätze
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidDate(date: string | Date) {
|
|
||||||
const parsed = new Date(date)
|
|
||||||
return !isNaN(parsed.getTime())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ cookies }) => {
|
export const GET: RequestHandler = async ({ cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
return new Response(
|
if (adminAuth !== 'authenticated') {
|
||||||
JSON.stringify({ error: 'Nicht autorisiert' }),
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const zeitraeume = await prisma.praktikumszeitraum.findMany();
|
const zeitraeume = await prisma.praktikumszeitraum.findMany({
|
||||||
|
orderBy: { startDatum: 'desc' }
|
||||||
|
});
|
||||||
return json(zeitraeume);
|
return json(zeitraeume);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Praktikumszeiträume:', error);
|
console.error('Fehler beim Laden der Zeiträume:', error);
|
||||||
return json({ error: 'Fehler beim Laden der Praktikumszeiträume' }, { status: 500 });
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ cookies, request }) => {
|
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { bezeichnung, startDatum, endDatum } = await request.json();
|
const { bezeichnung, startDatum, endDatum } = await request.json();
|
||||||
|
|
||||||
// Validierung
|
if (!bezeichnung || !startDatum || !endDatum) {
|
||||||
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
|
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||||
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
|
const start = new Date(startDatum);
|
||||||
return json({ error: 'Ungültiges Datum' }, { status: 400 });
|
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: {
|
data: {
|
||||||
bezeichnung: bezeichnung.trim(),
|
bezeichnung,
|
||||||
startDatum: new Date(startDatum),
|
startDatum: start,
|
||||||
endDatum: new Date(endDatum)
|
endDatum: end
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return json(created, { status: 201 });
|
|
||||||
} catch (error) {
|
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
|
||||||
console.error('Fehler beim Erstellen des Praktikumszeitraums:', error);
|
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
|
||||||
return json({ error: 'Fehler beim Erstellen des Praktikumszeitraums' }, { status: 500 });
|
|
||||||
|
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 }) => {
|
export const PATCH: RequestHandler = async ({ request, cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
||||||
|
|
||||||
// Validierung
|
if (!id || !bezeichnung || !startDatum || !endDatum) {
|
||||||
if (typeof id !== 'number' || isNaN(id)) {
|
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
|
const start = new Date(startDatum);
|
||||||
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
|
const end = new Date(endDatum);
|
||||||
|
|
||||||
|
if (end <= start) {
|
||||||
|
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
|
const zeitraum = await prisma.praktikumszeitraum.update({
|
||||||
return json({ error: 'Ungültiges Datum' }, { status: 400 });
|
where: { id: parseInt(id) },
|
||||||
}
|
|
||||||
|
|
||||||
// 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 },
|
|
||||||
data: {
|
data: {
|
||||||
bezeichnung: bezeichnung.trim(),
|
bezeichnung,
|
||||||
startDatum: new Date(startDatum),
|
startDatum: start,
|
||||||
endDatum: new Date(endDatum)
|
endDatum: end
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(updated);
|
return json(zeitraum);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Fehler beim Aktualisieren des Praktikumszeitraums:', error);
|
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
|
||||||
return json({ error: 'Fehler beim Aktualisieren des Praktikumszeitraums' }, { status: 500 });
|
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 }) => {
|
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||||
if (!checkAuth(cookies)) {
|
const adminAuth = cookies.get('admin-auth');
|
||||||
|
if (adminAuth !== 'authenticated') {
|
||||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
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 {
|
try {
|
||||||
// Prüfe ob Praktikumszeitraum existiert
|
const id = url.searchParams.get('id');
|
||||||
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
|
if (!id) {
|
||||||
if (!existing) {
|
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||||
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hier könntest du prüfen, ob noch Anmeldungen mit diesem Zeitraum verknüpft sind
|
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||||
// const assignedCount = await prisma.anmeldung.count({
|
await prisma.praktikumszeitraum.delete({
|
||||||
// where: { praktikumszeitraumId: id }
|
where: { id: parseInt(id) }
|
||||||
// });
|
});
|
||||||
//
|
|
||||||
// if (assignedCount > 0) {
|
|
||||||
// return json({
|
|
||||||
// error: 'Praktikumszeitraum kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
|
|
||||||
// }, { status: 400 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
await prisma.praktikumszeitraum.delete({ where: { id } });
|
return json({ success: true });
|
||||||
return json({ success: true, message: 'Praktikumszeitraum erfolgreich gelöscht' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Löschen des Praktikumszeitraums:', error);
|
console.error('Fehler beim Löschen des Zeitraums:', error);
|
||||||
return json({ error: 'Fehler beim Löschen des Praktikumszeitraums' }, { status: 500 });
|
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
295
src/routes/api/admin/zeitraum-plaetze/+page.svelte
Normal file
295
src/routes/api/admin/zeitraum-plaetze/+page.svelte
Normal 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>
|
||||||
114
src/routes/api/admin/zeitraum-plaetze/+server.ts
Normal file
114
src/routes/api/admin/zeitraum-plaetze/+server.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
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 () => {
|
export const GET: RequestHandler = async () => {
|
||||||
|
const prisma = await getPrismaClient(); // Hier Prisma Client holen
|
||||||
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
|
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
|
||||||
return json(dienststellen);
|
return json(dienststellen);
|
||||||
};
|
};
|
||||||
@@ -2,5 +2,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()]
|
plugins: [sveltekit()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['@prisma/client']
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
noExternal: process.env.NODE_ENV === 'production' ? ['@prisma/client'] : [],
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user