feat: implement working version of substitution marketplace
This commit is contained in:
parent
3e2b6d46e6
commit
53244457c1
7 changed files with 590 additions and 206 deletions
|
|
@ -107,8 +107,10 @@ func createTables(db *sql.DB) {
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
taken_by_user_id INTEGER,
|
taken_by_user_id INTEGER,
|
||||||
|
schedule_id INTEGER NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY(taken_by_user_id) REFERENCES users(id)
|
FOREIGN KEY(taken_by_user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY(schedule_id) REFERENCES schedules(id)
|
||||||
)`,
|
)`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -637,32 +639,53 @@ func DeleteNonManualTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, w
|
||||||
for day := 0; day <= 4; day++ {
|
for day := 0; day <= 4; day++ {
|
||||||
dateList = append(dateList, dates.Dates[fmt.Sprint(day)])
|
dateList = append(dateList, dates.Dates[fmt.Sprint(day)])
|
||||||
}
|
}
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
UPDATE substitutions
|
||||||
|
SET taken_by_user_id = NULL
|
||||||
|
WHERE taken_by_user_id = ?
|
||||||
|
AND date IN (?, ?, ?, ?, ?)
|
||||||
|
`, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
query := `DELETE FROM time_entries
|
query := `DELETE FROM time_entries
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND type != 'manual'
|
AND type != 'manual'
|
||||||
AND date IN (?, ?, ?, ?, ?)`
|
AND date IN (?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
_, err := db.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
|
_, err = tx.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSubstitution(db *sql.DB, date, start, end, title, notes string) error {
|
func CreateSubstitution(db *sql.DB, date, start, end, title, notes string, scheduleID int) error {
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT INTO substitutions (date, start_time, end_time, title, notes)
|
INSERT INTO substitutions (date, start_time, end_time, title, notes, schedule_id)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`, date, start, end, title, notes)
|
`, date, start, end, title, notes, scheduleID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) {
|
func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT id, date, start_time, end_time, title, notes, created_at
|
SELECT id, date, start_time, end_time, title, notes, schedule_id, created_at
|
||||||
FROM substitutions
|
FROM substitutions
|
||||||
WHERE taken_by_user_id IS NULL
|
WHERE taken_by_user_id IS NULL
|
||||||
AND date >= date('now')
|
AND date >= ?
|
||||||
ORDER BY date ASC, start_time ASC
|
ORDER BY date ASC, start_time ASC
|
||||||
`)
|
`, today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -671,7 +694,7 @@ func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) {
|
||||||
var subs []Substitution
|
var subs []Substitution
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s Substitution
|
var s Substitution
|
||||||
if err := rows.Scan(&s.ID, &s.Date, &s.StartTime, &s.EndTime, &s.Title, &s.Notes, &s.CreatedAt); err != nil {
|
if err := rows.Scan(&s.ID, &s.Date, &s.StartTime, &s.EndTime, &s.Title, &s.Notes, &s.ScheduleID, &s.CreatedAt); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
subs = append(subs, s)
|
subs = append(subs, s)
|
||||||
|
|
@ -679,50 +702,19 @@ func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) {
|
||||||
return subs, nil
|
return subs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AcceptSubstitution(db *sql.DB, substitutionID int, userID int) error {
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
var currentDate, start, end, title string
|
|
||||||
err = tx.QueryRow(`
|
|
||||||
SELECT date, start_time, end_time, title
|
|
||||||
FROM substitutions
|
|
||||||
WHERE id = ? AND taken_by_user_id IS NULL
|
|
||||||
`, substitutionID).Scan(¤tDate, &start, &end, &title)
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return fmt.Errorf("Vertretung wurde bereits vergeben oder existiert nicht")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(`
|
|
||||||
UPDATE substitutions
|
|
||||||
SET taken_by_user_id = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, userID, substitutionID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(`
|
|
||||||
INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time)
|
|
||||||
VALUES (?, 0, ?, 'lesson', ?, ?)
|
|
||||||
`, userID, currentDate, start, end)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAllSubstitutions(db *sql.DB) ([]Substitution, error) {
|
func GetAllSubstitutions(db *sql.DB) ([]Substitution, error) {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT s.id, s.date, s.start_time, s.end_time, s.title, s.notes, s.taken_by_user_id, u.username
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.date,
|
||||||
|
s.start_time,
|
||||||
|
s.end_time,
|
||||||
|
s.title,
|
||||||
|
s.notes,
|
||||||
|
s.schedule_id,
|
||||||
|
s.created_at,
|
||||||
|
s.taken_by_user_id,
|
||||||
|
u.username
|
||||||
FROM substitutions s
|
FROM substitutions s
|
||||||
LEFT JOIN users u ON s.taken_by_user_id = u.id
|
LEFT JOIN users u ON s.taken_by_user_id = u.id
|
||||||
ORDER BY s.date DESC
|
ORDER BY s.date DESC
|
||||||
|
|
@ -735,18 +727,116 @@ func GetAllSubstitutions(db *sql.DB) ([]Substitution, error) {
|
||||||
var subs []Substitution
|
var subs []Substitution
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s Substitution
|
var s Substitution
|
||||||
|
|
||||||
var takenID sql.NullInt64
|
var takenID sql.NullInt64
|
||||||
var takenName sql.NullString
|
var takenName sql.NullString
|
||||||
|
|
||||||
if err := rows.Scan(&s.ID, &s.Date, &s.StartTime, &s.EndTime, &s.Title, &s.Notes, &takenID, &takenName); err != nil {
|
if err := rows.Scan(
|
||||||
|
&s.ID,
|
||||||
|
&s.Date,
|
||||||
|
&s.StartTime,
|
||||||
|
&s.EndTime,
|
||||||
|
&s.Title,
|
||||||
|
&s.Notes,
|
||||||
|
&s.ScheduleID,
|
||||||
|
&s.CreatedAt,
|
||||||
|
&takenID,
|
||||||
|
&takenName,
|
||||||
|
); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if takenID.Valid {
|
if takenID.Valid {
|
||||||
id := int(takenID.Int64)
|
id := int(takenID.Int64)
|
||||||
s.TakenByUserID = &id
|
s.TakenByUserID = &id
|
||||||
s.TakenByUsername = takenName.String
|
s.TakenByUsername = takenName.String
|
||||||
}
|
}
|
||||||
|
|
||||||
subs = append(subs, s)
|
subs = append(subs, s)
|
||||||
}
|
}
|
||||||
return subs, nil
|
return subs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteSubstitution(db *sql.DB, id int) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
var takenByUserID sql.NullInt64
|
||||||
|
var scheduleID int
|
||||||
|
var date string
|
||||||
|
|
||||||
|
err = tx.QueryRow(`
|
||||||
|
SELECT taken_by_user_id, schedule_id, date
|
||||||
|
FROM substitutions
|
||||||
|
WHERE id = ?
|
||||||
|
`, id).Scan(&takenByUserID, &scheduleID, &date)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if takenByUserID.Valid {
|
||||||
|
userID := int(takenByUserID.Int64)
|
||||||
|
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
DELETE FROM time_entries
|
||||||
|
WHERE user_id = ? AND schedule_id = ? AND date = ?
|
||||||
|
`, userID, scheduleID, date)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM substitutions WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AcceptSubstitution(db *sql.DB, substitutionID int, userID int) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
var currentDate, start, end string
|
||||||
|
var scheduleID int
|
||||||
|
var scheduleType string
|
||||||
|
|
||||||
|
err = tx.QueryRow(`
|
||||||
|
SELECT s.date, s.start_time, s.end_time, s.schedule_id, sch.type
|
||||||
|
FROM substitutions s
|
||||||
|
JOIN schedules sch ON s.schedule_id = sch.id
|
||||||
|
WHERE s.id = ? AND s.taken_by_user_id IS NULL
|
||||||
|
`, substitutionID).Scan(¤tDate, &start, &end, &scheduleID, &scheduleType)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return fmt.Errorf("Vertretung wurde bereits vergeben oder existiert nicht")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(`UPDATE substitutions SET taken_by_user_id = ? WHERE id = ?`, userID, substitutionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, userID, scheduleID, currentDate, scheduleType, start, end)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -854,22 +854,6 @@ func (app *App) UploadLicenseHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, map[string]string{"message": "Lizenz erfolgreich aktiviert"})
|
return c.JSON(http.StatusOK, map[string]string{"message": "Lizenz erfolgreich aktiviert"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) CreateSubstitutionHandler(c echo.Context) error {
|
|
||||||
var req CreateSubstitutionRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return HandleError(c, ErrInvalidInputMsg("Eingabedaten"))
|
|
||||||
}
|
|
||||||
if req.Date == "" || req.StartTime == "" || req.Title == "" {
|
|
||||||
return HandleError(c, ErrMissingFieldMsg("Pflichtfelder"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := CreateSubstitution(app.DB, req.Date, req.StartTime, req.EndTime, req.Title, req.Notes); err != nil {
|
|
||||||
return HandleError(c, ErrDatabaseMsg(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, map[string]string{"message": "Vertretung ausgeschrieben"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *App) GetAllSubstitutionsHandler(c echo.Context) error {
|
func (app *App) GetAllSubstitutionsHandler(c echo.Context) error {
|
||||||
subs, err := GetAllSubstitutions(app.DB)
|
subs, err := GetAllSubstitutions(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -912,3 +896,25 @@ func (app *App) AcceptSubstitutionHandler(c echo.Context) error {
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, map[string]string{"message": "Vertretung erfolgreich übernommen!"})
|
return c.JSON(http.StatusOK, map[string]string{"message": "Vertretung erfolgreich übernommen!"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) CreateSubstitutionHandler(c echo.Context) error {
|
||||||
|
var req CreateSubstitutionRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return HandleError(c, ErrInvalidInputMsg("Eingabedaten"))
|
||||||
|
}
|
||||||
|
if err := CreateSubstitution(app.DB, req.Date, req.StartTime, req.EndTime, req.Title, req.Notes, req.ScheduleID); err != nil {
|
||||||
|
return HandleError(c, ErrDatabaseMsg(err))
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusCreated, map[string]string{"message": "Vertretung ausgeschrieben"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) DeleteSubstitutionHandler(c echo.Context) error {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return HandleError(c, ErrInvalidInputMsg("ID"))
|
||||||
|
}
|
||||||
|
if err := DeleteSubstitution(app.DB, id); err != nil {
|
||||||
|
return HandleError(c, ErrDatabaseMsg(err))
|
||||||
|
}
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ func main() {
|
||||||
admin.POST("/settings/license", app.UploadLicenseHandler)
|
admin.POST("/settings/license", app.UploadLicenseHandler)
|
||||||
admin.GET("/substitutions", app.GetAllSubstitutionsHandler)
|
admin.GET("/substitutions", app.GetAllSubstitutionsHandler)
|
||||||
admin.POST("/substitutions", app.CreateSubstitutionHandler)
|
admin.POST("/substitutions", app.CreateSubstitutionHandler)
|
||||||
|
admin.DELETE("/substitutions/:id", app.DeleteSubstitutionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
distDir, err := fs.Sub(frontendDist, "dist")
|
distDir, err := fs.Sub(frontendDist, "dist")
|
||||||
|
|
|
||||||
|
|
@ -134,15 +134,17 @@ type Substitution struct {
|
||||||
EndTime string `json:"end_time"`
|
EndTime string `json:"end_time"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
|
ScheduleID int `json:"schedule_id"`
|
||||||
TakenByUserID *int `json:"taken_by_user_id,omitempty"`
|
TakenByUserID *int `json:"taken_by_user_id,omitempty"`
|
||||||
TakenByUsername string `json:"taken_by_username,omitempty"`
|
TakenByUsername string `json:"taken_by_username,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateSubstitutionRequest struct {
|
type CreateSubstitutionRequest struct {
|
||||||
Date string `json:"date" validate:"required"`
|
Date string `json:"date" validate:"required"`
|
||||||
StartTime string `json:"start_time" validate:"required"`
|
StartTime string `json:"start_time" validate:"required"`
|
||||||
EndTime string `json:"end_time" validate:"required"`
|
EndTime string `json:"end_time" validate:"required"`
|
||||||
Title string `json:"title" validate:"required"`
|
Title string `json:"title" validate:"required"`
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
|
ScheduleID int `json:"schedule_id"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@
|
||||||
let showPwModal = false;
|
let showPwModal = false;
|
||||||
let pwData = { old: "", new1: "", new2: "" };
|
let pwData = { old: "", new1: "", new2: "" };
|
||||||
|
|
||||||
// State für die Ansicht
|
|
||||||
let activeView = "schedule";
|
let activeView = "schedule";
|
||||||
let openSubstitutions = [];
|
let openSubstitutions = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,417 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { getAllSubstitutions, createSubstitution } from "../../lib/api";
|
import {
|
||||||
import { addToast, loading } from "../../lib/stores";
|
getAllSubstitutions,
|
||||||
|
createSubstitution,
|
||||||
|
deleteSubstitution,
|
||||||
|
getSchedules,
|
||||||
|
} from "../../lib/api";
|
||||||
|
import {
|
||||||
|
getISOWeek,
|
||||||
|
getISOYear,
|
||||||
|
getDateOfISOWeek,
|
||||||
|
formatDate,
|
||||||
|
} from "../../lib/utils";
|
||||||
|
import { addToast, loading } from "../../lib/stores";
|
||||||
|
import ScheduleItem from "../ScheduleItem.svelte";
|
||||||
|
|
||||||
let substitutions = [];
|
const today = new Date();
|
||||||
let newSub = { date: "", startTime: "", endTime: "", title: "", notes: "" };
|
let currentISOYear = getISOYear(today);
|
||||||
|
let currentWeek = getISOWeek(today);
|
||||||
|
|
||||||
onMount(loadData);
|
let substitutions = [];
|
||||||
|
let schedules = [];
|
||||||
|
|
||||||
async function loadData() {
|
let showModal = false;
|
||||||
try {
|
let selectedSchedule = null;
|
||||||
substitutions = await getAllSubstitutions();
|
let form = { date: "", startTime: "", endTime: "", title: "", notes: "" };
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreate() {
|
$: weekDates = Array.from({ length: 5 }, (_, i) => {
|
||||||
if (!newSub.date || !newSub.startTime || !newSub.title) {
|
const d = getDateOfISOWeek(currentWeek, currentISOYear);
|
||||||
addToast("Bitte Pflichtfelder ausfüllen", "warning");
|
d.setDate(d.getDate() + i);
|
||||||
return;
|
return {
|
||||||
|
name: ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"][
|
||||||
|
i
|
||||||
|
],
|
||||||
|
date: formatDate(d),
|
||||||
|
dayIndex: i,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(loadData);
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const [subs, scheds] = await Promise.all([
|
||||||
|
getAllSubstitutions(),
|
||||||
|
getSchedules(),
|
||||||
|
]);
|
||||||
|
substitutions = subs;
|
||||||
|
schedules = scheds;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await createSubstitution(newSub);
|
function changeWeek(delta) {
|
||||||
addToast("Vertretung ausgeschrieben", "success");
|
const d = getDateOfISOWeek(currentWeek, currentISOYear);
|
||||||
newSub = { date: "", startTime: "", endTime: "", title: "", notes: "" };
|
d.setDate(d.getDate() + delta * 7);
|
||||||
await loadData();
|
currentWeek = getISOWeek(d);
|
||||||
} catch (e) {
|
currentISOYear = getISOYear(d);
|
||||||
addToast(e.message, "error");
|
}
|
||||||
|
|
||||||
|
function openCreateModal(schedule, dateStr) {
|
||||||
|
const existing = findSubstitution(dateStr, schedule.startTime);
|
||||||
|
if (existing) {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Für diesen Termin existiert bereits eine Vertretung. Trotzdem noch eine anlegen?",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form = {
|
||||||
|
title: "Vertretung: " + (schedule.title || "Unterricht"),
|
||||||
|
date: dateStr,
|
||||||
|
startTime: schedule.startTime,
|
||||||
|
endTime: schedule.endTime,
|
||||||
|
notes: "",
|
||||||
|
scheduleId: schedule.id,
|
||||||
|
};
|
||||||
|
selectedSchedule = schedule;
|
||||||
|
showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
if (!form.title || !form.date) return;
|
||||||
|
try {
|
||||||
|
await createSubstitution(form);
|
||||||
|
addToast("Vertretung erfolgreich ausgeschrieben", "success");
|
||||||
|
showModal = false;
|
||||||
|
substitutions = await getAllSubstitutions();
|
||||||
|
loadData();
|
||||||
|
} catch (e) {
|
||||||
|
addToast(e.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id) {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Möchten Sie dieses Angebot wirklich löschen? Falls es schon übernommen wurde, wird es auch beim Mitarbeiter entfernt.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteSubstitution(id);
|
||||||
|
addToast("Eintrag gelöscht", "success");
|
||||||
|
substitutions = await getAllSubstitutions();
|
||||||
|
} catch (e) {
|
||||||
|
addToast(e.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSubstitution(date, startTime) {
|
||||||
|
return substitutions.find(
|
||||||
|
(s) => s.date === date && s.start_time === startTime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div class="space-y-8 fade-in">
|
||||||
<div class="card bg-base-100 shadow-xl border border-base-200 h-fit">
|
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title text-lg mb-4">Neue Vertretung ausschreiben</h3>
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="card-title">Vertretung planen</h3>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="flex items-center gap-4 bg-base-200 rounded-lg p-1">
|
||||||
<div class="form-control">
|
<button
|
||||||
<label class="label">Titel / Klasse</label>
|
class="btn btn-sm btn-ghost btn-circle"
|
||||||
<input
|
on:click={() => changeWeek(-1)}>❮</button
|
||||||
type="text"
|
>
|
||||||
class="input input-bordered"
|
<div class="text-center">
|
||||||
placeholder="z.B. Mathe 4a"
|
<div class="font-bold">KW {currentWeek}</div>
|
||||||
bind:value={newSub.title}
|
<div class="text-xs opacity-50">{currentISOYear}</div>
|
||||||
/>
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-ghost btn-circle"
|
||||||
|
on:click={() => changeWeek(1)}>❯</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm opacity-70 mb-4">
|
||||||
|
Klicken Sie auf eine Stunde, um eine Vertretung auszuschreiben.
|
||||||
|
Bereits ausgeschriebene Vertretungen werden farbig markiert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto border border-base-200 rounded-xl">
|
||||||
|
<table class="table table-fixed w-full min-w-[800px]">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-base-200/50">
|
||||||
|
{#each weekDates as day}
|
||||||
|
<th class="text-center py-3">
|
||||||
|
<div>{day.name}</div>
|
||||||
|
<div class="font-normal text-xs opacity-50">
|
||||||
|
{day.date}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{#each weekDates as day}
|
||||||
|
<td
|
||||||
|
class="align-top p-2 border-r border-base-200 last:border-0 h-40"
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each schedules.filter((s) => s.dayOfWeek === day.dayIndex) as schedule}
|
||||||
|
{@const sub = findSubstitution(
|
||||||
|
day.date,
|
||||||
|
schedule.startTime,
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() =>
|
||||||
|
openCreateModal(
|
||||||
|
schedule,
|
||||||
|
day.date,
|
||||||
|
)}
|
||||||
|
class="relative cursor-pointer hover:scale-[1.02] transition-transform group"
|
||||||
|
>
|
||||||
|
<ScheduleItem
|
||||||
|
{schedule}
|
||||||
|
dayOfWeek={day.dayIndex}
|
||||||
|
isClickable={false}
|
||||||
|
isSelected={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if sub}
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-base-100/90 rounded-lg border-2
|
||||||
|
{sub.taken_by_user_id
|
||||||
|
? 'border-success'
|
||||||
|
: 'border-warning'}
|
||||||
|
flex flex-col items-center justify-center text-center p-1 shadow-lg"
|
||||||
|
>
|
||||||
|
{#if sub.taken_by_user_id}
|
||||||
|
<div
|
||||||
|
class="badge badge-success badge-sm mb-1"
|
||||||
|
>
|
||||||
|
Übernommen
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-xs font-bold truncate w-full"
|
||||||
|
>
|
||||||
|
{sub.taken_by_username}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="badge badge-warning badge-sm mb-1"
|
||||||
|
>
|
||||||
|
Gesucht
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-xs opacity-70"
|
||||||
|
>
|
||||||
|
Offen
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-[10px] mt-1 opacity-50 truncate w-full"
|
||||||
|
>
|
||||||
|
{sub.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !sub}
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-primary/10 rounded-lg opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="badge badge-primary"
|
||||||
|
>+ Erstellen</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if schedules.filter((s) => s.dayOfWeek === day.dayIndex).length === 0}
|
||||||
|
<div
|
||||||
|
class="text-center text-xs opacity-30 mt-4"
|
||||||
|
>
|
||||||
|
- Frei -
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Datum</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
class="input input-bordered"
|
|
||||||
bind:value={newSub.date}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Von</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
class="input input-bordered"
|
|
||||||
bind:value={newSub.startTime}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Bis</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
class="input input-bordered"
|
|
||||||
bind:value={newSub.endTime}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">Notizen (Optional)</label>
|
|
||||||
<textarea
|
|
||||||
class="textarea textarea-bordered"
|
|
||||||
placeholder="z.B. Arbeitsblätter im Fach..."
|
|
||||||
bind:value={newSub.notes}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-primary w-full mt-4"
|
|
||||||
on:click={handleCreate}
|
|
||||||
disabled={$loading}
|
|
||||||
>
|
|
||||||
Veröffentlichen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||||
<h3 class="font-bold text-xl mb-4">Übersicht</h3>
|
<div class="card-body">
|
||||||
<div
|
<h3 class="card-title mb-4">Liste aller Vertretungen</h3>
|
||||||
class="overflow-x-auto bg-base-100 rounded-lg shadow border border-base-200"
|
<div class="overflow-x-auto">
|
||||||
>
|
<table class="table w-full">
|
||||||
<table class="table w-full">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>Datum</th>
|
||||||
<th>Datum</th>
|
<th>Zeit</th>
|
||||||
<th>Zeit</th>
|
<th>Titel / Notiz</th>
|
||||||
<th>Titel</th>
|
<th>Status</th>
|
||||||
<th>Status</th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each substitutions as s}
|
{#each substitutions as s}
|
||||||
<tr class="hover">
|
<tr class="hover group">
|
||||||
<td class="whitespace-nowrap font-mono text-sm">{s.date}</td>
|
<td class="font-mono text-sm">{s.date}</td>
|
||||||
<td>{s.start_time} - {s.end_time}</td>
|
<td>{s.start_time} - {s.end_time}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="font-bold">{s.title}</div>
|
<div class="font-bold">{s.title}</div>
|
||||||
{#if s.notes}<div class="text-xs opacity-60 truncate max-w-xs">
|
{#if s.notes}<div
|
||||||
{s.notes}
|
class="text-xs opacity-50"
|
||||||
</div>{/if}
|
>
|
||||||
</td>
|
{s.notes}
|
||||||
<td>
|
</div>{/if}
|
||||||
{#if s.taken_by_user_id}
|
</td>
|
||||||
<span class="badge badge-success gap-2"> Übernommen </span>
|
<td>
|
||||||
<div class="text-xs mt-1 opacity-70">
|
{#if s.taken_by_user_id}
|
||||||
von {s.taken_by_username}
|
<span class="badge badge-success gap-2"
|
||||||
</div>
|
>✓ {s.taken_by_username}</span
|
||||||
{:else}
|
>
|
||||||
<span class="badge badge-warning badge-outline">Offen</span>
|
{:else}
|
||||||
{/if}
|
<span
|
||||||
</td>
|
class="badge badge-warning badge-outline"
|
||||||
</tr>
|
>Offen</span
|
||||||
{:else}
|
>
|
||||||
<tr
|
{/if}
|
||||||
><td colspan="4" class="text-center opacity-50 py-8"
|
</td>
|
||||||
>Keine Einträge</td
|
<td class="text-right">
|
||||||
></tr
|
<button
|
||||||
>
|
class="btn btn-ghost btn-xs text-error opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
{/each}
|
on:click={() => handleDelete(s.id)}
|
||||||
</tbody>
|
title="Löschen"
|
||||||
</table>
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<tr
|
||||||
|
><td
|
||||||
|
colspan="5"
|
||||||
|
class="text-center opacity-50 py-8"
|
||||||
|
>Keine Einträge</td
|
||||||
|
></tr
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog class="modal {showModal ? 'modal-open' : ''}">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg mb-4">Vertretung ausschreiben</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Titel</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered"
|
||||||
|
bind:value={form.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Datum</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered"
|
||||||
|
bind:value={form.date}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Zeitraum</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={form.startTime}
|
||||||
|
/>
|
||||||
|
<span>-</span>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={form.endTime}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">Notizen</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea-bordered h-24"
|
||||||
|
bind:value={form.notes}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn" on:click={() => (showModal = false)}
|
||||||
|
>Abbrechen</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={handleCreate}
|
||||||
|
disabled={$loading}
|
||||||
|
>
|
||||||
|
{#if $loading}<span class="loading loading-spinner"></span>{/if}
|
||||||
|
Veröffentlichen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -326,11 +326,24 @@ export const getAllSubstitutions = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSubstitution = (sub) => {
|
export const createSubstitution = (sub) => {
|
||||||
return request("/admin/substitutions", "POST", sub);
|
console.log(sub.scheduleId);
|
||||||
|
return request("/admin/substitutions", "POST", {
|
||||||
|
title: sub.title,
|
||||||
|
date: sub.date,
|
||||||
|
start_time: sub.startTime,
|
||||||
|
end_time: sub.endTime,
|
||||||
|
notes: sub.notes,
|
||||||
|
schedule_id: sub.scheduleId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSubstitution = (id) => {
|
||||||
|
return request(`/admin/substitutions/${id}`, "DELETE");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOpenSubstitutions = async () => {
|
export const getOpenSubstitutions = async () => {
|
||||||
const data = await request("/substitutions/open");
|
const data = await request("/substitutions/open");
|
||||||
|
console.log(data);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue