From 24d208e3213507652296dd78b645dbea068b9acf Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sat, 7 Sep 2024 21:30:32 +0200 Subject: [PATCH 01/29] Refactor Attendance struct field names for consistency --- occupi-backend/pkg/analytics/analytics.go | 389 +++++++++++++--------- 1 file changed, 231 insertions(+), 158 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 87928460..a2c9d8ea 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -41,179 +41,252 @@ func MinTime(t1, t2 time.Time) time.Time { return t2 } -// GroupOfficeHoursByDay function with total hours calculation -func GroupOfficeHoursByDay(officeHours []models.OfficeHours) []bson.M { - grouped := make(map[string]float64) - - for _, oh := range officeHours { - // Extract date without time as the key - dateKey := oh.Entered.Format("2006-01-02") +func createMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson.D { + // Create a match filter + matchFilter := bson.D{} - // Calculate the duration in hours for this office hour - duration := oh.Exited.Sub(oh.Entered).Hours() - - // Sum the duration to the corresponding date's total - grouped[dateKey] += duration + // Conditionally add the email filter if email is not empty + if email != "" { + matchFilter = append(matchFilter, bson.E{Key: "email", Value: bson.D{{Key: "$eq", Value: email}}}) } - // Convert the map to a slice of bson.M - // prealloc - result := make([]bson.M, 0, len(grouped)+1) - var overallTotal float64 - for date, totalHours := range grouped { - dayData := bson.M{ - "date": date, - "totalHours": totalHours, - } - result = append(result, dayData) - overallTotal += totalHours - } - result = append(result, bson.M{"overallTotal": overallTotal}) - - return result -} - -// AverageOfficeHoursByWeekday function -func AverageOfficeHoursByWeekday(officeHours []models.OfficeHours) []bson.M { - // Initialize the maps for storing total hours and count of entries for each weekday - weekdayHours := make(map[time.Weekday]float64) - weekdayCount := make(map[time.Weekday]int) - - // Initialize Monday to Friday in the map with zero values - for _, weekday := range []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday} { - weekdayHours[weekday] = 0 - weekdayCount[weekday] = 0 + // Conditionally add the time range filter if provided + timeRangeFilter := bson.D{} + if filter.Filter["timeFrom"] != "" { + timeRangeFilter = append(timeRangeFilter, bson.E{Key: "$gte", Value: filter.Filter["timeFrom"]}) } - - // Iterate over each office hour entry - for _, oh := range officeHours { - // Get the weekday (Monday = 1, ..., Friday = 5) - weekday := oh.Entered.Weekday() - - // Skip Saturday and Sunday - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - - // Calculate the duration in hours for this office hour - duration := oh.Exited.Sub(oh.Entered).Hours() - - // Accumulate the duration and increment the count for the weekday - weekdayHours[weekday] += duration - weekdayCount[weekday]++ - } - - // Prepare the result as a slice of bson.M - // prealloc - result := make([]bson.M, 0, 6) - overallTotal := 0.0 - overallWeekdayCount := 0 - - for _, weekday := range []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday} { - var averageHours float64 - - if weekdayCount[weekday] > 0 { - averageHours = weekdayHours[weekday] / float64(weekdayCount[weekday]) - } else { - averageHours = 0 - } - - dayData := bson.M{ - "weekday": weekday.String(), - "averageHours": averageHours, - } - result = append(result, dayData) - - overallTotal += weekdayHours[weekday] - overallWeekdayCount += weekdayCount[weekday] + if filter.Filter["timeTo"] != "" { + timeRangeFilter = append(timeRangeFilter, bson.E{Key: "$lte", Value: filter.Filter["timeTo"]}) } - // Calculate the overall average if there are any entries, otherwise set to 0 - if overallWeekdayCount == 0 { - result = append(result, bson.M{"overallAverage": 0, "overallTotal": 0, "overallWeekdayCount": 0}) - } else { - result = append(result, bson.M{"overallAverage": overallTotal / float64(overallWeekdayCount), "overallTotal": overallTotal, "overallWeekdayCount": overallWeekdayCount}) + // If there are time range filters, append them to the match filter + if len(timeRangeFilter) > 0 { + matchFilter = append(matchFilter, bson.E{Key: "entered", Value: timeRangeFilter}) } - return result + return matchFilter } -// RatioInOutOfficeByWeekday function -func RatioInOutOfficeByWeekday(officeHours []models.OfficeHours) []bson.M { - weekdayInHours := make(map[time.Weekday]float64) - totalWeekdayOfficeHours := make(map[time.Weekday]float64) - totalOfficeHours := 10.0 // 7 AM to 5 PM is 10 hours - - // Initialize Monday to Friday in the map with zero values - for _, weekday := range []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday} { - weekdayInHours[weekday] = 0 - totalWeekdayOfficeHours[weekday] = totalOfficeHours - } - - // Iterate over each office hour entry - for _, oh := range officeHours { - // Get the weekday (Monday = 1, ..., Friday = 5) - weekday := oh.Entered.Weekday() - - // Skip Saturday and Sunday - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - - // Define the office hours for the day - officeStart := time.Date(oh.Entered.Year(), oh.Entered.Month(), oh.Entered.Day(), 7, 0, 0, 0, oh.Entered.Location()) - officeEnd := time.Date(oh.Entered.Year(), oh.Entered.Month(), oh.Entered.Day(), 17, 0, 0, 0, oh.Entered.Location()) - - // Calculate the overlap between actual office hours and standard office hours - actualStart := MaxTime(oh.Entered, officeStart) - actualEnd := MinTime(oh.Exited, officeEnd) - - // Calculate the duration in hours for the overlap (in-office time) - if actualStart.Before(actualEnd) { - inOfficeDuration := actualEnd.Sub(actualStart).Hours() - weekdayInHours[weekday] += inOfficeDuration - totalWeekdayOfficeHours[weekday] += totalOfficeHours - } +// GroupOfficeHoursByDay function with total hours calculation +func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.D { + return bson.D{ + // Stage 1: Match filter conditions (email and time range) + {Key: "$match", Value: createMatchFilter(email, filter)}, + // Stage 2: Apply skip for pagination + {Key: "$skip", Value: filter.Skip}, + // Stage 3: Apply limit for pagination + {Key: "$limit", Value: filter.Limit}, + // Stage 4: Project the date and calculate the duration + {Key: "$project", Value: bson.D{ + {Key: "email", Value: 1}, + {Key: "date", Value: bson.D{ + {Key: "$dateToString", Value: bson.D{ + {Key: "format", Value: "%Y-%m-%d"}, + {Key: "date", Value: "$entered"}, + }}, + }}, + {Key: "duration", Value: bson.D{ + {Key: "$divide", Value: bson.A{ + bson.D{{Key: "$subtract", Value: bson.A{"$exited", "$entered"}}}, + 1000 * 60 * 60, // Convert milliseconds to hours + }}, + }}, + }}, + // Stage 5: Group by the date and sum the durations + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$date"}, + {Key: "totalHours", Value: bson.D{ + {Key: "$sum", Value: "$duration"}, + }}, + }}, + // Stage 6: Group to calculate the overall total and prepare the days array + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "days", Value: bson.D{ + {Key: "$push", Value: bson.D{ + {Key: "date", Value: "$_id"}, + {Key: "totalHours", Value: "$totalHours"}, + }}, + }}, + {Key: "overallTotal", Value: bson.D{ + {Key: "$sum", Value: "$totalHours"}, + }}, + }}, + // Stage 7: Unwind the days array for individual results + {Key: "$unwind", Value: "$days"}, + // Stage 8: Project the final result format + {Key: "$project", Value: bson.D{ + {Key: "date", Value: "$days.date"}, + {Key: "totalHours", Value: "$days.totalHours"}, + {Key: "overallTotal", Value: "$overallTotal"}, + }}, } +} - // Prepare the result as a slice of bson.M - // prealloc - result := make([]bson.M, 0, 6) - overallOutHours := 0.0 - overallInHours := 0.0 - - for _, weekday := range []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday} { - inHours := weekdayInHours[weekday] - outHours := totalWeekdayOfficeHours[weekday] - inHours - - // Calculate ratio, ensuring no division by zero - var ratio float64 - if outHours > 0 { - ratio = inHours / outHours - } else { - ratio = 0 - } - - dayData := bson.M{ - "weekday": weekday.String(), - "inOfficeHours": inHours, - "outOfficeHours": outHours, - "ratio": ratio, - } - result = append(result, dayData) - - overallInHours += inHours - overallOutHours += outHours +func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.D { + // Create the match filter using the reusable function + matchFilter := createMatchFilter(email, filter) + + return bson.D{ + // Stage 1: Match filter conditions (email and time range) + {Key: "$match", Value: matchFilter}, + // Stage 2: Apply skip for pagination + {Key: "$skip", Value: filter.Skip}, + // Stage 3: Apply limit for pagination + {Key: "$limit", Value: filter.Limit}, + // Stage 4: Project the weekday and duration + {Key: "$project", Value: bson.D{ + {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, + {Key: "duration", Value: bson.D{ + {Key: "$divide", Value: bson.A{ + bson.D{{Key: "$subtract", Value: bson.A{"$exited", "$entered"}}}, + 1000 * 60 * 60, + }}, + }}, + }}, + // Stage 5: Group by the weekday and calculate the total hours and count + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$weekday"}, + {Key: "totalHours", Value: bson.D{{Key: "$sum", Value: "$duration"}}}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}, + // Stage 6: Project the weekday name and average hours + {Key: "$project", Value: bson.D{ + {Key: "weekday", Value: bson.D{ + {Key: "$switch", Value: bson.D{ + {Key: "branches", Value: bson.A{ + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 1}}}}, {Key: "then", Value: "Sunday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 2}}}}, {Key: "then", Value: "Monday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 3}}}}, {Key: "then", Value: "Tuesday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 4}}}}, {Key: "then", Value: "Wednesday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 5}}}}, {Key: "then", Value: "Thursday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 6}}}}, {Key: "then", Value: "Friday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 7}}}}, {Key: "then", Value: "Saturday"}}, + }}, + {Key: "default", Value: "Unknown"}, + }}, + }}, + {Key: "averageHours", Value: bson.D{ + {Key: "$cond", Value: bson.D{ + {Key: "if", Value: bson.D{{Key: "$gt", Value: bson.A{"$count", 0}}}}, + {Key: "then", Value: bson.D{{Key: "$divide", Value: bson.A{"$totalHours", "$count"}}}}, + {Key: "else", Value: 0}, + }}, + }}, + {Key: "totalHours", Value: "$totalHours"}, + {Key: "count", Value: "$count"}, + }}, + // Stage 7: Group all results together to calculate the overall totals + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "days", Value: bson.D{{Key: "$push", Value: bson.D{ + {Key: "weekday", Value: "$weekday"}, + {Key: "averageHours", Value: "$averageHours"}, + }}}}, + {Key: "overallTotal", Value: bson.D{{Key: "$sum", Value: "$totalHours"}}}, + {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: "$count"}}}, + }}, + // Stage 8: Project final structure with overall average + {Key: "$project", Value: bson.D{ + {Key: "days", Value: 1}, + {Key: "overallAverage", Value: bson.D{ + {Key: "$cond", Value: bson.D{ + {Key: "if", Value: bson.D{{Key: "$gt", Value: bson.A{"$overallWeekdayCount", 0}}}}, + {Key: "then", Value: bson.D{{Key: "$divide", Value: bson.A{"$overallTotal", "$overallWeekdayCount"}}}}, + {Key: "else", Value: 0}, + }}, + }}, + {Key: "overallTotal", Value: 1}, + {Key: "overallWeekdayCount", Value: 1}, + }}, } +} - // Calculate the overall ratio if there are any entries, otherwise set to 0 - if overallOutHours == 0 { - result = append(result, bson.M{"overallRatio": 0, "overallInHours": 0, "overallOutHours": 0}) - } else { - result = append(result, bson.M{"overallRatio": overallInHours / overallOutHours, "overallInHours": overallInHours, "overallOutHours": overallOutHours}) +func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.D { + // Create the match filter using the reusable function + matchFilter := createMatchFilter(email, filter) + + return bson.D{ + // Stage 1: Match filter conditions (email and time range) + {Key: "$match", Value: matchFilter}, + // Stage 2: Apply skip for pagination + {Key: "$skip", Value: filter.Skip}, + // Stage 3: Apply limit for pagination + {Key: "$limit", Value: filter.Limit}, + // Stage 4: Project the weekday, entered and exited times + {Key: "$addFields", Value: bson.D{ + {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, + {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, + {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, + }}, + // Stage 5: Project the weekday, email, enteredHour, exitedHour and hoursInOffice + {Key: "$project", Value: bson.D{ + {Key: "weekday", Value: 1}, + {Key: "email", Value: 1}, + {Key: "enteredHour", Value: bson.D{ + {Key: "$cond", Value: bson.A{ + bson.D{{Key: "$lt", Value: bson.A{"$enteredHour", 7}}}, + 7, + "$enteredHour", + }}, + }}, + {Key: "exitedHour", Value: bson.D{ + {Key: "$cond", Value: bson.A{ + bson.D{{Key: "$gt", Value: bson.A{"$exitedHour", 17}}}, + 17, + "$exitedHour", + }}, + }}, + {Key: "hoursInOffice", Value: bson.D{ + {Key: "$subtract", Value: bson.A{"$exitedHour", "$enteredHour"}}, + }}, + }}, + // Stage 6: Group by the weekday and calculate the total hours in office and count + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$weekday"}, + {Key: "totalHoursInOffice", Value: bson.D{{Key: "$sum", Value: "$hoursInOffice"}}}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}, + {Key: "$addFields", Value: bson.D{ + {Key: "ratio", Value: bson.D{ + {Key: "$divide", Value: bson.A{"$totalHoursInOffice", "$count"}}, + }}, + {Key: "weekdayName", Value: bson.D{ + {Key: "$switch", Value: bson.D{ + {Key: "branches", Value: bson.A{ + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 1}}}}, {Key: "then", Value: "Sunday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 2}}}}, {Key: "then", Value: "Monday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 3}}}}, {Key: "then", Value: "Tuesday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 4}}}}, {Key: "then", Value: "Wednesday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 5}}}}, {Key: "then", Value: "Thursday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 6}}}}, {Key: "then", Value: "Friday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 7}}}}, {Key: "then", Value: "Saturday"}}, + }}, + {Key: "default", Value: "Unknown"}, + }}, + }}, + }}, + // Stage 7: Sort by weekday + {Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}, + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "days", Value: bson.D{ + {Key: "$push", Value: bson.D{ + {Key: "weekday", Value: "$weekdayName"}, + {Key: "ratio", Value: "$ratio"}, + }}, + }}, + {Key: "overallRatio", Value: bson.D{{Key: "$avg", Value: "$ratio"}}}, + {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}, + // Stage 8: Project the final result format + {Key: "$project", Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "days", Value: 1}, + {Key: "ratio", Value: "$overallRatio"}, + {Key: "overallWeekdayCount", Value: 1}, + }}, } - - return result } // BusiestHoursByWeekday function to return the 3 busiest hours per weekday From f16ea22b40a218d09031fb659b2fa79326d7485d Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sat, 7 Sep 2024 21:30:40 +0200 Subject: [PATCH 02/29] Refactor GetAnalyticsOnHours function to use aggregation pipeline for improved performance --- occupi-backend/pkg/database/database.go | 63 ++++++++++--------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index d77ea4d7..005a668e 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1721,36 +1721,44 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email return nil, 0, errors.New("database is nil") } - // Prepare the filter based on time range and email if email is not == "" - mongoFilter := bson.M{} - if email != "" { - mongoFilter["email"] = email - } - if filter.Filter["timeFrom"] != "" { - mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} - } - if filter.Filter["timeTo"] != "" { - mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} + // Prepare the aggregate + var pipeline primitive.D + switch calculate { + case "hoursbyday": + pipeline = analytics.GroupOfficeHoursByDay(email, filter) + case "hoursbyweekday": + pipeline = analytics.AverageOfficeHoursByWeekday(email, filter) + case "ratio": + pipeline = analytics.RatioInOutOfficeByWeekday(email, filter) + default: + return nil, 0, errors.New("invalid calculate value") } collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("OfficeHoursArchive") - findOptions := options.Find() - findOptions.SetLimit(filter.Limit) - findOptions.SetSkip(filter.Skip) - - cursor, err := collection.Find(ctx, mongoFilter, findOptions) + cursor, err := collection.Aggregate(ctx, pipeline) if err != nil { logrus.Error(err) return nil, 0, err } - var hours []models.OfficeHours - if err = cursor.All(ctx, &hours); err != nil { + var results []bson.M + if err = cursor.All(ctx, &results); err != nil { logrus.WithError(err).Error("Failed to get hours") return nil, 0, err } + mongoFilter := bson.M{} + if email != "" { + mongoFilter["email"] = email + } + if filter.Filter["timeFrom"] != "" { + mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} + } + if filter.Filter["timeTo"] != "" { + mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} + } + // count documents totalResults, err := collection.CountDocuments(ctx, mongoFilter) if err != nil { @@ -1758,24 +1766,5 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email return nil, 0, err } - switch calculate { - case "hoursbyday": - return analytics.GroupOfficeHoursByDay(hours), totalResults, nil - case "hoursbyweekday": - return analytics.AverageOfficeHoursByWeekday(hours), totalResults, nil - case "ratio": - return analytics.RatioInOutOfficeByWeekday(hours), totalResults, nil - case "peakhours": - return analytics.BusiestHoursByWeekday(hours), totalResults, nil - case "most": - return analytics.MostInOfficeWorker(hours), 0, nil - case "least": - return analytics.LeastInOfficeWorker(hours), 0, nil - case "arrivaldeparture": - return analytics.AverageArrivalAndDepartureTimesByWeekday(hours), 0, nil - case "inofficehours": - return analytics.CalculateInOfficeRate(hours), 0, nil - default: - return nil, 0, errors.New("invalid calculation") - } + return results, totalResults, nil } From 3e404e2ea01fe5914a2164bc38f42175c5ef4736 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sat, 7 Sep 2024 22:55:52 +0200 Subject: [PATCH 03/29] Refactor GetAnalyticsOnHours function to use aggregation pipeline for improved performance Refactor Attendance struct field names for consistency Refactor Attendance struct field names for consistency Refactor ToggleOnsite function to use switch statement for clarity Preallocate result slices in analytics functions --- occupi-backend/pkg/analytics/analytics.go | 210 +++++++++++----------- occupi-backend/pkg/database/database.go | 2 + 2 files changed, 108 insertions(+), 104 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index a2c9d8ea..d819dc56 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -2,29 +2,12 @@ package analytics import ( "fmt" - "sort" "time" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "go.mongodb.org/mongo-driver/bson" ) -// Helper function to get the minimum of two integers -func min(a, b int) int { - if a < b { - return a - } - return b -} - -// Helper function to get the maximum of two integers -func max(a, b int) int { - if a > b { - return a - } - return b -} - // Helper function to get the maximum of two time values func MaxTime(t1, t2 time.Time) time.Time { if t1.After(t2) { @@ -290,95 +273,114 @@ func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStru } // BusiestHoursByWeekday function to return the 3 busiest hours per weekday -func BusiestHoursByWeekday(officeHours []models.OfficeHours) []bson.M { - // Map to store the total overlaps for each weekday and hour - hourlyActivity := make(map[time.Weekday]map[int]int) - overallActivity := make(map[int]int) - - // Initialize the map - for _, weekday := range []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday} { - hourlyActivity[weekday] = make(map[int]int) - } - - // Iterate over office hours entries - for _, oh := range officeHours { - // Get the weekday (Monday = 1, ..., Friday = 5) - weekday := oh.Entered.Weekday() - - // Skip Saturday and Sunday - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - - // Calculate the hours between Entered and Exited, ensuring they are within 07:00 and 17:00 - for hour := max(7, oh.Entered.Hour()); hour < min(17, oh.Exited.Hour()); hour++ { - hourlyActivity[weekday][hour]++ - overallActivity[hour]++ - } - } - - // Determine the top 3 busiest hours for each weekday - // prealloc - result := make([]bson.M, 0, 6) - for weekday, hours := range hourlyActivity { - // Create a slice to store the hours and their activity counts - type hourActivity struct { - Hour int - Activity int - } - var activities []hourActivity - - // Collect the hourly activity for the current weekday - for hour, activity := range hours { - activities = append(activities, hourActivity{Hour: hour, Activity: activity}) - } - - // Sort the activities by activity count in descending order - sort.Slice(activities, func(i, j int) bool { - return activities[i].Activity > activities[j].Activity - }) - - // Get the top 3 busiest hours - busiestHours := make([]int, 0, 3) - for i := 0; i < min(3, len(activities)); i++ { - busiestHours = append(busiestHours, activities[i].Hour) - } - - // Store the result for the current weekday - peakData := bson.M{ - "weekday": weekday.String(), - "busiestHours": busiestHours, - } - result = append(result, peakData) - } - - // Determine the overall top 3 busiest hours across the entire week - type overallHourActivity struct { - Hour int - Activity int - } - // prealloc - overallActivities := make([]overallHourActivity, 0, len(overallActivity)) - - for hour, activity := range overallActivity { - overallActivities = append(overallActivities, overallHourActivity{Hour: hour, Activity: activity}) - } - - // Sort the overall activities by activity count in descending order - sort.Slice(overallActivities, func(i, j int) bool { - return overallActivities[i].Activity > overallActivities[j].Activity - }) +func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.D { + // Create the match filter using the reusable function + matchFilter := createMatchFilter(email, filter) - // Get the top 3 busiest hours overall - overallBusiestHours := make([]int, 0, 3) - for i := 0; i < min(3, len(overallActivities)); i++ { - overallBusiestHours = append(overallBusiestHours, overallActivities[i].Hour) + return bson.D{ + // Stage 1: Match filter conditions (email and time range) + {Key: "$match", Value: matchFilter}, + // Stage 2: Apply skip for pagination + {Key: "$skip", Value: filter.Skip}, + // Stage 3: Apply limit for pagination + {Key: "$limit", Value: filter.Limit}, + // Stage 4: Project the weekday, entered and exited times + {Key: "$addFields", Value: bson.D{ + {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, + {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, + {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, + }}, + // Stage 5: Project the weekday, enteredHour and exitedHour + {Key: "$addFields", Value: bson.D{ + {Key: "hoursInOffice", Value: bson.D{ + {Key: "$map", Value: bson.D{ + {Key: "input", Value: bson.D{ + {Key: "$range", Value: bson.A{ + "$enteredHour", + bson.D{{Key: "$add", Value: bson.A{"$exitedHour", 1}}}, + }}, + }}, + {Key: "as", Value: "hour"}, + {Key: "in", Value: "$$hour"}, + }}, + }}, + }}, + // Stage 6: Unwind the hoursInOffice array + {Key: "$unwind", Value: "$hoursInOffice"}, + // Stage 7: Group by the weekday and hour to count the occurrences + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: bson.D{ + {Key: "weekday", Value: "$weekday"}, + {Key: "hour", Value: "$hoursInOffice"}, + }}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}, + // Stage 8: Group by the weekday to prepare the top 3 busiest hours + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$_id.weekday"}, + {Key: "hours", Value: bson.D{ + {Key: "$push", Value: bson.D{ + {Key: "hour", Value: "$_id.hour"}, + {Key: "count", Value: "$count"}, + }}, + }}, + }}, + // Stage 9: Add the weekday name and sort the hours by count + {Key: "$addFields", Value: bson.D{ + {Key: "topHours", Value: bson.D{ + {Key: "$slice", Value: bson.A{ + bson.D{ + {Key: "$sortArray", Value: bson.D{ + {Key: "input", Value: "$hours"}, + {Key: "sortBy", Value: bson.D{{Key: "count", Value: -1}}}, + }}, + }, + 3, + }}, + }}, + }}, + // Stage 10: Project the final result format + {Key: "$project", Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "weekday", Value: bson.D{ + {Key: "$switch", Value: bson.D{ + {Key: "branches", Value: bson.A{ + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 1}}}}, {Key: "then", Value: "Sunday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 2}}}}, {Key: "then", Value: "Monday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 3}}}}, {Key: "then", Value: "Tuesday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 4}}}}, {Key: "then", Value: "Wednesday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 5}}}}, {Key: "then", Value: "Thursday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 6}}}}, {Key: "then", Value: "Friday"}}, + bson.D{{Key: "case", Value: bson.D{{Key: "$eq", Value: bson.A{"$_id", 7}}}}, {Key: "then", Value: "Saturday"}}, + }}, + {Key: "default", Value: "Unknown"}, + }}, + }}, + {Key: "hours", Value: bson.D{ + {Key: "$map", Value: bson.D{ + {Key: "input", Value: "$topHours"}, + {Key: "as", Value: "topHour"}, + {Key: "in", Value: "$$topHour.hour"}, + }}, + }}, + }}, + // Stage 11: Sort by weekday + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "days", Value: bson.D{ + {Key: "$push", Value: bson.D{ + {Key: "weekday", Value: "$weekday"}, + {Key: "hours", Value: "$hours"}, + }}, + }}, + {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}, + // Stage 12: Project the final result format + {Key: "$project", Value: bson.D{ + {Key: "days", Value: 1}, + {Key: "overallWeekdayCount", Value: 1}, + }}, } - - // Append the overall busiest hours to the result - result = append(result, bson.M{"overallBusiestHours": overallBusiestHours}) - - return result } // LeastInOfficeWorker function to calculate the least "in office" worker diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 005a668e..c73322d5 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1730,6 +1730,8 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email pipeline = analytics.AverageOfficeHoursByWeekday(email, filter) case "ratio": pipeline = analytics.RatioInOutOfficeByWeekday(email, filter) + case "peakhours": + pipeline = analytics.BusiestHoursByWeekday(email, filter) default: return nil, 0, errors.New("invalid calculate value") } From 02946620ffed47edb91e79fc6c0c77d83ba986e0 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 13:50:31 +0200 Subject: [PATCH 04/29] Refactor GetAnalyticsOnHours function to improve performance using aggregation pipeline --- occupi-backend/pkg/analytics/analytics.go | 1035 ++++++++++++++------- 1 file changed, 709 insertions(+), 326 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index d819dc56..4a56a997 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -1,29 +1,10 @@ package analytics import ( - "fmt" - "time" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "go.mongodb.org/mongo-driver/bson" ) -// Helper function to get the maximum of two time values -func MaxTime(t1, t2 time.Time) time.Time { - if t1.After(t2) { - return t1 - } - return t2 -} - -// Helper function to get the minimum of two time values -func MinTime(t1, t2 time.Time) time.Time { - if t1.Before(t2) { - return t1 - } - return t2 -} - func createMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson.D { // Create a match filter matchFilter := bson.D{} @@ -51,16 +32,16 @@ func createMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson } // GroupOfficeHoursByDay function with total hours calculation -func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.D { - return bson.D{ +func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.A { + return bson.A{ // Stage 1: Match filter conditions (email and time range) - {Key: "$match", Value: createMatchFilter(email, filter)}, + bson.D{{Key: "$match", Value: createMatchFilter(email, filter)}}, // Stage 2: Apply skip for pagination - {Key: "$skip", Value: filter.Skip}, + bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination - {Key: "$limit", Value: filter.Limit}, + bson.D{{Key: "$limit", Value: filter.Limit}}, // Stage 4: Project the date and calculate the duration - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "email", Value: 1}, {Key: "date", Value: bson.D{ {Key: "$dateToString", Value: bson.D{ @@ -74,16 +55,16 @@ func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) 1000 * 60 * 60, // Convert milliseconds to hours }}, }}, - }}, + }}}, // Stage 5: Group by the date and sum the durations - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$date"}, {Key: "totalHours", Value: bson.D{ {Key: "$sum", Value: "$duration"}, }}, - }}, + }}}, // Stage 6: Group to calculate the overall total and prepare the days array - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: nil}, {Key: "days", Value: bson.D{ {Key: "$push", Value: bson.D{ @@ -94,31 +75,31 @@ func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) {Key: "overallTotal", Value: bson.D{ {Key: "$sum", Value: "$totalHours"}, }}, - }}, + }}}, // Stage 7: Unwind the days array for individual results - {Key: "$unwind", Value: "$days"}, + bson.D{{Key: "$unwind", Value: "$days"}}, // Stage 8: Project the final result format - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "date", Value: "$days.date"}, {Key: "totalHours", Value: "$days.totalHours"}, {Key: "overallTotal", Value: "$overallTotal"}, - }}, + }}}, } } -func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.D { +func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := createMatchFilter(email, filter) - return bson.D{ + return bson.A{ // Stage 1: Match filter conditions (email and time range) - {Key: "$match", Value: matchFilter}, + bson.D{{Key: "$match", Value: matchFilter}}, // Stage 2: Apply skip for pagination - {Key: "$skip", Value: filter.Skip}, + bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination - {Key: "$limit", Value: filter.Limit}, + bson.D{{Key: "$limit", Value: filter.Limit}}, // Stage 4: Project the weekday and duration - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, {Key: "duration", Value: bson.D{ {Key: "$divide", Value: bson.A{ @@ -126,15 +107,15 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt 1000 * 60 * 60, }}, }}, - }}, + }}}, // Stage 5: Group by the weekday and calculate the total hours and count - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$weekday"}, {Key: "totalHours", Value: bson.D{{Key: "$sum", Value: "$duration"}}}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}, + }}}, // Stage 6: Project the weekday name and average hours - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "weekday", Value: bson.D{ {Key: "$switch", Value: bson.D{ {Key: "branches", Value: bson.A{ @@ -158,9 +139,9 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt }}, {Key: "totalHours", Value: "$totalHours"}, {Key: "count", Value: "$count"}, - }}, + }}}, // Stage 7: Group all results together to calculate the overall totals - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: nil}, {Key: "days", Value: bson.D{{Key: "$push", Value: bson.D{ {Key: "weekday", Value: "$weekday"}, @@ -168,9 +149,9 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt }}}}, {Key: "overallTotal", Value: bson.D{{Key: "$sum", Value: "$totalHours"}}}, {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: "$count"}}}, - }}, + }}}, // Stage 8: Project final structure with overall average - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "days", Value: 1}, {Key: "overallAverage", Value: bson.D{ {Key: "$cond", Value: bson.D{ @@ -181,29 +162,29 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt }}, {Key: "overallTotal", Value: 1}, {Key: "overallWeekdayCount", Value: 1}, - }}, + }}}, } } -func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.D { +func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := createMatchFilter(email, filter) - return bson.D{ + return bson.A{ // Stage 1: Match filter conditions (email and time range) - {Key: "$match", Value: matchFilter}, + bson.D{{Key: "$match", Value: matchFilter}}, // Stage 2: Apply skip for pagination - {Key: "$skip", Value: filter.Skip}, + bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination - {Key: "$limit", Value: filter.Limit}, + bson.D{{Key: "$limit", Value: filter.Limit}}, // Stage 4: Project the weekday, entered and exited times - {Key: "$addFields", Value: bson.D{ + bson.D{{Key: "$addFields", Value: bson.D{ {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, - }}, + }}}, // Stage 5: Project the weekday, email, enteredHour, exitedHour and hoursInOffice - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "weekday", Value: 1}, {Key: "email", Value: 1}, {Key: "enteredHour", Value: bson.D{ @@ -223,14 +204,14 @@ func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStru {Key: "hoursInOffice", Value: bson.D{ {Key: "$subtract", Value: bson.A{"$exitedHour", "$enteredHour"}}, }}, - }}, + }}}, // Stage 6: Group by the weekday and calculate the total hours in office and count - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$weekday"}, {Key: "totalHoursInOffice", Value: bson.D{{Key: "$sum", Value: "$hoursInOffice"}}}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}, - {Key: "$addFields", Value: bson.D{ + }}}, + bson.D{{Key: "$addFields", Value: bson.D{ {Key: "ratio", Value: bson.D{ {Key: "$divide", Value: bson.A{"$totalHoursInOffice", "$count"}}, }}, @@ -248,10 +229,10 @@ func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStru {Key: "default", Value: "Unknown"}, }}, }}, - }}, + }}}, // Stage 7: Sort by weekday - {Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}, - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: nil}, {Key: "days", Value: bson.D{ {Key: "$push", Value: bson.D{ @@ -261,37 +242,37 @@ func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStru }}, {Key: "overallRatio", Value: bson.D{{Key: "$avg", Value: "$ratio"}}}, {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}, + }}}, // Stage 8: Project the final result format - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "_id", Value: 0}, {Key: "days", Value: 1}, {Key: "ratio", Value: "$overallRatio"}, {Key: "overallWeekdayCount", Value: 1}, - }}, + }}}, } } // BusiestHoursByWeekday function to return the 3 busiest hours per weekday -func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.D { +func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function matchFilter := createMatchFilter(email, filter) - return bson.D{ + return bson.A{ // Stage 1: Match filter conditions (email and time range) - {Key: "$match", Value: matchFilter}, + bson.D{{Key: "$match", Value: matchFilter}}, // Stage 2: Apply skip for pagination - {Key: "$skip", Value: filter.Skip}, + bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination - {Key: "$limit", Value: filter.Limit}, + bson.D{{Key: "$limit", Value: filter.Limit}}, // Stage 4: Project the weekday, entered and exited times - {Key: "$addFields", Value: bson.D{ + bson.D{{Key: "$addFields", Value: bson.D{ {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, - }}, + }}}, // Stage 5: Project the weekday, enteredHour and exitedHour - {Key: "$addFields", Value: bson.D{ + bson.D{{Key: "$addFields", Value: bson.D{ {Key: "hoursInOffice", Value: bson.D{ {Key: "$map", Value: bson.D{ {Key: "input", Value: bson.D{ @@ -304,19 +285,19 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) {Key: "in", Value: "$$hour"}, }}, }}, - }}, + }}}, // Stage 6: Unwind the hoursInOffice array - {Key: "$unwind", Value: "$hoursInOffice"}, + bson.D{{Key: "$unwind", Value: "$hoursInOffice"}}, // Stage 7: Group by the weekday and hour to count the occurrences - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: bson.D{ {Key: "weekday", Value: "$weekday"}, {Key: "hour", Value: "$hoursInOffice"}, }}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}, + }}}, // Stage 8: Group by the weekday to prepare the top 3 busiest hours - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$_id.weekday"}, {Key: "hours", Value: bson.D{ {Key: "$push", Value: bson.D{ @@ -324,9 +305,9 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) {Key: "count", Value: "$count"}, }}, }}, - }}, + }}}, // Stage 9: Add the weekday name and sort the hours by count - {Key: "$addFields", Value: bson.D{ + bson.D{{Key: "$addFields", Value: bson.D{ {Key: "topHours", Value: bson.D{ {Key: "$slice", Value: bson.A{ bson.D{ @@ -338,9 +319,9 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) 3, }}, }}, - }}, + }}}, // Stage 10: Project the final result format - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "_id", Value: 0}, {Key: "weekday", Value: bson.D{ {Key: "$switch", Value: bson.D{ @@ -363,9 +344,9 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) {Key: "in", Value: "$$topHour.hour"}, }}, }}, - }}, + }}}, // Stage 11: Sort by weekday - {Key: "$group", Value: bson.D{ + bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: nil}, {Key: "days", Value: bson.D{ {Key: "$push", Value: bson.D{ @@ -374,265 +355,667 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) }}, }}, {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}, + }}}, // Stage 12: Project the final result format - {Key: "$project", Value: bson.D{ + bson.D{{Key: "$project", Value: bson.D{ {Key: "days", Value: 1}, {Key: "overallWeekdayCount", Value: 1}, - }}, - } -} - -// LeastInOfficeWorker function to calculate the least "in office" worker -func LeastInOfficeWorker(officeHours []models.OfficeHours) []bson.M { - // Map to store total hours worked per email - hoursWorked := make(map[string]float64) - // Map to store number of days worked per email - daysWorked := make(map[string]int) - - // Iterate over the office hours entries - for _, oh := range officeHours { - weekday := oh.Entered.Weekday() - - // Skip Saturday and Sunday - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - - // Calculate hours worked in the office - startTime := MaxTime(oh.Entered, time.Date(oh.Entered.Year(), oh.Entered.Month(), oh.Entered.Day(), 7, 0, 0, 0, oh.Entered.Location())) - endTime := MinTime(oh.Exited, time.Date(oh.Exited.Year(), oh.Exited.Month(), oh.Exited.Day(), 17, 0, 0, 0, oh.Exited.Location())) - - hours := endTime.Sub(startTime).Hours() - - // Accumulate hours worked for the email - hoursWorked[oh.Email] += hours - - // Track the number of days worked (only count unique weekdays) - if _, exists := daysWorked[oh.Email]; !exists { - daysWorked[oh.Email] = 0 - } - daysWorked[oh.Email]++ + }}}, } - - // Determine the least "in office" worker - leastEmail := "" - leastHours := float64(1<<63 - 1) // Set to a large value initially - - for email, hours := range hoursWorked { - if hours < leastHours { - leastEmail = email - leastHours = hours - } - } - - // Calculate average hours for the least "in office" worker - totalDays := float64(daysWorked[leastEmail]) - var averageHours float64 - if totalDays > 0 { - averageHours = leastHours / totalDays - } - - var result []bson.M - result = append(result, bson.M{"email": leastEmail, "totalHours": leastHours, "averageHours": averageHours}) - - return result } -// MostInOfficeWorker function to calculate the most "in office" worker -func MostInOfficeWorker(officeHours []models.OfficeHours) []bson.M { - // Map to store total hours worked per email - hoursWorked := make(map[string]float64) - // Map to store number of days worked per email - daysWorked := make(map[string]int) - - // Iterate over the office hours entries - for _, oh := range officeHours { - weekday := oh.Entered.Weekday() - - // Skip Saturday and Sunday - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - - // Calculate hours worked in the office - startTime := MaxTime(oh.Entered, time.Date(oh.Entered.Year(), oh.Entered.Month(), oh.Entered.Day(), 7, 0, 0, 0, oh.Entered.Location())) - endTime := MinTime(oh.Exited, time.Date(oh.Exited.Year(), oh.Exited.Month(), oh.Exited.Day(), 17, 0, 0, 0, oh.Exited.Location())) - - hours := endTime.Sub(startTime).Hours() - - // Accumulate hours worked for the email - hoursWorked[oh.Email] += hours - - // Track the number of days worked (only count unique weekdays) - if _, exists := daysWorked[oh.Email]; !exists { - daysWorked[oh.Email] = 0 - } - daysWorked[oh.Email]++ - } - - // Determine the most "in office" worker - mostEmail := "" - mostHours := float64(0) +// LeastMostInOfficeWorker function to calculate the least or most "in office" worker +func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct, sort bool) bson.A { + // Create the match filter using the reusable function + matchFilter := createMatchFilter(email, filter) - for email, hours := range hoursWorked { - if hours > mostHours { - mostEmail = email - mostHours = hours - } + var sortV int + if sort { + sortV = 1 + } else { + sortV = -1 } - // Calculate average hours for the most "in office" worker - totalDays := float64(daysWorked[mostEmail]) - var averageHours float64 - if totalDays > 0 { - averageHours = mostHours / totalDays + return bson.A{ + // Stage 1: Match filter conditions (email and time range) + bson.D{{Key: "$match", Value: matchFilter}}, + // Stage 2: Project the weekday, entered and exited times + bson.D{{Key: "$addFields", Value: bson.D{ + {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, + {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, + {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, + }}}, + // Stage 3: Project the weekday, enteredHour, exitedHour and hoursWorked + bson.D{{Key: "$addFields", Value: bson.D{ + {Key: "hoursWorked", Value: bson.D{ + {Key: "$subtract", Value: bson.A{ + "$exitedHour", + "$enteredHour", + }}, + }}, + }}}, + // Stage 4: Group by the email and weekday to calculate the total hours and count + bson.D{{Key: "$group", Value: bson.D{ + {Key: "_id", Value: bson.D{ + {Key: "email", Value: "$email"}, + {Key: "weekday", Value: "$weekday"}, + }}, + {Key: "totalHours", Value: bson.D{{Key: "$sum", Value: "$hoursWorked"}}}, + {Key: "weekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, + }}}, + // Stage 5: Group by the email to calculate the overall total hours and average hours + bson.D{{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$_id.email"}, + {Key: "days", Value: bson.D{ + {Key: "$push", Value: bson.D{ + {Key: "weekday", Value: bson.D{ + {Key: "$switch", Value: bson.D{ + {Key: "branches", Value: bson.A{ + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 1}}, + }}, + {Key: "then", Value: "Sunday"}, + }, + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 2}}, + }}, + {Key: "then", Value: "Monday"}, + }, + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 3}}, + }}, + {Key: "then", Value: "Tuesday"}, + }, + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 4}}, + }}, + {Key: "then", Value: "Wednesday"}, + }, + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 5}}, + }}, + {Key: "then", Value: "Thursday"}, + }, + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 6}}, + }}, + {Key: "then", Value: "Friday"}, + }, + bson.D{ + {Key: "case", Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id.weekday", 7}}, + }}, + {Key: "then", Value: "Saturday"}, + }, + }}, + {Key: "default", Value: "Unknown"}, + }}, + }}, + {Key: "totalHour", Value: "$totalHours"}, + {Key: "weekdayCount", Value: "$weekdayCount"}, + }}, + }}, + {Key: "totalHours", Value: bson.D{{Key: "$sum", Value: "$totalHours"}}}, + {Key: "averageHours", Value: bson.D{{Key: "$avg", Value: "$totalHours"}}}, + }}}, + // Stage 6: Sort by total hours and limit to 1 result + bson.D{{Key: "$sort", Value: bson.D{{Key: "totalHours", Value: sortV}}}}, + bson.D{{Key: "$limit", Value: 1}}, + bson.D{{Key: "$project", Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "email", Value: "$_id"}, + {Key: "days", Value: bson.D{ + {Key: "$map", Value: bson.D{ + {Key: "input", Value: "$days"}, + {Key: "as", Value: "day"}, + {Key: "in", Value: bson.D{ + {Key: "weekday", Value: "$$day.weekday"}, + {Key: "avgHour", Value: bson.D{ + {Key: "$divide", Value: bson.A{ + "$$day.totalHour", + "$$day.weekdayCount", + }}, + }}, + {Key: "totalHour", Value: "$$day.totalHour"}, + }}, + }}, + }}, + {Key: "averageHours", Value: "$averageHours"}, + {Key: "overallTotalHours", Value: "$totalHours"}, + {Key: "overallWeekdayCount", Value: bson.D{{Key: "$size", Value: "$days"}}}, + }}}, } - - var result []bson.M - - result = append(result, bson.M{"email": mostEmail, "totalHours": mostHours, "averageHours": averageHours}) - - return result } // AverageArrivalAndDepartureTimesByWeekday function to calculate the average arrival and departure times for each weekday -func AverageArrivalAndDepartureTimesByWeekday(officeHours []models.OfficeHours) []bson.M { - weekdayArrivalTimes := make(map[time.Weekday]time.Duration) - weekdayDepartureTimes := make(map[time.Weekday]time.Duration) - weekdayCount := make(map[time.Weekday]int) - - for _, oh := range officeHours { - weekday := oh.Entered.Weekday() - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - arrivalTime := time.Duration(oh.Entered.Hour())*time.Hour + time.Duration(oh.Entered.Minute())*time.Minute - departureTime := time.Duration(oh.Exited.Hour())*time.Hour + time.Duration(oh.Exited.Minute())*time.Minute - - weekdayArrivalTimes[weekday] += arrivalTime - weekdayDepartureTimes[weekday] += departureTime - weekdayCount[weekday]++ - } - - var result []bson.M - var totalArrivalTime, totalDepartureTime time.Duration - var totalCount int - - for weekday := time.Monday; weekday <= time.Friday; weekday++ { - averageArrivalTime := time.Duration(0) - averageDepartureTime := time.Duration(0) - - if weekdayCount[weekday] > 0 { - averageArrivalTime = weekdayArrivalTimes[weekday] / time.Duration(weekdayCount[weekday]) - averageDepartureTime = weekdayDepartureTimes[weekday] / time.Duration(weekdayCount[weekday]) - } - - arrivalHours := averageArrivalTime / time.Hour - arrivalMinutes := (averageArrivalTime % time.Hour) / time.Minute - - departureHours := averageDepartureTime / time.Hour - departureMinutes := (averageDepartureTime % time.Hour) / time.Minute - - dayData := bson.M{ - "weekday": weekday.String(), - "averageArrivalTime": fmt.Sprintf("%02d:%02d", arrivalHours, arrivalMinutes), - "averageDepartureTime": fmt.Sprintf("%02d:%02d", departureHours, departureMinutes), - } - result = append(result, dayData) - - totalArrivalTime += weekdayArrivalTimes[weekday] - totalDepartureTime += weekdayDepartureTimes[weekday] - totalCount += weekdayCount[weekday] - } - - overallAverageArrivalTime := time.Duration(0) - overallAverageDepartureTime := time.Duration(0) +func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { + // Create the match filter using the reusable function + matchFilter := createMatchFilter(email, filter) - if totalCount > 0 { - overallAverageArrivalTime = totalArrivalTime / time.Duration(totalCount) - overallAverageDepartureTime = totalDepartureTime / time.Duration(totalCount) + return bson.A{ + // Stage 1: Match filter conditions (email and time range) + bson.D{{Key: "$match", Value: matchFilter}}, + // Stage 2: Apply skip for pagination + bson.D{{Key: "$skip", Value: filter.Skip}}, + // Stage 3: Apply limit for pagination + bson.D{{Key: "$limit", Value: filter.Limit}}, + // Stage 4: Project the weekday, entered and exited times + bson.D{{Key: "$addFields", + Value: bson.D{ + {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, + {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, + {Key: "enteredMinute", Value: bson.D{{Key: "$minute", Value: "$entered"}}}, + {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, + {Key: "exitedMinute", Value: bson.D{{Key: "$minute", Value: "$exited"}}}, + }, + }}, + // Stage 5: Group by the weekday and calculate the average arrival and departure times + bson.D{{Key: "$group", + Value: bson.D{ + {Key: "_id", Value: "$weekday"}, + {Key: "avgArrivalHour", Value: bson.D{{Key: "$avg", Value: "$enteredHour"}}}, + {Key: "avgArrivalMinute", Value: bson.D{{Key: "$avg", Value: "$enteredMinute"}}}, + {Key: "avgDepartureHour", Value: bson.D{{Key: "$avg", Value: "$exitedHour"}}}, + {Key: "avgDepartureMinute", Value: bson.D{{Key: "$avg", Value: "$exitedMinute"}}}, + {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, + }, + }}, + // Stage 6: Project the final result format + bson.D{{Key: "$project", + Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "weekday", + Value: bson.D{ + {Key: "$switch", + Value: bson.D{ + {Key: "branches", + Value: bson.A{ + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 1}}, + }, + }, + {Key: "then", Value: "Sunday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 2}}, + }, + }, + {Key: "then", Value: "Monday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 3}}, + }, + }, + {Key: "then", Value: "Tuesday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 4}}, + }, + }, + {Key: "then", Value: "Wednesday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 5}}, + }, + }, + {Key: "then", Value: "Thursday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 6}}, + }, + }, + {Key: "then", Value: "Friday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", Value: bson.A{"$_id", 7}}, + }, + }, + {Key: "then", Value: "Saturday"}, + }, + }, + }, + {Key: "default", Value: "Unknown"}, + }, + }, + }, + }, + {Key: "avgArrival", + Value: bson.D{ + {Key: "$concat", + Value: bson.A{ + bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$avgArrivalHour"}}}}, + ":", + bson.D{ + {Key: "$cond", + Value: bson.D{ + {Key: "if", + Value: bson.D{ + {Key: "$lt", Value: bson.A{bson.D{{Key: "$floor", Value: "$avgArrivalMinute"}}, 10}}, + }, + }, + {Key: "then", + Value: bson.D{ + {Key: "$concat", Value: bson.A{"0", bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$avgArrivalMinute"}}}}}}, + }, + }, + {Key: "else", Value: bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$avgArrivalMinute"}}}}}, + }, + }, + }, + }, + }, + }, + }, + {Key: "avgDeparture", + Value: bson.D{ + {Key: "$concat", + Value: bson.A{ + bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$avgDepartureHour"}}}}, + ":", + bson.D{ + {Key: "$cond", + Value: bson.D{ + {Key: "if", + Value: bson.D{ + {Key: "$lt", Value: bson.A{bson.D{{Key: "$floor", Value: "$avgDepartureMinute"}}, 10}}, + }, + }, + {Key: "then", + Value: bson.D{ + {Key: "$concat", Value: bson.A{"0", bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$avgDepartureMinute"}}}}}}, + }, + }, + {Key: "else", Value: bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$avgDepartureMinute"}}}}}, + }, + }, + }, + }, + }, + }, + }, + {Key: "avgArrivalHour", Value: 1}, + {Key: "avgArrivalMinute", Value: 1}, + {Key: "avgDepartureHour", Value: 1}, + {Key: "avgDepartureMinute", Value: 1}, + }, + }}, + // Stage 7: Sort by weekday + bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, + // Stage 8: Group all results together to calculate the overall totals + bson.D{{Key: "$group", + Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "days", + Value: bson.D{ + {Key: "$push", + Value: bson.D{ + {Key: "weekday", Value: "$weekday"}, + {Key: "avgArrival", Value: "$avgArrival"}, + {Key: "avgDeparture", Value: "$avgDeparture"}, + }, + }, + }, + }, + {Key: "overallAvgArrivalHour", Value: bson.D{{Key: "$avg", Value: "$avgArrivalHour"}}}, + {Key: "overallAvgArrivalMinute", Value: bson.D{{Key: "$avg", Value: "$avgArrivalMinute"}}}, + {Key: "overallAvgDepartureHour", Value: bson.D{{Key: "$avg", Value: "$avgDepartureHour"}}}, + {Key: "overallAvgDepartureMinute", Value: bson.D{{Key: "$avg", Value: "$avgDepartureMinute"}}}, + {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, + }, + }}, + // Stage 9: Project the final result format + bson.D{{Key: "$project", + Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "days", Value: 1}, + {Key: "overallavgArrival", + Value: bson.D{ + {Key: "$concat", + Value: bson.A{ + bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$overallAvgArrivalHour"}}}}, + ":", + bson.D{ + {Key: "$cond", + Value: bson.D{ + {Key: "if", + Value: bson.D{ + {Key: "$lt", Value: bson.A{bson.D{{Key: "$floor", Value: "$overallAvgArrivalMinute"}}, 10}}, + }, + }, + {Key: "then", + Value: bson.D{ + {Key: "$concat", Value: bson.A{"0", bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$overallAvgArrivalMinute"}}}}}}, + }, + }, + {Key: "else", Value: bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$overallAvgArrivalMinute"}}}}}, + }, + }, + }, + }, + }, + }, + }, + {Key: "overallavgDeparture", + Value: bson.D{ + {Key: "$concat", + Value: bson.A{ + bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$overallAvgDepartureHour"}}}}, + ":", + bson.D{ + {Key: "$cond", + Value: bson.D{ + {Key: "if", + Value: bson.D{ + {Key: "$lt", Value: bson.A{bson.D{{Key: "$floor", Value: "$overallAvgDepartureMinute"}}, 10}}, + }, + }, + {Key: "then", + Value: bson.D{ + {Key: "$concat", Value: bson.A{"0", bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$overallAvgDepartureMinute"}}}}}}, + }, + }, + {Key: "else", Value: bson.D{{Key: "$toString", Value: bson.D{{Key: "$floor", Value: "$overallAvgDepartureMinute"}}}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }}, } - - overallArrivalHours := overallAverageArrivalTime / time.Hour - overallArrivalMinutes := (overallAverageArrivalTime % time.Hour) / time.Minute - - overallDepartureHours := overallAverageDepartureTime / time.Hour - overallDepartureMinutes := (overallAverageDepartureTime % time.Hour) / time.Minute - - result = append(result, bson.M{ - "overallAverageArrivalTime": fmt.Sprintf("%02d:%02d", overallArrivalHours, overallArrivalMinutes), - "overallAverageDepartureTime": fmt.Sprintf("%02d:%02d", overallDepartureHours, overallDepartureMinutes), - }) - - return result } // CalculateInOfficeRate function to calculate absenteeism rates -func CalculateInOfficeRate(officeHours []models.OfficeHours) []bson.M { - expectedDailyHours := 10.0 // 7 AM to 5 PM - weekdayInHours := make(map[time.Weekday]float64) - weekdayAbsenteeism := make(map[time.Weekday]float64) - weekdayCount := make(map[time.Weekday]int) - - // Iterate over office hours entries - for _, oh := range officeHours { - weekday := oh.Entered.Weekday() - if weekday == time.Saturday || weekday == time.Sunday { - continue - } - - // Define the office hours for the day - officeStart := time.Date(oh.Entered.Year(), oh.Entered.Month(), oh.Entered.Day(), 7, 0, 0, 0, oh.Entered.Location()) - officeEnd := time.Date(oh.Entered.Year(), oh.Entered.Month(), oh.Entered.Day(), 17, 0, 0, 0, oh.Entered.Location()) - - // Calculate the overlap between actual office hours and standard office hours - actualStart := MaxTime(oh.Entered, officeStart) - actualEnd := MinTime(oh.Exited, officeEnd) - - // Calculate the duration in hours for the overlap (in-office time) - inOfficeDuration := 0.0 - if actualStart.Before(actualEnd) { - inOfficeDuration = actualEnd.Sub(actualStart).Hours() - } - - weekdayInHours[weekday] += inOfficeDuration - weekdayAbsenteeism[weekday] += expectedDailyHours - inOfficeDuration - weekdayCount[weekday]++ - } - - // Prepare the result as a slice of bson.M - var result []bson.M - var overallAbsenteeism float64 - var overallInHours float64 - var totalCount int - - for weekday := time.Monday; weekday <= time.Friday; weekday++ { - absenteeismRate := 0.0 - if weekdayCount[weekday] > 0 { - absenteeismRate = (weekdayAbsenteeism[weekday] / (float64(weekdayCount[weekday]) * expectedDailyHours)) * 100 - } - - dayData := bson.M{ - "weekday": weekday.String(), - "inOfficeRate": 100.0 - absenteeismRate, - } - result = append(result, dayData) - - overallAbsenteeism += weekdayAbsenteeism[weekday] - overallInHours += weekdayInHours[weekday] - totalCount += weekdayCount[weekday] - } +func CalculateInOfficeRate(email string, filter models.OfficeHoursFilterStruct) bson.A { + // Create the match filter using the reusable function + matchFilter := createMatchFilter(email, filter) - // Calculate overall absenteeism rate - overallAbsenteeismRate := 0.0 - if totalCount > 0 { - overallAbsenteeismRate = (overallAbsenteeism / (float64(totalCount) * expectedDailyHours)) * 100 + return bson.A{ + // Stage 1: Match filter conditions (email and time range) + bson.D{{Key: "$match", Value: matchFilter}}, + // Stage 2: Apply skip for pagination + bson.D{{Key: "$skip", Value: filter.Skip}}, + // Stage 3: Apply limit for pagination + bson.D{{Key: "$limit", Value: filter.Limit}}, + // Stage 4: Project the weekday, entered and exited times + bson.D{ + {Key: "$addFields", + Value: bson.D{ + {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, + {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, + {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, + {Key: "inOfficeStart", + Value: bson.D{ + {Key: "$cond", + Value: bson.D{ + {Key: "if", + Value: bson.D{ + {Key: "$lt", + Value: bson.A{ + bson.D{{Key: "$hour", Value: "$entered"}}, + 7, + }, + }, + }, + }, + {Key: "then", Value: 7}, + {Key: "else", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, + }, + }, + }, + }, + {Key: "inOfficeEnd", + Value: bson.D{ + {Key: "$cond", + Value: bson.D{ + {Key: "if", + Value: bson.D{ + {Key: "$gt", + Value: bson.A{ + bson.D{{Key: "$hour", Value: "$exited"}}, + 17, + }, + }, + }, + }, + {Key: "then", Value: 17}, + {Key: "else", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, + }, + }, + }, + }, + }, + }, + }, + // Stage 5: Filter the results to only include the hours between 7 and 17 + bson.D{ + {Key: "$match", + Value: bson.D{ + {Key: "inOfficeEnd", Value: bson.D{{Key: "$gt", Value: 7}}}, + {Key: "inOfficeStart", Value: bson.D{{Key: "$lt", Value: 17}}}, + }, + }, + }, + // Stage 6: Calculate the total in office hours, total entries and total possible office hours + bson.D{ + {Key: "$addFields", + Value: bson.D{ + {Key: "inOfficeHours", + Value: bson.D{ + {Key: "$subtract", + Value: bson.A{ + "$inOfficeEnd", + "$inOfficeStart", + }, + }, + }, + }, + }, + }, + }, + // Stage 7: Group by the weekday to calculate the total in office hours, total entries and total possible office hours + bson.D{ + {Key: "$group", + Value: bson.D{ + {Key: "_id", Value: "$weekday"}, + {Key: "totalInOfficeHours", Value: bson.D{{Key: "$sum", Value: "$inOfficeHours"}}}, + {Key: "totalEntries", Value: bson.D{{Key: "$sum", Value: 1}}}, + {Key: "totalPossibleOfficeHours", Value: bson.D{{Key: "$sum", Value: 10}}}, + }, + }, + }, + // Stage 8: Project the final result format + bson.D{ + {Key: "$project", + Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "weekday", + Value: bson.D{ + {Key: "$switch", + Value: bson.D{ + {Key: "branches", + Value: bson.A{ + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 1, + }, + }, + }, + }, + {Key: "then", Value: "Sunday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 2, + }, + }, + }, + }, + {Key: "then", Value: "Monday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 3, + }, + }, + }, + }, + {Key: "then", Value: "Tuesday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 4, + }, + }, + }, + }, + {Key: "then", Value: "Wednesday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 5, + }, + }, + }, + }, + {Key: "then", Value: "Thursday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 6, + }, + }, + }, + }, + {Key: "then", Value: "Friday"}, + }, + bson.D{ + {Key: "case", + Value: bson.D{ + {Key: "$eq", + Value: bson.A{ + "$_id", + 7, + }, + }, + }, + }, + {Key: "then", Value: "Saturday"}, + }, + }, + }, + {Key: "default", Value: "Unknown"}, + }, + }, + }, + }, + {Key: "rate", + Value: bson.D{ + {Key: "$multiply", + Value: bson.A{ + bson.D{ + {Key: "$divide", + Value: bson.A{ + "$totalInOfficeHours", + "$totalPossibleOfficeHours", + }, + }, + }, + 100, + }, + }, + }, + }, + }, + }, + }, + // Stage 9: Sort by weekday + bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: 1}}}}, + // Stage 10: Group all results together to calculate the overall totals + bson.D{ + {Key: "$group", + Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "days", + Value: bson.D{ + {Key: "$push", + Value: bson.D{ + {Key: "weekday", Value: "$weekday"}, + {Key: "rate", Value: "$rate"}, + }, + }, + }, + }, + {Key: "accumulatedRate", Value: bson.D{{Key: "$sum", Value: "$rate"}}}, + {Key: "overallWeekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, + }, + }, + }, + // Stage 11: Project the final result format + bson.D{ + {Key: "$project", + Value: bson.D{ + {Key: "_id", Value: 0}, + {Key: "days", Value: 1}, + {Key: "overallRate", + Value: bson.D{ + {Key: "$divide", + Value: bson.A{ + "$accumulatedRate", + "$overallWeekdayCount", + }, + }, + }, + }, + {Key: "overallWeekdayCount", Value: 1}, + }, + }, + }, } - - result = append(result, bson.M{ - "overallInOfficeRate": 100.0 - overallAbsenteeismRate, - }) - - return result } From d9b51dc63e022f75b6631365386beeb351d0ef00 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 13:50:38 +0200 Subject: [PATCH 05/29] Refactor GetAnalyticsOnHours function to add new pipeline stages --- occupi-backend/pkg/database/database.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index c73322d5..677fb3cf 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1722,7 +1722,7 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email } // Prepare the aggregate - var pipeline primitive.D + var pipeline bson.A switch calculate { case "hoursbyday": pipeline = analytics.GroupOfficeHoursByDay(email, filter) @@ -1732,6 +1732,12 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email pipeline = analytics.RatioInOutOfficeByWeekday(email, filter) case "peakhours": pipeline = analytics.BusiestHoursByWeekday(email, filter) + case "most": + pipeline = analytics.LeastMostInOfficeWorker(email, filter, false) + case "least": + pipeline = analytics.LeastMostInOfficeWorker(email, filter, true) + case "arrivaldeparture": + pipeline = analytics.AverageArrivalAndDepartureTimesByWeekday(email, filter) default: return nil, 0, errors.New("invalid calculate value") } From d4793bc630fd691f252514a10d0ad295ada287bd Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 13:51:25 +0200 Subject: [PATCH 06/29] Refactor GetAnalyticsOnHours function to add new pipeline stage for calculating average in-office hours by weekday --- occupi-backend/pkg/database/database.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 677fb3cf..a8c7c432 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1738,6 +1738,8 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email pipeline = analytics.LeastMostInOfficeWorker(email, filter, true) case "arrivaldeparture": pipeline = analytics.AverageArrivalAndDepartureTimesByWeekday(email, filter) + case "inofficehours": + pipeline = analytics.AverageInOfficeHoursByWeekday(email, filter) default: return nil, 0, errors.New("invalid calculate value") } From 9901b7a14d83c3129916eb5d8d72e550219d419a Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 13:52:06 +0200 Subject: [PATCH 07/29] Refactor GetAnalyticsOnHours function to use CalculateInOfficeRate instead of AverageInOfficeHoursByWeekday --- occupi-backend/pkg/database/database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index a8c7c432..e2e08ebd 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1739,7 +1739,7 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email case "arrivaldeparture": pipeline = analytics.AverageArrivalAndDepartureTimesByWeekday(email, filter) case "inofficehours": - pipeline = analytics.AverageInOfficeHoursByWeekday(email, filter) + pipeline = analytics.CalculateInOfficeRate(email, filter) default: return nil, 0, errors.New("invalid calculate value") } From 8377f42dce0834932ab6cb483aa8d987dee902eb Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 13:59:00 +0200 Subject: [PATCH 08/29] Refactor GetAnalyticsOnHours function to add pagination and improve performance using aggregation pipeline --- occupi-backend/pkg/analytics/analytics.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 4a56a997..2feb0e98 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -379,13 +379,17 @@ func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct return bson.A{ // Stage 1: Match filter conditions (email and time range) bson.D{{Key: "$match", Value: matchFilter}}, - // Stage 2: Project the weekday, entered and exited times + // Stage 2: Apply skip for pagination + bson.D{{Key: "$skip", Value: filter.Skip}}, + // Stage 3: Apply limit for pagination + bson.D{{Key: "$limit", Value: filter.Limit}}, + // Stage 4: Project the weekday, entered and exited times bson.D{{Key: "$addFields", Value: bson.D{ {Key: "weekday", Value: bson.D{{Key: "$dayOfWeek", Value: "$entered"}}}, {Key: "enteredHour", Value: bson.D{{Key: "$hour", Value: "$entered"}}}, {Key: "exitedHour", Value: bson.D{{Key: "$hour", Value: "$exited"}}}, }}}, - // Stage 3: Project the weekday, enteredHour, exitedHour and hoursWorked + // Stage 5: Project the weekday, enteredHour, exitedHour and hoursWorked bson.D{{Key: "$addFields", Value: bson.D{ {Key: "hoursWorked", Value: bson.D{ {Key: "$subtract", Value: bson.A{ @@ -394,7 +398,7 @@ func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct }}, }}, }}}, - // Stage 4: Group by the email and weekday to calculate the total hours and count + // Stage 6: Group by the email and weekday to calculate the total hours and count bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: bson.D{ {Key: "email", Value: "$email"}, @@ -403,7 +407,7 @@ func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct {Key: "totalHours", Value: bson.D{{Key: "$sum", Value: "$hoursWorked"}}}, {Key: "weekdayCount", Value: bson.D{{Key: "$sum", Value: 1}}}, }}}, - // Stage 5: Group by the email to calculate the overall total hours and average hours + // Stage 7: Group by the email to calculate the overall total hours and average hours bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$_id.email"}, {Key: "days", Value: bson.D{ @@ -464,7 +468,7 @@ func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct {Key: "totalHours", Value: bson.D{{Key: "$sum", Value: "$totalHours"}}}, {Key: "averageHours", Value: bson.D{{Key: "$avg", Value: "$totalHours"}}}, }}}, - // Stage 6: Sort by total hours and limit to 1 result + // Stage 8: Sort by total hours and limit to 1 result bson.D{{Key: "$sort", Value: bson.D{{Key: "totalHours", Value: sortV}}}}, bson.D{{Key: "$limit", Value: 1}}, bson.D{{Key: "$project", Value: bson.D{ From e620341587232cfe74e9c32c7ecbc519e80e7f4b Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 14:46:31 +0200 Subject: [PATCH 09/29] Refactor createMatchFilter function to use PascalCase for consistency --- occupi-backend/pkg/analytics/analytics.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 2feb0e98..5c82f111 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -5,7 +5,7 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -func createMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson.D { +func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson.D { // Create a match filter matchFilter := bson.D{} @@ -35,7 +35,7 @@ func createMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.A { return bson.A{ // Stage 1: Match filter conditions (email and time range) - bson.D{{Key: "$match", Value: createMatchFilter(email, filter)}}, + bson.D{{Key: "$match", Value: CreateMatchFilter(email, filter)}}, // Stage 2: Apply skip for pagination bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination @@ -89,7 +89,7 @@ func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := createMatchFilter(email, filter) + matchFilter := CreateMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -168,7 +168,7 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := createMatchFilter(email, filter) + matchFilter := CreateMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -256,7 +256,7 @@ func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStru // BusiestHoursByWeekday function to return the 3 busiest hours per weekday func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := createMatchFilter(email, filter) + matchFilter := CreateMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -367,7 +367,7 @@ func BusiestHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) // LeastMostInOfficeWorker function to calculate the least or most "in office" worker func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct, sort bool) bson.A { // Create the match filter using the reusable function - matchFilter := createMatchFilter(email, filter) + matchFilter := CreateMatchFilter(email, filter) var sortV int if sort { @@ -500,7 +500,7 @@ func LeastMostInOfficeWorker(email string, filter models.OfficeHoursFilterStruct // AverageArrivalAndDepartureTimesByWeekday function to calculate the average arrival and departure times for each weekday func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := createMatchFilter(email, filter) + matchFilter := CreateMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) @@ -754,7 +754,7 @@ func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.Office // CalculateInOfficeRate function to calculate absenteeism rates func CalculateInOfficeRate(email string, filter models.OfficeHoursFilterStruct) bson.A { // Create the match filter using the reusable function - matchFilter := createMatchFilter(email, filter) + matchFilter := CreateMatchFilter(email, filter) return bson.A{ // Stage 1: Match filter conditions (email and time range) From b1f140e7c93c632ae88343b7cc2a699bd1df1001 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 15:59:02 +0200 Subject: [PATCH 10/29] Refactor CreateMatchFilter function to append time range filters only if both time range and filter are present --- occupi-backend/pkg/analytics/analytics.go | 2 +- occupi-backend/tests/analytics_test.go | 79 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 occupi-backend/tests/analytics_test.go diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 5c82f111..91cc2cd0 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -24,7 +24,7 @@ func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson } // If there are time range filters, append them to the match filter - if len(timeRangeFilter) > 0 { + if len(timeRangeFilter) > 0 && len(filter.Filter) > 0 { matchFilter = append(matchFilter, bson.E{Key: "entered", Value: timeRangeFilter}) } diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go new file mode 100644 index 00000000..75d8b130 --- /dev/null +++ b/occupi-backend/tests/analytics_test.go @@ -0,0 +1,79 @@ +package tests + +import ( + "reflect" + "testing" + + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" + "go.mongodb.org/mongo-driver/bson" +) + +func TestCreateMatchFilter(t *testing.T) { + tests := []struct { + name string + email string + filter models.OfficeHoursFilterStruct + expected bson.D + }{ + { + name: "empty filter with no email", + email: "", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{}}, + expected: bson.D{}, + }, + { + name: "empty filter with email", + email: "test@example.com", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{}}, + expected: bson.D{{Key: "email", Value: bson.D{{Key: "$eq", Value: "test@example.com"}}}}, + }, + { + name: "filter with no email", + email: "", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "", "timeTo": ""}}, + expected: bson.D{}, + }, + { + name: "filter with no email and timeFrom", + email: "", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": ""}}, + expected: bson.D{{Key: "entered", Value: bson.D{{Key: "$gte", Value: "09:00"}}}}, + }, + { + name: "filter with no email and timeTo", + email: "", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "", "timeTo": "17:00"}}, + expected: bson.D{{Key: "entered", Value: bson.D{{Key: "$lte", Value: "17:00"}}}}, + }, + { + name: "filter with no email and timeFrom and timeTo", + email: "", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": "17:00"}}, + expected: bson.D{{Key: "entered", Value: bson.D{{Key: "$gte", Value: "09:00"}, {Key: "$lte", Value: "17:00"}}}}, + }, + { + name: "filter with email and timeFrom and timeTo", + email: "test@example.com", + filter: models.OfficeHoursFilterStruct{Filter: bson.M{"timeFrom": "09:00", "timeTo": "17:00"}}, + expected: bson.D{ + {Key: "email", Value: bson.D{{Key: "$eq", Value: "test@example.com"}}}, + {Key: "entered", Value: bson.D{{Key: "$gte", Value: "09:00"}, {Key: "$lte", Value: "17:00"}}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := analytics.CreateMatchFilter(tt.email, tt.filter) + if !equalBsonD(result, tt.expected) { + t.Errorf("%s for CreateMatchFilter() = %v, want %v", tt.name, result, tt.expected) + } + }) + } +} + +// Helper function to compare bson.D objects +func equalBsonD(a, b bson.D) bool { + return len(a) == len(b) && reflect.DeepEqual(a, b) +} From 695cca77f68e68cd38bc060ef61ef0fc7a40ec95 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:01:50 +0200 Subject: [PATCH 11/29] Refactor CreateMatchFilter function to use a variable for the match filter in GroupOfficeHoursByDay --- occupi-backend/pkg/analytics/analytics.go | 4 +++- occupi-backend/tests/analytics_test.go | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/occupi-backend/pkg/analytics/analytics.go b/occupi-backend/pkg/analytics/analytics.go index 91cc2cd0..0e30d685 100644 --- a/occupi-backend/pkg/analytics/analytics.go +++ b/occupi-backend/pkg/analytics/analytics.go @@ -33,9 +33,11 @@ func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson // GroupOfficeHoursByDay function with total hours calculation func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.A { + matchFilter := CreateMatchFilter(email, filter) + return bson.A{ // Stage 1: Match filter conditions (email and time range) - bson.D{{Key: "$match", Value: CreateMatchFilter(email, filter)}}, + bson.D{{Key: "$match", Value: matchFilter}}, // Stage 2: Apply skip for pagination bson.D{{Key: "$skip", Value: filter.Skip}}, // Stage 3: Apply limit for pagination diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 75d8b130..da9e2738 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -77,3 +77,15 @@ func TestCreateMatchFilter(t *testing.T) { func equalBsonD(a, b bson.D) bool { return len(a) == len(b) && reflect.DeepEqual(a, b) } + +func TestGroupOfficeHoursByDay(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.GroupOfficeHoursByDay(email, filter) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("GroupOfficeHoursByDay() = %v, want greater than 0", res) + } +} From c3fad5f62dae2773e99c0ff205b6722b5150c2c3 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:02:54 +0200 Subject: [PATCH 12/29] Refactor analytics_test.go to add test for AverageOfficeHoursByWeekday function --- occupi-backend/tests/analytics_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index da9e2738..7742e9c9 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -89,3 +89,15 @@ func TestGroupOfficeHoursByDay(t *testing.T) { t.Errorf("GroupOfficeHoursByDay() = %v, want greater than 0", res) } } + +func TestAverageOfficeHoursByWeekday(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.AverageOfficeHoursByWeekday(email, filter) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("AverageOfficeHoursByWeekday() = %v, want greater than 0", res) + } +} From 2a2e1f8dd1fe99f68bf7f48e61e8a878258b28cc Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:04:03 +0200 Subject: [PATCH 13/29] Refactor analytics_test.go to add tests for RatioInOutOfficeByWeekday function --- occupi-backend/tests/analytics_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 7742e9c9..2777e492 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -101,3 +101,27 @@ func TestAverageOfficeHoursByWeekday(t *testing.T) { t.Errorf("AverageOfficeHoursByWeekday() = %v, want greater than 0", res) } } + +func TestRatioInOutOfficeByWeekday(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.RatioInOutOfficeByWeekday(email, filter) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("RatioInOutOfficeByWeekday() = %v, want greater than 0", res) + } +} + +func TestRatioInOutOfficeByWeekday(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.RatioInOutOfficeByWeekday(email, filter) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("RatioInOutOfficeByWeekday() = %v, want greater than 0", res) + } +} From d8659ca0f5227d88b595917680c1df8907bc133b Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:05:59 +0200 Subject: [PATCH 14/29] Refactor analytics_test.go to rename TestRatioInOutOfficeByWeekday to TestBusiestHoursByWeekday and update the function call in the test case --- occupi-backend/tests/analytics_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 2777e492..db72530f 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -114,14 +114,14 @@ func TestRatioInOutOfficeByWeekday(t *testing.T) { } } -func TestRatioInOutOfficeByWeekday(t *testing.T) { +func TestBusiestHoursByWeekday(t *testing.T) { email := "test@example.com" filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} - res := analytics.RatioInOutOfficeByWeekday(email, filter) + res := analytics.BusiestHoursByWeekday(email, filter) // check len is greater than 0 if len(res) == 0 { - t.Errorf("RatioInOutOfficeByWeekday() = %v, want greater than 0", res) + t.Errorf("BusiestHoursByWeekday() = %v, want greater than 0", res) } } From 9a7a668029324b730ff204276402e4054395adb1 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:06:22 +0200 Subject: [PATCH 15/29] Refactor analytics_test.go to add test for LeastInOfficeWorker function --- occupi-backend/tests/analytics_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index db72530f..3288d289 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -125,3 +125,15 @@ func TestBusiestHoursByWeekday(t *testing.T) { t.Errorf("BusiestHoursByWeekday() = %v, want greater than 0", res) } } + +func TestLeastInOfficeWorker(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.LeastMostInOfficeWorker(email, filter, true) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("LeastMostInOfficeWorker() = %v, want greater than 0", res) + } +} From 31aaee7918336e8fc04f80615d2bc0aa433d845d Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:06:41 +0200 Subject: [PATCH 16/29] Refactor analytics_test.go to add test for MostInOfficeWorker function --- occupi-backend/tests/analytics_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 3288d289..89dee966 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -137,3 +137,15 @@ func TestLeastInOfficeWorker(t *testing.T) { t.Errorf("LeastMostInOfficeWorker() = %v, want greater than 0", res) } } + +func TestMostInOfficeWorker(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.LeastMostInOfficeWorker(email, filter, false) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("LeastMostInOfficeWorker() = %v, want greater than 0", res) + } +} From bb881260b484421f162db45e0485d10c602f56bc Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:07:35 +0200 Subject: [PATCH 17/29] Refactor analytics_test.go to add test for AverageArrivalAndDepartureTimesByWeekday function --- occupi-backend/tests/analytics_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 89dee966..980c7b84 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -149,3 +149,15 @@ func TestMostInOfficeWorker(t *testing.T) { t.Errorf("LeastMostInOfficeWorker() = %v, want greater than 0", res) } } + +func TestAverageArrivalAndDepartureTimesByWeekday(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.AverageArrivalAndDepartureTimesByWeekday(email, filter) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("AverageArrivalAndDepartureTimesByWeekday() = %v, want greater than 0", res) + } +} From 942994b023d2abbfd0c97ff11e5c1d13911b0d15 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:08:16 +0200 Subject: [PATCH 18/29] Add test for CalculateInOfficeRate function in analytics_test.go --- occupi-backend/tests/analytics_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/occupi-backend/tests/analytics_test.go b/occupi-backend/tests/analytics_test.go index 980c7b84..002b3313 100644 --- a/occupi-backend/tests/analytics_test.go +++ b/occupi-backend/tests/analytics_test.go @@ -161,3 +161,15 @@ func TestAverageArrivalAndDepartureTimesByWeekday(t *testing.T) { t.Errorf("AverageArrivalAndDepartureTimesByWeekday() = %v, want greater than 0", res) } } + +func TestCalculateInOfficeRate(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{Filter: bson.M{}} + + res := analytics.CalculateInOfficeRate(email, filter) + + // check len is greater than 0 + if len(res) == 0 { + t.Errorf("CalculateInOfficeRate() = %v, want greater than 0", res) + } +} From 38f7246100f3d19326dcbbfcaa2f6faca5c1301d Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:10:47 +0200 Subject: [PATCH 19/29] Refactor test commands in GitHub workflows to include analytics package in coverage --- .github/workflows/lint-test-build-golang.yml | 2 +- .github/workflows/test-and-cov.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index f1df8b58..3c26995f 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -117,7 +117,7 @@ jobs: - name: ๐Ÿงช Run tests run: | - gotestsum --format testname --junitfile tmp/test-results/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out + gotestsum --format testname --junitfile tmp/test-results/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out - name: ๐Ÿ“‹ Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/.github/workflows/test-and-cov.yml b/.github/workflows/test-and-cov.yml index 53e9514a..d7629b57 100644 --- a/.github/workflows/test-and-cov.yml +++ b/.github/workflows/test-and-cov.yml @@ -102,7 +102,7 @@ jobs: - name: ๐Ÿงช Run tests run: | - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out + gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out - name: ๐Ÿ“‹ Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 From 552aeea62ad8f5b25563297be65be636706d8f5e Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:11:08 +0200 Subject: [PATCH 20/29] Refactor test commands in sh files for coverage report --- occupi-backend/occupi.bat | 8 ++++---- occupi-backend/occupi.sh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/occupi-backend/occupi.bat b/occupi-backend/occupi.bat index f492762c..07db7a03 100644 --- a/occupi-backend/occupi.bat +++ b/occupi-backend/occupi.bat @@ -26,10 +26,10 @@ if "%1 %2" == "run dev" ( gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v ./tests/... exit /b 0 ) else if "%1 %2" == "test codecov" ( - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out + gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out exit /b 0 ) else if "%1 %2" == "report codecov" ( - gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out + gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out exit /b 0 ) else if "%1" == "lint" ( golangci-lint run @@ -55,8 +55,8 @@ echo docker build : docker compose -f docker-compose.localdev.yml build echo docker up : docker compose -f docker-compose.localdev.yml up -d echo test : gotestsum --format testname -- -v ./tests/... echo report : gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v ./tests/... -echo test codecov : gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out -echo report codecov : gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out +echo test codecov : gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out +echo report codecov : gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out echo lint : golangci-lint run echo convert report : python reports/convert_report.py reports.json -o allure-results echo help : Show this help message diff --git a/occupi-backend/occupi.sh b/occupi-backend/occupi.sh index 58b92b00..fab75fdd 100644 --- a/occupi-backend/occupi.sh +++ b/occupi-backend/occupi.sh @@ -10,8 +10,8 @@ print_help() { echo " build prod -> go build cmd/occupi-backend/main.go" echo " test -> gotestsum --format testname -- -v ./tests/..." echo " report -> gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v ./tests/..." - echo " test codecov -> gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out" - echo " report codecov -> gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out" + echo " test codecov -> gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out" + echo " report codecov -> gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out" echo " lint -> golangci-lint run" echo " decrypt env -> cd scripts && chmod +x decrypt_env_variables.sh && ./decrypt_env_variables.sh" echo " encrypt env -> cd scripts && chmod +x encrypt_env_variables.sh && ./encrypt_env_variables.sh" @@ -31,9 +31,9 @@ elif [ "$1" = "test" ]; then elif [ "$1" = "report" ]; then gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v ./tests/... elif [ "$1" = "test" ] && [ "$2" = "codecov" ]; then - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out + gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out elif [ "$1" = "report" ] && [ "$2" = "codecov" ]; then - gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out + gotestsum --format testname --junitfile reports/gotestsum-report.xml -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/analytics,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out elif [ "$1" = "lint" ]; then golangci-lint run elif [ "$1" = "decrypt" ] && [ "$2" = "env" ]; then From 6bb79ef23c0de67d3ed927becee20edeb75a9b75 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:42:54 +0200 Subject: [PATCH 21/29] Refactor database_helpers.go and tests/database_test.go --- .../pkg/database/database_helpers.go | 17 ++ occupi-backend/tests/database_test.go | 153 +++++++++++++++--- 2 files changed, 149 insertions(+), 21 deletions(-) diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index a4fa5100..99a45fef 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/ipinfo/go/v2/ipinfo" "github.com/umahmood/haversine" + "go.mongodb.org/mongo-driver/bson" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" @@ -214,3 +215,19 @@ func DayOfTheWeek(date time.Time) string { func Month(date time.Time) int { return int(date.Month()) } + +func MakeEmailAndTimeFilter(email string, filter models.OfficeHoursFilterStruct) bson.M { + mongoFilter := bson.M{} + if email != "" { + mongoFilter["email"] = email + } + if filter.Filter["timeFrom"] != "" && filter.Filter["timeTo"] != "" { + mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"], "$lte": filter.Filter["timeTo"]} + } else if filter.Filter["timeTo"] != "" { + mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} + } else if filter.Filter["timeFrom"] != "" { + mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} + } + + return mongoFilter +} diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index 3098dd99..d8edc262 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -8379,7 +8379,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) // Define an example OfficeHours object - _ = models.OfficeHours{ + officeHours := models.OfficeHours{ Email: "test@example.com", Entered: time.Now(), Exited: time.Now().Add(2 * time.Hour), @@ -8414,7 +8414,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { assert.EqualError(t, err, "database is nil", "Expected error for nil database") }) - /*mt.Run("Successful query and analytics calculation", func(mt *mtest.T) { + mt.Run("Successful query and hoursbyday calculation", func(mt *mtest.T) { // Mock Find to return the OfficeHours document mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ {Key: "email", Value: officeHours.Email}, @@ -8437,28 +8437,30 @@ func TestGetAnalyticsOnHours(t *testing.T) { assert.NotNil(t, results, "Expected non-nil results") }) - mt.Run("Invalid calculation type", func(mt *mtest.T) { - // Mock Find to return the OfficeHours document - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ - {Key: "email", Value: officeHours.Email}, - {Key: "entered", Value: officeHours.Entered}, - {Key: "exited", Value: officeHours.Exited}, - {Key: "closed", Value: officeHours.Closed}, - })) + /* - // mock count documents - mt.AddMockResponses(mtest.CreateSuccessResponse()) + mt.Run("Invalid calculation type", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) - // Create a mock AppSession with a valid database - appsession := &models.AppSession{ - DB: mt.Client, - } + // mock count documents + mt.AddMockResponses(mtest.CreateSuccessResponse()) - results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "invalid") - assert.Nil(t, results) - assert.Equal(t, int64(0), total) - assert.EqualError(t, err, "invalid calculation", "Expected error for invalid calculation type") - })*/ + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "invalid") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "invalid calculation", "Expected error for invalid calculation type") + })*/ mt.Run("Failed query", func(mt *mtest.T) { // Mock Find to return an error @@ -8504,3 +8506,112 @@ func TestGetAnalyticsOnHours(t *testing.T) { assert.EqualError(t, err, "count failed", "Expected error for failed count documents") })*/ } + +func TestMakeEmailAndTimeFilter(t *testing.T) { + // Define a base filter for testing + baseFilter := models.OfficeHoursFilterStruct{ + Filter: bson.M{ + "timeFrom": "", + "timeTo": "", + }, + } + + // Test case: Email is provided, but no time filters + t.Run("EmailOnly", func(t *testing.T) { + email := "test@example.com" + filter := baseFilter + expected := bson.M{"email": email} + + result := database.MakeEmailAndTimeFilter(email, filter) + assert.Equal(t, expected, result, "The filter should only contain the email field.") + }) + + // Test case: No email and no time filters + t.Run("NoEmailNoTime", func(t *testing.T) { + email := "" + filter := baseFilter + expected := bson.M{} + + result := database.MakeEmailAndTimeFilter(email, filter) + assert.Equal(t, expected, result, "The filter should be empty when no email or time filters are provided.") + }) + + // Test case: Email and timeFrom provided + t.Run("EmailAndTimeFrom", func(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{ + Filter: bson.M{ + "timeFrom": "2023-09-01T09:00:00", + "timeTo": "", + }, + } + expected := bson.M{ + "email": email, + "entered": bson.M{"$gte": "2023-09-01T09:00:00"}, + } + + result := database.MakeEmailAndTimeFilter(email, filter) + assert.Equal(t, expected, result, "The filter should contain the email and timeFrom fields.") + }) + + // Test case: Email and timeTo provided + t.Run("EmailAndTimeTo", func(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{ + Filter: bson.M{ + "timeFrom": "", + "timeTo": "2023-09-01T17:00:00", + }, + } + + expected := bson.M{ + "email": email, + "entered": bson.M{"$lte": "2023-09-01T17:00:00"}, + } + + result := database.MakeEmailAndTimeFilter(email, filter) + + assert.Equal(t, expected, result, "The filter should contain the email and timeTo fields.") + }) + + // Test case: timeFrom and timeTo provided, but no email + t.Run("TimeFromAndTimeTo", func(t *testing.T) { + email := "" + filter := models.OfficeHoursFilterStruct{ + Filter: bson.M{ + "timeFrom": "2023-09-01T09:00:00", + "timeTo": "2023-09-01T17:00:00", + }, + } + expected := bson.M{ + "entered": bson.M{ + "$gte": "2023-09-01T09:00:00", + "$lte": "2023-09-01T17:00:00", + }, + } + + result := database.MakeEmailAndTimeFilter(email, filter) + assert.Equal(t, expected, result, "The filter should contain timeFrom and timeTo, but no email.") + }) + + // Test case: Email, timeFrom, and timeTo provided + t.Run("EmailAndFullTimeRange", func(t *testing.T) { + email := "test@example.com" + filter := models.OfficeHoursFilterStruct{ + Filter: bson.M{ + "timeFrom": "2023-09-01T09:00:00", + "timeTo": "2023-09-01T17:00:00", + }, + } + expected := bson.M{ + "email": email, + "entered": bson.M{ + "$gte": "2023-09-01T09:00:00", + "$lte": "2023-09-01T17:00:00", + }, + } + + result := database.MakeEmailAndTimeFilter(email, filter) + assert.Equal(t, expected, result, "The filter should contain the email and full time range.") + }) +} From b6ef9b564d753a54ce5f0f4874e1528431486063 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 16:42:59 +0200 Subject: [PATCH 22/29] Refactor GetAnalyticsOnHours function to use MakeEmailAndTimeFilter helper --- occupi-backend/pkg/database/database.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index e2e08ebd..2942d519 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -1758,16 +1758,7 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email return nil, 0, err } - mongoFilter := bson.M{} - if email != "" { - mongoFilter["email"] = email - } - if filter.Filter["timeFrom"] != "" { - mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} - } - if filter.Filter["timeTo"] != "" { - mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} - } + mongoFilter := MakeEmailAndTimeFilter(email, filter) // count documents totalResults, err := collection.CountDocuments(ctx, mongoFilter) From 1a227405f53438587dbff5092c9643f2d3e31b11 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Sun, 8 Sep 2024 17:39:19 +0200 Subject: [PATCH 23/29] Refactor GetAnalyticsOnHours test to use specific time range for accurate results --- occupi-backend/tests/database_test.go | 45 ++++++++++----------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index d8edc262..4aef4bc5 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -8387,8 +8387,10 @@ func TestGetAnalyticsOnHours(t *testing.T) { } filterMap := map[string]string{ - "timeFrom": time.Now().Add(-24 * time.Hour).Format(time.RFC3339), - "timeTo": time.Now().Format(time.RFC3339), + // 1970-01-01T00:00:00Z + "timeFrom": time.Unix(0, 0).Format(time.RFC3339), + // time now + "timeTo": time.Now().Format(time.RFC3339), } // Convert filterMap to primitive.M @@ -8414,6 +8416,18 @@ func TestGetAnalyticsOnHours(t *testing.T) { assert.EqualError(t, err, "database is nil", "Expected error for nil database") }) + mt.Run("Invalid calculation type", func(mt *mtest.T) { + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "invalid") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "invalid calculate value", "Expected error for invalid calculation type") + }) + mt.Run("Successful query and hoursbyday calculation", func(mt *mtest.T) { // Mock Find to return the OfficeHours document mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ @@ -8431,37 +8445,12 @@ func TestGetAnalyticsOnHours(t *testing.T) { DB: mt.Client, } - results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "hoursbyday") + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "hoursbyday") assert.NoError(t, err, "Expected no error for successful query") assert.Equal(t, int64(1), total, "Expected 1 total result") assert.NotNil(t, results, "Expected non-nil results") }) - /* - - mt.Run("Invalid calculation type", func(mt *mtest.T) { - // Mock Find to return the OfficeHours document - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ - {Key: "email", Value: officeHours.Email}, - {Key: "entered", Value: officeHours.Entered}, - {Key: "exited", Value: officeHours.Exited}, - {Key: "closed", Value: officeHours.Closed}, - })) - - // mock count documents - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - // Create a mock AppSession with a valid database - appsession := &models.AppSession{ - DB: mt.Client, - } - - results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "invalid") - assert.Nil(t, results) - assert.Equal(t, int64(0), total) - assert.EqualError(t, err, "invalid calculation", "Expected error for invalid calculation type") - })*/ - mt.Run("Failed query", func(mt *mtest.T) { // Mock Find to return an error mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ From 01c3c311e5424f2b882d3fe4f6fb3ebc8e51d2ad Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Mon, 9 Sep 2024 09:58:20 +0200 Subject: [PATCH 24/29] Refactor GetAnalyticsOnHours test to use specific time range for accurate results --- occupi-backend/tests/database_test.go | 243 ++++++++++++++++++++++++-- 1 file changed, 232 insertions(+), 11 deletions(-) diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index 4aef4bc5..797e4271 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -8430,7 +8430,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { mt.Run("Successful query and hoursbyday calculation", func(mt *mtest.T) { // Mock Find to return the OfficeHours document - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8438,7 +8438,12 @@ func TestGetAnalyticsOnHours(t *testing.T) { })) // Mock CountDocuments to return a count of 1 - mt.AddMockResponses(mtest.CreateSuccessResponse()) + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) // Create a mock AppSession with a valid database appsession := &models.AppSession{ @@ -8447,11 +8452,207 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "hoursbyday") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(1), total, "Expected 1 total result") - assert.NotNil(t, results, "Expected non-nil results") + assert.Equal(t, int64(0), total, "Expected 1 total result") + assert.Nil(t, results, "Expected non-nil results") + }) + + mt.Run("Successful query and hoursbyweekday calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "hoursbyweekday") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") + }) + + mt.Run("Successful query and ratio calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "ratio") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") + }) + + mt.Run("Successful query and peakhours calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "peakhours") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") + }) + + mt.Run("Successful query and most calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "most") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") + }) + + mt.Run("Successful query and least calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "least") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") + }) + + mt.Run("Successful query and arrivaldeparture calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "arrivaldeparture") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") + }) + + mt.Run("Successful query and inofficehours calculation", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, + })) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "inofficehours") + assert.NoError(t, err, "Expected no error for successful query") + assert.Equal(t, int64(0), total, "Expected 0 total result") + assert.Nil(t, results, "Expected nil results") }) - mt.Run("Failed query", func(mt *mtest.T) { + mt.Run("Failed aggregate query", func(mt *mtest.T) { // Mock Find to return an error mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ Code: 1, @@ -8469,7 +8670,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { assert.EqualError(t, err, "query failed", "Expected error for failed query") }) - /*mt.Run("Failed count documents", func(mt *mtest.T) { + mt.Run("Failed cursor", func(mt *mtest.T) { // Mock Find to return the OfficeHours document mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ {Key: "email", Value: officeHours.Email}, @@ -8479,11 +8680,31 @@ func TestGetAnalyticsOnHours(t *testing.T) { })) // Mock CountDocuments to return an error - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 1, - Message: "count failed", + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + // Create a mock AppSession with a valid database + appsession := &models.AppSession{ + DB: mt.Client, + } + + results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "hoursbyday") + assert.Nil(t, results) + assert.Equal(t, int64(0), total) + assert.EqualError(t, err, "cursor.id should be an int64 but is a BSON invalid", "Expected error for failed count documents") + }) + + mt.Run("Failed count", func(mt *mtest.T) { + // Mock Find to return the OfficeHours document + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + {Key: "closed", Value: officeHours.Closed}, })) + // Mock CountDocuments to return an error + mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Create a mock AppSession with a valid database appsession := &models.AppSession{ DB: mt.Client, @@ -8492,8 +8713,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "test@example.com", filter, "hoursbyday") assert.Nil(t, results) assert.Equal(t, int64(0), total) - assert.EqualError(t, err, "count failed", "Expected error for failed count documents") - })*/ + assert.EqualError(t, err, "database response does not contain a cursor", "Expected error for failed count documents") + }) } func TestMakeEmailAndTimeFilter(t *testing.T) { From bbff6881399617ede34d2c05e45be97c2667e5b1 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Mon, 9 Sep 2024 09:58:27 +0200 Subject: [PATCH 25/29] Refactor GetAnalyticsOnHours and FilterCollectionWithProjection functions to use GetResultsAndCount helper --- occupi-backend/pkg/database/database.go | 25 ++++++------------- .../pkg/database/database_helpers.go | 21 ++++++++++++++++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 2942d519..2673f190 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -828,13 +828,10 @@ func FilterCollectionWithProjection(ctx *gin.Context, appsession *models.AppSess return nil, 0, err } - var results []bson.M - if err = cursor.All(ctx, &results); err != nil { - return nil, 0, err - } + results, totalResults, errv := GetResultsAndCount(ctx, collection, cursor, filter.Filter) - totalResults, err := collection.CountDocuments(ctx, filter.Filter) - if err != nil { + if errv != nil { + logrus.Error(err) return nil, 0, err } @@ -1752,19 +1749,13 @@ func GetAnalyticsOnHours(ctx *gin.Context, appsession *models.AppSession, email return nil, 0, err } - var results []bson.M - if err = cursor.All(ctx, &results); err != nil { - logrus.WithError(err).Error("Failed to get hours") - return nil, 0, err - } - mongoFilter := MakeEmailAndTimeFilter(email, filter) - // count documents - totalResults, err := collection.CountDocuments(ctx, mongoFilter) - if err != nil { - logrus.WithError(err).Error("Failed to count documents") - return nil, 0, err + results, totalResults, errv := GetResultsAndCount(ctx, collection, cursor, mongoFilter) + + if errv != nil { + logrus.Error(errv) + return nil, 0, errv } return results, totalResults, nil diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index 99a45fef..71509092 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -5,9 +5,13 @@ import ( "strings" "time" + "github.com/gin-gonic/gin" "github.com/ipinfo/go/v2/ipinfo" + "github.com/sirupsen/logrus" "github.com/umahmood/haversine" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" @@ -231,3 +235,20 @@ func MakeEmailAndTimeFilter(email string, filter models.OfficeHoursFilterStruct) return mongoFilter } + +func GetResultsAndCount(ctx *gin.Context, collection *mongo.Collection, cursor *mongo.Cursor, mongoFilter primitive.M) ([]bson.M, int64, error) { + var results []bson.M + if err := cursor.All(ctx, &results); err != nil { + logrus.WithError(err).Error("Failed to get data") + return []bson.M{}, 0, err + } + + // count documents + totalResults, err := collection.CountDocuments(ctx, mongoFilter) + if err != nil { + logrus.WithError(err).Error("Failed to count documents") + return []bson.M{}, 0, err + } + + return results, totalResults, nil +} From 557f720ce1d9fa4198dead6eefa40b167f457825 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Mon, 9 Sep 2024 10:00:53 +0200 Subject: [PATCH 26/29] Refactor API documentation to remove unnecessary email field in JSON examples --- .../occupi-docs/pages/api-documentation/analytics.mdx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/documentation/occupi-docs/pages/api-documentation/analytics.mdx b/documentation/occupi-docs/pages/api-documentation/analytics.mdx index c335d8ac..bf5d27b7 100644 --- a/documentation/occupi-docs/pages/api-documentation/analytics.mdx +++ b/documentation/occupi-docs/pages/api-documentation/analytics.mdx @@ -386,7 +386,6 @@ The most active employee endpoint is used to get the most active employee in the ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -441,7 +440,6 @@ The least active employee endpoint is used to get the least active employee in t ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -496,7 +494,6 @@ The workers hours endpoint is used to get the hours of all the workers in the of ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -553,7 +550,6 @@ The workers average hours endpoint is used to get the average hours of all the w ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -610,7 +606,6 @@ The workers work ratio endpoint is used to get the work ratio of all the workers ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -666,7 +661,6 @@ The workers peak office hours endpoint is used to get the peak office hours of a ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -722,7 +716,6 @@ The workers arrival departure average endpoint is used to get the arrival depart ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 @@ -778,7 +771,6 @@ The workers in office rate endpoint is used to get the in office rate of all the ```json { - "email": "abcd@gmail", // this is optional "timeFrom": "2021-01-01T00:00:00.000Z", // this is optional and will default to 1970-01-01T00:00:00.000Z "timeTo": "2021-01-01T00:00:00.000Z", // this is optional and will default to current date "limit": 50, // this is optional and will default to 50 From 9f2f782312d738d77687da40dbddfa40de1ac93f Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Mon, 9 Sep 2024 10:25:52 +0200 Subject: [PATCH 27/29] Refactor MakeEmailAndTimeFilter function to handle time range filters --- occupi-backend/pkg/database/database_helpers.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/occupi-backend/pkg/database/database_helpers.go b/occupi-backend/pkg/database/database_helpers.go index 71509092..33e8e1d2 100644 --- a/occupi-backend/pkg/database/database_helpers.go +++ b/occupi-backend/pkg/database/database_helpers.go @@ -225,11 +225,13 @@ func MakeEmailAndTimeFilter(email string, filter models.OfficeHoursFilterStruct) if email != "" { mongoFilter["email"] = email } - if filter.Filter["timeFrom"] != "" && filter.Filter["timeTo"] != "" { + + switch { + case filter.Filter["timeFrom"] != "" && filter.Filter["timeTo"] != "": mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"], "$lte": filter.Filter["timeTo"]} - } else if filter.Filter["timeTo"] != "" { + case filter.Filter["timeTo"] != "": mongoFilter["entered"] = bson.M{"$lte": filter.Filter["timeTo"]} - } else if filter.Filter["timeFrom"] != "" { + case filter.Filter["timeFrom"] != "": mongoFilter["entered"] = bson.M{"$gte": filter.Filter["timeFrom"]} } From d739dc107b8bae52c0c33ddb10792a21da9a365b Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Mon, 9 Sep 2024 10:39:30 +0200 Subject: [PATCH 28/29] Refactor GetCentrifugoSecret function to use a more descriptive variable name --- occupi-backend/configs/config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/occupi-backend/configs/config.go b/occupi-backend/configs/config.go index f1e6c411..136beec0 100644 --- a/occupi-backend/configs/config.go +++ b/occupi-backend/configs/config.go @@ -442,11 +442,11 @@ func GetCentrifugoPort() string { } func GetCentrifugoSecret() string { - secret := viper.GetString(CentrifugoSC) - if secret == "" { - secret = "CENTRIFUGO_SECRET" + csc := viper.GetString(CentrifugoSC) + if csc == "" { + csc = "CENTRIFUGO_SECRET" } - return secret + return csc } // gets the config license as defined in the config.yaml file From a9f051bbad15877cbbc1086aa1b0f45297811fe8 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Mon, 9 Sep 2024 11:46:47 +0200 Subject: [PATCH 29/29] Refactor GetAnalyticsOnHours test to include proper assertions --- occupi-backend/tests/database_test.go | 49 +++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index 50872eb3..9adde68d 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -8454,7 +8454,15 @@ func TestGetAnalyticsOnHours(t *testing.T) { mt.Run("Successful query and hoursbyday calculation", func(mt *mtest.T) { // Mock Find to return the OfficeHours document - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "email", Value: officeHours.Email}, + {Key: "entered", Value: officeHours.Entered}, + {Key: "exited", Value: officeHours.Exited}, + })) + + // Mock CountDocuments to return a count of 1 + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8467,8 +8475,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "hoursbyday") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 1 total result") - assert.Nil(t, results, "Expected non-nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and hoursbyweekday calculation", func(mt *mtest.T) { @@ -8481,6 +8489,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8493,8 +8502,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "hoursbyweekday") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and ratio calculation", func(mt *mtest.T) { @@ -8507,6 +8516,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8519,8 +8529,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "ratio") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and peakhours calculation", func(mt *mtest.T) { @@ -8533,6 +8543,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8545,8 +8556,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "peakhours") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and most calculation", func(mt *mtest.T) { @@ -8559,6 +8570,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8571,8 +8583,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "most") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and least calculation", func(mt *mtest.T) { @@ -8585,6 +8597,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8597,8 +8610,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "least") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and arrivaldeparture calculation", func(mt *mtest.T) { @@ -8611,6 +8624,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8623,8 +8637,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "arrivaldeparture") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Successful query and inofficehours calculation", func(mt *mtest.T) { @@ -8637,6 +8651,7 @@ func TestGetAnalyticsOnHours(t *testing.T) { // Mock CountDocuments to return a count of 1 mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".OfficeHoursArchive", mtest.FirstBatch, bson.D{ + {Key: "n", Value: int64(1)}, {Key: "email", Value: officeHours.Email}, {Key: "entered", Value: officeHours.Entered}, {Key: "exited", Value: officeHours.Exited}, @@ -8649,8 +8664,8 @@ func TestGetAnalyticsOnHours(t *testing.T) { results, total, err := database.GetAnalyticsOnHours(ctx, appsession, "", filter, "inofficehours") assert.NoError(t, err, "Expected no error for successful query") - assert.Equal(t, int64(0), total, "Expected 0 total result") - assert.Nil(t, results, "Expected nil results") + assert.Equal(t, int64(1), total, "Expected 1 total result") + assert.NotNil(t, results, "Expected non-nil results") }) mt.Run("Failed aggregate query", func(mt *mtest.T) {