Merge branch 'frontend' into gruppPP

This commit is contained in:
Peter KW 2024-03-17 15:39:16 +01:00
commit 060dc1ee3d
11 changed files with 368 additions and 139 deletions

View file

@ -2,7 +2,6 @@ package database
import ( import (
"embed" "embed"
"os"
"path/filepath" "path/filepath"
"ttime/internal/types" "ttime/internal/types"
@ -19,7 +18,7 @@ type Database interface {
PromoteToAdmin(username string) error PromoteToAdmin(username string) error
GetUserId(username string) (int, error) GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error AddProject(name string, description string, username string) error
Migrate(dirname string) error Migrate() error
GetProjectId(projectname string) (int, error) GetProjectId(projectname string) (int, error)
AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
AddUserToProject(username string, projectname string, role string) error AddUserToProject(username string, projectname string, role string) error
@ -259,13 +258,18 @@ func (d *Db) GetAllUsersApplication() ([]string, error) {
// Reads a directory of migration files and applies them to the database. // Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory // This will eventually be used on an embedded directory
func (d *Db) Migrate(dirname string) error { func (d *Db) Migrate() error {
// Read the embedded scripts directory // Read the embedded scripts directory
files, err := scripts.ReadDir("migrations") files, err := scripts.ReadDir("migrations")
if err != nil { if err != nil {
return err return err
} }
if len(files) == 0 {
println("No migration files found")
return nil
}
tr := d.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
@ -275,8 +279,7 @@ func (d *Db) Migrate(dirname string) error {
} }
// This is perhaps not the most elegant way to do this // This is perhaps not the most elegant way to do this
sqlFile := filepath.Join("migrations", file.Name()) sqlBytes, err := scripts.ReadFile("migrations/" + file.Name())
sqlBytes, err := os.ReadFile(sqlFile)
if err != nil { if err != nil {
return err return err
} }

View file

@ -8,7 +8,7 @@ import (
func setupState() (Database, error) { func setupState() (Database, error) {
db := DbConnect(":memory:") db := DbConnect(":memory:")
err := db.Migrate("../../migrations") err := db.Migrate()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -43,6 +43,11 @@ func main() {
// Connect to the database // Connect to the database
db := database.DbConnect(conf.DbPath) db := database.DbConnect(conf.DbPath)
// Migrate the database
if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err)
}
// Get our global state // Get our global state
gs := handlers.NewGlobalState(db) gs := handlers.NewGlobalState(db)
// Create the server // Create the server

View file

@ -1,5 +1,10 @@
import { NewProject, Project } from "../Types/Project"; import {
import { NewUser, User } from "../Types/Users"; NewWeeklyReport,
NewUser,
User,
Project,
NewProject,
} from "../Types/goTypes";
// This type of pattern should be hard to misuse // This type of pattern should be hard to misuse
interface APIResponse<T> { interface APIResponse<T> {
@ -20,8 +25,20 @@ interface API {
project: NewProject, project: NewProject,
token: string, token: string,
): Promise<APIResponse<Project>>; ): Promise<APIResponse<Project>>;
/** Submit a weekly report */
submitWeeklyReport(
project: NewWeeklyReport,
token: string,
): Promise<APIResponse<Project>>;
/** Renew the token */ /** Renew the token */
renewToken(token: string): Promise<APIResponse<string>>; renewToken(token: string): Promise<APIResponse<string>>;
/** Gets all the projects of a user*/
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Login */
login(NewUser: NewUser): Promise<APIResponse<JSON>>;
} }
// Export an instance of the API // Export an instance of the API
@ -117,4 +134,87 @@ export const api: API = {
return { success: false, message: "Failed to renew token" }; return { success: false, message: "Failed to renew token" };
} }
}, },
async getUserProjects(token: string): Promise<APIResponse<Project[]>> {
try {
const response = await fetch("/api/getUserProjects", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
} else {
const data = (await response.json()) as Project[];
return Promise.resolve({ success: true, data });
}
} catch (e) {
return Promise.resolve({
success: false,
message: "Failed to get user projects",
});
}
},
async submitWeeklyReport(
project: NewWeeklyReport,
token: string,
): Promise<APIResponse<Project>> {
try {
return fetch("/api/submitWeeklyReport", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(project),
})
.then((response) => {
if (!response.ok) {
return {
success: false,
message: "Failed to submit weekly report",
};
} else {
return response.json();
}
})
.then((data: Project) => {
return { success: true, data };
});
} catch (e) {
return Promise.resolve({
success: false,
message: "Failed to submit weekly report",
});
}
},
async login(NewUser: NewUser): Promise<APIResponse<string>> {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(NewUser),
});
if (!response.ok) {
return { success: false, message: "Failed to login" };
} else {
const data = (await response.json()) as { token: string }; // Fix: Change the type of 'data'
const token = data.token;
return { success: true, data: token };
}
} catch (e) {
return { success: false, message: "Failed to login" };
}
},
}; };

View file

@ -4,6 +4,32 @@ import { api } from "../API/API";
import Logo from "../assets/Logo.svg"; import Logo from "../assets/Logo.svg";
import Button from "./Button"; import Button from "./Button";
function InputField(props: {
label: string;
type: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-sans font-bold mb-2"
htmlFor={props.label}
>
{props.label}
</label>
<input
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}
value={props.value}
onChange={props.onChange}
/>
</div>
);
}
export default function Register(): JSX.Element { export default function Register(): JSX.Element {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -14,7 +40,7 @@ export default function Register(): JSX.Element {
}; };
return ( return (
<div className="flex flex-col h-screen w-screen items-center justify-center"> <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"> <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 <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 items-center justify-center flex flex-col w-fit h-fit"
@ -31,42 +57,22 @@ export default function Register(): JSX.Element {
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]"> <h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Register New User Register New User
</h3> </h3>
<div className="mb-4"> <InputField
<label label="Username"
className="block text-gray-700 text-sm font-sans font-bold mb-2"
htmlFor="username"
>
Username
</label>
<input
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="username"
type="text" type="text"
placeholder="Username"
value={username} value={username}
onChange={(e) => { onChange={(e) => {
setUsername(e.target.value); setUsername(e.target.value);
}} }}
/> />
</div> <InputField
<div className="mb-6"> label="Password"
<label
className="block text-gray-700 text-sm font-sans font-bold mb-2"
htmlFor="password"
>
Password
</label>
<input
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="password"
type="password" type="password"
placeholder="Choose your password"
value={password} value={password}
onChange={(e) => { onChange={(e) => {
setPassword(e.target.value); setPassword(e.target.value);
}} }}
/> />
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button <Button
text="Register" text="Register"

View file

@ -1,20 +1,58 @@
function NewTimeReport(): JSX.Element { import { useState } from "react";
const activities = [ import { TimeReport } from "../Types/TimeReport";
"Development", import { api } from "../API/API";
"Meeting", import { useNavigate } from "react-router-dom";
"Administration", import Button from "./Button";
"Own Work",
"Studies", export default function NewTimeReport(): JSX.Element {
"Testing", const [week, setWeek] = useState("");
]; const [development, setDevelopment] = useState("0");
const [meeting, setMeeting] = useState("0");
const [administration, setAdministration] = useState("0");
const [ownwork, setOwnWork] = useState("0");
const [studies, setStudies] = useState("0");
const [testing, setTesting] = useState("0");
const handleNewTimeReport = async (): Promise<void> => {
const newTimeReport: TimeReport = {
week,
development,
meeting,
administration,
ownwork,
studies,
testing,
};
await Promise.resolve();
// await api.registerTimeReport(newTimeReport); This needs to be implemented!
};
const navigate = useNavigate();
return ( return (
<> <>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center"> <div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form
onSubmit={(e) => {
if (week === "") {
alert("Please enter a week number");
e.preventDefault();
return;
}
e.preventDefault();
void handleNewTimeReport();
navigate("/project");
}}
>
<div className="flex flex-col items-center">
<input <input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black" className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="week" type="week"
placeholder="Week" placeholder="Week"
onChange={(e) => {
const weekNumber = e.target.value.split("-W")[1];
setWeek(weekNumber);
}}
onKeyDown={(event) => { onKeyDown={(event) => {
event.preventDefault(); event.preventDefault();
}} }}
@ -25,21 +63,121 @@ function NewTimeReport(): JSX.Element {
<table className="w-full text-center divide-y divide-x divide-white text-[30px]"> <table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead> <thead>
<tr> <tr>
<th className="w-1/2 py-2 border-b-2 border-black">Activity</th> <th className="w-1/2 py-2 border-b-2 border-black">
Activity
</th>
<th className="w-1/2 py-2 border-b-2 border-black"> <th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min) Total Time (min)
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-black"> <tbody className="divide-y divide-black">
{activities.map((activity, index) => ( <tr className="h-[10vh]">
<tr key={index} className="h-[10vh]"> <td>Development</td>
<td>{activity}</td>
<td> <td>
<input <input
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={development}
onChange={(e) => {
setDevelopment(e.target.value);
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Meeting</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meeting}
onChange={(e) => {
setMeeting(e.target.value);
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Administration</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={administration}
onChange={(e) => {
setAdministration(e.target.value);
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Own Work</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownwork}
onChange={(e) => {
setOwnWork(e.target.value);
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Studies</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studies}
onChange={(e) => {
setStudies(e.target.value);
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Testing</td>
<td>
<input
type="number"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testing}
onChange={(e) => {
setTesting(e.target.value);
}}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace") if (!/\d/.test(keyValue) && keyValue !== "Backspace")
@ -48,12 +186,18 @@ function NewTimeReport(): JSX.Element {
/> />
</td> </td>
</tr> </tr>
))}
</tbody> </tbody>
</table> </table>
<Button
text="Submit"
onClick={(): void => {
return;
}}
type="submit"
/>
</div>
</form>
</div> </div>
</> </>
); );
} }
export default NewTimeReport;

View file

@ -1,18 +1,16 @@
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import Register from "../../Components/Register";
function AdminAddUser(): JSX.Element { function AdminAddUser(): JSX.Element {
const content = <></>; const content = (
<>
<Register />
</>
);
const buttons = ( const buttons = (
<> <>
<Button
text="Finish"
onClick={(): void => {
return;
}}
type="button"
/>
<Button <Button
text="Back" text="Back"
onClick={(): void => { onClick={(): void => {

View file

@ -13,14 +13,15 @@ function UserNewTimeReportPage(): JSX.Element {
const buttons = ( const buttons = (
<> <>
<Link to="/project">
<Button <Button
text="Submit" text="Back"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="button" type="button"
/> />
<BackButton /> </Link>
</> </>
); );

View file

@ -1,13 +0,0 @@
export interface Project {
id: number;
name: string;
description: string;
owner: string;
created: string; // This is a date
}
export interface NewProject {
name: string;
description: string;
owner: string;
}

View file

@ -1,11 +0,0 @@
// This is how the API responds
export interface User {
id: number;
userName: string;
}
// Used to create a new user
export interface NewUser {
userName: string;
password: string;
}

View file

@ -20,10 +20,6 @@ const router = createBrowserRouter([
path: "/pm", path: "/pm",
element: <YourProjectsPage />, element: <YourProjectsPage />,
}, },
{
path: "/user",
element: <YourProjectsPage />,
},
]); ]);
// Semi-hacky way to get the root element // Semi-hacky way to get the root element