diff --git a/components/user/SelectionActions.tsx b/components/user/SelectionActions.tsx new file mode 100644 index 0000000..8cb85b4 --- /dev/null +++ b/components/user/SelectionActions.tsx @@ -0,0 +1,29 @@ +import PlusOneIcon from "@mui/icons-material/PlusOne"; +import { IconButton, Tooltip } from "@mui/material"; + +type SelectionActionsPropsType = { + checked: any; + increaseGrade: any; +}; + +export default function SelectionActions({ + checked, + increaseGrade, +}: SelectionActionsPropsType) { + const checkedItems = Object.values(checked).filter((item) => item != false); + if (checkedItems.length > 0) { + //console.log("Checked Items", checkedItems); + return ( + + + + + + ); + } else return
; +} diff --git a/components/user/UserAdminList.tsx b/components/user/UserAdminList.tsx index 848eff6..b7a4f6b 100644 --- a/components/user/UserAdminList.tsx +++ b/components/user/UserAdminList.tsx @@ -1,6 +1,6 @@ import Typography from "@mui/material/Typography"; -import { Avatar, Grid } from "@mui/material"; +import { Avatar, Checkbox, Grid, Paper } from "@mui/material"; import { UserType } from "@/entities/UserType"; import palette from "@/styles/palette"; @@ -12,22 +12,40 @@ import AccordionSummary from "@mui/material/AccordionSummary"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { RentalsUserType } from "@/entities/RentalsUserType"; +import { useEffect } from "react"; import UserDetailsCard from "./UserDetailsCard"; type UserAdminListPropsType = { users: Array; rentals: Array; searchString: string; + checked: any; + setChecked: any; }; export default function UserAdminList({ users, rentals, searchString, + checked, + setChecked, }: UserAdminListPropsType) { //attach amount of rented books to the user + const rentalAmount: { [key: number]: number } = {}; + //initialise user checked array + + useEffect(() => { + const checkSet: { [key: string]: boolean } = users.reduce((acc, obj) => { + const userID = obj.id!.toString(); + acc[userID] = false; // Initialize each key-value pair using the "id" field + return acc; + }, {} as { [key: string]: boolean }); + setChecked(checkSet); + //console.log("Initialised user checkboxes", checkSet); + }, [users]); + rentals.map((r: any) => { if (r.userid in rentalAmount) { rentalAmount[r.userid] = rentalAmount[r.userid] + 1; @@ -38,53 +56,93 @@ export default function UserAdminList({
{users.map((u: UserType) => { const lowerCaseSearch = searchString.toLowerCase(); + //console.log("Checked", checked); + const userID = u.id!.toString(); + const checkBoxValue = + userID in checked ? (checked[userID] as boolean) : false; if ( u.lastName.toLowerCase().includes(lowerCaseSearch) || u.firstName.toLowerCase().includes(lowerCaseSearch) || u.id!.toString().includes(lowerCaseSearch) ) return ( - - } - aria-controls="panel1a-content" - id="panel1a-header" + + - - - {rentalAmount[u.id!] != undefined ? ( - - {rentalAmount[u.id!]} - - ) : ( - - 0 - - )} - - - {u.lastName + ", " + u.firstName} - - {"Klasse " + u.schoolGrade + " - " + u.schoolTeacherName} - - + + { + setChecked({ ...checked, [userID]: !checkBoxValue }); + }} + inputProps={{ "aria-label": "controlled" }} + /> + + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + {" "} + + + {rentalAmount[u.id!] != undefined ? ( + + {rentalAmount[u.id!]} + + ) : ( + + 0 + + )} + + + + {u.lastName + ", " + u.firstName} + + + {"Klasse " + + u.schoolGrade + + " - " + + u.schoolTeacherName} + + + + + + parseInt(r.userid) == u.id + )} + /> + + - - - parseInt(r.userid) == u.id - )} - /> - - + + ); })}
diff --git a/entities/user.ts b/entities/user.ts index fe2f889..99010af 100644 --- a/entities/user.ts +++ b/entities/user.ts @@ -110,6 +110,38 @@ export async function updateUser( } } +export async function increaseUserGrade( + client: PrismaClient, + newGrades: Array<{ id: number; grade: string }> +) { + try { + //create a transaction otherwise for single API calls, there's a connection pool issue + const transaction = [] as Array; + newGrades.map((i: { id: number; grade: string }) => { + transaction.push( + client.user.update({ + where: { + id: i.id, + }, + data: { schoolGrade: i.grade }, + }) + ); + }); + + const result = await client.$transaction(transaction); + console.log("Batch update database operation succeeded: ", result); + return result; + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError || + e instanceof Prisma.PrismaClientValidationError + ) { + console.log("ERROR in updating batch grades for user : ", e); + } + throw e; + } +} + export async function disableUser(client: PrismaClient, id: number) { await addAudit(client, "Disable user", id.toString(), 0, id); return await client.user.update({ diff --git a/pages/api/batch/grade.ts b/pages/api/batch/grade.ts new file mode 100644 index 0000000..e026ae4 --- /dev/null +++ b/pages/api/batch/grade.ts @@ -0,0 +1,31 @@ +import { increaseUserGrade } from "@/entities/user"; +import { PrismaClient } from "@prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +const prisma = new PrismaClient(); + +export default async function handle( + req: NextApiRequest, + res: NextApiResponse +) { + switch (req.method) { + case "POST": + try { + if (!req.body) return res.status(404).end("No data provided"); + //gets a list of user IDs to update the grade + const userdata = req.body; + + const updateResult = await increaseUserGrade(prisma, userdata); + + res.status(200).json(updateResult); + } catch (error) { + console.log(error); + res.status(400).json({ data: "ERROR DELETE: " + error }); + } + break; + + default: + res.status(405).end(`${req.method} Not Allowed`); + break; + } +} diff --git a/pages/user/index.tsx b/pages/user/index.tsx index d457741..1887c74 100644 --- a/pages/user/index.tsx +++ b/pages/user/index.tsx @@ -16,11 +16,22 @@ import dayjs from "dayjs"; import { convertDateToDayString } from "@/utils/dateutils"; +import SelectionActions from "@/components/user/SelectionActions"; import UserDetailsCard from "@/components/user/UserDetailsCard"; import { BookType } from "@/entities/BookType"; import { RentalsUserType } from "@/entities/RentalsUserType"; import { UserType } from "@/entities/UserType"; -import { Divider, IconButton, InputBase, Paper, Tooltip } from "@mui/material"; +import { increaseNumberInString } from "@/utils/increaseNumberInString"; +import DoneAllIcon from "@mui/icons-material/DoneAll"; +import { + Alert, + Divider, + IconButton, + InputBase, + Paper, + Snackbar, + Tooltip, +} from "@mui/material"; const prisma = new PrismaClient(); /* @@ -40,12 +51,25 @@ export default function Users({ users, books, rentals }: UsersPropsType) { const [userSearchInput, setUserSearchInput] = useState(""); const [displayDetail, setDisplayDetail] = useState(0); const [userCreating, setUserCreating] = useState(false); + const [checked, setChecked] = useState({} as any); + const [batchEditSnackbar, setBatchEditSnackbar] = useState(false); const router = useRouter(); const theme = useTheme(); useEffect(() => {}, []); + const handleBatchEditSnackbar = ( + event?: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === "clickaway") { + return; + } + + setBatchEditSnackbar(false); + }; + const handleInputChange = (e: ChangeEvent) => { setUserSearchInput(e.target.value); }; @@ -75,11 +99,53 @@ export default function Users({ users, books, rentals }: UsersPropsType) { router.push("user/" + id); }; + const handleSelectAll = () => { + var resultCheck = true; + + //if there is something selected, deselect all + Object.values(checked).some((value) => value === true) + ? (resultCheck = false) + : (resultCheck = true); + //console.log("Selecting or deselecting all users ", users); + const newChecked = users.reduce((acc: any, u: any) => { + if (u.id !== undefined) { + acc = { ...acc, [u.id]: resultCheck }; + } + return acc; + }, {}); + //console.log("New checked users", newChecked); + setChecked(newChecked); + }; + const selectItem = (id: string) => { console.log("selected user", users, rentals); setDisplayDetail(parseInt(id)); }; + const handleIncreaseGrade = () => { + //console.log("Increasing grade for users ", users, checked); + //the user IDs that are checked are marked as true + const updatedUserIDs = users.reduce((acc: any, u: UserType) => { + if (checked[u.id!]) + acc.push({ id: u.id, grade: increaseNumberInString(u.schoolGrade) }); + return acc; + }, []); + + fetch("/api/batch/grade", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedUserIDs), + }) + .then((res) => res.json()) + .then((data) => { + console.log("Users increased", data); + setBatchEditSnackbar(true); + router.push("user"); + }); + }; + const booksForUser = (id: number) => { const userRentals = rentals.filter((r: RentalsUserType) => r.userid == id); //console.log("Filtered rentals", userRentals); @@ -89,6 +155,19 @@ export default function Users({ users, books, rentals }: UsersPropsType) { return ( + + + Selektierte Benutzer angepasst, super! + + + + + + + + + + + {" "} {displayDetail > 0 ? ( @@ -152,6 +248,8 @@ export default function Users({ users, books, rentals }: UsersPropsType) { users={users} rentals={rentals} searchString={userSearchInput} + checked={checked} + setChecked={setChecked} /> diff --git a/utils/increaseNumberInString.ts b/utils/increaseNumberInString.ts new file mode 100644 index 0000000..ebe2de0 --- /dev/null +++ b/utils/increaseNumberInString.ts @@ -0,0 +1,6 @@ +export function increaseNumberInString(text: any) { + return text.replace(/\d+/g, function (match: any) { + // Convert the matched substring to a number, add 1, and return it + return parseInt(match, 10) + 1; + }); +}