From 3c71ab35ffbbd222cc63bf5723b95c2ebbcd70dd Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 24 Jan 2025 04:34:51 +0530 Subject: [PATCH] Feat: Add SLAs per team - Feat: Provide the ability to change SLAs for conversations; this will start a new deadline calculation from the time the SLA is set. --- cmd/business_hours.go | 7 +- cmd/conversation.go | 61 +-- cmd/init.go | 8 +- cmd/sla.go | 5 +- cmd/teams.go | 6 +- .../components/admin/team/teams/TeamForm.vue | 68 +++- .../admin/team/teams/teamFormSchema.js | 1 + .../list/ConversationListItem.vue | 2 +- .../conversation/sidebar/ConversationInfo.vue | 2 +- internal/business_hours/business_hours.go | 21 +- internal/conversation/conversation.go | 24 +- internal/conversation/message.go | 2 +- internal/conversation/models/models.go | 6 +- internal/conversation/queries.sql | 5 + internal/sla/calculator.go | 13 +- internal/sla/models/models.go | 32 +- internal/sla/queries.sql | 144 +++---- internal/sla/sla.go | 371 +++++++++--------- internal/team/models/models.go | 3 +- internal/team/queries.sql | 12 +- internal/team/team.go | 8 +- internal/user/models/models.go | 6 +- internal/workerpool/workerpool.go | 46 --- schema.sql | 29 +- 24 files changed, 424 insertions(+), 458 deletions(-) delete mode 100644 internal/workerpool/workerpool.go diff --git a/cmd/business_hours.go b/cmd/business_hours.go index ed7e1cab..3c75a587 100644 --- a/cmd/business_hours.go +++ b/cmd/business_hours.go @@ -3,6 +3,7 @@ package main import ( "strconv" + businessHours "github.com/abhinavxd/libredesk/internal/business_hours" models "github.com/abhinavxd/libredesk/internal/business_hours/models" "github.com/abhinavxd/libredesk/internal/envelope" "github.com/valyala/fasthttp" @@ -30,10 +31,12 @@ func handleGetBusinessHour(r *fastglue.Request) error { if err != nil || id == 0 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid business hour `id`.", nil, envelope.InputError) } - businessHour, err := app.businessHours.Get(id) if err != nil { - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "") + if err == businessHours.ErrBusinessHoursNotFound { + return r.SendErrorEnvelope(fasthttp.StatusNotFound, err.Error(), nil, envelope.NotFoundError) + } + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error fetching business hour", nil, "") } return r.SendEnvelope(businessHour) } diff --git a/cmd/conversation.go b/cmd/conversation.go index 582bcdce..aae2fc1d 100644 --- a/cmd/conversation.go +++ b/cmd/conversation.go @@ -39,10 +39,10 @@ func handleGetAllConversations(r *fastglue.Request) error { total = conversations[0].Total } - // Calculate SLA deadlines if conversation has an SLA policy. + // Set deadlines for SLA if conversation has a policy for i := range conversations { if conversations[i].SLAPolicyID.Int != 0 { - calculateSLA(app, &conversations[i]) + setSLADeadlines(app, &conversations[i]) } } @@ -75,10 +75,10 @@ func handleGetAssignedConversations(r *fastglue.Request) error { total = conversations[0].Total } - // Calculate SLA deadlines if conversation has an SLA policy. + // Set deadlines for SLA if conversation has a policy for i := range conversations { if conversations[i].SLAPolicyID.Int != 0 { - calculateSLA(app, &conversations[i]) + setSLADeadlines(app, &conversations[i]) } } @@ -111,10 +111,10 @@ func handleGetUnassignedConversations(r *fastglue.Request) error { total = conversations[0].Total } - // Calculate SLA deadlines if conversation has an SLA policy. + // Set deadlines for SLA if conversation has a policy for i := range conversations { if conversations[i].SLAPolicyID.Int != 0 { - calculateSLA(app, &conversations[i]) + setSLADeadlines(app, &conversations[i]) } } @@ -189,10 +189,10 @@ func handleGetViewConversations(r *fastglue.Request) error { total = conversations[0].Total } - // Calculate SLA deadlines if conversation has an SLA policy. + // Set deadlines for SLA if conversation has a policy for i := range conversations { if conversations[i].SLAPolicyID.Int != 0 { - calculateSLA(app, &conversations[i]) + setSLADeadlines(app, &conversations[i]) } } @@ -241,10 +241,10 @@ func handleGetTeamUnassignedConversations(r *fastglue.Request) error { total = conversations[0].Total } - // Calculate SLA deadlines if conversation has an SLA policy. + // Set deadlines for SLA if conversation has a policy for i := range conversations { if conversations[i].SLAPolicyID.Int != 0 { - calculateSLA(app, &conversations[i]) + setSLADeadlines(app, &conversations[i]) } } @@ -280,9 +280,9 @@ func handleGetConversation(r *fastglue.Request) error { return sendErrorEnvelope(r, envelope.NewError(envelope.PermissionError, "Permission denied", nil)) } - // Calculate SLA deadlines if conversation has an SLA policy. + // Set deadlines for SLA if conversation has a policy if conversation.SLAPolicyID.Int != 0 { - calculateSLA(app, &conversation) + setSLADeadlines(app, &conversation) } return r.SendEnvelope(conversation) } @@ -406,22 +406,26 @@ func handleUpdateTeamAssignee(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } - conversation, err := app.conversation.GetConversation(0, uuid) + conversation, err := enforceConversationAccess(app, uuid, user) if err != nil { return sendErrorEnvelope(r, err) } - allowed, err := app.authz.EnforceConversationAccess(user, conversation) - if err != nil { - return sendErrorEnvelope(r, err) - } - if !allowed { - return sendErrorEnvelope(r, envelope.NewError(envelope.PermissionError, "Permission denied", nil)) - } if err := app.conversation.UpdateConversationTeamAssignee(uuid, assigneeID, user); err != nil { return sendErrorEnvelope(r, err) } - // TODO: Set SLA if the team has an SLA policy set. + // Apply SLA policy if team has changed and has an SLA policy. + if conversation.AssignedTeamID.Int != assigneeID && assigneeID != 0 { + team, err := app.team.Get(assigneeID) + if err != nil { + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error fetching team for setting SLA", nil, envelope.GeneralError) + } + if team.SLAPolicyID.Int != 0 { + if err := app.conversation.ApplySLA(conversation.UUID, conversation.ID, team.SLAPolicyID.Int, user); err != nil { + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error applying SLA policy", nil, envelope.GeneralError) + } + } + } // Evaluate automation rules on team assignment. app.automation.EvaluateConversationUpdateRules(uuid, models.EventConversationTeamAssigned) @@ -596,15 +600,18 @@ func enforceConversationAccess(app *App, uuid string, user umodels.User) (*cmode return &conversation, nil } -// calculateSLA calculates the SLA deadlines and sets them on the conversation. -func calculateSLA(app *App, conversation *cmodels.Conversation) error { - firstRespAt, resolutionDueAt, err := app.sla.CalculateConversationDeadlines(conversation.CreatedAt, conversation.AssignedTeamID.Int, conversation.SLAPolicyID.Int) +// setSLADeadlines gets the latest SLA deadlines for a conversation and sets them. +func setSLADeadlines(app *App, conversation *cmodels.Conversation) error { + if conversation.ID < 1 { + return nil + } + first, resolution, err := app.sla.GetLatestDeadlines(conversation.ID) if err != nil { - app.lo.Error("error calculating SLA deadlines for conversation", "id", conversation.ID, "error", err) + app.lo.Error("error getting SLA deadlines", "id", conversation.ID, "error", err) return err } - conversation.FirstReplyDueAt = null.NewTime(firstRespAt, firstRespAt != time.Time{}) - conversation.ResolutionDueAt = null.NewTime(resolutionDueAt, resolutionDueAt != time.Time{}) + conversation.FirstResponseDueAt = null.NewTime(first, first != time.Time{}) + conversation.ResolutionDueAt = null.NewTime(resolution, resolution != time.Time{}) return nil } diff --git a/cmd/init.go b/cmd/init.go index 466b2fdd..3f100f03 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -41,7 +41,6 @@ import ( tmpl "github.com/abhinavxd/libredesk/internal/template" "github.com/abhinavxd/libredesk/internal/user" "github.com/abhinavxd/libredesk/internal/view" - "github.com/abhinavxd/libredesk/internal/workerpool" "github.com/abhinavxd/libredesk/internal/ws" "github.com/jmoiron/sqlx" "github.com/knadh/go-i18n" @@ -278,10 +277,9 @@ func initBusinessHours(db *sqlx.DB) *businesshours.Manager { func initSLA(db *sqlx.DB, teamManager *team.Manager, settings *setting.Manager, businessHours *businesshours.Manager) *sla.Manager { var lo = initLogger("sla") m, err := sla.New(sla.Opts{ - DB: db, - Lo: lo, - ScannerInterval: ko.MustDuration("sla.scanner_interval"), - }, workerpool.New(ko.MustInt("sla.worker_count"), ko.MustInt("sla.queue_size")), teamManager, settings, businessHours) + DB: db, + Lo: lo, + }, teamManager, settings, businessHours) if err != nil { log.Fatalf("error initializing SLA manager: %v", err) } diff --git a/cmd/sla.go b/cmd/sla.go index dfccfa35..b1ec60dc 100644 --- a/cmd/sla.go +++ b/cmd/sla.go @@ -44,7 +44,6 @@ func handleCreateSLA(r *fastglue.Request) error { firstRespTime = string(r.RequestCtx.PostArgs().Peek("first_response_time")) resTime = string(r.RequestCtx.PostArgs().Peek("resolution_time")) ) - // Validate time duration strings if _, err := time.ParseDuration(firstRespTime); err != nil { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid `first_response_time` duration.", nil, envelope.InputError) @@ -52,12 +51,10 @@ func handleCreateSLA(r *fastglue.Request) error { if _, err := time.ParseDuration(resTime); err != nil { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid `resolution_time` duration.", nil, envelope.InputError) } - if err := app.sla.Create(name, desc, firstRespTime, resTime); err != nil { return sendErrorEnvelope(r, err) } - - return r.SendEnvelope(true) + return r.SendEnvelope("SLA created successfully.") } func handleDeleteSLA(r *fastglue.Request) error { diff --git a/cmd/teams.go b/cmd/teams.go index 6e069213..746cc362 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -58,8 +58,9 @@ func handleCreateTeam(r *fastglue.Request) error { emoji = string(r.RequestCtx.PostArgs().Peek("emoji")) conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type")) businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id"))) + slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id"))) ) - if err := app.team.Create(name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID > 0), emoji); err != nil { + if err := app.team.Create(name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji); err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope("Team created successfully.") @@ -75,11 +76,12 @@ func handleUpdateTeam(r *fastglue.Request) error { conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type")) id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id"))) + slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id"))) ) if id < 1 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid team `id`", nil, envelope.InputError) } - if err := app.team.Update(id, name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID > 0), emoji); err != nil { + if err := app.team.Update(id, name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji); err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope("Team updated successfully.") diff --git a/frontend/src/components/admin/team/teams/TeamForm.vue b/frontend/src/components/admin/team/teams/TeamForm.vue index 8b17ae41..797684b2 100644 --- a/frontend/src/components/admin/team/teams/TeamForm.vue +++ b/frontend/src/components/admin/team/teams/TeamForm.vue @@ -1,6 +1,5 @@