feat: implement working version of substitution marketplace

This commit is contained in:
Patryk Hegenberg 2026-01-17 09:36:55 +01:00
parent 3e2b6d46e6
commit 53244457c1
7 changed files with 590 additions and 206 deletions

View file

@ -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(&currentDate, &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(&currentDate, &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()
}

View file

@ -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)
}

View file

@ -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")

View file

@ -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"`
} }

View file

@ -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 = [];

View file

@ -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>

View file

@ -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;
}; };