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,
|
||||
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])
|
||||
_, 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ 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"`
|
||||
|
|
@ -145,4 +146,5 @@ type CreateSubstitutionRequest struct {
|
|||
EndTime string `json:"end_time" validate:"required"`
|
||||
Title string `json:"title" validate:"required"`
|
||||
Notes string `json:"notes"`
|
||||
ScheduleID int `json:"schedule_id"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
let showPwModal = false;
|
||||
let pwData = { old: "", new1: "", new2: "" };
|
||||
|
||||
// State für die Ansicht
|
||||
let activeView = "schedule";
|
||||
let openSubstitutions = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,138 +1,333 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { getAllSubstitutions, createSubstitution } from "../../lib/api";
|
||||
import {
|
||||
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";
|
||||
|
||||
const today = new Date();
|
||||
let currentISOYear = getISOYear(today);
|
||||
let currentWeek = getISOWeek(today);
|
||||
|
||||
let substitutions = [];
|
||||
let newSub = { date: "", startTime: "", endTime: "", title: "", notes: "" };
|
||||
let schedules = [];
|
||||
|
||||
let showModal = false;
|
||||
let selectedSchedule = null;
|
||||
let form = { date: "", startTime: "", endTime: "", title: "", notes: "" };
|
||||
|
||||
$: weekDates = Array.from({ length: 5 }, (_, i) => {
|
||||
const d = getDateOfISOWeek(currentWeek, currentISOYear);
|
||||
d.setDate(d.getDate() + i);
|
||||
return {
|
||||
name: ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"][
|
||||
i
|
||||
],
|
||||
date: formatDate(d),
|
||||
dayIndex: i,
|
||||
};
|
||||
});
|
||||
|
||||
onMount(loadData);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
substitutions = await getAllSubstitutions();
|
||||
} catch (e) {}
|
||||
const [subs, scheds] = await Promise.all([
|
||||
getAllSubstitutions(),
|
||||
getSchedules(),
|
||||
]);
|
||||
substitutions = subs;
|
||||
schedules = scheds;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function changeWeek(delta) {
|
||||
const d = getDateOfISOWeek(currentWeek, currentISOYear);
|
||||
d.setDate(d.getDate() + delta * 7);
|
||||
currentWeek = getISOWeek(d);
|
||||
currentISOYear = getISOYear(d);
|
||||
}
|
||||
|
||||
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 (!newSub.date || !newSub.startTime || !newSub.title) {
|
||||
addToast("Bitte Pflichtfelder ausfüllen", "warning");
|
||||
return;
|
||||
}
|
||||
if (!form.title || !form.date) return;
|
||||
try {
|
||||
await createSubstitution(newSub);
|
||||
addToast("Vertretung ausgeschrieben", "success");
|
||||
newSub = { date: "", startTime: "", endTime: "", title: "", notes: "" };
|
||||
await loadData();
|
||||
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>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200 h-fit">
|
||||
<div class="space-y-8 fade-in">
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4">Neue Vertretung ausschreiben</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">Titel / Klasse</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
placeholder="z.B. Mathe 4a"
|
||||
bind:value={newSub.title}
|
||||
/>
|
||||
</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>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="card-title">Vertretung planen</h3>
|
||||
|
||||
<div class="flex items-center gap-4 bg-base-200 rounded-lg p-1">
|
||||
<button
|
||||
class="btn btn-primary w-full mt-4"
|
||||
on:click={handleCreate}
|
||||
disabled={$loading}
|
||||
class="btn btn-sm btn-ghost btn-circle"
|
||||
on:click={() => changeWeek(-1)}>❮</button
|
||||
>
|
||||
Veröffentlichen
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<div class="font-bold">KW {currentWeek}</div>
|
||||
<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="lg:col-span-2">
|
||||
<h3 class="font-bold text-xl mb-4">Übersicht</h3>
|
||||
<div
|
||||
class="overflow-x-auto bg-base-100 rounded-lg shadow border border-base-200"
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title mb-4">Liste aller Vertretungen</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Zeit</th>
|
||||
<th>Titel</th>
|
||||
<th>Titel / Notiz</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each substitutions as s}
|
||||
<tr class="hover">
|
||||
<td class="whitespace-nowrap font-mono text-sm">{s.date}</td>
|
||||
<tr class="hover group">
|
||||
<td class="font-mono text-sm">{s.date}</td>
|
||||
<td>{s.start_time} - {s.end_time}</td>
|
||||
<td>
|
||||
<div class="font-bold">{s.title}</div>
|
||||
{#if s.notes}<div class="text-xs opacity-60 truncate max-w-xs">
|
||||
{#if s.notes}<div
|
||||
class="text-xs opacity-50"
|
||||
>
|
||||
{s.notes}
|
||||
</div>{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if s.taken_by_user_id}
|
||||
<span class="badge badge-success gap-2"> Übernommen </span>
|
||||
<div class="text-xs mt-1 opacity-70">
|
||||
von {s.taken_by_username}
|
||||
</div>
|
||||
<span class="badge badge-success gap-2"
|
||||
>✓ {s.taken_by_username}</span
|
||||
>
|
||||
{:else}
|
||||
<span class="badge badge-warning badge-outline">Offen</span>
|
||||
<span
|
||||
class="badge badge-warning badge-outline"
|
||||
>Offen</span
|
||||
>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button
|
||||
class="btn btn-ghost btn-xs text-error opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
on:click={() => handleDelete(s.id)}
|
||||
title="Löschen"
|
||||
>
|
||||
<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="4" class="text-center opacity-50 py-8"
|
||||
><td
|
||||
colspan="5"
|
||||
class="text-center opacity-50 py-8"
|
||||
>Keine Einträge</td
|
||||
></tr
|
||||
>
|
||||
|
|
@ -141,4 +336,82 @@
|
|||
</table>
|
||||
</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) => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue