diff --git a/src/goapp/controller/item/item-controller-dto.go b/src/goapp/controller/item/item-controller-dto.go index 5df672f..1edcad5 100644 --- a/src/goapp/controller/item/item-controller-dto.go +++ b/src/goapp/controller/item/item-controller-dto.go @@ -24,3 +24,27 @@ type RespondePageData struct { ApproveText string RejectText string } + +type GetItemsByApproverResponse struct { + Data []Item `json:"data"` + Page int `json:"page"` + Filter int `json:"filter"` + Total int `json:"total"` +} + +type PostProcessMultipleResponseRequest struct { + Requests []model.ProcessResponseRequest `json:"request"` +} + +type Item struct { + Id string `json:"id"` + Subject string `json:"subject"` + Application string `json:"application"` + ApplicationId string `json:"applicationId"` + Module string `json:"module"` + ModuleId string `json:"moduleId"` + RequestedBy string `json:"requestedBy"` + RequestedOn string `json:"requestedOn"` + Approvers []string `json:"approvers"` + Body string `json:"body"` +} diff --git a/src/goapp/controller/item/item-controller-interface.go b/src/goapp/controller/item/item-controller-interface.go index 060d742..c1aa48a 100644 --- a/src/goapp/controller/item/item-controller-interface.go +++ b/src/goapp/controller/item/item-controller-interface.go @@ -4,14 +4,17 @@ import "net/http" type ItemController interface { GetItems(w http.ResponseWriter, r *http.Request) + GetItemsByApprover(w http.ResponseWriter, r *http.Request) CreateItem(w http.ResponseWriter, r *http.Request) ProcessResponse(w http.ResponseWriter, r *http.Request) + ProcessMultipleResponse(w http.ResponseWriter, r *http.Request) ReassignItem(w http.ResponseWriter, r *http.Request) } type ItemPageController interface { MyRequests(w http.ResponseWriter, r *http.Request) MyApprovals(w http.ResponseWriter, r *http.Request) + MultipleApprovals(w http.ResponseWriter, r *http.Request) RespondToItem(w http.ResponseWriter, r *http.Request) ReassignApproval(w http.ResponseWriter, r *http.Request) } diff --git a/src/goapp/controller/item/item-controller.go b/src/goapp/controller/item/item-controller.go index 1155a9c..e4d9de2 100644 --- a/src/goapp/controller/item/item-controller.go +++ b/src/goapp/controller/item/item-controller.go @@ -8,6 +8,7 @@ import ( "main/service" "net/http" "strconv" + "sync" "time" "github.com/gorilla/mux" @@ -102,6 +103,84 @@ func (c *itemController) GetItems(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(result) } +func (c *itemController) GetItemsByApprover(w http.ResponseWriter, r *http.Request) { + // Get all items + var filterOptions model.FilterOptions + + user, err := c.Authenticator.GetAuthenticatedUser(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + params := r.URL.Query() + + var requestType string + if params.Has("requestType") { + requestType = params["requestType"][0] + } + + var organization string + if params.Has("organization") { + organization = params["organization"][0] + } + + filterOptions.Page = 0 // Default page is 1 which is 0 in the database + if params.Has("page") { + filterOptions.Page, err = strconv.Atoi(params["page"][0]) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + filterOptions.Page = filterOptions.Page - 1 + } + + if params.Has("filter") { + filterOptions.Filter, err = strconv.Atoi(params["filter"][0]) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + filterOptions.Filter = 50 + } + + result, total, err := c.Service.Item.GetByApprover(user.Email, requestType, organization, filterOptions) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var response GetItemsByApproverResponse + for _, item := range result { + itemResponse := Item{ + Id: item.Id, + Subject: item.Subject, + Application: item.Application, + ApplicationId: item.ApplicationId, + Module: item.Module, + ModuleId: item.ModuleId, + RequestedBy: item.RequestedBy, + RequestedOn: item.Created, + Approvers: item.Approvers, + Body: item.Body, + } + + response.Data = append(response.Data, itemResponse) + } + if len(response.Data) == 0 { + response.Data = []Item{} + } + response.Page = filterOptions.Page + 1 + response.Filter = filterOptions.Filter + response.Total = total + + // Return the result + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + func (c *itemController) CreateItem(w http.ResponseWriter, r *http.Request) { // Decode payload var req model.ItemInsertRequest @@ -204,6 +283,78 @@ func (c *itemController) ProcessResponse(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusOK) } +func (c *itemController) ProcessMultipleResponse(w http.ResponseWriter, r *http.Request) { + // Decode payload + var req PostProcessMultipleResponseRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + user, err := c.Authenticator.GetAuthenticatedUser(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + vars := mux.Vars(r) + response := vars["response"] + + var isApproved string + if response == "approve" { + isApproved = "true" + } else if response == "reject" { + isApproved = "false" + } else { + http.Error(w, "Invalid response", http.StatusBadRequest) + return + } + + for index, _ := range req.Requests { + req.Requests[index].ApproverEmail = user.Email + req.Requests[index].IsApproved = isApproved + } + + var wg sync.WaitGroup + concurrencyLimit := make(chan struct{}, 50) // Limit to 50 concurrent goroutines + + // Validate All Requests + for _, request := range req.Requests { + wg.Add(1) + concurrencyLimit <- struct{}{} // Acquire a slot + + go func() { + defer wg.Done() + defer func() { <-concurrencyLimit }() // Release the slot + + // Validate payload + valid, err := c.Service.Item.ValidateItem(request) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if !valid { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + // Update item response + err = c.Service.Item.UpdateItemResponse(request) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Post callback + c.postCallback(request.ItemId) + }() + } + + w.WriteHeader(http.StatusOK) +} + func (c *itemController) ReassignItem(w http.ResponseWriter, r *http.Request) { // Get user info user, err := c.Authenticator.GetAuthenticatedUser(r) diff --git a/src/goapp/controller/item/item-page-controller.go b/src/goapp/controller/item/item-page-controller.go index b050ead..7e46abf 100644 --- a/src/goapp/controller/item/item-page-controller.go +++ b/src/goapp/controller/item/item-page-controller.go @@ -214,3 +214,30 @@ func (c *itemPageController) ReassignApproval(w http.ResponseWriter, r *http.Req } } } + +func (c *itemPageController) MultipleApprovals(w http.ResponseWriter, r *http.Request) { + user, err := c.Service.Authenticator.GetAuthenticatedUser(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + application, err := c.Service.Application.GetApplicationById(c.CommunityPortalAppId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + b, err := json.Marshal(application) + if err != nil { + fmt.Println(err) + return + } + + t, d := c.Service.Template.UseTemplate("multiple-approvals", r.URL.Path, *user, string(b)) + + err = t.Execute(w, d) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/src/goapp/model/common.go b/src/goapp/model/common.go new file mode 100644 index 0000000..d576812 --- /dev/null +++ b/src/goapp/model/common.go @@ -0,0 +1,9 @@ +package model + +type FilterOptions struct { + Filter int + Page int + Search string + Orderby string + Ordertype string +} diff --git a/src/goapp/model/item.go b/src/goapp/model/item.go index 5cba394..8b920d7 100644 --- a/src/goapp/model/item.go +++ b/src/goapp/model/item.go @@ -3,6 +3,7 @@ package model type Item struct { Id string `json:"id"` Application string `json:"application"` + ApplicationId string `json:"applicationId"` ApproverRemarks string `json:"approverRemarks"` Body string `json:"body"` Created string `json:"created"` @@ -10,6 +11,7 @@ type Item struct { DateSent string `json:"dateSent"` IsApproved bool `json:"isApproved"` Module string `json:"module"` + ModuleId string `json:"moduleId"` Subject string `json:"subject"` ApproveText string `json:"approveText"` CallbackUrl string `json:"callbackUrl"` diff --git a/src/goapp/public/css/output.css b/src/goapp/public/css/output.css index 2c929e6..c8a9035 100644 --- a/src/goapp/public/css/output.css +++ b/src/goapp/public/css/output.css @@ -1811,6 +1811,14 @@ select { bottom: 0px; } +.bottom-0 { + bottom: 0px; +} + +.bottom-10 { + bottom: 2.5rem; +} + .bottom-full { bottom: 100%; } @@ -1831,6 +1839,14 @@ select { right: 0px; } +.right-1 { + right: 0.25rem; +} + +.right-5 { + right: 1.25rem; +} + .right-full { right: 100%; } @@ -1847,6 +1863,10 @@ select { top: 50%; } +.top-5 { + top: 1.25rem; +} + .top-8 { top: 2rem; } @@ -1871,6 +1891,10 @@ select { z-index: 40; } +.col-span-2 { + grid-column: span 2 / span 2; +} + .float-right { float: right; } @@ -1917,6 +1941,11 @@ select { margin-bottom: -0.5rem; } +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + .mx-auto { margin-left: auto; margin-right: auto; @@ -2182,6 +2211,14 @@ select { height: 2rem; } +.h-96 { + height: 24rem; +} + +.h-\[calc\(100vh-13rem\)\] { + height: calc(100vh - 13rem); +} + .max-h-60 { max-height: 15rem; } @@ -2198,6 +2235,10 @@ select { min-height: 100vh; } +.w-1\/2 { + width: 50%; +} + .w-10 { width: 2.5rem; } @@ -2242,6 +2283,10 @@ select { width: 100%; } +.w-screen { + width: 100vw; +} + .max-w-4xl { max-width: 56rem; } @@ -2364,6 +2409,10 @@ select { cursor: default; } +.cursor-not-allowed { + cursor: not-allowed; +} + .cursor-pointer { cursor: pointer; } @@ -2644,6 +2693,10 @@ select { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-rows-2 { + grid-template-rows: repeat(2, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -3335,6 +3388,11 @@ select { border-style: none; } +.border-blue-500 { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + .border-gray-100 { --tw-border-opacity: 1; border-color: rgb(243 244 246 / var(--tw-border-opacity)); @@ -3360,6 +3418,11 @@ select { border-color: rgb(252 165 165 / var(--tw-border-opacity)); } +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + .border-transparent { border-color: transparent; } @@ -3404,6 +3467,10 @@ select { background-color: rgb(107 114 128 / var(--tw-bg-opacity)); } +.bg-gray-500\/75 { + background-color: rgb(107 114 128 / 0.75); +} + .bg-gray-600 { --tw-bg-opacity: 1; background-color: rgb(75 85 99 / var(--tw-bg-opacity)); @@ -3449,6 +3516,11 @@ select { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + .bg-opacity-75 { --tw-bg-opacity: 0.75; } @@ -3571,6 +3643,10 @@ select { padding: 0.25rem; } +.p-2 { + padding: 0.5rem; +} + .p-3 { padding: 0.75rem; } @@ -3593,6 +3669,11 @@ select { padding-right: 0.125rem; } +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -3658,6 +3739,11 @@ select { padding-bottom: 1rem; } +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; @@ -3672,6 +3758,10 @@ select { padding-bottom: 0.5rem; } +.pb-3 { + padding-bottom: 0.75rem; +} + .pb-4 { padding-bottom: 1rem; } @@ -3696,6 +3786,10 @@ select { padding-left: 1rem; } +.pl-5 { + padding-left: 1.25rem; +} + .pl-6 { padding-left: 1.5rem; } @@ -3720,6 +3814,10 @@ select { padding-right: 3rem; } +.pr-2 { + padding-right: 0.5rem; +} + .pr-4 { padding-right: 1rem; } @@ -3834,6 +3932,11 @@ select { line-height: 1.25rem; } +.text-sm\/6 { + font-size: 0.875rem; + line-height: 1.5rem; +} + .text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -5998,6 +6101,11 @@ select { background-color: rgb(254 202 202 / var(--tw-bg-opacity)); } +.hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + .hover\:font-bold:hover { font-weight: 700; } @@ -6174,6 +6282,27 @@ select { background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } +.group:hover .group-hover\:block { + display: block; +} + +.group:hover .group-hover\:hidden { + display: none; +} + +.group:hover .group-hover\:h-6 { + height: 1.5rem; +} + +.group:hover .group-hover\:w-6 { + width: 1.5rem; +} + +.group:hover .group-hover\:text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + .group:hover .group-hover\:text-gray-700 { --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity)); diff --git a/src/goapp/repository/item/item-repository-interface.go b/src/goapp/repository/item/item-repository-interface.go index e7befba..efaaf63 100644 --- a/src/goapp/repository/item/item-repository-interface.go +++ b/src/goapp/repository/item/item-repository-interface.go @@ -7,6 +7,7 @@ import ( type ItemRepository interface { GetFailedCallbacks() ([]string, error) GetItemById(id string) (*model.Item, error) + GetItemsByApprover(approver, requestType, organization string, filterOptions model.FilterOptions) (items []model.Item, total int, err error) GetItemsBy(itemOptions model.ItemOptions) ([]model.Item, error) GetTotalItemsBy(itemOptions model.ItemOptions) (int, error) InsertItem(appModuleId, subject, body, requesterEmail string) (string, error) diff --git a/src/goapp/repository/item/item-repository.go b/src/goapp/repository/item/item-repository.go index 87607bf..875db49 100644 --- a/src/goapp/repository/item/item-repository.go +++ b/src/goapp/repository/item/item-repository.go @@ -6,6 +6,7 @@ import ( db "main/infrastructure/database" "main/model" "strconv" + "strings" "time" ) @@ -98,6 +99,72 @@ func (r *itemRepository) GetItemById(id string) (*model.Item, error) { return &item, nil } +func (r *itemRepository) GetItemsByApprover(approver, requestType, organization string, filterOptions model.FilterOptions) (items []model.Item, total int, err error) { + var params []interface{} + + if requestType != "" { + params = append(params, sql.Named("RequestType", requestType)) + } + + if organization != "" { + params = append(params, sql.Named("Organization", organization)) + } + + if filterOptions.Filter != 0 { + params = append(params, sql.Named("Filter", filterOptions.Filter)) + } + + if filterOptions.Page != 0 { + offset := filterOptions.Page * filterOptions.Filter + params = append(params, sql.Named("Offset", offset)) + } + + params = append(params, sql.Named("Approver", approver)) + + rows, err := r.Query("PR_Items_Select_ByApprover", params...) + if err != nil { + return []model.Item{}, 0, err + } + defer rows.Close() + + result, err := r.RowsToMap(rows) + if err != nil { + return []model.Item{}, 0, err + } + + for _, v := range result { + item := model.Item{ + Id: v["Id"].(string), + Subject: v["Subject"].(string), + Application: v["ApplicationName"].(string), + ApplicationId: v["ApplicationId"].(string), + Module: v["ApplicationModuleName"].(string), + ModuleId: v["ApplicationModuleId"].(string), + Created: v["Created"].(time.Time).String(), + RequestedBy: v["RequestedBy"].(string), + Body: v["Body"].(string), + } + approvers := v["Approvers"].(string) + approversArray := strings.Split(approvers, ",") + if len(approversArray) > 0 { + item.Approvers = approversArray + } + + items = append(items, item) + } + + if rows.NextResultSet() { + if rows.Next() { + err = rows.Scan(&total) + if err != nil { + return []model.Item{}, 0, err + } + } + } + + return items, total, nil +} + func (r *itemRepository) GetItemsBy(itemOptions model.ItemOptions) ([]model.Item, error) { var params []interface{} diff --git a/src/goapp/routes.go b/src/goapp/routes.go index 25a586b..4f71e4c 100644 --- a/src/goapp/routes.go +++ b/src/goapp/routes.go @@ -2,6 +2,7 @@ package main func setPageRoutes() { httpRouter.GET("/", m.Chain(ctrl.ItemPage.MyRequests, m.AzureAuth())) + httpRouter.GET("/multiple-approvals", m.Chain(ctrl.ItemPage.MultipleApprovals, m.AzureAuth())) httpRouter.GET("/myapprovals", m.Chain(ctrl.ItemPage.MyApprovals, m.AzureAuth())) httpRouter.GET("/response/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}", m.Chain(ctrl.ItemPage.RespondToItem, m.AzureAuth())) httpRouter.GET("/responsereassigned/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}/{ApproveText}/{RejectText}", m.Chain(ctrl.ItemPage.ReassignApproval, m.AzureAuth())) @@ -16,6 +17,8 @@ func setApiRoutes() { httpRouter.GET("/api/request/types", m.Chain(ctrl.ApplicationModule.GetRequestTypes, m.AzureAuth())) httpRouter.POST("/api/request", m.Chain(ctrl.Item.CreateItem, m.ManagedIdentityAuth())) httpRouter.POST("/api/process", m.Chain(ctrl.Item.ProcessResponse, m.AzureAuth())) + httpRouter.POST("/api/multiple/response/{response}", m.Chain(ctrl.Item.ProcessMultipleResponse, m.AzureAuth())) + httpRouter.GET("/api/approver/me/items", m.Chain(ctrl.Item.GetItemsByApprover, m.AzureAuth())) httpRouter.GET("/api/items/type/{type:[0-2]+}/status/{status:[0-3]+}", m.Chain(ctrl.Item.GetItems, m.AzureAuth())) httpRouter.GET("/api/search/users/{search}", m.Chain(ctrl.User.SearchUserFromActiveDirectory, m.AzureAuth())) httpRouter.GET("/api/responsereassignedapi/{itemGuid}/{approver}/{ApplicationId}/{ApplicationModuleId}/{ApproveText}/{RejectText}", m.Chain(ctrl.Item.ReassignItem, m.AzureAuth())) diff --git a/src/goapp/service/item/item-service-interface.go b/src/goapp/service/item/item-service-interface.go index f267fd8..7e39d38 100644 --- a/src/goapp/service/item/item-service-interface.go +++ b/src/goapp/service/item/item-service-interface.go @@ -8,6 +8,7 @@ type ItemService interface { GetFailedCallbacks() ([]string, error) GetItemById(id string) (*model.Item, error) GetAll(itemOptions model.ItemOptions) (model.Response, error) + GetByApprover(approver, requestType, organization string, filterOptions model.FilterOptions) (items []model.Item, total int, err error) InsertItem(item model.ItemInsertRequest) (string, error) ItemIsAuthorized(appId, appModuleId, itemId, approverEmail string) (*model.ItemIsAuthorized, error) UpdateItemApproverEmail(itemId, approverEmail, username string) error diff --git a/src/goapp/service/item/item-service.go b/src/goapp/service/item/item-service.go index 1715218..c2803f3 100644 --- a/src/goapp/service/item/item-service.go +++ b/src/goapp/service/item/item-service.go @@ -83,6 +83,14 @@ func (s *itemService) GetAll(itemOptions model.ItemOptions) (model.Response, err return result, nil } +func (s *itemService) GetByApprover(approver, requestType, organization string, filterOptions model.FilterOptions) (items []model.Item, total int, err error) { + items, total, err = s.Repository.Item.GetItemsByApprover(approver, requestType, organization, filterOptions) + if err != nil { + return nil, 0, err + } + return items, total, nil +} + func (s *itemService) InsertItem(item model.ItemInsertRequest) (string, error) { id, err := s.Repository.Item.InsertItem(item.ApplicationModuleId, item.Subject, item.Body, item.RequesterEmail) if err != nil { diff --git a/src/goapp/templates/multiple-approvals.html b/src/goapp/templates/multiple-approvals.html new file mode 100644 index 0000000..a49db6d --- /dev/null +++ b/src/goapp/templates/multiple-approvals.html @@ -0,0 +1,733 @@ +{{ define "content" }} +
Approve Multiple Requests
+ +
+
+
+
+

Pending Requests

+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + + + +
+ +
+
+
+
+

Selected Requests

+
+
+
    + +
+
+
+ Total items selected: +
+
+
+ +
+ +
+

+ General remarks will apply to all requests without any custom remarks. +

+
+
+
+ + +
+
+
+ + + + + + +
+ + +{{ end }} \ No newline at end of file diff --git a/src/goapp/templates/myapprovals.html b/src/goapp/templates/myapprovals.html index 37d02ff..0ea0306 100644 --- a/src/goapp/templates/myapprovals.html +++ b/src/goapp/templates/myapprovals.html @@ -4,6 +4,10 @@
+