diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 9a92b80..e0f97c3 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,11 +2,13 @@ package database import ( "embed" + "encoding/json" "errors" "fmt" "path/filepath" "ttime/internal/types" + "github.com/gofiber/fiber/v2/log" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" ) @@ -41,6 +43,7 @@ type Database interface { SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) IsProjectManager(username string, projectname string) (bool, error) + ReportStatistics(username string, projectName string) (*types.Statistics, 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 RemoveProject(projectname string) error @@ -94,6 +97,17 @@ const removeUserFromProjectQuery = `DELETE FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)` +const reportStatistics = `SELECT SUM(development_time) AS total_development_time, + SUM(meeting_time) AS total_meeting_time, + SUM(admin_time) AS total_admin_time, + SUM(own_work_time) AS total_own_work_time, + SUM(study_time) AS total_study_time, + SUM(testing_time) AS total_testing_time + FROM weekly_reports + WHERE user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?) + GROUP BY user_id, project_id` + // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database @@ -111,6 +125,24 @@ func DbConnect(dbpath string) Database { return &Db{db} } +func (d *Db) ReportStatistics(username string, projectName string) (*types.Statistics, error) { + var result types.Statistics + + err := d.Get(&result, reportStatistics, username, projectName) + if err != nil { + return nil, err + } + + serialized, err := json.Marshal(result) + if err != nil { + return nil, err + } + + log.Info(string(serialized)) + + return &result, nil +} + func (d *Db) CheckUser(username string, password string) bool { var dbPassword string err := d.Get(&dbPassword, "SELECT password FROM users WHERE username = ?", username) diff --git a/backend/internal/handlers/reports/Statistics.go b/backend/internal/handlers/reports/Statistics.go new file mode 100644 index 0000000..8afa0f0 --- /dev/null +++ b/backend/internal/handlers/reports/Statistics.go @@ -0,0 +1,50 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func GetStatistics(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) + + // Extract project name from query parameters + projectName := c.Query("projectName") + + log.Info(username, " trying to get statistics for project: ", projectName) + + if projectName == "" { + log.Info("Missing project name") + return c.Status(400).SendString("Missing project name") + } + + // If the user is not a project manager, they can't view statistics + pm, err := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !pm { + log.Info("Unauthorized access") + return c.Status(403).SendString("Unauthorized access") + } + + // Retrieve statistics for the project from the database + statistics, err := db.GetDb(c).ReportStatistics(username, projectName) + if err != nil { + log.Error("Error getting statistics for project:", projectName, ":", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning statistics") + // Return the retrieved statistics + return c.JSON(statistics) + +} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 234781b..5550b3f 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -66,6 +66,15 @@ type WeeklyReport struct { SignedBy *int `json:"signedBy" db:"signed_by"` } +type Statistics struct { + TotalDevelopmentTime int `json:"totalDevelopmentTime" db:"total_development_time"` + TotalMeetingTime int `json:"totalMeetingTime" db:"total_meeting_time"` + TotalAdminTime int `json:"totalAdminTime" db:"total_admin_time"` + TotalOwnWorkTime int `json:"totalOwnWorkTime" db:"total_own_work_time"` + TotalStudyTime int `json:"totalStudyTime" db:"total_study_time"` + TotalTestingTime int `json:"totalTestingTime" db:"total_testing_time"` +} + type UpdateWeeklyReport struct { // The name of the project, as it appears in the database ProjectName string `json:"projectName"` diff --git a/backend/main.go b/backend/main.go index dbf5151..accae7a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -126,12 +126,12 @@ func main() { api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) - // All report related routes // reportGroup := api.Group("/report") // Not currently in use api.Get("/getWeeklyReport", reports.GetWeeklyReport) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports) + api.Get("/getStatistics", reports.GetStatistics) api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Put("/signReport/:reportId", reports.SignReport) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index de5f19d..eb9f3f0 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -11,6 +11,7 @@ import { NewProject, WeeklyReport, StrNameChange, + Statistics, } from "../Types/goTypes"; /** @@ -258,6 +259,17 @@ interface API { reportId: number, token: string, ): Promise>; + + /** + * 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>; } /** An instance of the API */ @@ -962,4 +974,30 @@ export const api: API = { return { success: false, message: "Failed to delete report" }; } }, + async getStatistics( + token: string, + projectName: string, + ): Promise> { + 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" }; + } + }, }; diff --git a/frontend/src/Types/goTypes.ts b/frontend/src/Types/goTypes.ts index 7a15741..27e7370 100644 --- a/frontend/src/Types/goTypes.ts +++ b/frontend/src/Types/goTypes.ts @@ -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 diff --git a/testing/helpers.py b/testing/helpers.py index 7269260..8d6b148 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -36,6 +36,7 @@ removeProjectPath = base_url + "/api/removeProject" promoteToPmPath = base_url + "/api/promoteToPm" unsignReportPath = base_url + "/api/unsignReport" deleteReportPath = base_url + "/api/deleteReport" +getStatisticsPath = base_url + "/api/getStatistics" debug_output = False @@ -162,3 +163,11 @@ def deleteReport(report_id: int): return requests.delete( deleteReportPath + "/" + str(report_id), ) + +def getStatistics(token: string, projectName: string): + response = requests.get( + getStatisticsPath, + headers = {"Authorization": "Bearer " + token}, + params={"projectName": projectName} + ) + return response.json() \ No newline at end of file diff --git a/testing/testing.py b/testing/testing.py index a68124a..daad215 100644 --- a/testing/testing.py +++ b/testing/testing.py @@ -625,6 +625,46 @@ def test_delete_report(): gprint("test_delete_report successful") +def test_get_statistics(): + # Create admin + admin_username = randomString() + admin_password = randomString() + + project_name = "project" + randomString() + + token = register_and_login(admin_username, admin_password) + + response = create_project(token, project_name) + assert response.status_code == 200, "Create project failed" + + response = submitReport(token, { + "projectName": project_name, + "week": 1, + "developmentTime": 10, + "meetingTime": 5, + "adminTime": 5, + "ownWorkTime": 10, + "studyTime": 10, + "testingTime": 10, + }) + + response = submitReport(token, { + "projectName": project_name, + "week": 2, + "developmentTime": 10, + "meetingTime": 5, + "adminTime": 5, + "ownWorkTime": 10, + "studyTime": 10, + "testingTime": 10, + }) + + assert response.status_code == 200, "Submit report failed" + + stats = getStatistics(token, project_name) + + assert stats["totalDevelopmentTime"] == 20, "Total development time is not correct" + gprint("test_get_statistics successful") if __name__ == "__main__": @@ -650,3 +690,4 @@ if __name__ == "__main__": test_change_user_name() test_update_weekly_report() test_get_other_users_report_as_pm() + test_get_statistics()