Skip to content

Commit 2a382d6

Browse files
committed
feat: Toggle button for user to reassign replies to conversations if they are away, user status now actually affects the conversation workflow.
Online: Conversations are auto-assigned. Auto-away (inactivity in browser): Marks agent as away without stopping assignment (nothing changes for agent). Manual away: Prevents new conversations from being assigned. (option available in the sidebar) Reassign replies: Customer replies unassigns the conversation, returning it to the team inbox / unassigned inbox.
1 parent c639bfb commit 2a382d6

File tree

21 files changed

+209
-111
lines changed

21 files changed

+209
-111
lines changed

cmd/handlers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
9898
g.PUT("/api/v1/users/me", auth(handleUpdateCurrentUser))
9999
g.GET("/api/v1/users/me/teams", auth(handleGetCurrentUserTeams))
100100
g.PUT("/api/v1/users/me/availability", auth(handleUpdateUserAvailability))
101+
g.PUT("/api/v1/users/me/reassign-replies/toggle", auth(handleToggleReassignReplies))
101102
g.DELETE("/api/v1/users/me/avatar", auth(handleDeleteAvatar))
102103
g.GET("/api/v1/users/compact", auth(handleGetUsersCompact))
103104
g.GET("/api/v1/users", perm(handleGetUsers, "users:manage"))

cmd/middlewares.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111
"github.com/zerodha/simplesessions/v3"
1212
)
1313

14-
// tryAuth is a middleware that attempts to authenticate the user and add them to the context
15-
// but doesn't enforce authentication. Handlers can check if user exists in context optionally.
14+
// tryAuth attempts to authenticate the user and add them to the context but doesn't enforce authentication.
15+
// Handlers can check if user exists in context optionally.
1616
func tryAuth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
1717
return func(r *fastglue.Request) error {
1818
app := r.Context.(*App)
@@ -41,7 +41,7 @@ func tryAuth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
4141
}
4242
}
4343

44-
// auth makes sure the user is logged in.
44+
// auth validates the session and adds the user to the request context.
4545
func auth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
4646
return func(r *fastglue.Request) error {
4747
var app = r.Context.(*App)
@@ -69,7 +69,8 @@ func auth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
6969
}
7070
}
7171

72-
// perm does session validation, CSRF, and permission enforcement.
72+
// perm matches the CSRF token and checks if the user has the required permission to access the endpoint.
73+
// and sets the user in the request context.
7374
func perm(handler fastglue.FastRequestHandler, perm string) fastglue.FastRequestHandler {
7475
return func(r *fastglue.Request) error {
7576
var (

cmd/upgrade.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var migList = []migFunc{
3333
{"v0.3.0", migrations.V0_3_0},
3434
{"v0.4.0", migrations.V0_4_0},
3535
{"v0.5.0", migrations.V0_5_0},
36+
{"v0.6.0", migrations.V0_6_0},
3637
}
3738

3839
// upgrade upgrades the database to the current version by running SQL migration files

cmd/users.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ func handleUpdateUserAvailability(r *fastglue.Request) error {
7676
return r.SendEnvelope(true)
7777
}
7878

79+
// handleToggleReassignReplies toggles the reassign replies setting for the current user.
80+
func handleToggleReassignReplies(r *fastglue.Request) error {
81+
var (
82+
app = r.Context.(*App)
83+
auser = r.RequestCtx.UserValue("user").(amodels.User)
84+
enabled = r.RequestCtx.PostArgs().GetBool("enabled")
85+
)
86+
if err := app.user.ToggleReassignReplies(auser.ID, enabled); err != nil {
87+
return sendErrorEnvelope(r, err)
88+
}
89+
return r.SendEnvelope(true)
90+
}
91+
7992
// handleGetCurrentUserTeams returns the teams of a user.
8093
func handleGetCurrentUserTeams(r *fastglue.Request) error {
8194
var (

frontend/src/api/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ const deleteView = (id) => http.delete(`/api/v1/views/me/${id}`)
276276
const getAiPrompts = () => http.get('/api/v1/ai/prompts')
277277
const aiCompletion = (data) => http.post('/api/v1/ai/completion', data)
278278
const updateAIProvider = (data) => http.put('/api/v1/ai/provider', data)
279+
const toggleReassignReplies = (data) => http.put('/api/v1/users/me/reassign-replies/toggle', data)
279280

280281
export default {
281282
login,
@@ -390,4 +391,5 @@ export default {
390391
searchMessages,
391392
searchContacts,
392393
removeAssignee,
394+
toggleReassignReplies,
393395
}

frontend/src/components/sidebar/SidebarNavUser.vue

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,27 @@
4646
<span class="truncate text-xs">{{ userStore.email }}</span>
4747
</div>
4848
</div>
49-
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm justify-between">
50-
<span class="text-muted-foreground">
51-
{{ t('navigation.away') }}
52-
</span>
53-
<Switch
54-
:checked="
55-
userStore.user.availability_status === 'away' ||
56-
userStore.user.availability_status === 'away_manual'
57-
"
58-
@update:checked="(val) => userStore.updateUserAvailability(val ? 'away' : 'online')"
59-
/>
49+
<div class="space-y-2">
50+
<template
51+
v-for="(item, index) in [
52+
{
53+
label: t('navigation.away'),
54+
checked: userStore.user.availability_status === 'away_manual',
55+
action: (val) => userStore.updateUserAvailability(val ? 'away' : 'online')
56+
},
57+
{
58+
label: t('navigation.reassign_replies'),
59+
checked: userStore.user.reassign_replies,
60+
action: (val) => userStore.toggleAssignReplies(val)
61+
}
62+
]"
63+
:key="index"
64+
>
65+
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
66+
<span class="text-muted-foreground">{{ item.label }}</span>
67+
<Switch :checked="item.checked" @update:checked="item.action" />
68+
</div>
69+
</template>
6070
</div>
6171
</DropdownMenuLabel>
6272
<DropdownMenuSeparator />

frontend/src/stores/user.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const useUserStore = defineStore('user', () => {
1717
email: '',
1818
teams: [],
1919
permissions: [],
20-
availability_status: 'offline'
20+
availability_status: 'offline',
21+
reassign_replies: false
2122
})
2223
const emitter = useEmitter()
2324

@@ -105,6 +106,22 @@ export const useUserStore = defineStore('user', () => {
105106
}
106107
}
107108

109+
const toggleAssignReplies = async (enabled) => {
110+
const prev = user.value.reassign_replies
111+
user.value.reassign_replies = enabled
112+
try {
113+
await api.toggleReassignReplies({
114+
enabled: enabled
115+
})
116+
} catch (error) {
117+
user.value.reassign_replies = prev
118+
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
119+
variant: 'destructive',
120+
description: handleHTTPError(error).message
121+
})
122+
}
123+
}
124+
108125
return {
109126
user,
110127
userID,
@@ -123,6 +140,7 @@ export const useUserStore = defineStore('user', () => {
123140
clearAvatar,
124141
setAvatar,
125142
updateUserAvailability,
143+
toggleAssignReplies,
126144
can
127145
}
128146
})

i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
"navigation.views": "Views",
237237
"navigation.edit": "Edit",
238238
"navigation.delete": "Delete",
239+
"navigation.reassign_replies": "Reassign replies",
239240
"form.field.name": "Name",
240241
"form.field.inbox": "Inbox",
241242
"form.field.provider": "Provider",

i18n/mr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
"navigation.views": "दृश्ये",
237237
"navigation.edit": "संपादित करा",
238238
"navigation.delete": "हटवा",
239+
"navigation.reassign_replies": "प्रतिसाद पुन्हा नियुक्त करा",
239240
"form.field.name": "नाव",
240241
"form.field.inbox": "इनबॉक्स",
241242
"form.field.provider": "प्रदाता",

internal/autoassigner/autoassigner.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,12 @@ func New(teamStore teamStore, conversationStore conversationStore, systemUser um
6464
teamMaxAutoAssignments: make(map[int]int),
6565
roundRobinBalancer: make(map[int]*balance.Balance),
6666
}
67-
if err := e.populateTeamBalancer(); err != nil {
68-
return nil, err
69-
}
7067
return &e, nil
7168
}
7269

7370
// Run initiates the conversation assignment process and is to be invoked as a goroutine.
7471
// This function continuously assigns unassigned conversations to agents at regular intervals.
7572
func (e *Engine) Run(ctx context.Context, autoAssignInterval time.Duration) {
76-
time.Sleep(2 * time.Second)
7773
ticker := time.NewTicker(autoAssignInterval)
7874
defer ticker.Stop()
7975

@@ -159,8 +155,14 @@ func (e *Engine) populateTeamBalancer() error {
159155

160156
balancer := e.roundRobinBalancer[team.ID]
161157
existingUsers := make(map[string]struct{})
162-
163158
for _, user := range users {
159+
// Skip user if availability status is `away_manual`
160+
if user.AvailabilityStatus == umodels.AwayManual {
161+
e.lo.Debug("skipping user with away_manual status", "user_id", user.ID)
162+
continue
163+
}
164+
165+
// Add user to the balancer pool
164166
uid := strconv.Itoa(user.ID)
165167
existingUsers[uid] = struct{}{}
166168
if err := balancer.Add(uid, 1); err != nil {
@@ -227,7 +229,7 @@ func (e *Engine) assignConversations() error {
227229

228230
teamMaxAutoAssignments := e.teamMaxAutoAssignments[conversation.AssignedTeamID.Int]
229231
// Check if user has reached the max auto assigned conversations limit,
230-
// If the limit is set to 0, it means there is no limit.
232+
// 0 is unlimited.
231233
if teamMaxAutoAssignments != 0 {
232234
if activeConversationsCount >= teamMaxAutoAssignments {
233235
e.lo.Debug("user has reached max auto assigned conversations limit, skipping auto assignment", "user_id", userID,

0 commit comments

Comments
 (0)