Co-authored-by: Imbus <imbus64@users.noreply.github.com>

This commit is contained in:
al8763be 2024-03-28 16:43:34 +01:00
commit 9a54175d49
36 changed files with 735 additions and 151 deletions

3
.gitignore vendored
View file

@ -15,6 +15,9 @@ backend/*.png
backend/*.jpg backend/*.jpg
backend/*.svg backend/*.svg
/go.work.sum
/package-lock.json
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View file

@ -23,10 +23,13 @@ load-release file:
# Tests every part of the project # Tests every part of the project
testall: testall:
cd frontend && npm install
cd frontend && npm test cd frontend && npm test
cd frontend && npm run lint cd frontend && npm run lint
cd frontend && npm run build
cd backend && make test cd backend && make test
cd backend && make lint cd backend && make lint
cd backend && make itest
# Cleans up everything related to the project # Cleans up everything related to the project
clean: remove-podman-containers clean: remove-podman-containers

View file

@ -13,10 +13,13 @@ remove-podman-containers:
# Tests every part of the project # Tests every part of the project
testall: testall:
cd frontend && npm install
cd frontend && npm test cd frontend && npm test
cd frontend && npm run lint cd frontend && npm run lint
cd frontend && npm run build
cd backend && make test cd backend && make test
cd backend && make lint cd backend && make lint
cd backend && make itest
# Cleans up everything related to the project # Cleans up everything related to the project
clean: remove-podman-containers clean: remove-podman-containers

View file

@ -8,17 +8,19 @@ GOGET = $(GOCMD) get
# SQLite database filename # SQLite database filename
DB_FILE = db.sqlite3 DB_FILE = db.sqlite3
PROC_NAME = ttime_server
# Directory containing migration SQL scripts # Directory containing migration SQL scripts
MIGRATIONS_DIR = internal/database/migrations MIGRATIONS_DIR = internal/database/migrations
SAMPLE_DATA_DIR = internal/database/sample_data SAMPLE_DATA_DIR = internal/database/sample_data
# Build target # Build target
build: build:
$(GOBUILD) -o bin/server main.go $(GOBUILD) -o bin/$(PROC_NAME) main.go
# Run target # Run target
run: build run: build
./bin/server ./bin/$(PROC_NAME)
watch: build watch: build
watchexec -c -w . -r make run watchexec -c -w . -r make run
@ -37,6 +39,16 @@ clean:
test: db.sqlite3 test: db.sqlite3
$(GOTEST) ./... -count=1 $(GOTEST) ./... -count=1
# Integration test target
.PHONY: itest
itest:
pgrep $(PROC_NAME) && echo "Server already running" && exit 1 || true
make build
./bin/$(PROC_NAME) >/dev/null 2>&1 &
sleep 1 # Adjust if needed
python ../testing.py
pkill $(PROC_NAME)
# Get dependencies target # Get dependencies target
deps: deps:
$(GOGET) -v ./... $(GOGET) -v ./...

View file

@ -40,7 +40,8 @@ type Database interface {
SignWeeklyReport(reportId int, projectManagerId int) error SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error) IsSiteAdmin(username string) (bool, error)
IsProjectManager(username string, projectname string) (bool, error) IsProjectManager(username string, projectname string) (bool, error)
GetTotalTimePerActivity(projectName string) (map[string]int, error) GetProjectTimes(projectName string) (map[string]int, error)
UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
@ -498,6 +499,26 @@ func (d *Db) IsProjectManager(username string, projectname string) (bool, error)
return manager, err return manager, err
} }
func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error {
query := `
UPDATE weekly_reports
SET
development_time = ?,
meeting_time = ?,
admin_time = ?,
own_work_time = ?,
study_time = ?,
testing_time = ?
WHERE
user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
AND week = ?
`
_, err := d.Exec(query, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime, userName, projectName, week)
return err
}
// MigrateSampleData applies sample data to the database. // MigrateSampleData applies sample data to the database.
func (d *Db) MigrateSampleData() error { func (d *Db) MigrateSampleData() error {
// Insert sample data // Insert sample data
@ -537,11 +558,11 @@ func (d *Db) MigrateSampleData() error {
return nil return nil
} }
func (d *Db) GetTotalTimePerActivity(projectName string) (map[string]int, error) { // GetProjectTimes retrieves a map with times per "Activity" for a given project
func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) {
query := ` query := `
SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time
FROM weekly_reports FROM weekly_reports
JOIN projects ON weekly_reports.project_id = projects.id JOIN projects ON weekly_reports.project_id = projects.id
WHERE projects.name = ? WHERE projects.name = ?
` `

View file

@ -770,27 +770,90 @@ func TestIsProjectManager(t *testing.T) {
} }
} }
func TestGetTotalTimePerActivity(t *testing.T) { func TestGetProjectTimes(t *testing.T) {
// Initialize your test database connection // Initialize
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
t.Error("setupState failed:", err) t.Error("setupState failed:", err)
return
} }
// Run the query to get total time per activity // Create a user
totalTime, err := db.GetTotalTimePerActivity("projecttest") user := "TeaUser"
password := "Vanilla"
err = db.AddUser(user, password)
if err != nil {
t.Error("AddUser failed:", err)
return
}
// Create a project
projectName := "ProjectVanilla"
projectDescription := "When tea tastes its best"
err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here
if err != nil {
t.Error("AddProject failed:", err)
return
}
// Tests the func in db.go
totalTime, err := db.GetProjectTimes(projectName)
if err != nil { if err != nil {
t.Error("GetTotalTimePerActivity failed:", err) t.Error("GetTotalTimePerActivity failed:", err)
return
} }
// Check if the totalTime map is not nil // Check if the totalTime map is not nil
if totalTime == nil { if totalTime == nil {
t.Error("Expected non-nil totalTime map, got nil") t.Error("Expected non-nil totalTime map, got nil")
return
} }
// ska lägga till fler assertions // Define the expected valeus
} expectedTotalTime := map[string]int{
"development": 0,
"meeting": 0,
"admin": 0,
"own_work": 0,
"study": 0,
"testing": 0,
}
// Compare the expectedTotalTime with the totalTime retrieved from the database
for activity, expectedTime := range expectedTotalTime {
if totalTime[activity] != expectedTime {
t.Errorf("Expected %s time to be %d, got %d", activity, expectedTime, totalTime[activity])
}
}
// Insert some data into the database for different activities
err = db.AddWeeklyReport(projectName, user, 1, 1, 3, 2, 1, 4, 5)
if err != nil {
t.Error("Failed to insert data into the database:", err)
return
}
newTotalTime, err := db.GetProjectTimes(projectName)
if err != nil {
t.Error("GetTotalTimePerActivity failed:", err)
return
}
newExpectedTotalTime := map[string]int{
"development": 1,
"meeting": 3,
"admin": 2,
"own_work": 1,
"study": 4,
"testing": 5,
}
for activity, newExpectedTime := range newExpectedTotalTime {
if newTotalTime[activity] != newExpectedTime {
t.Errorf("Expected %s time to be %d, got %d", activity, newExpectedTime, newTotalTime[activity])
}
}
}
func TestEnsureManagerOfCreatedProject(t *testing.T) { func TestEnsureManagerOfCreatedProject(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -824,3 +887,51 @@ func TestEnsureManagerOfCreatedProject(t *testing.T) {
t.Error("Expected testuser to be a project manager, but it's not.") t.Error("Expected testuser to be a project manager, but it's not.")
} }
} }
// TestUpdateWeeklyReport tests the UpdateWeeklyReport function of the database
func TestUpdateWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a project
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add a weekly report
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Update the weekly report
err = db.UpdateWeeklyReport("testproject", "testuser", 1, 2, 2, 2, 2, 2, 2)
if err != nil {
t.Error("UpdateWeeklyReport failed:", err)
}
// Retrieve the updated report
updatedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Check if the report was updated correctly
if updatedReport.DevelopmentTime != 2 ||
updatedReport.MeetingTime != 2 ||
updatedReport.AdminTime != 2 ||
updatedReport.OwnWorkTime != 2 ||
updatedReport.StudyTime != 2 ||
updatedReport.TestingTime != 2 {
t.Error("UpdateWeeklyReport failed: report not updated correctly")
}
}

View file

@ -29,6 +29,7 @@ type GlobalState interface {
ChangeUserName(c *fiber.Ctx) error // WIP ChangeUserName(c *fiber.Ctx) error // WIP
GetAllUsersProject(c *fiber.Ctx) error // WIP GetAllUsersProject(c *fiber.Ctx) error // WIP
GetUnsignedReports(c *fiber.Ctx) error // GetUnsignedReports(c *fiber.Ctx) error //
UpdateWeeklyReport(c *fiber.Ctx) error
} }
// "Constructor" // "Constructor"

View file

@ -233,3 +233,57 @@ func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error {
// Return the result as JSON // Return the result as JSON
return c.JSON(fiber.Map{"isProjectManager": isManager}) return c.JSON(fiber.Map{"isProjectManager": isManager})
} }
func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error {
// Get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Get project
projectName := c.Params("projectName")
if projectName == "" {
log.Info("No project name provided")
return c.Status(400).SendString("No project name provided")
}
// Get all users in the project and roles
userProjects, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
log.Info("Error getting users in project:", err)
return c.Status(500).SendString(err.Error())
}
// If the user is member
isMember := false
for _, userProject := range userProjects {
if userProject.Username == username {
isMember = true
break
}
}
// If the user is admin
if !isMember {
isAdmin, err := gs.Db.IsSiteAdmin(username)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("User is neither a project member nor a site admin:", username)
return c.Status(403).SendString("User is neither a project member nor a site admin")
}
}
// Get project times
projectTimes, err := gs.Db.GetProjectTimes(projectName)
if err != nil {
log.Info("Error getting project times:", err)
return c.Status(500).SendString(err.Error())
}
// Return project times as JSON
log.Info("Returning project times for project:", projectName)
return c.JSON(projectTimes)
}

View file

@ -177,3 +177,37 @@ func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error {
// Return the list of reports as JSON // Return the list of reports as JSON
return c.JSON(reports) return c.JSON(reports)
} }
func (gs *GState) UpdateWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Parse the request body into an UpdateWeeklyReport struct
var updateReport types.UpdateWeeklyReport
if err := c.BodyParser(&updateReport); err != nil {
log.Info("Error parsing weekly report")
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if updateReport.Week < 1 || updateReport.Week > 52 {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number")
}
if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 {
log.Info("Invalid time report")
return c.Status(400).SendString("Invalid time report")
}
// Update the weekly report in the database
if err := gs.Db.UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil {
log.Info("Error updating weekly report in db:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Weekly report updated")
return c.Status(200).SendString("Weekly report updated")
}

View file

@ -207,8 +207,8 @@ func (gs *GState) GetAllUsersProject(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce plain // @Produce plain
// @Param NewUser body types.NewUser true "user info" // @Param NewUser body types.NewUser true "user info"
// @Success 200 {json} json "Successfully prometed user" // @Success 200 {json} json "Successfully promoted user"
// @Failure 400 {string} string "bad request" // @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized" // @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error" // @Failure 500 {string} string "Internal server error"
// @Router /promoteToAdmin [post] // @Router /promoteToAdmin [post]
@ -234,33 +234,33 @@ func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
} }
// Changes a users name in the database // ChangeUserName changes a user's username in the database
func (gs *GState) ChangeUserName(c *fiber.Ctx) error { func (gs *GState) ChangeUserName(c *fiber.Ctx) error {
// Check token and get username of current user
//check token and get username of current user
user := c.Locals("user").(*jwt.Token) user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims) claims := user.Claims.(jwt.MapClaims)
projectManagerUsername := claims["name"].(string) adminUsername := claims["name"].(string)
log.Info(projectManagerUsername) log.Info(adminUsername)
// Extract the necessary parameters from the request // Extract the necessary parameters from the request
data := new(types.NameChange) data := new(types.StrNameChange)
if err := c.BodyParser(data); err != nil { if err := c.BodyParser(data); err != nil {
log.Info("error parsing username, project or role") log.Info("Error parsing username")
return c.Status(400).SendString(err.Error()) return c.Status(400).SendString(err.Error())
} }
// dubble diping and checcking if current user is // Check if the current user is an admin
isAdmin, err := gs.Db.IsSiteAdmin(adminUsername)
if ismanager, err := gs.Db.IsProjectManager(projectManagerUsername, c.Params(data.Name)); err != nil { if err != nil {
log.Warn("Error checking if projectmanager:", err) log.Warn("Error checking if admin:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} else if !ismanager { } else if !isAdmin {
log.Warn("tried changing name when not projectmanager:", err) log.Warn("Tried changing name when not admin")
return c.Status(401).SendString("you can not change name when not projectmanager") return c.Status(401).SendString("You cannot change name unless you are an admin")
} }
// Change the user's name within the project in the database // Change the user's name in the database
if err := gs.Db.ChangeUserName(projectManagerUsername, data.Name); err != nil { if err := gs.Db.ChangeUserName(data.PrevName, data.NewName); err != nil {
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }

View file

@ -65,3 +65,24 @@ type WeeklyReport struct {
// The project manager who signed it // The project manager who signed it
SignedBy *int `json:"signedBy" db:"signed_by"` SignedBy *int `json:"signedBy" db:"signed_by"`
} }
type UpdateWeeklyReport struct {
// The name of the project, as it appears in the database
ProjectName string `json:"projectName"`
// The name of the user
UserName string `json:"userName"`
// The week number
Week int `json:"week"`
// Total time spent on development
DevelopmentTime int `json:"developmentTime"`
// Total time spent in meetings
MeetingTime int `json:"meetingTime"`
// Total time spent on administrative tasks
AdminTime int `json:"adminTime"`
// Total time spent on personal projects
OwnWorkTime int `json:"ownWorkTime"`
// Total time spent on studying
StudyTime int `json:"studyTime"`
// Total time spent on testing
TestingTime int `json:"testingTime"`
}

View file

@ -33,6 +33,12 @@ import (
// @externalDocs.description OpenAPI // @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/ // @externalDocs.url https://swagger.io/resources/open-api/
/**
Main function for starting the server and initializing configurations.
Reads configuration from file, pretty prints it, connects to the database,
migrates it, and sets up routes for the server.
*/
func main() { func main() {
conf, err := config.ReadConfigFromFile("config.toml") conf, err := config.ReadConfigFromFile("config.toml")
if err != nil { if err != nil {
@ -102,6 +108,7 @@ func main() {
server.Get("/api/checkIfProjectManager/:projectName", gs.IsProjectManagerHandler) server.Get("/api/checkIfProjectManager/:projectName", gs.IsProjectManagerHandler)
server.Post("/api/ProjectRoleChange", gs.ProjectRoleChange) server.Post("/api/ProjectRoleChange", gs.ProjectRoleChange)
server.Get("/api/getUsersProject/:projectName", gs.ListAllUsersProject) server.Get("/api/getUsersProject/:projectName", gs.ListAllUsersProject)
server.Put("/api/updateWeeklyReport", gs.UpdateWeeklyReport)
// Announce the port we are listening on and start the server // Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port)) err = server.Listen(fmt.Sprintf(":%d", conf.Port))

View file

@ -13,7 +13,6 @@ FROM docker.io/golang:alpine as go
RUN apk add gcompat RUN apk add gcompat
RUN apk add gcc RUN apk add gcc
RUN apk add musl-dev RUN apk add musl-dev
RUN apk add make
RUN apk add sqlite RUN apk add sqlite
WORKDIR /build WORKDIR /build
ADD backend/go.mod backend/go.sum ./ ADD backend/go.mod backend/go.sum ./
@ -24,9 +23,7 @@ RUN go mod download
# Add the source code # Add the source code
ADD backend . ADD backend .
RUN make migrate RUN go build -o server
# RUN go build -o server
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go
# Strip the binary for a smaller image # Strip the binary for a smaller image
@ -37,6 +34,7 @@ FROM docker.io/alpine:latest as runner
RUN adduser -D nonroot RUN adduser -D nonroot
RUN addgroup nonroot nonroot RUN addgroup nonroot nonroot
WORKDIR /app WORKDIR /app
RUN chown nonroot:nonroot /app
# Copy the frontend SPA build into public # Copy the frontend SPA build into public
COPY --from=client /build/dist static COPY --from=client /build/dist static
@ -44,9 +42,6 @@ COPY --from=client /build/dist static
# Copy the server binary # Copy the server binary
COPY --from=go /build/server server COPY --from=go /build/server server
# Copy the database
COPY --from=go /build/db.sqlite3 db.sqlite3
# Expose port 8080 # Expose port 8080
EXPOSE 8080 EXPOSE 8080

View file

@ -4,6 +4,7 @@ import {
User, User,
Project, Project,
NewProject, NewProject,
UserProjectMember,
WeeklyReport, WeeklyReport,
} from "../Types/goTypes"; } from "../Types/goTypes";
@ -84,7 +85,7 @@ interface API {
submitWeeklyReport( submitWeeklyReport(
weeklyReport: NewWeeklyReport, weeklyReport: NewWeeklyReport,
token: string, token: string,
): Promise<APIResponse<NewWeeklyReport>>; ): Promise<APIResponse<string>>;
/** Gets a weekly report for a specific user, project and week /** Gets a weekly report for a specific user, project and week
* @param {string} projectName The name of the project. * @param {string} projectName The name of the project.
@ -127,6 +128,11 @@ interface API {
* @returns {Promise<APIResponse<string[]>>} A promise resolving to an API response containing the list of users. * @returns {Promise<APIResponse<string[]>>} A promise resolving to an API response containing the list of users.
*/ */
getAllUsers(token: string): Promise<APIResponse<string[]>>; getAllUsers(token: string): Promise<APIResponse<string[]>>;
/** Gets all users in a project from name*/
getAllUsersProject(
projectName: string,
token: string,
): Promise<APIResponse<UserProjectMember[]>>;
} }
/** An instance of the API */ /** An instance of the API */
@ -288,7 +294,7 @@ export const api: API = {
async submitWeeklyReport( async submitWeeklyReport(
weeklyReport: NewWeeklyReport, weeklyReport: NewWeeklyReport,
token: string, token: string,
): Promise<APIResponse<NewWeeklyReport>> { ): Promise<APIResponse<string>> {
try { try {
const response = await fetch("/api/submitWeeklyReport", { const response = await fetch("/api/submitWeeklyReport", {
method: "POST", method: "POST",
@ -306,8 +312,8 @@ export const api: API = {
}; };
} }
const data = (await response.json()) as NewWeeklyReport; const data = await response.text();
return { success: true, data }; return { success: true, message: data };
} catch (e) { } catch (e) {
return { return {
success: false, success: false,
@ -448,4 +454,34 @@ export const api: API = {
}); });
} }
}, },
//Gets all users in a project
async getAllUsersProject(
projectName: string,
token: string,
): Promise<APIResponse<UserProjectMember[]>> {
try {
const response = await fetch(`/api/getUsersProject/${projectName}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
});
if (!response.ok) {
return Promise.resolve({
success: false,
message: "Failed to get users",
});
} else {
const data = (await response.json()) as UserProjectMember[];
return Promise.resolve({ success: true, data });
}
} catch (e) {
return Promise.resolve({
success: false,
message: "API is not ok",
});
}
},
}; };

View file

@ -13,24 +13,24 @@ function AllTimeReportsInProject(): JSX.Element {
const { projectName } = useParams(); const { projectName } = useParams();
const [weeklyReports, setWeeklyReports] = useState<WeeklyReport[]>([]); const [weeklyReports, setWeeklyReports] = useState<WeeklyReport[]>([]);
const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getWeeklyReportsForUser(
projectName ?? "",
token,
);
console.log(response);
if (response.success) {
setWeeklyReports(response.data ?? []);
} else {
console.error(response.message);
}
};
// Call getProjects when the component mounts // Call getProjects when the component mounts
useEffect(() => { useEffect(() => {
const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getWeeklyReportsForUser(
projectName ?? "",
token,
);
console.log(response);
if (response.success) {
setWeeklyReports(response.data ?? []);
} else {
console.error(response.message);
}
};
void getWeeklyReports(); void getWeeklyReports();
}, []); }, [projectName]);
return ( return (
<> <>

View file

@ -18,13 +18,11 @@ export default function GetWeeklyReport(): JSX.Element {
const [testingTime, setTestingTime] = useState(0); const [testingTime, setTestingTime] = useState(0);
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const username = localStorage.getItem("username") ?? "";
const { projectName } = useParams(); const { projectName } = useParams();
const { fetchedWeek } = useParams(); const { fetchedWeek } = useParams();
const fetchWeeklyReport = async (): Promise<void> => { const fetchWeeklyReport = async (): Promise<void> => {
const response = await api.getWeeklyReport( const response = await api.getWeeklyReport(
username,
projectName ?? "", projectName ?? "",
fetchedWeek?.toString() ?? "0", fetchedWeek?.toString() ?? "0",
token, token,

View file

@ -0,0 +1,37 @@
import { Dispatch, useEffect } from "react";
import { Project } from "../Types/goTypes";
import { api } from "../API/API";
/**
* Gets all projects that user is a member of
* @param props - A setStateAction for the array you want to put projects in
* @returns {void} Nothing
* @example
* const [projects, setProjects] = useState<Project[]>([]);
* GetAllUsers({ setProjectsProp: setProjects });
*/
function GetProjects(props: {
setProjectsProp: Dispatch<React.SetStateAction<Project[]>>;
}): void {
const setProjects: Dispatch<React.SetStateAction<Project[]>> =
props.setProjectsProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getUserProjects(token);
if (response.success) {
setProjects(response.data ?? []);
} else {
console.error("Failed to fetch projects:", response.message);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
void fetchUsers();
}, [setProjects]);
}
export default GetProjects;

View file

@ -0,0 +1,37 @@
import { Dispatch, useEffect } from "react";
import { UserProjectMember } from "../Types/goTypes";
import { api } from "../API/API";
/**
* Gets all projects that user is a member of
* @param props - A setStateAction for the array you want to put projects in
* @returns {void} Nothing
* @example
* const [projects, setProjects] = useState<Project[]>([]);
* GetAllUsers({ setProjectsProp: setProjects });
*/
function GetUsersInProject(props: {
projectName: string;
setUsersProp: Dispatch<React.SetStateAction<UserProjectMember[]>>;
}): void {
const setUsers: Dispatch<React.SetStateAction<UserProjectMember[]>> =
props.setUsersProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllUsersProject(props.projectName, token);
if (response.success) {
setUsers(response.data ?? []);
} else {
console.error("Failed to fetch projects:", response.message);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
void fetchUsers();
}, [props.projectName, setUsers]);
}
export default GetUsersInProject;

View file

@ -60,8 +60,7 @@ export default function NewWeeklyReport(): JSX.Element {
type="week" type="week"
placeholder="Week" placeholder="Week"
onChange={(e) => { onChange={(e) => {
const weekNumber = parseInt(e.target.value.split("-W")[1]); setWeek(parseInt(e.target.value));
setWeek(weekNumber);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;

View file

@ -0,0 +1,66 @@
import { useState } from "react";
import Button from "./Button";
import { UserProjectMember } from "../Types/goTypes";
import GetUsersInProject from "./GetUsersInProject";
function ProjectInfoModal(props: {
isVisible: boolean;
projectname: string;
onClose: () => void;
onClick: (username: string) => void;
}): JSX.Element {
const [users, setUsers] = useState<UserProjectMember[]>([]);
GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers });
if (!props.isVisible) return <></>;
return (
<div
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-[41vh] w-[40vw] flex flex-col">
<div className="pl-20 pr-20">
<h1 className="font-bold text-[32px] mb-[20px]">Project members:</h1>
<div className="border-2 border-black p-2 rounded-lg text-center overflow-scroll h-[26vh]">
<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>
))}
</ul>
</div>
</div>
<div className="space-x-16">
<Button
text={"Delete"}
onClick={function (): void {
//DELETE PROJECT
}}
type="button"
/>
<Button
text={"Close"}
onClick={function (): void {
props.onClose();
}}
type="button"
/>
</div>
</div>
</div>
);
}
export default ProjectInfoModal;

View file

@ -0,0 +1,79 @@
import { useState } from "react";
import { NewProject } from "../Types/goTypes";
import ProjectInfoModal from "./ProjectInfoModal";
import UserInfoModal from "./UserInfoModal";
import DeleteUser from "./DeleteUser";
/**
* A list of projects for admin manage projects page, that sets an onClick
* function for eact project <li> element, which displays a modul with
* user info.
* @param props - An array of projects to display
* @returns {JSX.Element} The project list
* @example
* const projects: NewProject[] = [{ name: "Project", description: "New" }];
* return <ProjectListAdmin projects={projects} />
*/
export function ProjectListAdmin(props: {
projects: NewProject[];
}): JSX.Element {
const [projectModalVisible, setProjectModalVisible] = useState(false);
const [projectname, setProjectname] = useState("");
const [userModalVisible, setUserModalVisible] = useState(false);
const [username, setUsername] = useState("");
const handleClickUser = (username: string): void => {
setUsername(username);
setUserModalVisible(true);
};
const handleClickProject = (username: string): void => {
setProjectname(username);
setProjectModalVisible(true);
};
const handleCloseProject = (): void => {
setProjectname("");
setProjectModalVisible(false);
};
const handleCloseUser = (): void => {
setProjectname("");
setUserModalVisible(false);
};
return (
<>
<ProjectInfoModal
onClose={handleCloseProject}
onClick={handleClickUser}
isVisible={projectModalVisible}
projectname={projectname}
/>
<UserInfoModal
manageMember={true}
onClose={handleCloseUser}
//TODO: CHANGE TO REMOVE USER FROM PROJECT
onDelete={() => DeleteUser}
isVisible={userModalVisible}
username={username}
/>
<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>
))}
</ul>
</div>
</>
);
}

View file

@ -5,23 +5,38 @@ import UserProjectListAdmin from "./UserProjectListAdmin";
function UserInfoModal(props: { function UserInfoModal(props: {
isVisible: boolean; isVisible: boolean;
manageMember: boolean;
username: string; username: string;
onClose: () => void; onClose: () => void;
onDelete: (username: string) => void;
}): JSX.Element { }): JSX.Element {
if (!props.isVisible) return <></>; if (!props.isVisible) return <></>;
const ManageUserOrMember = (check: boolean): JSX.Element => {
if (check) {
return (
<Link to="/AdminChangeRole">
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Role)
</p>
</Link>
);
}
return (
<Link to="/AdminChangeUserName">
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Username)
</p>
</Link>
);
};
return ( return (
<div <div
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center" flex justify-center items-center"
> >
<div className="border-4 border-black bg-white p-2 rounded-lg text-center"> <div className="border-4 border-black bg-white p-2 rounded-lg text-center flex flex-col">
<p className="font-bold text-[30px]">{props.username}</p> <p className="font-bold text-[30px]">{props.username}</p>
<Link to="/AdminChangeUserName"> {ManageUserOrMember(props.manageMember)}
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Username)
</p>
</Link>
<div> <div>
<h2 className="font-bold text-[22px] mb-[20px]"> <h2 className="font-bold text-[22px] mb-[20px]">
Member of these projects: Member of these projects:

View file

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import UserInfoModal from "./UserInfoModal"; import UserInfoModal from "./UserInfoModal";
import DeleteUser from "./DeleteUser";
/** /**
* A list of users for admin manage users page, that sets an onClick * A list of users for admin manage users page, that sets an onClick
@ -29,7 +30,9 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element {
return ( return (
<> <>
<UserInfoModal <UserInfoModal
manageMember={false}
onClose={handleClose} onClose={handleClose}
onDelete={() => DeleteUser}
isVisible={modalVisible} isVisible={modalVisible}
username={username} username={username}
/> />

View file

@ -2,9 +2,22 @@ import { Link } from "react-router-dom";
import BackButton from "../../Components/BackButton"; import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
import { ProjectListAdmin } from "../../Components/ProjectListAdmin";
import { Project } from "../../Types/goTypes";
import GetProjects from "../../Components/GetProjects";
import { useState } from "react";
function AdminManageProjects(): JSX.Element { function AdminManageProjects(): JSX.Element {
const content = <></>; const [projects, setProjects] = useState<Project[]>([]);
GetProjects({ setProjectsProp: setProjects });
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 buttons = ( const buttons = (
<> <>

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -13,13 +14,7 @@ function AdminProjectAddMember(): JSX.Element {
}} }}
type="button" type="button"
/> />
<Button <BackButton />
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -13,13 +14,7 @@ function AdminProjectChangeUserRole(): JSX.Element {
}} }}
type="button" type="button"
/> />
<Button <BackButton />
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -13,13 +14,7 @@ function AdminProjectManageMembers(): JSX.Element {
}} }}
type="button" type="button"
/> />
<Button <BackButton />
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -13,13 +14,7 @@ function AdminProjectPage(): JSX.Element {
}} }}
type="button" type="button"
/> />
<Button <BackButton />
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );

View file

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

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -13,13 +14,7 @@ function AdminProjectViewMemberInfo(): JSX.Element {
}} }}
type="button" type="button"
/> />
<Button <BackButton />
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );

View file

@ -4,23 +4,23 @@ body{
@keyframes backgroundTransition { @keyframes backgroundTransition {
0% { 0% {
background-image: url('src/assets/1.jpg'); background-image: url('../assets/1.jpg');
animation-timing-function: ease-out; animation-timing-function: ease-out;
} }
25% { 25% {
background-image: url('src/assets/2.jpg'); background-image: url('../assets/2.jpg');
animation-timing-function: ease-in; animation-timing-function: ease-in;
} }
50% { 50% {
background-image: url('src/assets/3.jpg'); background-image: url('../assets/3.jpg');
animation-timing-function: ease-out; animation-timing-function: ease-out;
} }
75% { 75% {
background-image: url('src/assets/4.jpg'); background-image: url('../assets/4.jpg');
animation-timing-function: ease-in; animation-timing-function: ease-in;
} }
100% { 100% {
background-image: url('src/assets/1.jpg'); background-image: url('../assets/1.jpg');
animation-timing-function: ease-out; animation-timing-function: ease-out;
} }
} }

View file

@ -184,6 +184,11 @@ export interface PublicUser {
userId: string; userId: string;
username: string; username: string;
} }
export interface UserProjectMember {
Username: string;
UserRole: string;
}
/** /**
* wrapper type for token * wrapper type for token
*/ */

View file

@ -1,28 +0,0 @@
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

6
package-lock.json generated
View file

@ -1,6 +0,0 @@
{
"name": "TTime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -42,6 +42,8 @@ checkIfProjectManagerPath = base_url + "/api/checkIfProjectManager"
ProjectRoleChangePath = base_url + "/api/ProjectRoleChange" ProjectRoleChangePath = base_url + "/api/ProjectRoleChange"
getUsersProjectPath = base_url + "/api/getUsersProject" getUsersProjectPath = base_url + "/api/getUsersProject"
getUnsignedReportsPath = base_url + "/api/getUnsignedReports" getUnsignedReportsPath = base_url + "/api/getUnsignedReports"
getChangeUserNamePath = base_url + "/api/changeUserName"
getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport"
#ta bort auth i handlern för att få testet att gå igenom #ta bort auth i handlern för att få testet att gå igenom
def test_ProjectRoleChange(): def test_ProjectRoleChange():
@ -370,6 +372,98 @@ def test_ensure_manager_of_created_project():
assert response.json()["isProjectManager"] == True, "User is not project manager" assert response.json()["isProjectManager"] == True, "User is not project manager"
gprint("test_ensure_admin_of_created_project successful") gprint("test_ensure_admin_of_created_project successful")
def test_change_user_name():
# Register a new user
new_user = randomString()
register(new_user, "password")
# Log in as the new user
token = login(new_user, "password").json()["token"]
# Register a new admin
admin_username = randomString()
admin_password = "admin_password"
dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
admin_token = login(admin_username, admin_password).json()["token"]
# Promote to admin
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
# Change the user's name
response = requests.put(
getChangeUserNamePath,
json={"prevName": new_user, "newName": randomString()},
headers={"Authorization": "Bearer " + admin_token},
)
# Check if the change was successful
assert response.status_code == 200, "Change user name failed"
gprint("test_change_user_name successful")
def test_list_all_users_project():
# Log in as a user who is a member of the project
admin_username = randomString()
admin_password = "admin_password2"
dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
dprint(response.text)
# Log in as the admin
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
# Make a request to list all users associated with the project
response = requests.get(
getUsersProjectPath + "/" + projectName,
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "List all users project failed"
gprint("test_list_all_users_project sucessful")
def test_update_weekly_report():
# Log in as the user
token = login(username, "always_same").json()["token"]
# Prepare the JSON data for updating the weekly report
update_data = {
"projectName": projectName,
"userName": username,
"week": 1,
"developmentTime": 8,
"meetingTime": 6,
"adminTime": 4,
"ownWorkTime": 11,
"studyTime": 8,
"testingTime": 18,
}
# Send a request to update the weekly report
response = requests.put(
getUpdateWeeklyReportPath,
json=update_data,
headers={"Authorization": "Bearer " + token},
)
# Check if the update was successful
assert response.status_code == 200, "Update weekly report failed"
gprint("test_update_weekly_report successful")
if __name__ == "__main__": if __name__ == "__main__":
test_get_user_projects() test_get_user_projects()
@ -384,6 +478,8 @@ if __name__ == "__main__":
test_get_weekly_reports_user() test_get_weekly_reports_user()
test_check_if_project_manager() test_check_if_project_manager()
test_ProjectRoleChange() test_ProjectRoleChange()
#test_list_all_users_project()
test_ensure_manager_of_created_project() test_ensure_manager_of_created_project()
test_get_unsigned_reports() test_get_unsigned_reports()
test_list_all_users_project()
test_change_user_name()
test_update_weekly_report()