diff --git a/backend/pkg/db/tasks/tasks.go b/backend/pkg/db/tasks/tasks.go
index ee25112..095ad22 100644
--- a/backend/pkg/db/tasks/tasks.go
+++ b/backend/pkg/db/tasks/tasks.go
@@ -1,41 +1,34 @@
package tasks
import (
- "fmt"
- "time"
-
- cache "github.com/jfarleyx/go-simple-cache"
"github.com/johannesbuehl/golunteer/backend/pkg/db"
)
-type tasksDB struct {
- ID int `db:"id"`
- Text string `db:"text"`
- Enabled bool `db:"enabled"`
+type TaskDB struct {
+ ID int `json:"id" db:"id"`
+ Task
}
type Task struct {
- Text string `json:"text"`
- Enabled bool `json:"enabled"`
+ Text string `json:"text" db:"text"`
+ Enabled bool `json:"enabled" db:"enabled"`
}
-var c *cache.Cache
+func GetSlice() ([]TaskDB, error) {
+ var tasksRaw []TaskDB
-func Get() (map[int]Task, error) {
- if tasks, hit := c.Get("tasks"); !hit {
- refresh()
-
- return nil, fmt.Errorf("tasks not stored cached")
+ if err := db.DB.Select(&tasksRaw, "SELECT * FROM TASKS"); err != nil {
+ return nil, err
} else {
- return tasks.(map[int]Task), nil
+ return tasksRaw, nil
}
+
}
-func refresh() {
- // get the tasksRaw from the database
- var tasksRaw []tasksDB
-
- if err := db.DB.Select(&tasksRaw, "SELECT * FROM TASKS"); err == nil {
+func GetMap() (map[int]Task, error) {
+ if tasksRaw, err := GetSlice(); err != nil {
+ return nil, err
+ } else {
// convert the result in a map
tasks := map[int]Task{}
@@ -46,14 +39,18 @@ func refresh() {
}
}
- c.Set("tasks", tasks)
+ return tasks, nil
}
}
-func init() {
- c = cache.New(24 * time.Hour)
+func Add(t Task) error {
+ _, err := db.DB.NamedExec("INSERT INTO TASKS (text, enabled) VALUES (:text, :enabled)", &t)
- c.OnExpired(refresh)
-
- refresh()
+ return err
+}
+
+func Update(t TaskDB) error {
+ _, err := db.DB.NamedExec("UPDATE TASKS set text = :text, enabled = :enabled WHERE id = :id", &t)
+
+ return err
}
diff --git a/backend/pkg/router/router.go b/backend/pkg/router/router.go
index a70297d..d6c3a48 100644
--- a/backend/pkg/router/router.go
+++ b/backend/pkg/router/router.go
@@ -87,11 +87,13 @@ func init() {
"events": postEvent,
"users": postUser,
"availabilities": postAvailabilitie,
+ "tasks": postTask,
},
"PATCH": {
"users": patchUser,
"events": patchEvent,
"availabilities": patchAvailabilities,
+ "tasks": patchTask,
},
"PUT": {
"users/password": putPassword,
diff --git a/backend/pkg/router/tasks.go b/backend/pkg/router/tasks.go
index 6665647..0fc0e73 100644
--- a/backend/pkg/router/tasks.go
+++ b/backend/pkg/router/tasks.go
@@ -6,15 +6,112 @@ import (
)
func getTasks(args HandlerArgs) responseMessage {
- response := responseMessage{}
+ // check wether the "map"-query is given
+ if args.C.QueryBool("map") {
+ if tasks, err := tasks.GetMap(); err != nil {
+ logger.Error().Msgf("can't get tasks: %v", err)
- if tasks, err := tasks.Get(); err != nil {
- response.Status = fiber.StatusInternalServerError
-
- logger.Error().Msgf("can't get tasks: %v", err)
+ return responseMessage{
+ Status: fiber.StatusInternalServerError,
+ }
+ } else {
+ return responseMessage{
+ Data: tasks,
+ }
+ }
} else {
- response.Data = tasks
- }
+ if taskSlice, err := tasks.GetSlice(); err != nil {
+ logger.Error().Msgf("can't get tasks: %v", err)
- return response
+ return responseMessage{
+ Status: fiber.StatusInternalServerError,
+ }
+ } else {
+ return responseMessage{
+ Data: struct {
+ Tasks []tasks.TaskDB `json:"tasks"`
+ }{Tasks: taskSlice},
+ }
+ }
+ }
+}
+
+func postTask(args HandlerArgs) responseMessage {
+ // check admin
+ if !args.User.Admin {
+ logger.Log().Msgf("user is not admin")
+
+ return responseMessage{
+ Status: fiber.StatusUnauthorized,
+ }
+ } else {
+ // parse the body
+ var task tasks.Task
+
+ if err := args.C.BodyParser(&task); err != nil {
+ logger.Log().Msgf("can't parse body: %v", err)
+
+ return responseMessage{
+ Status: fiber.StatusBadRequest,
+ }
+
+ // validate the body
+ } else if err := validate.Struct(&task); err != nil {
+ logger.Log().Msgf("invalid body: %v", err)
+
+ return responseMessage{
+ Status: fiber.StatusBadRequest,
+ }
+
+ // insert the task into the database
+ } else if err := tasks.Add(task); err != nil {
+ logger.Error().Msgf("can't add task: %v", err)
+
+ return responseMessage{
+ Status: fiber.StatusInternalServerError,
+ }
+ } else {
+ return responseMessage{}
+ }
+ }
+}
+
+func patchTask(args HandlerArgs) responseMessage {
+ // check admin
+ if !args.User.Admin {
+ logger.Log().Msgf("user is not admin")
+
+ return responseMessage{
+ Status: fiber.StatusUnauthorized,
+ }
+ } else {
+ // parse the body
+ var task tasks.TaskDB
+
+ if err := args.C.BodyParser(&task); err != nil {
+ logger.Log().Msgf("can't parse body: %v", err)
+
+ return responseMessage{
+ Status: fiber.StatusBadRequest,
+ }
+
+ // validate the body
+ } else if err := validate.Struct(&task); err != nil {
+ logger.Log().Msgf("invalid body: %v", err)
+
+ return responseMessage{
+ Status: fiber.StatusBadRequest,
+ }
+
+ // insert the task into the database
+ } else if err := tasks.Update(task); err != nil {
+ logger.Error().Msgf("can't update task: %v", err)
+
+ return responseMessage{
+ Status: fiber.StatusInternalServerError,
+ }
+ } else {
+ return responseMessage{}
+ }
+ }
}
diff --git a/client/src/app/admin/AddAvailability.tsx b/client/src/app/admin/(availabilities)/AddAvailability.tsx
similarity index 100%
rename from client/src/app/admin/AddAvailability.tsx
rename to client/src/app/admin/(availabilities)/AddAvailability.tsx
diff --git a/client/src/app/admin/Availabilities.tsx b/client/src/app/admin/(availabilities)/Availabilities.tsx
similarity index 95%
rename from client/src/app/admin/Availabilities.tsx
rename to client/src/app/admin/(availabilities)/Availabilities.tsx
index 7564762..515c491 100644
--- a/client/src/app/admin/Availabilities.tsx
+++ b/client/src/app/admin/(availabilities)/Availabilities.tsx
@@ -1,6 +1,6 @@
import { color2Tailwind, colors } from "@/components/Colorselector";
import { apiCall } from "@/lib";
-import { Edit } from "@carbon/icons-react";
+import { AddLarge, Edit } from "@carbon/icons-react";
import {
Button,
Checkbox,
@@ -78,7 +78,11 @@ export default function Availabilities() {
const topContent = (
<>
-
>
@@ -90,6 +94,7 @@ export default function Availabilities() {
aria-label="Table with the availabilites"
shadow="none"
isHeaderSticky
+ isStriped
topContent={topContent}
sortDescriptor={availabilities.sortDescriptor}
onSortChange={availabilities.sort}
diff --git a/client/src/app/admin/AvailabilityEditor.tsx b/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx
similarity index 100%
rename from client/src/app/admin/AvailabilityEditor.tsx
rename to client/src/app/admin/(availabilities)/AvailabilityEditor.tsx
diff --git a/client/src/app/admin/EditAvailability.tsx b/client/src/app/admin/(availabilities)/EditAvailability.tsx
similarity index 75%
rename from client/src/app/admin/EditAvailability.tsx
rename to client/src/app/admin/(availabilities)/EditAvailability.tsx
index 78f84fc..72f7f82 100644
--- a/client/src/app/admin/EditAvailability.tsx
+++ b/client/src/app/admin/(availabilities)/EditAvailability.tsx
@@ -9,7 +9,7 @@ export default function EditAvailability(props: {
onOpenChange?: (isOpen: boolean) => void;
onSuccess?: () => void;
}) {
- async function addAvailability(a: Availability) {
+ async function updateAvailability(a: Availability) {
const result = await apiCall("PATCH", "availabilities", undefined, a);
if (result.ok) {
@@ -21,7 +21,14 @@ export default function EditAvailability(props: {
return (
+ Edit Availability{" "}
+
+ "{props.value?.text}"
+
+ >
+ }
footer={
}>
Update
@@ -30,7 +37,7 @@ export default function EditAvailability(props: {
value={props.value}
isOpen={props.isOpen}
onOpenChange={props.onOpenChange}
- onSubmit={addAvailability}
+ onSubmit={updateAvailability}
/>
);
}
diff --git a/client/src/app/admin/(tasks)/AddTask.tsx b/client/src/app/admin/(tasks)/AddTask.tsx
new file mode 100644
index 0000000..925979e
--- /dev/null
+++ b/client/src/app/admin/(tasks)/AddTask.tsx
@@ -0,0 +1,33 @@
+import { apiCall } from "@/lib";
+import TaskEditor, { Task } from "./TaskEditor";
+import { Button } from "@heroui/react";
+import { AddLarge } from "@carbon/icons-react";
+
+export default function AddTask(props: {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ onSuccess?: () => void;
+}) {
+ async function addTask(a: Task) {
+ const result = await apiCall("POST", "tasks", undefined, a);
+
+ if (result.ok) {
+ props.onSuccess?.();
+ props.onOpenChange?.(false);
+ }
+ }
+
+ return (
+ }>
+ Add
+
+ }
+ isOpen={props.isOpen}
+ onOpenChange={props.onOpenChange}
+ onSubmit={addTask}
+ />
+ );
+}
diff --git a/client/src/app/admin/(tasks)/EditTask.tsx b/client/src/app/admin/(tasks)/EditTask.tsx
new file mode 100644
index 0000000..5668f88
--- /dev/null
+++ b/client/src/app/admin/(tasks)/EditTask.tsx
@@ -0,0 +1,43 @@
+import { apiCall, Task } from "@/lib";
+import { Button } from "@heroui/react";
+import { Renew } from "@carbon/icons-react";
+import TaskEditor from "./TaskEditor";
+
+export default function EditTask(props: {
+ value: Task | undefined;
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ onSuccess?: () => void;
+}) {
+ async function updateTask(t: Task) {
+ const result = await apiCall("PATCH", "tasks", undefined, t);
+
+ if (result.ok) {
+ props.onSuccess?.();
+ props.onOpenChange?.(false);
+ }
+ }
+
+ return (
+
+ Edit Task{" "}
+
+ "{props.value?.text}"
+
+ >
+ }
+ footer={
+ }>
+ Update
+
+ }
+ value={props.value}
+ isOpen={props.isOpen}
+ onOpenChange={props.onOpenChange}
+ onSubmit={updateTask}
+ />
+ );
+}
diff --git a/client/src/app/admin/(tasks)/TaskEditor.tsx b/client/src/app/admin/(tasks)/TaskEditor.tsx
new file mode 100644
index 0000000..84513dc
--- /dev/null
+++ b/client/src/app/admin/(tasks)/TaskEditor.tsx
@@ -0,0 +1,80 @@
+import { Task } from "@/lib";
+import {
+ Checkbox,
+ Form,
+ Input,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+} from "@heroui/react";
+import React, { FormEvent, useState } from "react";
+
+export default function TaskEditor(props: {
+ header: React.ReactNode;
+ footer: React.ReactNode;
+ value?: Task;
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ onSubmit?: (e: Task) => void;
+}) {
+ const [text, setText] = useState(props.value?.text);
+ const [enabled, setEnabled] = useState(props.value?.enabled ?? true);
+
+ function submit(e: FormEvent) {
+ const formData = Object.fromEntries(new FormData(e.currentTarget)) as {
+ text: string;
+ color: string;
+ enabled: string;
+ };
+
+ props.onSubmit?.({
+ ...formData,
+ id: props.value?.id,
+ enabled: formData.enabled == "true",
+ });
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/app/admin/(tasks)/Tasks.tsx b/client/src/app/admin/(tasks)/Tasks.tsx
new file mode 100644
index 0000000..c1ba1a6
--- /dev/null
+++ b/client/src/app/admin/(tasks)/Tasks.tsx
@@ -0,0 +1,139 @@
+import { apiCall, Task } from "@/lib";
+import { AddLarge, Edit } from "@carbon/icons-react";
+import {
+ Button,
+ Checkbox,
+ Table,
+ TableBody,
+ TableCell,
+ TableColumn,
+ TableHeader,
+ TableRow,
+ Tooltip,
+} from "@heroui/react";
+import { useAsyncList } from "@react-stately/data";
+import { useState } from "react";
+import AddTask from "./AddTask";
+import EditTask from "./EditTask";
+
+export default function Tasks() {
+ const [showAddTask, setShowAddTask] = useState(false);
+ const [editTask, setEditTask] = useState();
+
+ const tasks = useAsyncList({
+ async load() {
+ const result = await apiCall("GET", "tasks");
+
+ if (result.ok) {
+ const json = await result.json();
+
+ return {
+ items: json.tasks,
+ };
+ } else {
+ return {
+ items: [],
+ };
+ }
+ },
+ async sort({ items, sortDescriptor }) {
+ return {
+ items: items.sort((a, b) => {
+ let cmp = 0;
+
+ switch (sortDescriptor.column) {
+ case "text":
+ cmp = a.text.localeCompare(b.text);
+ break;
+ case "enabled":
+ if (a.enabled && !b.enabled) {
+ cmp = -1;
+ } else if (!a.enabled && b.enabled) {
+ cmp = 1;
+ }
+ break;
+ }
+
+ if (sortDescriptor.direction === "descending") {
+ cmp *= -1;
+ }
+
+ return cmp;
+ }),
+ };
+ },
+ });
+
+ const topContent = (
+ <>
+ }
+ onPress={() => setShowAddTask(true)}
+ >
+ Add Task
+
+ >
+ );
+
+ return (
+
+
+
+
+ Text
+
+
+ Enabled
+
+
+ Edit
+
+
+
+ {(task) => (
+
+ {task.text}
+
+
+
+
+ setEditTask(task)}
+ >
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
(!isOpen ? setEditTask(undefined) : null)}
+ onSuccess={tasks.reload}
+ />
+
+ );
+}
diff --git a/client/src/app/admin/AddUser.tsx b/client/src/app/admin/(users)/AddUser.tsx
similarity index 100%
rename from client/src/app/admin/AddUser.tsx
rename to client/src/app/admin/(users)/AddUser.tsx
diff --git a/client/src/app/admin/EditUser.tsx b/client/src/app/admin/(users)/EditUser.tsx
similarity index 100%
rename from client/src/app/admin/EditUser.tsx
rename to client/src/app/admin/(users)/EditUser.tsx
diff --git a/client/src/app/admin/Users.tsx b/client/src/app/admin/(users)/Users.tsx
similarity index 93%
rename from client/src/app/admin/Users.tsx
rename to client/src/app/admin/(users)/Users.tsx
index fc1d334..f6d25f5 100644
--- a/client/src/app/admin/Users.tsx
+++ b/client/src/app/admin/(users)/Users.tsx
@@ -14,7 +14,7 @@ import {
import { useAsyncList } from "@react-stately/data";
import { FormEvent, useState } from "react";
import AddUser from "./AddUser";
-import { Edit } from "@carbon/icons-react";
+import { AddLarge, Edit } from "@carbon/icons-react";
import EditUser from "./EditUser";
export default function Users() {
@@ -81,7 +81,13 @@ export default function Users() {
// content above the user-tabel
const topContent = (
<>
- setShowAddUser(true)}>Add User
+ }
+ onPress={() => setShowAddUser(true)}
+ >
+ Add User
+
>
);
@@ -91,6 +97,7 @@ export default function Users() {
aria-label="Table with all users"
shadow="none"
isHeaderSticky
+ isStriped
topContent={topContent}
sortDescriptor={users.sortDescriptor}
onSortChange={users.sort}
diff --git a/client/src/app/admin/page.tsx b/client/src/app/admin/page.tsx
index a827de4..9fcf5f0 100644
--- a/client/src/app/admin/page.tsx
+++ b/client/src/app/admin/page.tsx
@@ -1,18 +1,23 @@
"use client";
import { Tab, Tabs } from "@heroui/react";
-import Users from "./Users";
-import Availabilities from "./Availabilities";
+import Users from "./(users)/Users";
+import Availabilities from "./(availabilities)/Availabilities";
+import Tasks from "./(tasks)/Tasks";
export default function AdminDashboard() {
return (
-
+
- Tasks
-
+
+
+
+
+
+
);
diff --git a/client/src/app/assignments/page.tsx b/client/src/app/assignments/page.tsx
index 4289d56..71b0bef 100644
--- a/client/src/app/assignments/page.tsx
+++ b/client/src/app/assignments/page.tsx
@@ -3,7 +3,7 @@
import AddEvent from "@/components/Event/AddEvent";
import EditEvent, { EventSubmitData } from "@/components/Event/EditEvent";
import LocalDate from "@/components/LocalDate";
-import { apiCall, getTasks } from "@/lib";
+import { apiCall, getTaskMap } from "@/lib";
import { EventData } from "@/Zustand";
import {
Add,
@@ -48,7 +48,7 @@ export default function AdminPanel() {
// get the available tasks and craft them into the headers
const headers = useAsyncList({
async load() {
- const tasks = await getTasks();
+ const tasks = await getTaskMap();
return {
items: [
diff --git a/client/src/components/Event/EditEvent.tsx b/client/src/components/Event/EditEvent.tsx
index 7011642..9850a3a 100644
--- a/client/src/components/Event/EditEvent.tsx
+++ b/client/src/components/Event/EditEvent.tsx
@@ -19,7 +19,7 @@ import {
Spinner,
Textarea,
} from "@heroui/react";
-import { getTasks, Task } from "@/lib";
+import { getTaskMap, Task } from "@/lib";
import { EventData } from "@/Zustand";
export interface EventSubmitData {
@@ -103,7 +103,7 @@ export default function EditEvent(props: {
// get the available tasks and initialize the state with them
useEffect(() => {
(async () => {
- const tasks = await getTasks();
+ const tasks = await getTaskMap();
setTasksMap(tasks);
diff --git a/client/src/lib.ts b/client/src/lib.ts
index 296295c..aac6f9d 100644
--- a/client/src/lib.ts
+++ b/client/src/lib.ts
@@ -94,12 +94,13 @@ export function vaidatePassword(password: string): string[] {
}
export interface Task {
+ id: number | undefined;
text: string;
enabled: boolean;
}
-export async function getTasks(): Promise> {
- const result = await apiCall("GET", "tasks");
+export async function getTaskMap(): Promise> {
+ const result = await apiCall("GET", "tasks", { map: true });
if (result.ok) {
const tasks = await result.json();