added my assigned events overview
This commit is contained in:
@@ -25,13 +25,13 @@ type EventAssignment struct {
|
|||||||
UserName *string `db:"userName" json:"userName"`
|
UserName *string `db:"userName" json:"userName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventWithAssignment struct {
|
type EventWithAssignments struct {
|
||||||
EventData
|
EventData
|
||||||
Tasks []EventAssignment `json:"tasks"`
|
Tasks []EventAssignment `json:"tasks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventWithAvailabilities struct {
|
type EventWithAvailabilities struct {
|
||||||
EventWithAssignment
|
EventWithAssignments
|
||||||
Availabilities availabilities.AvailabilityMap `json:"availabilities"`
|
Availabilities availabilities.AvailabilityMap `json:"availabilities"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,22 +41,22 @@ type EventCreate struct {
|
|||||||
Tasks []int `json:"tasks" validate:"required,min=1"`
|
Tasks []int `json:"tasks" validate:"required,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// transform the database-entry to an Event
|
// transform the database-entry to an WithAssignments
|
||||||
func (e EventData) Event() (EventWithAssignment, error) {
|
func (e EventData) WithAssignments() (EventWithAssignments, error) {
|
||||||
// get the assignments associated with the event
|
// get the assignments associated with the event
|
||||||
if assignemnts, err := Assignments(e.EventID); err != nil {
|
if assignemnts, err := Assignments(e.EventID); err != nil {
|
||||||
return EventWithAssignment{}, err
|
return EventWithAssignments{}, err
|
||||||
} else {
|
} else {
|
||||||
return EventWithAssignment{
|
return EventWithAssignments{
|
||||||
EventData: e,
|
EventData: e,
|
||||||
Tasks: assignemnts,
|
Tasks: assignemnts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e EventData) EventWithAvailabilities() (EventWithAvailabilities, error) {
|
func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) {
|
||||||
// get the event with assignments
|
// get the event with assignments
|
||||||
if event, err := e.Event(); err != nil {
|
if event, err := e.WithAssignments(); err != nil {
|
||||||
return EventWithAvailabilities{}, err
|
return EventWithAvailabilities{}, err
|
||||||
|
|
||||||
// get the availabilities
|
// get the availabilities
|
||||||
@@ -64,8 +64,8 @@ func (e EventData) EventWithAvailabilities() (EventWithAvailabilities, error) {
|
|||||||
return EventWithAvailabilities{}, err
|
return EventWithAvailabilities{}, err
|
||||||
} else {
|
} else {
|
||||||
return EventWithAvailabilities{
|
return EventWithAvailabilities{
|
||||||
EventWithAssignment: event,
|
EventWithAssignments: event,
|
||||||
Availabilities: availabilities,
|
Availabilities: availabilities,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,15 +173,15 @@ func All() ([]EventData, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAssignments() ([]EventWithAssignment, error) {
|
func WithAssignments() ([]EventWithAssignments, error) {
|
||||||
// get all events
|
// get all events
|
||||||
if eventsDB, err := All(); err != nil {
|
if eventsDB, err := All(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
events := make([]EventWithAssignment, len(eventsDB))
|
events := make([]EventWithAssignments, len(eventsDB))
|
||||||
|
|
||||||
for ii, e := range eventsDB {
|
for ii, e := range eventsDB {
|
||||||
if ev, err := e.Event(); err != nil {
|
if ev, err := e.WithAssignments(); err != nil {
|
||||||
logger.Logger.Error().Msgf("can't get assignments for event with assignmentID = %d: %v", e.EventID, err)
|
logger.Logger.Error().Msgf("can't get assignments for event with assignmentID = %d: %v", e.EventID, err)
|
||||||
} else {
|
} else {
|
||||||
events[ii] = ev
|
events[ii] = ev
|
||||||
@@ -200,8 +200,14 @@ func WithAvailabilities() ([]EventWithAvailabilities, error) {
|
|||||||
events := make([]EventWithAvailabilities, len(eventsDB))
|
events := make([]EventWithAvailabilities, len(eventsDB))
|
||||||
|
|
||||||
for ii, e := range eventsDB {
|
for ii, e := range eventsDB {
|
||||||
if ev, err := e.EventWithAvailabilities(); err != nil {
|
if ev, err := e.WithAvailabilities(); err != nil {
|
||||||
logger.Logger.Error().Msgf("can't get availabilities for event with eventID = %d: %v", e.EventID, err)
|
logger.Logger.Error().Msgf("can't get availabilities for event with eventID = %d: %v", e.EventID, err)
|
||||||
|
|
||||||
|
// remove the last element from the return-slice, since there is now one element less
|
||||||
|
if len(events) > 0 {
|
||||||
|
events = events[:len(events)-1]
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
events[ii] = ev
|
events[ii] = ev
|
||||||
}
|
}
|
||||||
@@ -250,20 +256,33 @@ func Assignments(eventID int) ([]EventAssignment, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func User(userName string) ([]EventWithAssignment, error) {
|
func User(userName string) ([]EventWithAssignments, error) {
|
||||||
// get all assignments of the user
|
// get all assignments of the user
|
||||||
|
|
||||||
// var events []EventWithAssignment
|
// var eventsDB []EventWithAssignment
|
||||||
var events []struct {
|
var eventsDB []EventData
|
||||||
EventData
|
|
||||||
TaskID int `db:"taskID"`
|
|
||||||
UserName string `db:"userName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.DB.Select(&events, "SELECT DISTINCT * FROM USER_ASSIGNMENTS INNER JOIN EVENTS ON USER_ASSIGNMENTS.eventID = EVENTS.eventID WHERE userName = $1", userName); err != nil {
|
// get all the events where the volunteer is assigned a task
|
||||||
|
if err := db.DB.Select(&eventsDB, "SELECT DISTINCT EVENTS.date, EVENTS.description, EVENTS.eventID FROM USER_ASSIGNMENTS INNER JOIN EVENTS ON USER_ASSIGNMENTS.eventID = EVENTS.eventID WHERE userName = $1", userName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return nil, nil
|
// for each event create an event with assignments
|
||||||
|
events := make([]EventWithAssignments, len(eventsDB))
|
||||||
|
|
||||||
|
for ii, event := range eventsDB {
|
||||||
|
if eventsWithAssignment, err := event.WithAssignments(); err != nil {
|
||||||
|
logger.Logger.Error().Msgf("can't get assignments for event with eventID = %d: %v", event.EventID, err)
|
||||||
|
|
||||||
|
// remove the last element from the return-slice, since there is now one element less
|
||||||
|
if len(events) > 0 {
|
||||||
|
events = events[:len(events)-1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
events[ii] = eventsWithAssignment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type EventData = BaseEvent & {
|
|||||||
tasks: TaskAssignment[];
|
tasks: TaskAssignment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TaskAssignment {
|
export interface TaskAssignment {
|
||||||
taskID: number;
|
taskID: number;
|
||||||
taskName: string;
|
taskName: string;
|
||||||
userName: string | null;
|
userName: string | null;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||||
|
import Event from "@/components/Event/Event";
|
||||||
import { apiCall } from "@/lib";
|
import { apiCall } from "@/lib";
|
||||||
import { EventData } from "@/Zustand";
|
import zustand, { EventData } from "@/Zustand";
|
||||||
import { useAsyncList } from "@react-stately/data";
|
import { useAsyncList } from "@react-stately/data";
|
||||||
|
|
||||||
export default function MyEvents() {
|
export default function MyEvents() {
|
||||||
|
const user = zustand((state) => state.user);
|
||||||
|
|
||||||
const events = useAsyncList({
|
const events = useAsyncList({
|
||||||
async load() {
|
async load() {
|
||||||
const result = await apiCall<EventData[]>("GET", "events/user/assigned");
|
const result = await apiCall<EventData[]>("GET", "events/user/assigned");
|
||||||
@@ -26,8 +30,12 @@ export default function MyEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex justify-center gap-4">
|
||||||
<h2>{events.items.map((e) => e.date)}</h2>
|
{events.items.map((e) => (
|
||||||
|
<Event key={e.eventID} event={e}>
|
||||||
|
<AssignmentTable tasks={e.tasks} highlightUser={user?.userName} />
|
||||||
|
</Event>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export default function Overview() {
|
|||||||
|
|
||||||
<h1 className="mb-4 text-center text-4xl">My Events</h1>
|
<h1 className="mb-4 text-center text-4xl">My Events</h1>
|
||||||
<MyEvents />
|
<MyEvents />
|
||||||
<h1 className="mb-4 text-center text-4xl">
|
<h1 className="mb-4 mt-8 text-center text-4xl">
|
||||||
events that I don't have entered an availability yet
|
events that I don't have entered an availability yet
|
||||||
</h1>
|
</h1>
|
||||||
<PengingEvents />
|
<PengingEvents />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
116
client/src/app/assignments/VolunteerSelector.tsx
Normal file
116
client/src/app/assignments/VolunteerSelector.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import AvailabilityChip from "@/components/AvailabilityChip";
|
||||||
|
import { TaskAssignment } from "@/Zustand";
|
||||||
|
import { AddLarge } from "@carbon/icons-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownSection,
|
||||||
|
DropdownTrigger,
|
||||||
|
} from "@heroui/react";
|
||||||
|
import { EventWithAvailabilities } from "./page";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Availability } from "../admin/(availabilities)/AvailabilityEditor";
|
||||||
|
import { apiCall, classNames } from "@/lib";
|
||||||
|
|
||||||
|
export default function VolunteerSelector({
|
||||||
|
event,
|
||||||
|
task,
|
||||||
|
getAvailabilityById,
|
||||||
|
onReloadRequest,
|
||||||
|
}: {
|
||||||
|
event: EventWithAvailabilities;
|
||||||
|
task: TaskAssignment;
|
||||||
|
getAvailabilityById: (availabilityID: number) => Availability;
|
||||||
|
onReloadRequest: () => void;
|
||||||
|
}) {
|
||||||
|
async function sendVolunteerAssignment(
|
||||||
|
eventID: number,
|
||||||
|
taskID: number,
|
||||||
|
userName: string,
|
||||||
|
) {
|
||||||
|
const result = await apiCall(
|
||||||
|
"PUT",
|
||||||
|
"events/assignments",
|
||||||
|
{ eventID, taskID },
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
onReloadRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a command to the backend to remove an volunteer-assignment
|
||||||
|
async function removeVolunteerAssignment(eventID: number, taskID: number) {
|
||||||
|
const result = await apiCall("DELETE", "events/assignments", {
|
||||||
|
eventID,
|
||||||
|
taskID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
onReloadRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
{!!event.tasks.find((t) => t.taskID == task.taskID)?.userName ? (
|
||||||
|
<Chip
|
||||||
|
onClose={() =>
|
||||||
|
removeVolunteerAssignment(event.eventID, task.taskID)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{event.tasks.find((t) => t.taskID == task.taskID)?.userName}
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<Button isIconOnly size="sm" radius="md" variant="flat">
|
||||||
|
<AddLarge className="mx-auto" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
onAction={(a) =>
|
||||||
|
sendVolunteerAssignment(event.eventID, task.taskID, a as string)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.entries(event.availabilities).map(
|
||||||
|
([availabilityId, volunteers], iAvailability, aAvailabilities) => (
|
||||||
|
<DropdownSection
|
||||||
|
key={availabilityId}
|
||||||
|
showDivider={iAvailability < aAvailabilities.length - 1}
|
||||||
|
classNames={{
|
||||||
|
base: "flex flex-col justify-start",
|
||||||
|
heading: "mx-auto",
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
(
|
||||||
|
<AvailabilityChip
|
||||||
|
availability={getAvailabilityById(parseInt(availabilityId))}
|
||||||
|
/>
|
||||||
|
) as ReactElement & string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{volunteers.map((v) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={v}
|
||||||
|
classNames={{
|
||||||
|
base: "", // this empty class is needed, else some styles are applied
|
||||||
|
title: classNames({
|
||||||
|
"text-primary font-bold": v === task.userName,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownSection>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import { apiCall, getAvailabilities, getTasks } from "@/lib";
|
|||||||
import { EventData } from "@/Zustand";
|
import { EventData } from "@/Zustand";
|
||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
AddLarge,
|
|
||||||
Copy,
|
Copy,
|
||||||
Edit,
|
Edit,
|
||||||
NotAvailable,
|
NotAvailable,
|
||||||
@@ -17,12 +16,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Chip,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownSection,
|
|
||||||
DropdownTrigger,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -38,11 +31,11 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
import { useAsyncList } from "@react-stately/data";
|
import { useAsyncList } from "@react-stately/data";
|
||||||
import React, { Key, ReactElement, useEffect, useState } from "react";
|
import React, { Key, useEffect, useState } from "react";
|
||||||
import { Availability } from "../admin/(availabilities)/AvailabilityEditor";
|
import { Availability } from "../admin/(availabilities)/AvailabilityEditor";
|
||||||
import AvailabilityChip from "@/components/AvailabilityChip";
|
import VolunteerSelector from "./VolunteerSelector";
|
||||||
|
|
||||||
type EventWithAvailabilities = EventData & {
|
export type EventWithAvailabilities = EventData & {
|
||||||
availabilities: Record<string, string[]>;
|
availabilities: Record<string, string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,35 +130,6 @@ export default function AdminPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send a command to the backend to assign a volunteer to a task
|
// send a command to the backend to assign a volunteer to a task
|
||||||
async function sendVolunteerAssignment(
|
|
||||||
eventID: number,
|
|
||||||
taskID: number,
|
|
||||||
userName: string,
|
|
||||||
) {
|
|
||||||
const result = await apiCall(
|
|
||||||
"PUT",
|
|
||||||
"events/assignments",
|
|
||||||
{ eventID, taskID },
|
|
||||||
userName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
events.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sends a command to the backend to remove an volunteer-assignment
|
|
||||||
async function removeVolunteerAssignment(eventID: number, taskID: number) {
|
|
||||||
const result = await apiCall("DELETE", "events/assignments", {
|
|
||||||
eventID,
|
|
||||||
taskID,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
events.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send a delete request to the backend and close the popup on success
|
// send a delete request to the backend and close the popup on success
|
||||||
async function sendDeleteEvent() {
|
async function sendDeleteEvent() {
|
||||||
if (deleteEvent !== undefined) {
|
if (deleteEvent !== undefined) {
|
||||||
@@ -236,72 +200,16 @@ export default function AdminPanel() {
|
|||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
// only show the selector, if the task is needed for the event
|
// only show the selector, if the task is needed for the event
|
||||||
if (event.tasks?.some((t) => t.taskID == key)) {
|
const task = event.tasks.find((t) => t.taskID == key);
|
||||||
|
|
||||||
|
if (!!task) {
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<VolunteerSelector
|
||||||
<DropdownTrigger>
|
event={event}
|
||||||
{!!event.tasks.find((t) => t.taskID == key)?.userName ? (
|
task={task}
|
||||||
<Chip
|
getAvailabilityById={getAvailabilityById}
|
||||||
onClose={() =>
|
onReloadRequest={events.reload}
|
||||||
removeVolunteerAssignment(event.eventID, key as number)
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
{event.tasks.find((t) => t.taskID == key)?.userName}
|
|
||||||
</Chip>
|
|
||||||
) : (
|
|
||||||
<Button isIconOnly size="sm" radius="md" variant="flat">
|
|
||||||
<AddLarge className="mx-auto" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu
|
|
||||||
onAction={(a) =>
|
|
||||||
sendVolunteerAssignment(
|
|
||||||
event.eventID,
|
|
||||||
key as number,
|
|
||||||
a as string,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Object.entries(event.availabilities).map(
|
|
||||||
(
|
|
||||||
[availabilityId, volunteers],
|
|
||||||
iAvailability,
|
|
||||||
aAvailabilities,
|
|
||||||
) => (
|
|
||||||
<DropdownSection
|
|
||||||
key={availabilityId}
|
|
||||||
showDivider={iAvailability < aAvailabilities.length - 1}
|
|
||||||
classNames={{
|
|
||||||
base: "flex flex-col justify-start",
|
|
||||||
heading: "mx-auto",
|
|
||||||
}}
|
|
||||||
className="justi"
|
|
||||||
title={
|
|
||||||
(
|
|
||||||
<AvailabilityChip
|
|
||||||
availability={getAvailabilityById(
|
|
||||||
parseInt(availabilityId),
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) as ReactElement & string
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{volunteers.map((v) => (
|
|
||||||
<DropdownItem
|
|
||||||
key={v}
|
|
||||||
classNames={{
|
|
||||||
base: "", // this empty class is needed, else some styles are applied
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{v}
|
|
||||||
</DropdownItem>
|
|
||||||
))}
|
|
||||||
</DropdownSection>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <NotAvailable className="mx-auto text-foreground-300" />;
|
return <NotAvailable className="mx-auto text-foreground-300" />;
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
|
import { classNames } from "@/lib";
|
||||||
import { EventData } from "@/Zustand";
|
import { EventData } from "@/Zustand";
|
||||||
|
|
||||||
export default function AssignmentTable({
|
export default function AssignmentTable({
|
||||||
tasks,
|
tasks,
|
||||||
|
highlightTask,
|
||||||
|
highlightUser,
|
||||||
}: {
|
}: {
|
||||||
tasks: EventData["tasks"];
|
tasks: EventData["tasks"];
|
||||||
|
highlightUser?: string;
|
||||||
|
highlightTask?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<tr key={task.taskID}>
|
<tr
|
||||||
|
key={task.taskID}
|
||||||
|
className={classNames({
|
||||||
|
"text-danger":
|
||||||
|
task.userName === highlightUser ||
|
||||||
|
task.taskName === highlightTask,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<th className="pr-4 text-left">{task.taskName}</th>
|
<th className="pr-4 text-left">{task.taskName}</th>
|
||||||
<td>
|
<td>
|
||||||
{task.userName ?? (
|
{task.userName ?? (
|
||||||
|
|||||||
Reference in New Issue
Block a user