Merge branch 'frontend' into gruppDM

This commit is contained in:
Davenludd 2024-04-14 15:30:37 +02:00
commit 97f810fce2
38 changed files with 1202 additions and 337 deletions

View file

@ -11,6 +11,7 @@ import {
NewProject,
WeeklyReport,
StrNameChange,
Statistics,
} from "../Types/goTypes";
/**
@ -258,6 +259,17 @@ interface API {
reportId: number,
token: string,
): Promise<APIResponse<string>>;
/**
* Retrieves the total time spent on a project for a particular user (the user is determined by the token)
*
* @param {string} projectName The name of the project
* @param {string} token The authentication token
*/
getStatistics(
projectName: string,
token: string,
): Promise<APIResponse<Statistics>>;
}
/** An instance of the API */
@ -664,7 +676,11 @@ export const api: API = {
});
if (!response.ok) {
return { success: false, message: "Failed to login" };
return {
success: false,
data: `${response.status}`,
message: "Failed to login",
};
} else {
const data = (await response.json()) as { token: string }; // Update the type of 'data'
return { success: true, data: data.token };
@ -962,4 +978,30 @@ export const api: API = {
return { success: false, message: "Failed to delete report" };
}
},
async getStatistics(
token: string,
projectName: string,
): Promise<APIResponse<Statistics>> {
try {
const response = await fetch(
`/api/getStatistics/?projectName=${projectName}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
},
);
if (!response.ok) {
return { success: false, message: "Failed to get statistics" };
} else {
const data = (await response.json()) as Statistics;
return { success: true, data };
}
} catch (e) {
return { success: false, message: "Failed to get statistics" };
}
},
};

View file

@ -1,9 +1,13 @@
import { useState } from "react";
import { api } from "../API/API";
import { NewProject } from "../Types/goTypes";
import InputField from "./InputField";
import Logo from "../assets/Logo.svg";
import Button from "./Button";
import { useNavigate } from "react-router-dom";
import ProjectNameInput from "./Inputs/ProjectNameInput";
import DescriptionInput from "./Inputs/DescriptionInput";
import { alphanumeric } from "../Data/regex";
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
/**
* Provides UI for adding a project to the system.
@ -12,11 +16,26 @@ import Button from "./Button";
function AddProject(): JSX.Element {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const navigate = useNavigate();
/**
* Tries to add a project to the system
*/
const handleCreateProject = async (): Promise<void> => {
if (
!alphanumeric.test(name) ||
name.length > projNameHighLimit ||
name.length < projNameLowLimit
) {
alert(
"Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)",
);
return;
}
if (description.length > projNameHighLimit) {
alert("Please provide valid description: \n-Max 100 characters");
return;
}
const project: NewProject = {
name: name.replace(/ /g, ""),
description: description.trim(),
@ -30,6 +49,7 @@ function AddProject(): JSX.Element {
alert(`${project.name} added!`);
setDescription("");
setName("");
navigate("/admin");
} else {
alert("Project not added, name could be taken");
console.error(response.message);
@ -44,7 +64,7 @@ function AddProject(): JSX.Element {
<div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit"
className="bg-white rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => {
e.preventDefault();
void handleCreateProject();
@ -52,33 +72,29 @@ function AddProject(): JSX.Element {
>
<img
src={Logo}
className="logo w-[7vw] mb-10 mt-10"
className="logo w-[7vw] self-center mb-10 mt-10"
alt="TTIME Logo"
/>
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Create a new project
</h3>
<div className="space-y-3">
<InputField
label="Name"
type="text"
value={name}
onChange={(e) => {
e.preventDefault();
setName(e.target.value);
}}
/>
<InputField
label="Description"
type="text"
value={description}
onChange={(e) => {
e.preventDefault();
setDescription(e.target.value);
}}
/>
</div>
<div className="flex items-center justify-between">
<ProjectNameInput
name={name}
onChange={(e) => {
e.preventDefault();
setName(e.target.value);
}}
/>
<div className="p-2"></div>
<DescriptionInput
desc={description}
onChange={(e) => {
e.preventDefault();
setDescription(e.target.value);
}}
placeholder={"Description (Optional)"}
/>
<div className="flex self-center mt-4 justify-between">
<Button
text="Create"
onClick={(): void => {

View file

@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import Button from "./Button";
import AddMember, { AddMemberInfo } from "./AddMember";
import BackButton from "./BackButton";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import GetAllUsers from "./GetAllUsers";
import InputField from "./InputField";
/**
* Provides UI for adding a member to a project.
@ -13,6 +13,7 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
const [names, setNames] = useState<string[]>([]);
const [users, setUsers] = useState<string[]>([]);
const [usersProj, setUsersProj] = useState<ProjectMember[]>([]);
const [search, setSearch] = useState("");
// Gets all users and project members for filtering
GetAllUsers({ setUsersProp: setUsers });
@ -36,8 +37,10 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
// Attempts to add all of the selected users to the project
const handleAddClick = async (): Promise<void> => {
if (names.length === 0)
if (names.length === 0) {
alert("You have to choose at least one user to add");
return;
}
for (const name of names) {
const newMember: AddMemberInfo = {
userName: name,
@ -60,32 +63,47 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
};
return (
<div className="border-4 border-black bg-white flex flex-col items-center pt-10 rounded-3xl content-center pl-20 pr-20 h-[63vh] w-[50] overflow-auto">
<div className="border-4 border-black bg-white flex flex-col items-center py-10 px-20 rounded-3xl content-center overflow-auto">
<h1 className="text-center font-bold text-[36px] pb-10">
{props.projectName}
</h1>
<p className="p-1 text-center font-bold text-[26px]">
Choose users to add:
</p>
<div className="border-2 border-black pl-2 pr-2 pb-2 rounded-xl text-center overflow-auto h-[26vh] w-[26vh]">
<ul className="text-center font-medium space-y-2">
<div>
<InputField
placeholder={"Search users"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="font-medium space-y-2 border-2 border-black mt-2 px-2 pb-2 rounded-2xl text-center overflow-auto h-[26vh] w-[34vh]">
<div></div>
{users.map((user) => (
<li
className={
names.includes(user)
? "items-start p-1 border-2 border-transparent rounded-full bg-orange-500 hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 hover:text-slate-100 hover:cursor-pointer"
}
key={user}
value={user}
onClick={() => {
handleUserClick(user);
}}
>
<span>{user}</span>
</li>
))}
{users
.filter((user) => {
return search.toLowerCase() === ""
? user
: user.toLowerCase().includes(search.toLowerCase());
})
.map((user) => (
<li
className={
names.includes(user)
? "items-start p-1 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 transition-all hover:text-white hover:cursor-pointer"
}
key={user}
value={user}
onClick={() => {
handleUserClick(user);
}}
>
<span>{user}</span>
</li>
))}
</ul>
</div>
<p className="pt-10 pb-5 underline text-center font-bold text-[18px]">
@ -99,9 +117,7 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
}}
type="button"
/>
<BackButton />
</div>
<p className="text-center text-gray-500 text-xs"></p>
</div>
);
}

View file

@ -5,6 +5,7 @@ import ChangeRole, { ProjectRoleChange } from "./ChangeRole";
export default function ChangeRoleView(props: {
projectName: string;
username: string;
currentRole: string;
}): JSX.Element {
const [selectedRole, setSelectedRole] = useState<
"project_manager" | "member" | ""
@ -21,7 +22,12 @@ export default function ChangeRoleView(props: {
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
console.log("Cur: " + props.currentRole + " " + "new: " + selectedRole);
event.preventDefault();
if (selectedRole === props.currentRole) {
alert(`Already ${props.currentRole}, nothing changed`);
return;
}
const roleChangeInfo: ProjectRoleChange = {
username: props.username,
projectname: props.projectName,
@ -31,34 +37,31 @@ export default function ChangeRoleView(props: {
};
return (
<div className="overflow-auto rounded-lg">
<div className="overflow-auto">
<h1 className="font-bold text-[20px]">Select role:</h1>
<form onSubmit={handleSubmit}>
<div className="h-[7vh] self-start text-left font-medium overflow-auto border-2 border-black rounded-lg p-2">
<div className="hover:font-bold">
<label>
<input
type="radio"
value="project_manager"
checked={selectedRole === "project_manager"}
onChange={handleRoleChange}
className="ml-2 mr-2 mb-3"
/>
Project manager
</label>
</div>
<div className="hover:font-bold">
<label>
<input
type="radio"
value="member"
checked={selectedRole === "member"}
onChange={handleRoleChange}
className="ml-2 mr-2"
/>
Member
</label>
</div>
<div className="py-1 px-1 w-full self-start text-left font-medium overflow-auto border-2 border-black rounded-2xl">
<label className="hover:cursor-pointer hover:font-bold">
<input
type="radio"
value="project_manager"
checked={selectedRole === "project_manager"}
onChange={handleRoleChange}
className="m-2"
/>
Project manager
</label>
<br />
<label className="hover:cursor-pointer hover:font-bold">
<input
type="radio"
value="member"
checked={selectedRole === "member"}
onChange={handleRoleChange}
className="m-2 hover:cursor-pointer"
/>
Member
</label>
</div>
<Button
text="Change"

View file

@ -9,6 +9,10 @@ function ChangeUsername(props: { nameChange: StrNameChange }): void {
alert("You have to give a new name\n\nName not changed");
return;
}
if (props.nameChange.prevName === localStorage.getItem("username")) {
alert("You cannot change admin name");
return;
}
api
.changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => {

View file

@ -4,19 +4,21 @@
* @returns {JSX.Element} The input field
* @example
* <InputField
* type="text"
* label="Example"
* placeholder="New placeholder"
* type="text"
* value={example}
* onChange={(e) => {
* setExample(e.target.value);
* }}
* value={example}
* />
*/
function InputField(props: {
label: string;
type: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
label?: string;
placeholder?: string;
type?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<div className="">
@ -30,7 +32,7 @@ function InputField(props: {
className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id={props.label}
type={props.type}
placeholder={props.label}
placeholder={props.placeholder}
value={props.value}
onChange={props.onChange}
/>

View file

@ -0,0 +1,38 @@
import { projDescHighLimit, projDescLowLimit } from "../../Data/constants";
export default function DescriptionInput(props: {
desc: string;
placeholder: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<>
<input
className={
props.desc.length <= 100
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="true"
id="New desc"
type="text"
placeholder={props.placeholder}
value={props.desc}
onChange={props.onChange}
/>
<div className="my-1">
{props.desc.length > projDescHighLimit && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Description must be under 100 characters
</p>
)}
{props.desc.length <= projDescHighLimit &&
props.desc.length > projDescLowLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid project description!
</p>
)}
</div>
</>
);
}

View file

@ -0,0 +1,44 @@
import { passwordLength } from "../../Data/constants";
import { lowercase } from "../../Data/regex";
export default function PasswordInput(props: {
password: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const password = props.password;
return (
<>
<input
className={
password.length === passwordLength && lowercase.test(password)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New password"
type="password"
placeholder="Password"
value={password}
onChange={props.onChange}
/>
<div className="my-1">
{password.length === passwordLength &&
lowercase.test(props.password) && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid password!
</p>
)}
{password.length !== passwordLength && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Password must be 6 characters
</p>
)}
{!lowercase.test(password) && password !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No number, uppercase or special <br /> characters allowed
</p>
)}
</div>
</>
);
}

View file

@ -0,0 +1,48 @@
import { projNameHighLimit, projNameLowLimit } from "../../Data/constants";
import { alphanumeric } from "../../Data/regex";
export default function ProjectNameInput(props: {
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const name = props.name;
return (
<>
<input
className={
name.length >= projNameLowLimit &&
name.length <= projNameHighLimit &&
alphanumeric.test(name)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New name"
type="text"
placeholder="Project name"
value={name}
onChange={props.onChange}
/>
<div className="my-1">
{!alphanumeric.test(name) && name !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No special characters allowed
</p>
)}
{(name.length < projNameLowLimit ||
name.length > projNameHighLimit) && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Project name must be 10-99 characters
</p>
)}
{alphanumeric.test(props.name) &&
name.length >= projNameLowLimit &&
name.length <= projNameHighLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid project name!
</p>
)}
</div>
</>
);
}

View file

@ -0,0 +1,50 @@
import { usernameLowLimit, usernameUpLimit } from "../../Data/constants";
import { alphanumeric } from "../../Data/regex";
export default function UsernameInput(props: {
username: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const username = props.username;
return (
<>
<input
className={
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit &&
alphanumeric.test(props.username)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New username"
type="text"
placeholder="Username"
value={username}
onChange={props.onChange}
/>
<div className="my-1">
{alphanumeric.test(username) &&
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid username!
</p>
)}
{!alphanumeric.test(username) && username !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No special characters allowed
</p>
)}
{!(
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit
) && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Username must be 5-10 characters
</p>
)}
</div>
</>
);
}

View file

@ -11,6 +11,10 @@ function LoginCheck(props: {
password: string;
setAuthority: Dispatch<SetStateAction<number>>;
}): void {
if (props.username === "" || props.password === "") {
alert("Please enter username and password to login");
return;
}
const user: NewUser = {
username: props.username,
password: props.password,
@ -42,7 +46,15 @@ function LoginCheck(props: {
console.error("Token was undefined");
}
} else {
console.error("Token could not be fetched/No such user");
if (response.data === "500") {
console.error(response.message);
alert("No connection/Error");
} else {
console.error(
"Token could not be fetched/No such user" + response.message,
);
alert("Incorrect login information");
}
}
})
.catch((error) => {

View file

@ -33,6 +33,7 @@ function Login(props: {
props.setUsername(e.target.value);
}}
value={props.username}
placeholder={"Username"}
/>
<InputField
type="password"
@ -41,6 +42,7 @@ function Login(props: {
props.setPassword(e.target.value);
}}
value={props.password}
placeholder={"Password"}
/>
</div>
<Button

View file

@ -1,12 +1,13 @@
import Button from "./Button";
import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react";
import ChangeRoleView from "./ChangeRoleView";
import RemoveUserFromProj from "./RemoveUserFromProj";
import ChangeRoleInput from "./ChangeRoleView";
function MemberInfoModal(props: {
projectName: string;
username: string;
role: string;
onClose: () => void;
}): JSX.Element {
const [showRoles, setShowRoles] = useState(false);
@ -20,22 +21,24 @@ function MemberInfoModal(props: {
};
return (
<div
className="fixed inset-10 bg-opacity-30 backdrop-blur-sm
className="fixed inset-0 bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white rounded-lg text-center flex flex-col">
<div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p>
<p className="font-bold text-[20px]">{props.role}</p>
<p
className="hover:font-bold hover:cursor-pointer underline"
className="hover:font-bold hover:cursor-pointer underline mb-2 mt-1"
onClick={handleChangeRole}
>
(Change Role)
</p>
{showRoles && (
<ChangeRoleView
<ChangeRoleInput
projectName={props.projectName}
username={props.username}
currentRole={props.role}
/>
)}
<h2 className="font-bold text-[20px]">Member of these projects:</h2>

View file

@ -0,0 +1,24 @@
import { useNavigate } from "react-router-dom";
/**
* Renders a navigation button component for navigating to
* different page
* @returns The JSX element representing the navigation button.
*/
export default function NavButton(props: {
navTo: string;
label: string;
}): JSX.Element {
const navigate = useNavigate();
const goBack = (): void => {
navigate(props.navTo);
};
return (
<button
onClick={goBack}
className="inline-block py-1 px-8 font-bold bg-orange-500 text-white border-2 border-black rounded-full cursor-pointer mt-5 mb-5 transition-colors duration-10 hover:bg-orange-600 hover:text-gray-300 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 4vh;"
>
{props.label}
</button>
);
}

View file

@ -4,19 +4,60 @@ import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import { Link } from "react-router-dom";
import GetProjectTimes, { projectTimes } from "./GetProjectTimes";
import DeleteProject from "./DeleteProject";
import InputField from "./InputField";
import ProjectNameInput from "./Inputs/ProjectNameInput";
import { alphanumeric } from "../Data/regex";
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
function ProjectInfoModal(props: {
projectname: string;
onClose: () => void;
onClick: (username: string) => void;
onClick: (username: string, userRole: string) => void;
}): JSX.Element {
const [showInput, setShowInput] = useState(false);
const [users, setUsers] = useState<ProjectMember[]>([]);
const [times, setTimes] = useState<projectTimes>();
const [search, setSearch] = useState("");
const [newProjName, setNewProjName] = useState("");
const totalTime = useRef(0);
GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers });
GetProjectTimes({ setTimesProp: setTimes, projectName: props.projectname });
const handleChangeNameView = (): void => {
if (showInput) {
setNewProjName("");
setShowInput(false);
} else {
setShowInput(true);
}
};
const handleClickChangeName = (): void => {
if (
newProjName.length > projNameHighLimit ||
newProjName.length < projNameLowLimit ||
!alphanumeric.test(newProjName)
) {
alert(
"Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(
`Are you sure you want to change name of ${props.projectname} to ${newProjName}?`,
)
) {
//TODO: change and insert change name functionality
alert("Not implemented yet");
setNewProjName("");
} else {
alert("Name was not changed!");
}
};
useEffect(() => {
if (times?.totalTime !== undefined) {
totalTime.current = times.totalTime;
@ -28,44 +69,88 @@ function ProjectInfoModal(props: {
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white p-2 rounded-2xl text-center h-[61vh] w-[40] overflow-auto">
<div className="border-4 border-black bg-white p-2 rounded-2xl text-center h-[64vh] w-[40] overflow-auto">
<div className="pl-10 pr-10">
<h1 className="font-bold text-[32px] mb-[20px]">
{props.projectname}
</h1>
<div className="p-1 text-center">
<h2 className="text-[20px] font-bold">Statistics:</h2>
</div>
<div className="border-2 border-black rounded-lg h-[8vh] text-left divide-y-2 flex flex-col overflow-auto mx-10">
<p className="p-2">Number of members: {users.length}</p>
<p className="p-2">
<h1 className="font-bold text-[32px]">{props.projectname}</h1>
<p
className="mb-4 hover:font-bold hover:cursor-pointer hover:underline"
onClick={handleChangeNameView}
>
(Change project name)
</p>
{showInput && (
<>
<h2 className="text-[20px] font-bold pb-2">Change name:</h2>
<div className="border-2 rounded-2xl border-black px-6 pt-6 pb-1 mb-7">
<ProjectNameInput
name={newProjName}
onChange={function (e): void {
setNewProjName(e.target.value);
}}
/>
<div className="px-6 grid grid-cols-2 gap-10">
<Button
text={"Change"}
onClick={function (): void {
handleClickChangeName();
}}
type={"submit"}
/>
<Button
text={"Close"}
onClick={function (): void {
handleChangeNameView();
}}
type={"submit"}
/>
</div>
</div>
</>
)}
<h2 className="text-[20px] font-bold pb-2">Statistics:</h2>
<div className="border-2 border-black rounded-2xl px-2 py-1 text-left divide-y-2 flex flex-col overflow-auto">
<p>Number of members: {users.length}</p>
<p>
Total time reported:{" "}
{Math.floor(totalTime.current / 60 / 24) + " d "}
{Math.floor((totalTime.current / 60) % 24) + " h "}
{(totalTime.current % 60) + " m "}
</p>
</div>
<div className="h-[6vh] p-7 text-center">
<h2 className="text-[20px] font-bold">Project members:</h2>
</div>
<div className="border-2 border-black p-2 rounded-lg text-center overflow-auto h-[24vh] mx-10">
<ul className="text-left font-medium space-y-2">
<div></div>
{users.map((user) => (
<li
className="items-start p-1 border-2 border-black rounded-lg bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
key={user.Username}
onClick={() => {
props.onClick(user.Username);
}}
>
<span>
Name: {user.Username}
<div></div>
Role: {user.UserRole}
</span>
</li>
))}
<h3 className="pt-7 text-[20px] font-bold">Project members:</h3>
<div className="">
<InputField
placeholder={"Search member"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="border-2 border-black mt-2 p-2 rounded-2xl text-left overflow-auto h-[24vh] font-medium space-y-2">
{users
.filter((user) => {
return search.toLowerCase() === ""
? user.Username
: user.Username.toLowerCase().includes(
search.toLowerCase(),
);
})
.map((user) => (
<li
className="items-start px-2 py-1 border-2 border-black rounded-2xl bg-orange-200 transition-all hover:bg-orange-600 hover:text-white hover:cursor-pointer"
key={user.Username}
onClick={() => {
props.onClick(user.Username, user.UserRole);
}}
>
<span>
Name: {user.Username}
<div></div>
Role: {user.UserRole}
</span>
</li>
))}
</ul>
</div>
<div className="space-x-5 my-2">

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { NewProject } from "../Types/goTypes";
import ProjectInfoModal from "./ProjectInfoModal";
import MemberInfoModal from "./MemberInfoModal";
import InputField from "./InputField";
/**
* A list of projects for admin manage projects page, that sets an onClick
@ -21,9 +22,12 @@ export function ProjectListAdmin(props: {
const [projectName, setProjectName] = useState("");
const [userModalVisible, setUserModalVisible] = useState(false);
const [username, setUsername] = useState("");
const [userRole, setUserRole] = useState("");
const [search, setSearch] = useState("");
const handleClickUser = (username: string): void => {
const handleClickUser = (username: string, userRole: string): void => {
setUsername(username);
setUserRole(userRole);
setUserModalVisible(true);
};
@ -39,11 +43,13 @@ export function ProjectListAdmin(props: {
const handleCloseUser = (): void => {
setUsername("");
setUserRole("");
setUserModalVisible(false);
};
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</h1>
{projectModalVisible && (
<ProjectInfoModal
onClose={handleCloseProject}
@ -56,21 +62,36 @@ export function ProjectListAdmin(props: {
onClose={handleCloseUser}
username={username}
projectName={projectName}
role={userRole}
/>
)}
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.projects.map((project) => (
<li
className="pt-5"
key={project.name}
onClick={() => {
handleClickProject(project.name);
}}
>
{project.name}
</li>
))}
<InputField
placeholder={"Search"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="mt-3 border-2 text-left border-black rounded-2xl px-2 divide-y divide-gray-300 font-semibold text-[30px] cursor-pointer overflow-auto h-[60vh] w-[40vw]">
{props.projects
.filter((project) => {
return search.toLowerCase() === ""
? project.name
: project.name.toLowerCase().includes(search.toLowerCase());
})
.map((project) => (
<li
className="hover:font-extrabold hover:underline p-1"
key={project.name}
onClick={() => {
handleClickProject(project.name);
}}
>
{project.name}
</li>
))}
</ul>
</div>
</>

View file

@ -3,25 +3,44 @@ import { NewUser } from "../Types/goTypes";
import { api } from "../API/API";
import Logo from "../assets/Logo.svg";
import Button from "./Button";
import InputField from "./InputField";
import UsernameInput from "./Inputs/UsernameInput";
import PasswordInput from "./Inputs/PasswordInput";
import { alphanumeric, lowercase } from "../Data/regex";
import {
passwordLength,
usernameLowLimit,
usernameUpLimit,
} from "../Data/constants";
/**
* Renders a registration form for the admin to add new users in.
* @returns The JSX element representing the registration form.
*/
export default function Register(): JSX.Element {
const [username, setUsername] = useState<string>();
const [password, setPassword] = useState<string>();
const [errMessage, setErrMessage] = useState<string>();
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [errMessage, setErrMessage] = useState<string>("");
const handleRegister = async (): Promise<void> => {
if (username === "" || password === "") {
alert("Must provide username and password");
if (
username.length > usernameUpLimit ||
username.length < usernameLowLimit ||
!alphanumeric.test(username)
) {
alert(
"Please provide valid username: \n-Between 5-10 characters \n-No special characters (.-!?/*)",
);
return;
}
if (password.length !== passwordLength || !lowercase.test(password)) {
alert(
"Please provide valid password: \n-Exactly 6 characters \n-No uppercase letters \n-No numbers \n-No special characters (.-!?/*)",
);
return;
}
const newUser: NewUser = {
username: username?.replace(/ /g, "") ?? "",
password: password ?? "",
username: username,
password: password,
};
const response = await api.registerUser(newUser);
if (response.success) {
@ -39,7 +58,7 @@ export default function Register(): JSX.Element {
<div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit"
className="bg-white rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => {
e.preventDefault();
void handleRegister();
@ -47,31 +66,28 @@ export default function Register(): JSX.Element {
>
<img
src={Logo}
className="logo w-[7vw] mb-10 mt-10"
className="logo self-center w-[7vw] mb-10 mt-10"
alt="TTIME Logo"
/>
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Register New User
</h3>
<div className="space-y-3">
<InputField
label="Username"
type="text"
value={username ?? ""}
onChange={(e) => {
setUsername(e.target.value);
}}
/>
<InputField
label="Password"
type="password"
value={password ?? ""}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
</div>
<div className="flex items-center justify-between">
<UsernameInput
username={username}
onChange={(e) => {
setUsername(e.target.value);
}}
/>
<div className="py-2" />
<PasswordInput
password={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<div className="flex self-center justify-between">
<Button
text="Register"
onClick={(): void => {

View file

@ -2,35 +2,104 @@ import Button from "./Button";
import DeleteUser from "./DeleteUser";
import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react";
import InputField from "./InputField";
import ChangeUsername from "./ChangeUsername";
import { StrNameChange } from "../Types/goTypes";
import UsernameInput from "./Inputs/UsernameInput";
import PasswordInput from "./Inputs/PasswordInput";
import { alphanumeric, lowercase } from "../Data/regex";
import {
passwordLength,
usernameLowLimit,
usernameUpLimit,
} from "../Data/constants";
function UserInfoModal(props: {
isVisible: boolean;
username: string;
onClose: () => void;
}): JSX.Element {
const [showInput, setShowInput] = useState(false);
const [showNameInput, setShowNameInput] = useState(false);
const [showPwordInput, setShowPwordInput] = useState(false);
const [newUsername, setNewUsername] = useState("");
const [newPassword, setNewPassword] = useState("");
if (!props.isVisible) {
return <></>;
}
const handleChangeNameView = (): void => {
if (showInput) {
setShowInput(false);
/*
* Switches name input between visible/invisible
* and makes password input invisible
*/
const handleShowNameInput = (): void => {
if (showPwordInput) setShowPwordInput(false);
if (showNameInput) {
setShowNameInput(false);
setNewUsername("");
} else {
setShowInput(true);
setShowNameInput(true);
setNewPassword("");
}
};
/*
* Switches password input between visible/invisible
* and makes username input invisible
*/
const handleShowPwordInput = (): void => {
if (showNameInput) setShowNameInput(false);
if (showPwordInput) {
setShowPwordInput(false);
setNewPassword("");
} else {
setShowPwordInput(true);
setNewUsername("");
}
};
// Handles name change and checks if new name meets requirements
const handleClickChangeName = (): void => {
const nameChange: StrNameChange = {
prevName: props.username,
newName: newUsername.replace(/ /g, ""),
};
ChangeUsername({ nameChange: nameChange });
if (
!alphanumeric.test(newUsername) ||
newUsername.length > usernameUpLimit ||
newUsername.length < usernameLowLimit
) {
alert(
"Please provide valid username: \n-Between 5-10 characters \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(
`Do you really want to change username of ${props.username} to ${newUsername}?`,
)
) {
const nameChange: StrNameChange = {
prevName: props.username,
newName: newUsername.replace(/ /g, ""),
};
ChangeUsername({ nameChange: nameChange });
} else {
alert("Name was not changed!");
}
};
// Handles password change and checks if new password meets requirements
const handleClickChangePassword = (): void => {
if (newPassword.length !== passwordLength || !lowercase.test(newPassword)) {
alert(
"Please provide valid password: \n-Exactly 6 characters \n-No uppercase letters \n-No numbers \n-No special characters (.-!?/*)",
);
return;
}
if (
confirm(`Are you sure you want to change password of ${props.username}?`)
) {
//TODO: insert change password functionality
alert("Not implemented yet");
setNewPassword("");
} else {
alert("Password was not changed!");
}
};
return (
@ -38,23 +107,37 @@ function UserInfoModal(props: {
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white rounded-lg text-center flex flex-col">
<div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p>
<p
className="mb-[10px] hover:font-bold hover:cursor-pointer underline"
onClick={handleChangeNameView}
>
(Change Username)
<p className="mt-2 font-bold text-[20px]">Change:</p>
<p className="mt-2 space-x-3 mb-[10px]">
<span
className={
showNameInput
? "items-start font-semibold py-1 px-2 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start font-medium py-1 px-2 border-2 border-gray-500 text-white rounded-full bg-orange-300 hover:bg-orange-400 transition-all hover:text-gray-100 hover:border-gray-600 hover:cursor-pointer"
}
onClick={handleShowNameInput}
>
Username
</span>{" "}
<span
className={
showPwordInput
? "items-start font-semibold py-1 px-2 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start font-medium py-1 px-2 border-2 border-gray-500 text-white rounded-full bg-orange-300 hover:bg-orange-400 transition-all hover:text-gray-100 hover:border-gray-600 hover:cursor-pointer"
}
onClick={handleShowPwordInput}
>
Password
</span>
</p>
{showInput && (
<div>
<InputField
label={"New username"}
type={"text"}
value={newUsername}
onChange={function (e): void {
e.defaultPrevented;
{showNameInput && (
<div className="mt-7">
<UsernameInput
username={newUsername}
onChange={(e) => {
setNewUsername(e.target.value);
}}
/>
@ -67,6 +150,23 @@ function UserInfoModal(props: {
/>
</div>
)}
{showPwordInput && (
<div className="mt-7">
<PasswordInput
password={newPassword}
onChange={(e) => {
setNewPassword(e.target.value);
}}
/>
<Button
text={"Change"}
onClick={function (): void {
handleClickChangePassword();
}}
type={"submit"}
/>
</div>
)}
<h2 className="font-bold text-[20px]">Member of these projects:</h2>
<UserProjectListAdmin username={props.username} />
<div className="items-center space-x-6">
@ -87,7 +187,9 @@ function UserInfoModal(props: {
text={"Close"}
onClick={function (): void {
setNewUsername("");
setShowInput(false);
setNewPassword("");
setShowNameInput(false);
setShowPwordInput(false);
props.onClose();
}}
type="button"

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import UserInfoModal from "./UserInfoModal";
import InputField from "./InputField";
/**
* A list of users for admin manage users page, that sets an onClick
@ -15,6 +16,7 @@ import UserInfoModal from "./UserInfoModal";
export function UserListAdmin(props: { users: string[] }): JSX.Element {
const [modalVisible, setModalVisible] = useState(false);
const [username, setUsername] = useState("");
const [search, setSearch] = useState("");
const handleClick = (username: string): void => {
setUsername(username);
@ -28,24 +30,39 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element {
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Users</h1>
<UserInfoModal
onClose={handleClose}
isVisible={modalVisible}
username={username}
/>
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.users.map((user) => (
<li
className="pt-5"
key={user}
onClick={() => {
handleClick(user);
}}
>
{user}
</li>
))}
<InputField
placeholder={"Search"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="mt-3 border-2 text-left border-black rounded-2xl px-2 divide-y divide-gray-300 font-semibold text-[30px] transition-all cursor-pointer overflow-auto h-[60vh] w-[40vw]">
{props.users
.filter((user) => {
return search.toLowerCase() === ""
? user
: user.toLowerCase().includes(search.toLowerCase());
})
.map((user) => (
<li
className="hover:font-extrabold hover:underline p-1"
key={user}
onClick={() => {
handleClick(user);
}}
>
{user}
</li>
))}
</ul>
</div>
</>

View file

@ -8,7 +8,7 @@ function UserProjectListAdmin(props: { username: string }): JSX.Element {
GetProjects({ setProjectsProp: setProjects, username: props.username });
return (
<div className="border-2 border-black bg-white rounded-lg text-left overflow-auto h-[15vh] font-medium">
<div className="border-2 border-black bg-white rounded-2xl text-left overflow-auto h-[15vh] font-medium">
<ul className="divide-y-2">
{projects.map((project) => (
<li className="mx-2 my-1" key={project.id}>

View file

@ -0,0 +1,36 @@
//Different character limits certain strings
/**
* Allowed character length for password
*/
export const passwordLength = 6;
/**
* Lower limit for username length
*/
export const usernameLowLimit = 5;
/**
* Upper limit for password length
*/
export const usernameUpLimit = 10;
/**
* Lower limit for project name length
*/
export const projNameLowLimit = 10;
/**
* Upper limit for project name length
*/
export const projNameHighLimit = 99;
/**
* Upper limit for project description length
*/
export const projDescLowLimit = 0;
/**
* Upper limit for project description length
*/
export const projDescHighLimit = 99;

View file

@ -0,0 +1,9 @@
/**
* Only alphanumerical characters
*/
export const alphanumeric = /^[a-zA-Z0-9]+$/;
/**
* Only lowercase letters
*/
export const lowercase = /^[a-z]+$/;

View file

@ -1,11 +1,11 @@
import { Link } from "react-router-dom";
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button";
import { ProjectListAdmin } from "../../Components/ProjectListAdmin";
import { Project } from "../../Types/goTypes";
import GetProjects from "../../Components/GetProjects";
import { useState } from "react";
import NavButton from "../../Components/NavButton";
function AdminManageProjects(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]);
@ -13,14 +13,7 @@ function AdminManageProjects(): JSX.Element {
setProjectsProp: setProjects,
username: localStorage.getItem("username") ?? "",
});
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</h1>
<div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<ProjectListAdmin projects={projects} />
</div>
</>
);
const content = <ProjectListAdmin projects={projects} />;
const buttons = (
<>
@ -33,7 +26,7 @@ function AdminManageProjects(): JSX.Element {
type="button"
/>
</Link>
<BackButton />
<NavButton navTo="/admin" label={"Back"} />
</>
);

View file

@ -12,14 +12,7 @@ function AdminManageUsers(): JSX.Element {
const navigate = useNavigate();
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Manage Users</h1>
<div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<UserListAdmin users={users} />
</div>
</>
);
const content = <UserListAdmin users={users} />;
const buttons = (
<>

View file

@ -5,14 +5,14 @@ function AdminMenuPage(): JSX.Element {
const content = (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Administrator Menu</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-auto space-y-[10vh] p-[30px]">
<Link to="/adminManageUser">
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Manage Users
</h1>
</Link>
<Link to="/adminManageProject">
<h1 className="font-bold underline text-[30px] cursor-pointer">
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Manage Projects
</h1>
</Link>

View file

@ -1,11 +1,12 @@
import { useLocation } from "react-router-dom";
import AddUserToProject from "../../Components/AddUserToProject";
import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton";
function AdminProjectAddMember(): JSX.Element {
const projectName = useLocation().search.slice(1);
const content = <AddUserToProject projectName={projectName} />;
const buttons = <></>;
const buttons = <BackButton />;
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectAddMember;

View file

@ -1,15 +0,0 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow";
function AdminProjectStatistics(): JSX.Element {
const content = <></>;
const buttons = (
<>
<BackButton />
</>
);
return <BasicWindow content={content} buttons={buttons} />;
}
export default AdminProjectStatistics;

View file

@ -124,6 +124,14 @@ export interface WeeklyReport {
*/
signedBy?: number /* int */;
}
export interface Statistics {
totalDevelopmentTime: number /* int */;
totalMeetingTime: number /* int */;
totalAdminTime: number /* int */;
totalOwnWorkTime: number /* int */;
totalStudyTime: number /* int */;
totalTestingTime: number /* int */;
}
export interface UpdateWeeklyReport {
/**
* The name of the project, as it appears in the database

View file

@ -23,7 +23,6 @@ import AdminManageProjects from "./Pages/AdminPages/AdminManageProjects.tsx";
import AdminAddProject from "./Pages/AdminPages/AdminAddProject.tsx";
import AdminAddUser from "./Pages/AdminPages/AdminAddUser.tsx";
import AdminProjectAddMember from "./Pages/AdminPages/AdminProjectAddMember.tsx";
import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx";
import NotFoundPage from "./Pages/NotFoundPage.tsx";
import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx";
import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx";
@ -103,10 +102,6 @@ const router = createBrowserRouter([
path: "/adminProjectAddMember",
element: <AdminProjectAddMember />,
},
{
path: "/adminProjectStatistics",
element: <AdminProjectStatistics />,
},
{
path: "/addProject",
element: <AdminAddProject />,