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
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"overrides": {
|
||||
"cookie": "^0.7.0"
|
||||
"cookie": "^0.7.0"
|
||||
},
|
||||
"type": "module",
|
||||
"prisma": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prisma": "^6.12.0",
|
||||
"prisma": "^6.19.0",
|
||||
"svelte": "^5.36.17",
|
||||
"svelte-check": "^4.3.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
@@ -46,7 +46,9 @@
|
||||
"vite-plugin": "^0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.12.0",
|
||||
"@prisma/adapter-better-sqlite3": "^7.0.0",
|
||||
"@prisma/client": "^6.19.0",
|
||||
"@prisma/migrate": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.13",
|
||||
"bcryptjs": "^3.0.2"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
provider = "sqlite"
|
||||
url = "file:./praktika.db"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Admin {
|
||||
@@ -25,11 +25,14 @@ model EmailConfig {
|
||||
model Dienststelle {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
plaetze Int @default(0)
|
||||
plaetze Int @default(0) // Standard-Plätze, wird nicht mehr direkt verwendet
|
||||
anmeldungenWunsch1 Anmeldung[] @relation("Wunsch1")
|
||||
anmeldungenWunsch2 Anmeldung[] @relation("Wunsch2")
|
||||
anmeldungenWunsch3 Anmeldung[] @relation("Wunsch3")
|
||||
zugewiesene Anmeldung[] @relation("Zugewiesen")
|
||||
|
||||
// Neue Relation zu ZeitraumPlaetze
|
||||
zeitraumPlaetze ZeitraumPlaetze[]
|
||||
}
|
||||
|
||||
model Praktikumszeitraum {
|
||||
@@ -38,6 +41,26 @@ model Praktikumszeitraum {
|
||||
startDatum DateTime
|
||||
endDatum DateTime
|
||||
anmeldungen Anmeldung[] @relation("PraktikumszeitraumAnmeldungen")
|
||||
|
||||
// Neue Relation zu ZeitraumPlaetze
|
||||
zeitraumPlaetze ZeitraumPlaetze[]
|
||||
}
|
||||
|
||||
// Neue Zwischentabelle für Plätze pro Zeitraum und Dienststelle
|
||||
model ZeitraumPlaetze {
|
||||
id Int @id @default(autoincrement())
|
||||
zeitraumId Int
|
||||
dienststelleId Int
|
||||
plaetze Int @default(0)
|
||||
|
||||
zeitraum Praktikumszeitraum @relation(fields: [zeitraumId], references: [id], onDelete: Cascade)
|
||||
dienststelle Dienststelle @relation(fields: [dienststelleId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Unique constraint: Pro Zeitraum und Dienststelle nur ein Eintrag
|
||||
@@unique([zeitraumId, dienststelleId])
|
||||
@@index([zeitraumId])
|
||||
@@index([dienststelleId])
|
||||
@@map("zeitraum_plaetze")
|
||||
}
|
||||
|
||||
// Erweiterte Status-Enum für bessere Nachverfolgung
|
||||
@@ -70,7 +93,6 @@ model Anmeldung {
|
||||
status Status @default(OFFEN)
|
||||
|
||||
// Neue Felder für Status-Tracking
|
||||
// processedBy String? // Wer bearbeitet die Anmeldung
|
||||
processedAt DateTime? // Wann wurde sie bearbeitet
|
||||
|
||||
// Praktikumszeitraum Relation
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
description: 'Praktikumszeiträume verwalten',
|
||||
color: 'bg-purple-600 hover:bg-purple-700'
|
||||
},
|
||||
{
|
||||
href: '/admin/plaetze',
|
||||
title: 'Plätze verwalten',
|
||||
icon: '📊',
|
||||
description: 'Praktikumsplätze pro Zeitraum und Dienststelle festlegen',
|
||||
color: 'bg-indigo-600 hover:bg-indigo-700'
|
||||
},
|
||||
{
|
||||
href: '/admin/change-password',
|
||||
title: 'Passwort ändern',
|
||||
@@ -68,4 +75,4 @@
|
||||
.group:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -22,6 +22,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
function formatGeburtsdatum(dateString: string | undefined): string {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function formatProcessedDate(timestamp: number | undefined): string {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp).toLocaleDateString('de-DE', {
|
||||
@@ -33,7 +42,44 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Vereinfachte Logik ohne processing Status
|
||||
function formatZeitraum(zeitraum: any): string {
|
||||
if (!zeitraum) return '-';
|
||||
const start = new Date(zeitraum.startDatum).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
const end = new Date(zeitraum.endDatum).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
return `${zeitraum.bezeichnung} (${start} - ${end})`;
|
||||
}
|
||||
|
||||
// Schulart formatieren
|
||||
function formatSchulart(schulart: string | undefined): string {
|
||||
if (!schulart) return '-';
|
||||
const mapping: Record<string, string> = {
|
||||
'Gymnasium': 'Gymnasium',
|
||||
'KGS_Gymnasialzweig': 'KGS Gymnasialzweig',
|
||||
'Fachoberschule': 'Fachoberschule',
|
||||
'Realschule': 'Realschule',
|
||||
'KGSR': 'KGS Realschulzweig',
|
||||
'IGSR': 'IGS Realschulzweig'
|
||||
};
|
||||
return mapping[schulart] || schulart;
|
||||
}
|
||||
|
||||
// Sozialverhalten kürzen
|
||||
function formatSozialverhalten(sv: string | undefined): string {
|
||||
if (!sv) return '-';
|
||||
if (sv === 'Entspricht den Erwartungen in vollem Umfang') return 'Voll entspr.';
|
||||
if (sv === 'Entspricht den Erwartungen') return 'Entsprechend';
|
||||
if (sv === 'Entspricht den Erwartungen mit Einschränkungen') return 'Mit Einschr.';
|
||||
return sv;
|
||||
}
|
||||
|
||||
function canBeAccepted(status: string): boolean {
|
||||
return status === 'pending';
|
||||
}
|
||||
@@ -47,166 +93,234 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="min-w-0 w-1/4 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Bewerber/in
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Persönliche Daten
|
||||
</th>
|
||||
<th class="w-16 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Noten
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Kontakt & Adresse
|
||||
</th>
|
||||
<th class="min-w-0 w-1/3 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Wünsche / Zuweisung
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Schule & Noten
|
||||
</th>
|
||||
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Eingegangen
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Praktikum & Wünsche
|
||||
</th>
|
||||
<th class="w-40 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Dokumente
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each anmeldungen as anmeldung (anmeldung.id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<!-- Status (processing Styling entfernt) -->
|
||||
<td class="px-4 py-4 whitespace-nowrap">
|
||||
<tr class="hover:bg-gray-50 align-top">
|
||||
<!-- Status -->
|
||||
<td class="px-3 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {getStatusColor(anmeldung.status || 'pending')}">
|
||||
{getStatusText(anmeldung.status || 'pending')}
|
||||
</span>
|
||||
<div class="text-xs text-gray-400 mt-2">
|
||||
Eingang:<br>
|
||||
{formatDate(anmeldung.timestamp)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Bewerber/in -->
|
||||
<td class="px-4 py-4">
|
||||
<!-- Persönliche Daten -->
|
||||
<td class="px-3 py-4">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{anmeldung.anrede} {anmeldung.vorname} {anmeldung.nachname}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 break-all">
|
||||
{anmeldung.email}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
<span class="font-medium">Geb.:</span> {formatGeburtsdatum(anmeldung.geburtsdatum)}
|
||||
</div>
|
||||
{#if anmeldung.alter}
|
||||
<div class="text-xs text-gray-500">
|
||||
<span class="font-medium">Alter:</span> {anmeldung.alter} Jahre
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Kontakt & Adresse -->
|
||||
<td class="px-3 py-4 text-sm">
|
||||
<div class="text-xs space-y-1">
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Adresse:</span><br>
|
||||
<span class="text-gray-600">
|
||||
{anmeldung.strasse || '-'} {anmeldung.hausnummer || ''}<br>
|
||||
{anmeldung.plz || ''} {anmeldung.ort || ''}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Tel:</span>
|
||||
{#if anmeldung.telefon}
|
||||
<a href="tel:{anmeldung.telefon}" class="text-blue-600 hover:text-blue-800">{anmeldung.telefon}</a>
|
||||
{:else}
|
||||
<span class="text-gray-400">-</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">E-Mail:</span><br>
|
||||
<a href="mailto:{anmeldung.email}" class="text-blue-600 hover:text-blue-800 break-all text-xs">{anmeldung.email}</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Schule & Noten -->
|
||||
<td class="px-3 py-4 text-sm">
|
||||
<div class="text-xs space-y-1">
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Schulart:</span><br>
|
||||
<span class="text-gray-900">{formatSchulart(anmeldung.schulart)}</span>
|
||||
</div>
|
||||
{#if anmeldung.schulklasse}
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Klasse:</span>
|
||||
<span class="text-gray-900">{anmeldung.schulklasse}. Klasse</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pt-1 border-t border-gray-100">
|
||||
<span class="font-medium text-gray-700">Noten:</span>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
|
||||
D: <span class="font-bold ml-1">{anmeldung.noteDeutsch || '-'}</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100">
|
||||
M: <span class="font-bold ml-1">{anmeldung.noteMathe || '-'}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if anmeldung.sozialverhalten}
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Sozialverh.:</span>
|
||||
<span class="text-gray-900 text-xs">{formatSozialverhalten(anmeldung.sozialverhalten)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Praktikum & Wünsche -->
|
||||
<td class="px-3 py-4 text-sm">
|
||||
<div class="text-xs space-y-2">
|
||||
<!-- Zeitraum -->
|
||||
{#if anmeldung.zeitraum}
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Zeitraum:</span><br>
|
||||
<span class="text-gray-900">{formatZeitraum(anmeldung.zeitraum)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Zugewiesene Dienststelle -->
|
||||
{#if anmeldung.assignedDienststelle}
|
||||
<div class="p-2 bg-green-50 border border-green-200 rounded">
|
||||
<div class="flex items-center text-green-700">
|
||||
<svg class="w-4 h-4 mr-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="font-semibold">Zugewiesen:</span>
|
||||
</div>
|
||||
<div class="text-green-800 mt-1">{anmeldung.assignedDienststelle.name}</div>
|
||||
{#if anmeldung.processedAt}
|
||||
<div class="text-xs text-green-600 mt-1">
|
||||
{formatProcessedDate(anmeldung.processedAt)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Alle 3 Wünsche -->
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Wünsche:</span>
|
||||
<div class="space-y-1 mt-1">
|
||||
{#if anmeldung.wunsch1}
|
||||
<div class="flex items-start">
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-600 rounded-full mr-1 flex-shrink-0">1</span>
|
||||
<span class="text-gray-900 leading-tight">{anmeldung.wunsch1.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if anmeldung.wunsch2}
|
||||
<div class="flex items-start">
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full mr-1 flex-shrink-0">2</span>
|
||||
<span class="text-gray-900 leading-tight">{anmeldung.wunsch2.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if anmeldung.wunsch3}
|
||||
<div class="flex items-start">
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-400 rounded-full mr-1 flex-shrink-0">3</span>
|
||||
<span class="text-gray-900 leading-tight">{anmeldung.wunsch3.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
|
||||
<span class="text-gray-400">Keine Wünsche</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motivation -->
|
||||
{#if anmeldung.motivation}
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Motivation:</span>
|
||||
<p class="text-gray-600 mt-1 text-xs whitespace-pre-wrap line-clamp-3">{anmeldung.motivation}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Dokumente / PDFs -->
|
||||
<td class="px-3 py-4">
|
||||
{#if anmeldung.pdfs && anmeldung.pdfs.length > 0}
|
||||
<div class="mt-2">
|
||||
{#each anmeldung.pdfs as pdf}
|
||||
<div class="space-y-1">
|
||||
{#each anmeldung.pdfs as pdf, index}
|
||||
<a
|
||||
href="/api/admin/pdf?path={encodeURIComponent(pdf.pfad)}"
|
||||
target="_blank"
|
||||
class="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mr-2 mb-1"
|
||||
class="flex items-center text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 p-1 rounded"
|
||||
>
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg class="w-4 h-4 mr-1 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
PDF ansehen
|
||||
PDF {index + 1}
|
||||
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Noten -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{#if anmeldung.noteDeutsch || anmeldung.noteMathe}
|
||||
<div class="space-y-1">
|
||||
{#if anmeldung.noteDeutsch}
|
||||
<div class="text-xs">D: {anmeldung.noteDeutsch}</div>
|
||||
{/if}
|
||||
{#if anmeldung.noteMathe}
|
||||
<div class="text-xs">M: {anmeldung.noteMathe}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-gray-400">-</span>
|
||||
<span class="text-xs text-gray-400">Keine Dokumente</span>
|
||||
{/if}
|
||||
{#if anmeldung.sozialverhalten}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
SV: {anmeldung.sozialverhalten}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Wünsche / Zuweisung -->
|
||||
<td class="px-4 py-4 text-sm text-gray-900">
|
||||
<!-- Zugewiesene Dienststelle (falls vorhanden) -->
|
||||
{#if anmeldung.assignedDienststelle}
|
||||
<div class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="font-medium text-green-800 text-sm">Zugewiesen:</span>
|
||||
</div>
|
||||
<div class="text-sm text-green-700 mt-1 ml-6">
|
||||
{anmeldung.assignedDienststelle.name}
|
||||
</div>
|
||||
{#if anmeldung.processedAt}
|
||||
<div class="text-xs text-green-600 mt-1 ml-6">
|
||||
{formatProcessedDate(anmeldung.processedAt)}
|
||||
{#if anmeldung.processedBy}
|
||||
von {anmeldung.processedBy}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Wünsche -->
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-medium text-gray-500 uppercase tracking-wider">Wünsche:</div>
|
||||
{#if anmeldung.wunsch1}
|
||||
<div class="flex items-start">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-600 rounded-full mr-2 mt-0.5 flex-shrink-0">1</span>
|
||||
<span class="text-sm leading-5">{anmeldung.wunsch1.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if anmeldung.wunsch2}
|
||||
<div class="flex items-start">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-500 rounded-full mr-2 mt-0.5 flex-shrink-0">2</span>
|
||||
<span class="text-sm leading-5">{anmeldung.wunsch2.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if anmeldung.wunsch3}
|
||||
<div class="flex items-start">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-400 rounded-full mr-2 mt-0.5 flex-shrink-0">3</span>
|
||||
<span class="text-sm leading-5">{anmeldung.wunsch3.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !anmeldung.wunsch1 && !anmeldung.wunsch2 && !anmeldung.wunsch3}
|
||||
<span class="text-gray-400 text-sm">Keine Wünsche angegeben</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Eingegangen -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<div class="text-sm">{formatDate(anmeldung.timestamp)}</div>
|
||||
</td>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<td class="px-3 py-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
{#if canBeAccepted(anmeldung.status || 'pending')}
|
||||
<button
|
||||
on:click={() => dispatch('accept', { id: anmeldung.id })}
|
||||
class="text-green-600 hover:text-green-900 px-3 py-1 rounded text-xs font-medium border border-green-200 hover:bg-green-50 w-full text-center"
|
||||
class="text-green-600 hover:text-green-900 px-3 py-1.5 rounded text-xs font-medium border border-green-300 hover:bg-green-50 text-center whitespace-nowrap"
|
||||
>
|
||||
Annehmen
|
||||
✓ Annehmen
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if canBeRejected(anmeldung.status || 'pending')}
|
||||
<button
|
||||
on:click={() => dispatch('reject', { id: anmeldung.id })}
|
||||
class="text-red-600 hover:text-red-900 px-3 py-1 rounded text-xs font-medium border border-red-200 hover:bg-red-50 w-full text-center"
|
||||
class="text-red-600 hover:text-red-900 px-3 py-1.5 rounded text-xs font-medium border border-red-300 hover:bg-red-50 text-center whitespace-nowrap"
|
||||
>
|
||||
Ablehnen
|
||||
✗ Ablehnen
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click={() => dispatch('delete', { id: anmeldung.id })}
|
||||
class="text-gray-600 hover:text-gray-900 px-3 py-1 rounded text-xs font-medium border border-gray-200 hover:bg-gray-50 w-full text-center"
|
||||
class="text-gray-600 hover:text-gray-900 px-3 py-1.5 rounded text-xs font-medium border border-gray-300 hover:bg-gray-50 text-center whitespace-nowrap"
|
||||
>
|
||||
Löschen
|
||||
🗑 Löschen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -214,4 +328,13 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</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 ablehnungHinweis = '';
|
||||
let showAblehnungModal = false;
|
||||
let showIgsHinweis = false;
|
||||
let alter = '';
|
||||
|
||||
// Validierungsfehler für Echtzeit-Anzeige
|
||||
let alterFehler = '';
|
||||
let notenFehler = '';
|
||||
let sozialverhaltenFehler = '';
|
||||
|
||||
// Berechnung des Alters
|
||||
$: {
|
||||
if (geburtsdatum && zeitraum && zeitraeume.length > 0) {
|
||||
const gewaehlterZeitraum = zeitraeume.find(z => z.id == zeitraum);
|
||||
@@ -47,34 +53,81 @@
|
||||
if (monthDiff < 0 || (monthDiff === 0 && praktikumStart.getDate() < geburt.getDate())) {
|
||||
altersberechnung--;
|
||||
}
|
||||
alter = altersberechnung.toString();
|
||||
alter = altersberechnung.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Überwachung für IGS + Klasse 7 Kombination
|
||||
// Echtzeit-Validierung: Alter
|
||||
$: {
|
||||
if (["KGSR", "IGSR"].includes(schulart) && schulklasse === '7') {
|
||||
showIgsHinweis = true;
|
||||
const altersWert = parseInt(alter);
|
||||
if (alter && !isNaN(altersWert) && altersWert < 14) {
|
||||
alterFehler = 'Du musst mindestens 14 Jahre alt sein, um ein Praktikum beginnen zu können.';
|
||||
} else {
|
||||
alterFehler = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Echtzeit-Validierung: Noten
|
||||
$: {
|
||||
const deutsch = parseInt(noteDeutsch);
|
||||
const mathe = parseInt(noteMathe);
|
||||
const klasse = parseInt(schulklasse);
|
||||
|
||||
if (noteDeutsch && noteMathe && schulart) {
|
||||
// Gymnasium oder KGS_Gymnasialzweig: mindestens 4 in Deutsch UND Mathe
|
||||
if (['Gymnasium', 'KGS_Gymnasialzweig'].includes(schulart)) {
|
||||
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 4 || mathe > 4)) {
|
||||
notenFehler = 'Du brauchst mindestens eine 4 in Deutsch und Mathematik.';
|
||||
} else {
|
||||
notenFehler = '';
|
||||
}
|
||||
}
|
||||
// Fachoberschule Klasse 11 oder 12: mindestens 4 in Deutsch UND Mathe
|
||||
else if (schulart === 'Fachoberschule' && (klasse === 11 || klasse === 12)) {
|
||||
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 4 || mathe > 4)) {
|
||||
notenFehler = 'Du brauchst mindestens eine 4 in Deutsch und Mathematik.';
|
||||
} else {
|
||||
notenFehler = '';
|
||||
}
|
||||
}
|
||||
// Alle anderen: mindestens 3 in Deutsch UND Mathe
|
||||
else {
|
||||
if (!isNaN(deutsch) && !isNaN(mathe) && (deutsch > 3 || mathe > 3)) {
|
||||
notenFehler = 'Du brauchst mindestens eine 3 in Deutsch und Mathematik.';
|
||||
} else {
|
||||
notenFehler = '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notenFehler = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Echtzeit-Validierung: Sozialverhalten
|
||||
$: {
|
||||
if (sozialverhalten === 'Entspricht den Erwartungen mit Einschränkungen') {
|
||||
sozialverhaltenFehler = 'Dein Sozialverhalten muss mindestens den Erwartungen entsprechen.';
|
||||
} else {
|
||||
sozialverhaltenFehler = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen ob Formular gültig ist
|
||||
$: formHatFehler = alterFehler !== '' || notenFehler !== '' || sozialverhaltenFehler !== '';
|
||||
|
||||
$: filteredDienststellen = (dienststellen ?? []).filter(d => {
|
||||
if (d.plaetze <= 0) return false;
|
||||
|
||||
// PK Mitte nur anzeigen wenn mindestens 18 Jahre alt
|
||||
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
|
||||
return parseInt(alter) >= 18;
|
||||
}
|
||||
return true;
|
||||
if (d.name.includes('PK Mitte') || d.name.toLowerCase().includes('polizeikommissariat mitte')) {
|
||||
return parseInt(alter) >= 18;
|
||||
}
|
||||
);
|
||||
return true;
|
||||
});
|
||||
|
||||
$: filteredZeitraeume = (zeitraeume ?? []).filter(zeitraum => {
|
||||
const heute = new Date();
|
||||
const startDatum = new Date(zeitraum.startDatum);
|
||||
|
||||
// Nur Zeiträume anzeigen, die noch nicht gestartet haben
|
||||
return startDatum > heute;
|
||||
});
|
||||
|
||||
@@ -82,7 +135,7 @@
|
||||
|
||||
$: hideSozialVerhalten =
|
||||
Number(schulklasse) >= 11 &&
|
||||
["Gymnasium", "KGS_Gymnasialzweig", "IGS_Gymnasialzweig"].includes(schulart);
|
||||
["Gymnasium", "KGS_Gymnasialzweig", "Fachoberschule"].includes(schulart);
|
||||
|
||||
onMount(async () => {
|
||||
const resDienstelle = await fetch('/api/dienststellen');
|
||||
@@ -91,6 +144,7 @@
|
||||
const resZeitraeume = await fetch('/api/zeitraeume');
|
||||
zeitraeume = await resZeitraeume.json();
|
||||
});
|
||||
|
||||
interface Zeitraum {
|
||||
id: number;
|
||||
bezeichnung: string;
|
||||
@@ -98,9 +152,6 @@
|
||||
endDatum: string;
|
||||
}
|
||||
let zeitraeume: Zeitraum[] = [];
|
||||
//let neuerBezeichnung = '';
|
||||
//let neuerstartDatum = '';
|
||||
//let neuerendDatum = '';
|
||||
let fehlermeldung = '';
|
||||
let bearbeiteId: number | null = null;
|
||||
|
||||
@@ -124,12 +175,21 @@
|
||||
noteDeutsch = '';
|
||||
noteMathe = '';
|
||||
sozialverhalten = '';
|
||||
schulklasse = '';
|
||||
pdfDateien = [];
|
||||
fileInputKey += 1;
|
||||
success = false;
|
||||
alterFehler = '';
|
||||
notenFehler = '';
|
||||
sozialverhaltenFehler = '';
|
||||
}
|
||||
|
||||
async function anmelden() {
|
||||
// Abbrechen wenn Validierungsfehler vorhanden
|
||||
if (formHatFehler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
|
||||
data.append('anrede', anrede);
|
||||
@@ -158,36 +218,6 @@
|
||||
data.append('pdfs', pdf);
|
||||
}
|
||||
|
||||
const altersWert = parseInt(alter);
|
||||
if (isNaN(altersWert) || altersWert < 14) {
|
||||
ablehnungHinweis = 'Du musst mindestens 14 Jahre alt sein, um ein Praktikum beginnen zu können. Bewirb dich gern erneut, wenn du das Mindestalter erreicht hast.';
|
||||
showAblehnungModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const deutsch = parseInt(noteDeutsch);
|
||||
const mathe = parseInt(noteMathe);
|
||||
|
||||
if (['Gymnasium', 'KGS_Gymnasialzweig', 'IGS_Gymnasialzweig'].includes(schulart) ) {
|
||||
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 4 && mathe > 4) {
|
||||
ablehnungHinweis = 'Du brauchst mindestens eine 4 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
|
||||
showAblehnungModal = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (isNaN(deutsch) || isNaN(mathe) || deutsch > 3 && mathe > 3) {
|
||||
ablehnungHinweis = 'Du brauchst mindestens eine 3 in Deutsch oder Mathematik, um dich bewerben zu können. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
|
||||
showAblehnungModal = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sozialverhalten === 'Entspricht den Erwartungen mit Einschränkungen') {
|
||||
ablehnungHinweis = 'Dein Sozialverhalten muss mindestens den Erwartungen entsprechen. Bewirb dich gern erneut, wenn du die Voraussetzung erfüllst.';
|
||||
showAblehnungModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch('/api/anmelden', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
@@ -234,17 +264,13 @@
|
||||
<option value="" disabled selected hidden>Schulart wählen</option>
|
||||
<option value="Gymnasium">Gymnasium</option>
|
||||
<option value="KGS_Gymnasialzweig">KGS Gymnasialzweig</option>
|
||||
<option value="IGS_Gymnasialzweig">IGS Gymniasalzweig Fachoberschule</option>
|
||||
<option value="Fachoberschule">Fachoberschule</option>
|
||||
<option value="Realschule">Realschule</option>
|
||||
<option value="KGSR">Kooperative Gesamtschule Realschulzweg</option>
|
||||
<option value="IGSR">Integrierte Gesamtschule Realschulzweig</option>
|
||||
</select>
|
||||
<!-- Noten -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<input bind:value={noteDeutsch} type="number" min="1" max="6" placeholder="Note in Deutsch" required class="input" />
|
||||
<input bind:value={noteMathe} type="number" min="1" max="6" placeholder="Note in Mathe" required class="input" />
|
||||
</div>
|
||||
|
||||
<!-- Schulklasse -->
|
||||
<select bind:value={schulklasse} required class="input">
|
||||
<option value="" disabled selected hidden>Schulklasse</option>
|
||||
<option value="7">7. Klasse</option>
|
||||
@@ -254,16 +280,56 @@
|
||||
<option value="11">11. Klasse</option>
|
||||
<option value="12">12. Klasse</option>
|
||||
<option value="13">13. Klasse</option>
|
||||
</select>
|
||||
<!-- Sozialverhalten -->
|
||||
</select>
|
||||
|
||||
<!-- Sozialverhalten mit Echtzeit-Validierung -->
|
||||
{#if !hideSozialVerhalten}
|
||||
<select bind:value={sozialverhalten} required class="input">
|
||||
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
|
||||
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
|
||||
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</option>
|
||||
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
|
||||
</select>
|
||||
{/if}
|
||||
<div class="col-span-2">
|
||||
<select
|
||||
bind:value={sozialverhalten}
|
||||
required
|
||||
class="input"
|
||||
class:input-error={sozialverhaltenFehler}
|
||||
>
|
||||
<option value="" disabled selected hidden>Sozialverhalten auswählen</option>
|
||||
<option value="Entspricht den Erwartungen in vollem Umfang">Entspricht den Erwartungen in vollem Umfang</option>
|
||||
<option value="Entspricht den Erwartungen">Entspricht den Erwartungen</option>
|
||||
<option value="Entspricht den Erwartungen mit Einschränkungen">Entspricht den Erwartungen mit Einschränkungen</option>
|
||||
</select>
|
||||
{#if sozialverhaltenFehler}
|
||||
<p class="text-red-600 text-sm mt-1">{sozialverhaltenFehler}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Noten mit Echtzeit-Validierung -->
|
||||
<div class="col-span-2">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<input
|
||||
bind:value={noteDeutsch}
|
||||
type="number"
|
||||
min="1"
|
||||
max="6"
|
||||
placeholder="Note in Deutsch"
|
||||
required
|
||||
class="input"
|
||||
class:input-error={notenFehler}
|
||||
/>
|
||||
<input
|
||||
bind:value={noteMathe}
|
||||
type="number"
|
||||
min="1"
|
||||
max="6"
|
||||
placeholder="Note in Mathe"
|
||||
required
|
||||
class="input"
|
||||
class:input-error={notenFehler}
|
||||
/>
|
||||
</div>
|
||||
{#if notenFehler}
|
||||
<p class="text-red-600 text-sm mt-1">{notenFehler}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
@@ -273,13 +339,20 @@
|
||||
<option
|
||||
value={d.id}>{d.bezeichnung} ({new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} - {new Date(d.endDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})
|
||||
</option>
|
||||
startDatum = {new Date(d.startDatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||
{/each}
|
||||
|
||||
</select>
|
||||
<p>Startdatum: {startDatum}</p>
|
||||
</div>
|
||||
|
||||
<!-- Alter-Anzeige mit Echtzeit-Validierung -->
|
||||
{#if alter}
|
||||
<div class="text-sm" class:text-gray-600={!alterFehler} class:text-red-600={alterFehler}>
|
||||
Alter zu Praktikumsbeginn: {alter} Jahre
|
||||
{#if alterFehler}
|
||||
<p class="font-semibold mt-1">{alterFehler}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Wunschdienststellen -->
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<select bind:value={wunsch1Id} required>
|
||||
@@ -324,24 +397,6 @@
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<!-- IGS Klasse 7 Hinweis Modal -->
|
||||
{#if showIgsHinweis}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-blue-600">Wichtiger Hinweis</h3>
|
||||
<p class="text-gray-700">
|
||||
Bitte keinen Entwicklungsbericht beifügen, sondern Noten von der Schule bescheinigen lassen (zu Bewerbungszwecken).
|
||||
</p>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
on:click={() => showIgsHinweis = false}
|
||||
>
|
||||
Verstanden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showAblehnungModal}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
||||
@@ -356,12 +411,24 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Button -->
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white py-3 rounded-xl hover:bg-blue-700 transition-all">
|
||||
<!-- Button - deaktiviert bei Validierungsfehlern -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formHatFehler}
|
||||
class="w-full py-3 rounded-xl transition-all"
|
||||
class:bg-blue-600={!formHatFehler}
|
||||
class:hover:bg-blue-700={!formHatFehler}
|
||||
class:text-white={!formHatFehler}
|
||||
class:bg-gray-400={formHatFehler}
|
||||
class:cursor-not-allowed={formHatFehler}
|
||||
>
|
||||
Jetzt anmelden
|
||||
</button>
|
||||
|
||||
{#if formHatFehler}
|
||||
<p class="text-red-600 text-sm text-center">Bitte korrigiere die markierten Fehler, um fortzufahren.</p>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white p-6 rounded shadow-lg text-center space-y-4 max-w-sm w-full">
|
||||
@@ -376,17 +443,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if alter}
|
||||
<div class="text-sm text-gray-600">
|
||||
Alter zu Praktikumsbeginn: {alter} Jahre
|
||||
{#if parseInt(alter) < 14}
|
||||
<span class="text-red-600 font-semibold">
|
||||
(Mindestalter 14 Jahre erforderlich)
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if fehler}
|
||||
<p class="text-red-600">{fehler}</p>
|
||||
{/if}
|
||||
@@ -396,15 +452,23 @@
|
||||
<style>
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid #d1d5db; /* border-gray-300 */
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
padding: 0.75rem; /* p-3 */
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
outline: none;
|
||||
transition: box-shadow 0.2s;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #3b82f6; /* focus:ring-2 focus:ring-blue-500 */
|
||||
box-shadow: 0 0 0 2px #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.input-error {
|
||||
border-color: #dc2626;
|
||||
background-color: #fef2f2;
|
||||
}
|
||||
.input-error:focus {
|
||||
box-shadow: 0 0 0 2px #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
@@ -3,9 +3,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
import AdminHeader from '$lib/components/AdminHeader.svelte';
|
||||
|
||||
let dienststellen: { id: number; name: string; plaetze: number }[] = [];
|
||||
let dienststellen: { id: number; name: string }[] = [];
|
||||
let neuerName = '';
|
||||
let neuePlaetze = 0;
|
||||
let fehlermeldung = '';
|
||||
let bearbeiteId: number | null = null;
|
||||
let isLoading = true;
|
||||
@@ -30,9 +29,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function bearbeiten(d: { id: number; name: string; plaetze: number }) {
|
||||
function bearbeiten(d: { id: number; name: string }) {
|
||||
neuerName = d.name;
|
||||
neuePlaetze = d.plaetze;
|
||||
bearbeiteId = d.id;
|
||||
}
|
||||
|
||||
@@ -46,8 +44,8 @@
|
||||
try {
|
||||
const method = bearbeiteId ? 'PATCH' : 'POST';
|
||||
const body = bearbeiteId
|
||||
? { id: bearbeiteId, name: neuerName, plaetze: neuePlaetze }
|
||||
: { name: neuerName, plaetze: neuePlaetze };
|
||||
? { id: bearbeiteId, name: neuerName }
|
||||
: { name: neuerName };
|
||||
|
||||
const res = await fetch('/api/admin/dienststellen', {
|
||||
method,
|
||||
@@ -57,7 +55,6 @@
|
||||
|
||||
if (res.ok) {
|
||||
neuerName = '';
|
||||
neuePlaetze = 0;
|
||||
bearbeiteId = null;
|
||||
await ladeDienststellen();
|
||||
} else {
|
||||
@@ -71,7 +68,7 @@
|
||||
}
|
||||
|
||||
async function loeschen(id: number) {
|
||||
if (!confirm('Diese Dienststelle wirklich löschen?')) return;
|
||||
if (!confirm('Diese Dienststelle wirklich löschen? Alle zugehörigen Platzangaben werden ebenfalls gelöscht.')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/dienststellen?id=${id}`, { method: 'DELETE' });
|
||||
@@ -91,7 +88,6 @@
|
||||
|
||||
function resetForm() {
|
||||
neuerName = '';
|
||||
neuePlaetze = 0;
|
||||
bearbeiteId = null;
|
||||
}
|
||||
|
||||
@@ -108,7 +104,7 @@
|
||||
showBackButton={true}
|
||||
/>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||
<main class="max-w-5xl mx-auto px-4 py-6">
|
||||
{#if fehlermeldung}
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
@@ -125,14 +121,30 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700">
|
||||
Hier werden nur die Dienststellen verwaltet. Die Anzahl der Praktikumsplätze pro Zeitraum können Sie unter
|
||||
<a href="/admin/plaetze" class="font-medium underline hover:text-blue-800">Plätze verwalten</a> festlegen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Eingabeformular -->
|
||||
<div class="bg-white shadow-sm rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">
|
||||
{bearbeiteId !== null ? 'Dienststelle bearbeiten' : 'Neue Dienststelle hinzufügen'}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Dienststelle
|
||||
</label>
|
||||
@@ -145,20 +157,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="plaetze" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Anzahl Plätze
|
||||
</label>
|
||||
<input
|
||||
id="plaetze"
|
||||
type="number"
|
||||
bind:value={neuePlaetze}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-2">
|
||||
<button
|
||||
on:click={resetForm}
|
||||
@@ -192,7 +190,7 @@
|
||||
{:else}
|
||||
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900">Alle Dienststellen ({dienststellen.length})</h3>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
@@ -202,9 +200,6 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Dienststelle
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Plätze
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aktionen
|
||||
</th>
|
||||
@@ -216,9 +211,6 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{d.name}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-right">
|
||||
{d.plaetze}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
on:click={() => bearbeiten(d)}
|
||||
|
||||
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,
|
||||
email: anmeldung.email,
|
||||
|
||||
// === FEHLENDE FELDER HINZUGEFÜGT ===
|
||||
geburtsdatum: anmeldung.geburtsdatum,
|
||||
strasse: anmeldung.strasse,
|
||||
hausnummer: anmeldung.hausnummer,
|
||||
ort: anmeldung.ort,
|
||||
plz: anmeldung.plz,
|
||||
telefon: anmeldung.telefon,
|
||||
schulart: anmeldung.schulart,
|
||||
schulklasse: anmeldung.schulklasse,
|
||||
motivation: anmeldung.motivation,
|
||||
alter: anmeldung.alter,
|
||||
// === ENDE FEHLENDE FELDER ===
|
||||
|
||||
// Noten als String konvertieren (falls sie als Int gespeichert sind)
|
||||
noteDeutsch: anmeldung.noteDeutsch ? anmeldung.noteDeutsch.toString() : undefined,
|
||||
noteMathe: anmeldung.noteMathe ? anmeldung.noteMathe.toString() : undefined,
|
||||
@@ -37,7 +50,6 @@ export async function GET() {
|
||||
|
||||
// Status-Mapping für Frontend
|
||||
status: mapPrismaStatusToFrontend(anmeldung.status),
|
||||
// processedBy: anmeldung.processedBy,
|
||||
processedAt: anmeldung.processedAt ? anmeldung.processedAt.getTime() : undefined,
|
||||
|
||||
// Wünsche - sicherstellen dass sie existieren
|
||||
@@ -60,6 +72,14 @@ export async function GET() {
|
||||
name: anmeldung.zugewiesen.name
|
||||
} : undefined,
|
||||
|
||||
// Praktikumszeitraum
|
||||
zeitraum: anmeldung.praktikum ? {
|
||||
id: anmeldung.praktikum.id,
|
||||
bezeichnung: anmeldung.praktikum.bezeichnung,
|
||||
startDatum: anmeldung.praktikum.startDatum.toISOString(),
|
||||
endDatum: anmeldung.praktikum.endDatum.toISOString()
|
||||
} : undefined,
|
||||
|
||||
// Timestamp
|
||||
timestamp: anmeldung.timestamp ? anmeldung.timestamp.getTime() : Date.now(),
|
||||
|
||||
@@ -83,9 +103,12 @@ export async function POST({ request, url }) {
|
||||
return json({ error: 'ID und Dienststelle erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfen ob Anmeldung existiert
|
||||
// Prüfen ob Anmeldung existiert und Praktikumszeitraum laden
|
||||
const existingAnmeldung = await prisma.anmeldung.findUnique({
|
||||
where: { id }
|
||||
where: { id },
|
||||
include: {
|
||||
praktikum: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingAnmeldung) {
|
||||
@@ -96,8 +119,37 @@ export async function POST({ request, url }) {
|
||||
return json({ error: 'Anmeldung bereits angenommen' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Anmeldung als angenommen markieren
|
||||
const zeitraumId = existingAnmeldung.praktikumId;
|
||||
|
||||
if (!zeitraumId) {
|
||||
return json({ error: 'Kein Praktikumszeitraum für diese Anmeldung gefunden' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfen ob ZeitraumPlaetze Eintrag existiert
|
||||
const zeitraumPlaetze = await prisma.zeitraumPlaetze.findUnique({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!zeitraumPlaetze) {
|
||||
return json({
|
||||
error: 'Keine Plätze für diese Dienststelle in diesem Zeitraum konfiguriert'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (zeitraumPlaetze.plaetze <= 0) {
|
||||
return json({
|
||||
error: 'Keine freien Plätze mehr für diese Dienststelle in diesem Zeitraum'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Anmeldung als angenommen markieren und Plätze in ZeitraumPlaetze reduzieren
|
||||
await prisma.$transaction([
|
||||
// Anmeldung aktualisieren
|
||||
prisma.anmeldung.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -107,13 +159,19 @@ export async function POST({ request, url }) {
|
||||
}
|
||||
}),
|
||||
|
||||
prisma.dienststelle.update({
|
||||
where: { id: dienststelleId },
|
||||
data: {
|
||||
plaetze: {
|
||||
decrement: 1
|
||||
// Plätze in ZeitraumPlaetze reduzieren
|
||||
prisma.zeitraumPlaetze.update({
|
||||
where: {
|
||||
zeitraumId_dienststelleId: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelleId
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
plaetze: {
|
||||
decrement: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,193 +1,124 @@
|
||||
// src/routes/api/admin/dienststellen/+server.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
|
||||
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
|
||||
function checkAuth(cookies: Cookies) {
|
||||
return cookies.get('admin-auth') === 'authenticated';
|
||||
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für eine neue Dienststelle
|
||||
async function createZeitraumPlaetzeForDienststelle(dienststelleId: number) {
|
||||
const zeitraeume = await prisma.praktikumszeitraum.findMany();
|
||||
|
||||
// Erstelle für jeden existierenden Zeitraum einen Eintrag mit 0 Plätzen
|
||||
for (const zeitraum of zeitraeume) {
|
||||
await prisma.zeitraumPlaetze.create({
|
||||
data: {
|
||||
zeitraumId: zeitraum.id,
|
||||
dienststelleId: dienststelleId,
|
||||
plaetze: 0 // Standardwert: 0 Plätze
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Nicht autorisiert' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const dienststellen = await prisma.dienststelle.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
/*
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
Anmeldung: true // Use the correct relation name as defined in your Prisma schema
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const dienststellen = await prisma.dienststelle.findMany({
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
return json(dienststellen);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dienststellen:', error);
|
||||
return json({ error: 'Fehler beim Laden der Dienststellen' }, { status: 500 });
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, plaetze } = await request.json();
|
||||
const { name } = await request.json();
|
||||
|
||||
// Validierung
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
if (!name) {
|
||||
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
|
||||
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Name bereits existiert
|
||||
const existing = await prisma.dienststelle.findFirst({
|
||||
where: { name: name.trim() }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return json({ error: 'Eine Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
|
||||
}
|
||||
|
||||
const created = await prisma.dienststelle.create({
|
||||
data: {
|
||||
name: name.trim(),
|
||||
plaetze,
|
||||
}
|
||||
});
|
||||
|
||||
return json(created, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Dienststelle:', error);
|
||||
return json({ error: 'Fehler beim Erstellen der Dienststelle' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ cookies, request }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, name, plaetze } = await request.json();
|
||||
|
||||
// Validierung
|
||||
if (typeof id !== 'number' || isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
return json({ error: 'Name ist erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof plaetze !== 'number' || plaetze < 0 || !Number.isInteger(plaetze)) {
|
||||
return json({ error: 'Ungültige Anzahl an Plätzen' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Dienststelle existiert
|
||||
const existing = await prisma.dienststelle.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Prüfe ob neuer Name bereits bei anderer Dienststelle existiert
|
||||
const nameConflict = await prisma.dienststelle.findFirst({
|
||||
where: {
|
||||
name: name.trim(),
|
||||
NOT: { id },
|
||||
},
|
||||
});
|
||||
|
||||
if (nameConflict) {
|
||||
return json({ error: 'Eine andere Dienststelle mit diesem Namen existiert bereits' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Plätze reduziert werden und ob das möglich ist
|
||||
const assignedCount = await prisma.anmeldung.count({
|
||||
where: { zugewiesenId: id }
|
||||
});
|
||||
|
||||
if (plaetze < assignedCount) {
|
||||
return json({
|
||||
error: `Plätze können nicht auf ${plaetze} reduziert werden. ${assignedCount} Anmeldungen sind bereits zugewiesen.`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await prisma.dienststelle.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: name.trim(),
|
||||
plaetze
|
||||
},
|
||||
});
|
||||
|
||||
return json(updated);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
|
||||
return json({ error: 'Fehler beim Aktualisieren der Dienststelle' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ cookies, url }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Prüfe ob Dienststelle existiert
|
||||
const existing = await prisma.dienststelle.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Dienststelle nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Prüfe ob noch Anmeldungen zugewiesen sind
|
||||
const assignedCount = await prisma.anmeldung.count({
|
||||
where: {
|
||||
OR: [
|
||||
{ zugewiesenId: id },
|
||||
{ wunsch1Id: id },
|
||||
{ wunsch2Id: id },
|
||||
{ wunsch3Id: id }
|
||||
]
|
||||
const dienststelle = await prisma.dienststelle.create({
|
||||
data: {
|
||||
name,
|
||||
plaetze: 0 // Wird nicht mehr verwendet, aber bleibt im Schema für Kompatibilität
|
||||
}
|
||||
});
|
||||
|
||||
if (assignedCount > 0) {
|
||||
return json({
|
||||
error: 'Dienststelle kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
|
||||
}, { status: 400 });
|
||||
// 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: '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 { id, name } = await request.json();
|
||||
|
||||
if (!id || !name) {
|
||||
return json({ error: 'ID und Name sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.dienststelle.delete({ where: { id } });
|
||||
return json({ success: true, message: 'Dienststelle erfolgreich gelöscht' });
|
||||
const dienststelle = await prisma.dienststelle.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: {
|
||||
name
|
||||
}
|
||||
});
|
||||
|
||||
return json(dienststelle);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Aktualisieren der Dienststelle:', error);
|
||||
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 ({ url, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||
await prisma.dienststelle.delete({
|
||||
where: { id: parseInt(id) }
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Dienststelle:', error);
|
||||
return json({ error: 'Fehler beim Löschen der Dienststelle' }, { status: 500 });
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -1,59 +1,37 @@
|
||||
// src/routes/api/admin/login/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
//const ADMIN_PASSWORD_HASH = 'your-hashed-password-here'; // Ersetze mit deinem Hash
|
||||
|
||||
const adminRecord = await prisma.admin.findUnique({ where: { id: 1 } });
|
||||
if (!adminRecord || !adminRecord.password) {
|
||||
throw new Error('Admin password hash not found in database');
|
||||
} else {
|
||||
console.log('Admin password hash loaded successfully');
|
||||
}
|
||||
const ADMIN_PASSWORD_HASH = adminRecord.password;
|
||||
import { prisma } from '$lib/prisma';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
try {
|
||||
const adminRecord = await prisma.admin.findUnique({ where: { id: 1 } });
|
||||
|
||||
if (!adminRecord?.password) {
|
||||
return new Response(JSON.stringify({ message: 'Admin password not found' }), { status: 500 });
|
||||
}
|
||||
|
||||
const { passwort } = await request.json();
|
||||
|
||||
if (!passwort) {
|
||||
return new Response(
|
||||
JSON.stringify({ message: 'Passwort erforderlich' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
return new Response(JSON.stringify({ message: 'Passwort erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
// Hier solltest du den Hash aus der Datenbank oder Umgebungsvariable laden
|
||||
const isValid = await bcrypt.compare(passwort, ADMIN_PASSWORD_HASH);
|
||||
const isValid = await bcrypt.compare(passwort, adminRecord.password);
|
||||
|
||||
if (isValid) {
|
||||
// Setze konsistenten Cookie-Namen
|
||||
cookies.set('admin-auth', 'authenticated', {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 // 24 Stunden
|
||||
maxAge: 60 * 60 * 24,
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
} else {
|
||||
return new Response(
|
||||
JSON.stringify({ message: 'Falsches Passwort' }),
|
||||
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ message: 'Falsches Passwort' }), { status: 401 });
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ message: 'Serverfehler' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
return new Response(JSON.stringify({ message: 'Serverfehler' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
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 type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
|
||||
// Korrigierte Auth-Funktion mit neuem Cookie-Namen
|
||||
function checkAuth(cookies: Cookies) {
|
||||
return cookies.get('admin-auth') === 'authenticated';
|
||||
}
|
||||
|
||||
function isValidDate(date: string | Date) {
|
||||
const parsed = new Date(date)
|
||||
return !isNaN(parsed.getTime())
|
||||
// Hilfsfunktion: Erstelle ZeitraumPlaetze-Einträge für einen neuen Zeitraum
|
||||
async function createZeitraumPlaetzeForZeitraum(zeitraumId: number) {
|
||||
const dienststellen = await prisma.dienststelle.findMany();
|
||||
|
||||
// Erstelle für jede existierende Dienststelle einen Eintrag mit 0 Plätzen
|
||||
for (const dienststelle of dienststellen) {
|
||||
await prisma.zeitraumPlaetze.create({
|
||||
data: {
|
||||
zeitraumId: zeitraumId,
|
||||
dienststelleId: dienststelle.id,
|
||||
plaetze: 0 // Standardwert: 0 Plätze
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Nicht autorisiert' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const zeitraeume = await prisma.praktikumszeitraum.findMany();
|
||||
const zeitraeume = await prisma.praktikumszeitraum.findMany({
|
||||
orderBy: { startDatum: 'desc' }
|
||||
});
|
||||
return json(zeitraeume);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Praktikumszeiträume:', error);
|
||||
return json({ error: 'Fehler beim Laden der Praktikumszeiträume' }, { status: 500 });
|
||||
console.error('Fehler beim Laden der Zeiträume:', error);
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { bezeichnung, startDatum, endDatum } = await request.json();
|
||||
|
||||
// Validierung
|
||||
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
|
||||
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
|
||||
|
||||
if (!bezeichnung || !startDatum || !endDatum) {
|
||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
|
||||
return json({ error: 'Ungültiges Datum' }, { status: 400 });
|
||||
const start = new Date(startDatum);
|
||||
const end = new Date(endDatum);
|
||||
|
||||
if (end <= start) {
|
||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||
}
|
||||
|
||||
const created = await prisma.praktikumszeitraum.create({
|
||||
data: {
|
||||
bezeichnung: bezeichnung.trim(),
|
||||
startDatum: new Date(startDatum),
|
||||
endDatum: new Date(endDatum)
|
||||
}
|
||||
const zeitraum = await prisma.praktikumszeitraum.create({
|
||||
data: {
|
||||
bezeichnung,
|
||||
startDatum: start,
|
||||
endDatum: end
|
||||
}
|
||||
});
|
||||
return json(created, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Praktikumszeitraums:', error);
|
||||
return json({ error: 'Fehler beim Erstellen des Praktikumszeitraums' }, { status: 500 });
|
||||
|
||||
// Automatisch ZeitraumPlaetze für alle existierenden Dienststellen erstellen
|
||||
await createZeitraumPlaetzeForZeitraum(zeitraum.id);
|
||||
|
||||
return json(zeitraum);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Erstellen des Zeitraums:', error);
|
||||
if (error.code === 'P2002') {
|
||||
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
||||
}
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ cookies, request }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
export const PATCH: RequestHandler = async ({ request, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, bezeichnung, startDatum, endDatum } = await request.json();
|
||||
|
||||
// Validierung
|
||||
if (typeof id !== 'number' || isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
if (!id || !bezeichnung || !startDatum || !endDatum) {
|
||||
return json({ error: 'Alle Felder sind erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
|
||||
return json({ error: 'Bezeichnung ist erforderlich' }, { status: 400 });
|
||||
const start = new Date(startDatum);
|
||||
const end = new Date(endDatum);
|
||||
|
||||
if (end <= start) {
|
||||
return json({ error: 'Enddatum muss nach dem Startdatum liegen' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!isValidDate(startDatum) || !isValidDate(endDatum)) {
|
||||
return json({ error: 'Ungültiges Datum' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prüfe ob Praktikumszeitraum existiert
|
||||
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Prüfe ob neue Bezeichnung bereits bei anderem Zeitraum existiert
|
||||
const konflikt = await prisma.praktikumszeitraum.findFirst({
|
||||
where: {
|
||||
bezeichnung: bezeichnung.trim(),
|
||||
NOT: { id },
|
||||
},
|
||||
const zeitraum = await prisma.praktikumszeitraum.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: {
|
||||
bezeichnung,
|
||||
startDatum: start,
|
||||
endDatum: end
|
||||
}
|
||||
});
|
||||
|
||||
if (konflikt) {
|
||||
return json({ error: 'Ein anderer Praktikumszeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
||||
return json(zeitraum);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Aktualisieren des Zeitraums:', error);
|
||||
if (error.code === 'P2002') {
|
||||
return json({ error: 'Ein Zeitraum mit dieser Bezeichnung existiert bereits' }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await prisma.praktikumszeitraum.update({
|
||||
where: { id },
|
||||
data: {
|
||||
bezeichnung: bezeichnung.trim(),
|
||||
startDatum: new Date(startDatum),
|
||||
endDatum: new Date(endDatum)
|
||||
},
|
||||
});
|
||||
|
||||
return json(updated);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Praktikumszeitraums:', error);
|
||||
return json({ error: 'Fehler beim Aktualisieren des Praktikumszeitraums' }, { status: 500 });
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ cookies, url }) => {
|
||||
if (!checkAuth(cookies)) {
|
||||
export const DELETE: RequestHandler = async ({ url, cookies }) => {
|
||||
const adminAuth = cookies.get('admin-auth');
|
||||
if (adminAuth !== 'authenticated') {
|
||||
return json({ error: 'Nicht autorisiert' }, { status: 401 });
|
||||
}
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Ungültige ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Prüfe ob Praktikumszeitraum existiert
|
||||
const existing = await prisma.praktikumszeitraum.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return json({ error: 'Praktikumszeitraum nicht gefunden' }, { status: 404 });
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) {
|
||||
return json({ error: 'ID erforderlich' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Hier könntest du prüfen, ob noch Anmeldungen mit diesem Zeitraum verknüpft sind
|
||||
// const assignedCount = await prisma.anmeldung.count({
|
||||
// where: { praktikumszeitraumId: id }
|
||||
// });
|
||||
//
|
||||
// if (assignedCount > 0) {
|
||||
// return json({
|
||||
// error: 'Praktikumszeitraum kann nicht gelöscht werden. Es sind noch Anmeldungen damit verknüpft.'
|
||||
// }, { status: 400 });
|
||||
// }
|
||||
// ZeitraumPlaetze werden automatisch durch onDelete: Cascade gelöscht
|
||||
await prisma.praktikumszeitraum.delete({
|
||||
where: { id: parseInt(id) }
|
||||
});
|
||||
|
||||
await prisma.praktikumszeitraum.delete({ where: { id } });
|
||||
return json({ success: true, message: 'Praktikumszeitraum erfolgreich gelöscht' });
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Praktikumszeitraums:', error);
|
||||
return json({ error: 'Fehler beim Löschen des Praktikumszeitraums' }, { status: 500 });
|
||||
console.error('Fehler beim Löschen des Zeitraums:', error);
|
||||
return json({ error: 'Serverfehler' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
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 type { RequestHandler } from './$types';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
let prismaPromise: Promise<any> | null = null;
|
||||
|
||||
async function getPrismaClient() {
|
||||
if (!prismaPromise) {
|
||||
prismaPromise = import('@prisma/client').then(({ PrismaClient }) => new PrismaClient());
|
||||
}
|
||||
return prismaPromise;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const prisma = await getPrismaClient(); // Hier Prisma Client holen
|
||||
const dienststellen = await prisma.dienststelle.findMany({ orderBy: { name: 'asc' } });
|
||||
return json(dienststellen);
|
||||
};
|
||||
@@ -2,5 +2,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
plugins: [sveltekit()],
|
||||
optimizeDeps: {
|
||||
exclude: ['@prisma/client']
|
||||
},
|
||||
ssr: {
|
||||
noExternal: process.env.NODE_ENV === 'production' ? ['@prisma/client'] : [],
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user