diff --git a/backend/database.go b/backend/database.go index 42d1229..9e123a1 100644 --- a/backend/database.go +++ b/backend/database.go @@ -107,8 +107,10 @@ func createTables(db *sql.DB) { title TEXT NOT NULL, notes TEXT, taken_by_user_id INTEGER, + schedule_id INTEGER NOT NULL, 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++ { 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 WHERE user_id = ? AND type != 'manual' AND date IN (?, ?, ?, ?, ?)` - _, err := db.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]) - return err + _, err = tx.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]) + 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(` - INSERT INTO substitutions (date, start_time, end_time, title, notes) - VALUES (?, ?, ?, ?, ?) - `, date, start, end, title, notes) + INSERT INTO substitutions (date, start_time, end_time, title, notes, schedule_id) + VALUES (?, ?, ?, ?, ?, ?) + `, date, start, end, title, notes, scheduleID) return err } func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) { + today := time.Now().Format("2006-01-02") + 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 WHERE taken_by_user_id IS NULL - AND date >= date('now') + AND date >= ? ORDER BY date ASC, start_time ASC - `) + `, today) if err != nil { return nil, err } @@ -671,7 +694,7 @@ func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) { var subs []Substitution for rows.Next() { 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 } subs = append(subs, s) @@ -679,50 +702,19 @@ func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) { 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) { 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 LEFT JOIN users u ON s.taken_by_user_id = u.id ORDER BY s.date DESC @@ -735,18 +727,116 @@ func GetAllSubstitutions(db *sql.DB) ([]Substitution, error) { var subs []Substitution for rows.Next() { var s Substitution + var takenID sql.NullInt64 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 } + if takenID.Valid { id := int(takenID.Int64) s.TakenByUserID = &id s.TakenByUsername = takenName.String } + subs = append(subs, s) } 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() +} diff --git a/backend/handlers.go b/backend/handlers.go index 31c2b97..1d63a92 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -854,22 +854,6 @@ func (app *App) UploadLicenseHandler(c echo.Context) error { 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 { subs, err := GetAllSubstitutions(app.DB) 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!"}) } + +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) +} diff --git a/backend/main.go b/backend/main.go index 0cc162e..fd9e98d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -100,6 +100,7 @@ func main() { admin.POST("/settings/license", app.UploadLicenseHandler) admin.GET("/substitutions", app.GetAllSubstitutionsHandler) admin.POST("/substitutions", app.CreateSubstitutionHandler) + admin.DELETE("/substitutions/:id", app.DeleteSubstitutionHandler) } distDir, err := fs.Sub(frontendDist, "dist") diff --git a/backend/models.go b/backend/models.go index 4e2f2e8..9fc13fb 100644 --- a/backend/models.go +++ b/backend/models.go @@ -134,15 +134,17 @@ type Substitution struct { EndTime string `json:"end_time"` Title string `json:"title"` Notes string `json:"notes"` + ScheduleID int `json:"schedule_id"` TakenByUserID *int `json:"taken_by_user_id,omitempty"` TakenByUsername string `json:"taken_by_username,omitempty"` CreatedAt time.Time `json:"created_at"` } type CreateSubstitutionRequest struct { - Date string `json:"date" validate:"required"` - StartTime string `json:"start_time" validate:"required"` - EndTime string `json:"end_time" validate:"required"` - Title string `json:"title" validate:"required"` - Notes string `json:"notes"` + Date string `json:"date" validate:"required"` + StartTime string `json:"start_time" validate:"required"` + EndTime string `json:"end_time" validate:"required"` + Title string `json:"title" validate:"required"` + Notes string `json:"notes"` + ScheduleID int `json:"schedule_id"` } diff --git a/frontend/src/components/UserDashboard.svelte b/frontend/src/components/UserDashboard.svelte index 4c7fca2..beef746 100644 --- a/frontend/src/components/UserDashboard.svelte +++ b/frontend/src/components/UserDashboard.svelte @@ -38,7 +38,6 @@ let showPwModal = false; let pwData = { old: "", new1: "", new2: "" }; - // State für die Ansicht let activeView = "schedule"; let openSubstitutions = []; diff --git a/frontend/src/components/admin/AdminSubstitutionsTab.svelte b/frontend/src/components/admin/AdminSubstitutionsTab.svelte index 086647a..3aae90e 100644 --- a/frontend/src/components/admin/AdminSubstitutionsTab.svelte +++ b/frontend/src/components/admin/AdminSubstitutionsTab.svelte @@ -1,144 +1,417 @@ -
-
-
-

Neue Vertretung ausschreiben

+
+
+
+
+

Vertretung planen

-
-
- - +
+ +
+
KW {currentWeek}
+
{currentISOYear}
+
+ +
+
+ +

+ Klicken Sie auf eine Stunde, um eine Vertretung auszuschreiben. + Bereits ausgeschriebene Vertretungen werden farbig markiert. +

+ +
+ + + + {#each weekDates as day} + + {/each} + + + + + {#each weekDates as day} + + {/each} + + +
+
{day.name}
+
+ {day.date} +
+
+
+ {#each schedules.filter((s) => s.dayOfWeek === day.dayIndex) as schedule} + {@const sub = findSubstitution( + day.date, + schedule.startTime, + )} + +
+ openCreateModal( + schedule, + day.date, + )} + class="relative cursor-pointer hover:scale-[1.02] transition-transform group" + > + + + {#if sub} +
+ {#if sub.taken_by_user_id} +
+ Übernommen +
+
+ {sub.taken_by_username} +
+ {:else} +
+ Gesucht +
+
+ Offen +
+ {/if} + +
+ {sub.title} +
+
+ {/if} + + {#if !sub} +
+ + Erstellen +
+ {/if} +
+ {/each} + + {#if schedules.filter((s) => s.dayOfWeek === day.dayIndex).length === 0} +
+ - Frei - +
+ {/if} +
+
+
- -
- - -
- -
-
- - -
-
- - -
-
- -
- - -
- - -
-
-
-

Übersicht

-
- - - - - - - - - - - {#each substitutions as s} - - - - - - - {:else} - - {/each} - -
DatumZeitTitelStatus
{s.date}{s.start_time} - {s.end_time} -
{s.title}
- {#if s.notes}
- {s.notes} -
{/if} -
- {#if s.taken_by_user_id} - Übernommen -
- von {s.taken_by_username} -
- {:else} - Offen - {/if} -
Keine Einträge
+
+
+

Liste aller Vertretungen

+
+ + + + + + + + + + + + {#each substitutions as s} + + + + + + + + {:else} + + {/each} + +
DatumZeitTitel / NotizStatus
{s.date}{s.start_time} - {s.end_time} +
{s.title}
+ {#if s.notes}
+ {s.notes} +
{/if} +
+ {#if s.taken_by_user_id} + ✓ {s.taken_by_username} + {:else} + Offen + {/if} + + +
Keine Einträge
+
+
-
+ + + + + + diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index b76372f..2134c7e 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -326,11 +326,24 @@ export const getAllSubstitutions = async () => { }; 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 () => { const data = await request("/substitutions/open"); + console.log(data); return data; };