added availability changing to events view
This commit is contained in:
@@ -35,6 +35,11 @@ type EventWithAvailabilities struct {
|
|||||||
Availabilities availabilities.AvailabilityMap `json:"availabilities"`
|
Availabilities availabilities.AvailabilityMap `json:"availabilities"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventWithAssignmentsUserAvailability struct {
|
||||||
|
EventWithAssignments
|
||||||
|
Availability *int `json:"availability" db:"availabilityID"`
|
||||||
|
}
|
||||||
|
|
||||||
type EventCreate struct {
|
type EventCreate struct {
|
||||||
Date string `db:"date" json:"date" validate:"required,datetime=2006-01-02T15:04:05.999999999Z"`
|
Date string `db:"date" json:"date" validate:"required,datetime=2006-01-02T15:04:05.999999999Z"`
|
||||||
Description string `db:"description" json:"description"`
|
Description string `db:"description" json:"description"`
|
||||||
@@ -54,6 +59,19 @@ func (e EventData) WithAssignments() (EventWithAssignments, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e EventWithAssignments) WithUserAvailability(userName string) (EventWithAssignmentsUserAvailability, error) {
|
||||||
|
// get the availability of the user
|
||||||
|
event := EventWithAssignmentsUserAvailability{
|
||||||
|
EventWithAssignments: e,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.Select(&event, "SELECT availabilityID FROM USER_AVAILABILITIES WHERE eventID = $1 AND userName = $2", e.EventID, userName); err != nil {
|
||||||
|
return EventWithAssignmentsUserAvailability{}, err
|
||||||
|
} else {
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) {
|
func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) {
|
||||||
// get the event with assignments
|
// get the event with assignments
|
||||||
if event, err := e.WithAssignments(); err != nil {
|
if event, err := e.WithAssignments(); err != nil {
|
||||||
@@ -217,6 +235,26 @@ func WithAvailabilities() ([]EventWithAvailabilities, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithUserAvailability(userName string) ([]EventWithAssignmentsUserAvailability, error) {
|
||||||
|
var events []EventWithAssignmentsUserAvailability
|
||||||
|
|
||||||
|
if err := db.DB.Select(&events, "SELECT EVENTS.eventID, EVENTS.description, EVENTS.date, USER_AVAILABILITIES.availabilityID FROM EVENTS LEFT JOIN USER_AVAILABILITIES ON EVENTS.eventID = USER_AVAILABILITIES.eventID AND USER_AVAILABILITIES.userName = $1", userName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
// get the assignments for every event
|
||||||
|
for ii, event := range events {
|
||||||
|
if eventWithAssignments, err := event.EventWithAssignments.EventData.WithAssignments(); err != nil {
|
||||||
|
// remove the current event from the events
|
||||||
|
events = append(events[:ii], events[ii+1:]...)
|
||||||
|
} else {
|
||||||
|
events[ii].EventWithAssignments = eventWithAssignments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func UserPending(userName string) ([]EventData, error) {
|
func UserPending(userName string) ([]EventData, error) {
|
||||||
var result []EventData
|
var result []EventData
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ func (a *Handler) getEventsAvailabilities() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Handler) getEventUserAssignmentAvailability() {
|
||||||
|
// retrieve the assignments
|
||||||
|
if events, err := events.WithUserAvailability(a.UserName); err != nil {
|
||||||
|
a.Status = fiber.StatusBadRequest
|
||||||
|
|
||||||
|
logger.Log().Msgf("getting events with tasks and user-availability failed: %v", err)
|
||||||
|
} else {
|
||||||
|
a.Data = events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Handler) getEventsUserPending() {
|
func (a *Handler) getEventsUserPending() {
|
||||||
if events, err := events.UserPending(a.UserName); err != nil {
|
if events, err := events.UserPending(a.UserName); err != nil {
|
||||||
a.Status = fiber.StatusInternalServerError
|
a.Status = fiber.StatusInternalServerError
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ func init() {
|
|||||||
// all events with the availabilities of the individual users
|
// all events with the availabilities of the individual users
|
||||||
"events/availabilities": (*Handler).getEventsAvailabilities,
|
"events/availabilities": (*Handler).getEventsAvailabilities,
|
||||||
|
|
||||||
|
// all events with the task-assignments and the availability of the current user
|
||||||
|
"events/user/assignmentAvailability": (*Handler).getEventUserAssignmentAvailability,
|
||||||
|
|
||||||
// events the user has to enter his availability for
|
// events the user has to enter his availability for
|
||||||
"events/user/pending": (*Handler).getEventsUserPending,
|
"events/user/pending": (*Handler).getEventsUserPending,
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ export interface BaseEvent {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventAvailability = BaseEvent & {
|
||||||
|
availability: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type EventData = BaseEvent & {
|
export type EventData = BaseEvent & {
|
||||||
tasks: TaskAssignment[];
|
tasks: TaskAssignment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EventDataWithAvailability = EventData & EventAvailability;
|
||||||
|
|
||||||
export interface TaskAssignment {
|
export interface TaskAssignment {
|
||||||
taskID: number;
|
taskID: number;
|
||||||
taskName: string;
|
taskName: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import MyEvents from "./MyEvents";
|
import MyEvents from "./MyEvents";
|
||||||
import PengingEvents from "./PendingEvents";
|
import PendingEvents from "./PendingEvents";
|
||||||
|
|
||||||
export default function Overview() {
|
export default function Overview() {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +12,7 @@ export default function Overview() {
|
|||||||
<h1 className="mb-4 mt-8 text-center text-4xl lg:mt-0">
|
<h1 className="mb-4 mt-8 text-center text-4xl lg:mt-0">
|
||||||
Pending Events
|
Pending Events
|
||||||
</h1>
|
</h1>
|
||||||
<PengingEvents />
|
<PendingEvents />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import AvailabilityChip from "@/components/AvailabilityChip";
|
import AvailabilitySelector from "@/components/Event/AvailabilitySelector";
|
||||||
import Event from "@/components/Event/Event";
|
import Event from "@/components/Event/Event";
|
||||||
import { apiCall, getAvailabilities } from "@/lib";
|
import { apiCall } from "@/lib";
|
||||||
import { BaseEvent } from "@/Zustand";
|
import { EventAvailability } from "@/Zustand";
|
||||||
import { Select, SelectItem } from "@heroui/react";
|
|
||||||
import { useAsyncList } from "@react-stately/data";
|
import { useAsyncList } from "@react-stately/data";
|
||||||
|
|
||||||
type EventAvailability = BaseEvent & {
|
export default function PendingEvents() {
|
||||||
availability: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PengingEvents() {
|
|
||||||
// get the events the user hasn't yet inserted his availability for
|
// get the events the user hasn't yet inserted his availability for
|
||||||
const events = useAsyncList({
|
const events = useAsyncList({
|
||||||
async load() {
|
async load() {
|
||||||
@@ -32,56 +27,11 @@ export default function PengingEvents() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// the individual, selectable availabilities
|
|
||||||
const availabilities = useAsyncList({
|
|
||||||
async load() {
|
|
||||||
return {
|
|
||||||
items: (await getAvailabilities()).filter((a) => a.enabled),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function setAvailability(eventID: number, availabilityID: number) {
|
|
||||||
await apiCall(
|
|
||||||
"PUT",
|
|
||||||
"events/user/availability",
|
|
||||||
{ eventID },
|
|
||||||
availabilityID,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center gap-4">
|
<div className="flex justify-center gap-4">
|
||||||
{events.items.map((e) => (
|
{events.items.map((e) => (
|
||||||
<Event key={e.eventID} event={e}>
|
<Event key={e.eventID} event={e}>
|
||||||
<Select
|
<AvailabilitySelector event={e} className="mt-auto" />
|
||||||
items={availabilities.items}
|
|
||||||
label="Availability"
|
|
||||||
variant="bordered"
|
|
||||||
className="mt-auto"
|
|
||||||
isMultiline
|
|
||||||
renderValue={(availability) => (
|
|
||||||
<div>
|
|
||||||
{availability.map((a) =>
|
|
||||||
!!a.data ? (
|
|
||||||
<AvailabilityChip key={a.key} availability={a.data} />
|
|
||||||
) : null,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
onSelectionChange={(a) =>
|
|
||||||
setAvailability(e.eventID, parseInt(a.anchorKey ?? ""))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(availability) => (
|
|
||||||
<SelectItem
|
|
||||||
key={availability.availabilityID}
|
|
||||||
textValue={availability.availabilityName}
|
|
||||||
>
|
|
||||||
<AvailabilityChip availability={availability} />
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</Event>
|
</Event>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,23 +3,31 @@ import AddEvent from "@/components/Event/AddEvent";
|
|||||||
import AssignmentTable from "@/components/Event/AssignmentTable";
|
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||||
import Event from "@/components/Event/Event";
|
import Event from "@/components/Event/Event";
|
||||||
import { apiCall } from "@/lib";
|
import { apiCall } from "@/lib";
|
||||||
import zustand, { EventData } from "@/Zustand";
|
import zustand, { EventDataWithAvailability } from "@/Zustand";
|
||||||
import { Add } from "@carbon/icons-react";
|
import { Add } from "@carbon/icons-react";
|
||||||
import { Button } from "@heroui/react";
|
import { Button, Tab, Tabs } from "@heroui/react";
|
||||||
import { useAsyncList } from "@react-stately/data";
|
import { useAsyncList } from "@react-stately/data";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import AvailabilitySelector from "@/components/Event/AvailabilitySelector";
|
||||||
|
|
||||||
export default function Events() {
|
export default function Events() {
|
||||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||||
const admin = zustand((state) => state.user?.admin);
|
const [filter, setFilter] = useState<string | number>("");
|
||||||
|
|
||||||
const events = useAsyncList<EventData>({
|
const user = zustand((state) => state.user);
|
||||||
|
|
||||||
|
const events = useAsyncList({
|
||||||
async load() {
|
async load() {
|
||||||
const result = await apiCall<EventData[]>("GET", "events/assignments");
|
const result = await apiCall<EventDataWithAvailability[]>(
|
||||||
|
"GET",
|
||||||
|
"events/user/assignmentAvailability",
|
||||||
|
);
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
|
|
||||||
|
console.debug(data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: data,
|
items: data,
|
||||||
};
|
};
|
||||||
@@ -31,20 +39,52 @@ export default function Events() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showEvent(event: EventDataWithAvailability): boolean {
|
||||||
|
switch (filter) {
|
||||||
|
case "assigned":
|
||||||
|
return event.tasks.some((t) => {
|
||||||
|
return t.userName === user?.userName;
|
||||||
|
});
|
||||||
|
|
||||||
|
case "pending":
|
||||||
|
return event.availability === null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1">
|
<div className="relative flex flex-1 flex-col gap-4">
|
||||||
<h2 className="mb-4 text-center text-4xl">Upcoming Events</h2>
|
<h2 className="text-center text-4xl">Upcoming Events</h2>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
selectedKey={filter}
|
||||||
|
onSelectionChange={setFilter}
|
||||||
|
color="primary"
|
||||||
|
className="mx-auto"
|
||||||
|
>
|
||||||
|
<Tab key="all" title="All" />
|
||||||
|
<Tab key="pending" title="Pending" />
|
||||||
|
<Tab key="assigned" title="Assigned" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center gap-4">
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
{events.items.map((ee, ii) => (
|
{events.items.filter(showEvent).map((e) => (
|
||||||
<Event key={ii} event={ee}>
|
<Event key={e.eventID} event={e}>
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<AssignmentTable tasks={ee.tasks} />
|
<AvailabilitySelector
|
||||||
|
event={e}
|
||||||
|
className="mb-2"
|
||||||
|
startSelection={e.availability}
|
||||||
|
/>
|
||||||
|
<AssignmentTable tasks={e.tasks} />
|
||||||
</div>
|
</div>
|
||||||
</Event>
|
</Event>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{admin ? (
|
{user?.admin ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ export default function AssignmentTable({
|
|||||||
tasks,
|
tasks,
|
||||||
highlightTask,
|
highlightTask,
|
||||||
highlightUser,
|
highlightUser,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
tasks: EventData["tasks"];
|
tasks: EventData["tasks"];
|
||||||
highlightUser?: string;
|
highlightUser?: string;
|
||||||
highlightTask?: string;
|
highlightTask?: string;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<table>
|
<table className={className}>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<tr
|
<tr
|
||||||
|
|||||||
81
client/src/components/Event/AvailabilitySelector.tsx
Normal file
81
client/src/components/Event/AvailabilitySelector.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Select, Selection, SelectItem } from "@heroui/react";
|
||||||
|
import AvailabilityChip from "../AvailabilityChip";
|
||||||
|
import { apiCall, getAvailabilities } from "@/lib";
|
||||||
|
import { useAsyncList } from "@react-stately/data";
|
||||||
|
import { EventAvailability } from "@/Zustand";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function AvailabilitySelector({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
startSelection,
|
||||||
|
}: {
|
||||||
|
event: EventAvailability;
|
||||||
|
className?: string;
|
||||||
|
startSelection?: number;
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = useState<Selection>(new Set([]));
|
||||||
|
|
||||||
|
// the individual, selectable availabilities
|
||||||
|
const availabilities = useAsyncList({
|
||||||
|
async load() {
|
||||||
|
const availabilities = (await getAvailabilities()).filter(
|
||||||
|
(a) => a.enabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
// if the availabilities contain the startSelection, set it
|
||||||
|
if (
|
||||||
|
!!startSelection &&
|
||||||
|
availabilities.some((a) => a.availabilityID === startSelection)
|
||||||
|
) {
|
||||||
|
setValue(new Set([startSelection.toString()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: availabilities,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setAvailability(eventID: number, availabilityID: number) {
|
||||||
|
await apiCall(
|
||||||
|
"PUT",
|
||||||
|
"events/user/availability",
|
||||||
|
{ eventID },
|
||||||
|
availabilityID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
items={availabilities.items}
|
||||||
|
label="Availability"
|
||||||
|
variant="bordered"
|
||||||
|
className={className}
|
||||||
|
isMultiline
|
||||||
|
renderValue={(availability) => (
|
||||||
|
<div>
|
||||||
|
{availability.map((a) =>
|
||||||
|
!!a.data ? (
|
||||||
|
<AvailabilityChip key={a.key} availability={a.data} />
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
selectedKeys={value}
|
||||||
|
onSelectionChange={(a) => {
|
||||||
|
void setAvailability(event.eventID, parseInt(a.anchorKey ?? ""));
|
||||||
|
setValue(a);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(availability) => (
|
||||||
|
<SelectItem
|
||||||
|
key={availability.availabilityID}
|
||||||
|
textValue={availability.availabilityName}
|
||||||
|
>
|
||||||
|
<AvailabilityChip availability={availability} />
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import LocalDate from "../LocalDate";
|
import LocalDate from "../LocalDate";
|
||||||
import { BaseEvent } from "@/Zustand";
|
import { BaseEvent } from "@/Zustand";
|
||||||
import { Card, CardBody, CardHeader, Divider } from "@heroui/react";
|
import { Card, CardBody, CardHeader, Divider, Textarea } from "@heroui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function Event({
|
export default function Event({
|
||||||
@@ -34,7 +34,12 @@ export default function Event({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div>{event.description}</div>
|
<Textarea
|
||||||
|
isReadOnly
|
||||||
|
label="Description"
|
||||||
|
defaultValue={event.description}
|
||||||
|
variant="bordered"
|
||||||
|
/>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
Reference in New Issue
Block a user