Skip to content

Commit

Permalink
Merge pull request #364 from COS301-SE-2024/feat/backend/booking-anal…
Browse files Browse the repository at this point in the history
…ytics

Feat/backend/booking analytics
  • Loading branch information
waveyboym authored Sep 17, 2024
2 parents e809880 + 47cfe8d commit f1c0587
Show file tree
Hide file tree
Showing 10 changed files with 966 additions and 48 deletions.
176 changes: 175 additions & 1 deletion documentation/occupi-docs/pages/api-documentation/analytics-usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ all the users in the office space.
- [Workers peak office hours](#workers-peak-office-hours)
- [Workers arrival departure average](#workers-arival-departure-average)
- [Workers in office rate](#workers-in-office-rate)
- [Top Bookings](#top-bookings)
- [Bookings historical](#bookings-historical)
- [Bookings current](#bookings-current)

## Base URL

Expand Down Expand Up @@ -808,4 +811,175 @@ The workers in office rate endpoint is used to get the in office rate of all the
"error": "Failed to fetch user analytics!",
"status": 500,
}
```
```

### Top Bookings

The top bookings endpoint is used to get the top 3 bookings in the office space.

- **URL**

`/analytics/top-bookings`

- **Method**

`GET`

- **Request Body**

```json
{
"creator": "abcd@gmail", // this is optional
"attendees": ["abcd@gmail", "efgh@gmail.com"], // 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 bookings to select
"page": 1 // this is optional and will default to 1
}
```

- **URL Params**

```
/analytics/top-bookings?creator=abcd@gmail&attendees=abcd@gmail,egfh@gmail.com&timeFrom=2021-01-01T00:00:00.000Z&timeTo=2021-01-01T00:00:00.000Z&limit=50&page=1
```
- **Success Response**
- **Code:** 200
- **Content:**
```json
{
"response": "Successfully fetched top bookings!",
"data": [data],
"totalResults": 1,
"totalPages": 1,
"currentPage": 1,
"status": 200,
}
```

- **Error Response**

- **Code:** 500
- **Content:**
```json
{
"error": "Failed to fetch top bookings!",
"status": 500,
}
```

### Bookings historical

The bookings historical endpoint is used to get the historical bookings in the office space.

- **URL**

`/analytics/bookings-historical`

- **Method**

`GET`

- **Request Body**

```json
{
"creator": "abcd@gmail", // this is optional
"attendees": ["abcd@gmail", "efgh@gmail.com"], // 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 bookings to select
"page": 1 // this is optional and will default to 1
}
```

- **URL Params**

```
/analytics/bookings-historical?creator=abcd@gmail&attendees=abcd@gmail,egfh@gmail.com&timeFrom=2021-01-01T00:00:00.000Z&timeTo=2021-01-01T00:00:00.000Z&limit=50&page=1
```
- **Success Response**
- **Code:** 200
- **Content:**
```json
{
"response": "Successfully fetched historical bookings!",
"data": [data],
"totalResults": 1,
"totalPages": 1,
"currentPage": 1,
"status": 200,
}
```

- **Error Response**

- **Code:** 500
- **Content:**
```json
{
"error": "Failed to fetch historical bookings!",
"status": 500,
}
```

### Bookings current

The bookings current endpoint is used to get the current bookings in the office space.

- **URL**

`/analytics/bookings-current`

- **Method**

`GET`

- **Request Body**

```json
{
"creator": "abcd@gmail", // this is optional
"attendees": ["abcd@gmail", "efgh@gmail.com"], // 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 bookings to select
"page": 1 // this is optional and will default to 1
}
```

- **URL Params**

```
/analytics/bookings-current?creator=abcd@gmail&attendees=abcd@gmail,egfh@gmail.com&timeFrom=2021-01-01T00:00:00.000Z&timeTo=2021-01-01T00:00:00.000Z&limit=50&page=1
```
- **Success Response**
- **Code:** 200
- **Content:**
```json
{
"response": "Successfully fetched current bookings!",
"data": [data],
"totalResults": 1,
"totalPages": 1,
"currentPage": 1,
"status": 200,
}
```

- **Error Response**

- **Code:** 500
- **Content:**
```json
{
"error": "Failed to fetch current bookings!",
"status": 500,
}
```
122 changes: 107 additions & 15 deletions occupi-backend/pkg/analytics/analytics.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package analytics

import (
"fmt"

"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models"
"go.mongodb.org/mongo-driver/bson"
)

func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson.D {
func CreateOfficeHoursMatchFilter(email string, filter models.AnalyticsFilterStruct) bson.D {
// Create a match filter
matchFilter := bson.D{}

Expand All @@ -31,9 +33,43 @@ func CreateMatchFilter(email string, filter models.OfficeHoursFilterStruct) bson
return matchFilter
}

func CreateBookingMatchFilter(creatorEmail string, attendeesEmail []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.D {
// Create a match filter
matchFilter := bson.D{}

// Conditionally add the email filter if email is not empty
if creatorEmail != "" {
matchFilter = append(matchFilter, bson.E{Key: "creator", Value: bson.D{{Key: "$eq", Value: creatorEmail}}})
}

// Conditionally add the attendees filter if emails is not of length 0
if len(attendeesEmail) > 0 {
fmt.Println(attendeesEmail)
// print len of attendeesEmail
fmt.Println(len(attendeesEmail))
matchFilter = append(matchFilter, bson.E{Key: "emails", Value: bson.D{{Key: "$in", Value: attendeesEmail}}})
}

// 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"]})
}
if filter.Filter["timeTo"] != "" {
timeRangeFilter = append(timeRangeFilter, bson.E{Key: "$lte", Value: filter.Filter["timeTo"]})
}

// If there are time range filters, append them to the match filter
if len(timeRangeFilter) > 0 && len(filter.Filter) > 0 {
matchFilter = append(matchFilter, bson.E{Key: dateFilter, Value: timeRangeFilter})
}

return matchFilter
}

// GroupOfficeHoursByDay function with total hours calculation
func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct) bson.A {
matchFilter := CreateMatchFilter(email, filter)
func GroupOfficeHoursByDay(email string, filter models.AnalyticsFilterStruct) bson.A {
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

return bson.A{
// Stage 1: Match filter conditions (email and time range)
Expand Down Expand Up @@ -91,9 +127,9 @@ func GroupOfficeHoursByDay(email string, filter models.OfficeHoursFilterStruct)
}
}

func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A {
func AverageOfficeHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateMatchFilter(email, filter)
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

return bson.A{
// Stage 1: Match filter conditions (email and time range)
Expand Down Expand Up @@ -172,9 +208,9 @@ func AverageOfficeHoursByWeekday(email string, filter models.OfficeHoursFilterSt
}
}

func RatioInOutOfficeByWeekday(email string, filter models.OfficeHoursFilterStruct) bson.A {
func RatioInOutOfficeByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateMatchFilter(email, filter)
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

return bson.A{
// Stage 1: Match filter conditions (email and time range)
Expand Down Expand Up @@ -262,9 +298,9 @@ 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 {
func BusiestHoursByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateMatchFilter(email, filter)
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

return bson.A{
// Stage 1: Match filter conditions (email and time range)
Expand Down Expand Up @@ -373,9 +409,9 @@ 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 {
func LeastMostInOfficeWorker(email string, filter models.AnalyticsFilterStruct, sort bool) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateMatchFilter(email, filter)
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

var sortV int
if sort {
Expand Down Expand Up @@ -506,9 +542,9 @@ 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 {
func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.AnalyticsFilterStruct) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateMatchFilter(email, filter)
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

return bson.A{
// Stage 1: Match filter conditions (email and time range)
Expand Down Expand Up @@ -760,9 +796,9 @@ func AverageArrivalAndDepartureTimesByWeekday(email string, filter models.Office
}

// CalculateInOfficeRate function to calculate absenteeism rates
func CalculateInOfficeRate(email string, filter models.OfficeHoursFilterStruct) bson.A {
func CalculateInOfficeRate(email string, filter models.AnalyticsFilterStruct) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateMatchFilter(email, filter)
matchFilter := CreateOfficeHoursMatchFilter(email, filter)

return bson.A{
// Stage 1: Match filter conditions (email and time range)
Expand Down Expand Up @@ -1031,3 +1067,59 @@ func CalculateInOfficeRate(email string, filter models.OfficeHoursFilterStruct)
},
}
}

func GetTop3MostBookedRooms(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, dateFilter)

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: Group by the room ID to calculate the total bookings
bson.D{{Key: "$group", Value: bson.D{
{Key: "_id", Value: "$roomId"},
{Key: "roomName", Value: bson.D{{Key: "$first", Value: "$roomName"}}},
{Key: "floorNo", Value: bson.D{{Key: "$first", Value: "$floorNo"}}},
{Key: "creators", Value: bson.D{{Key: "$push", Value: "$creator"}}},
{Key: "emails", Value: bson.D{{Key: "$push", Value: "$emails"}}},
{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
}}},
// Stage 5: Sort by count
bson.D{{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},
// Stage 6: Limit to the top 3 results
bson.D{{Key: "$limit", Value: 3}},
}
}

func AggregateBookings(creatorEmail string, attendeeEmails []string, filter models.AnalyticsFilterStruct, dateFilter string) bson.A {
// Create the match filter using the reusable function
matchFilter := CreateBookingMatchFilter(creatorEmail, attendeeEmails, filter, dateFilter)
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: Get all bookings without grouping
bson.D{{Key: "$project", Value: bson.D{
{Key: "_id", Value: 0},
{Key: "occupiID", Value: "$occupiId"},
{Key: "roomName", Value: "$roomName"},
{Key: "roomId", Value: "$roomId"},
{Key: "emails", Value: "$emails"},
{Key: "checkedIn", Value: "$checkedIn"},
{Key: "creators", Value: "$creator"},
{Key: "floorNo", Value: "$floorNo"},
{Key: "date", Value: "$date"},
{Key: "start", Value: "$start"},
{Key: "end", Value: "$end"},
}}},
// Stage 5: Sort by date
bson.D{{Key: "$sort", Value: bson.D{{Key: "date", Value: 1}}}},
}
}
Loading

0 comments on commit f1c0587

Please sign in to comment.