-
{{ $t('form.field.lastLogin') }}
+
{{ $t('form.field.lastLogin') }}
{{
props.initialValues.last_login_at
@@ -114,6 +113,35 @@
+
+
+ {{ t('form.field.availabilityStatus') }}
+
+
+
+
+
+
+
{{ t('form.field.setPassword') }}
@@ -148,23 +176,6 @@
-
-
-
-
-
-
- {{ $t('navigation.reassign_replies') }}
-
-
-
-
-
@@ -182,6 +193,14 @@ import { Badge } from '@/components/ui/badge'
import { Clock, LogIn } from 'lucide-vue-next'
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from '@/components/ui/select'
import { SelectTag } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import { useI18n } from 'vue-i18n'
@@ -228,9 +247,11 @@ onMounted(async () => {
const availabilityStatus = computed(() => {
const status = form.values.availability_status
- if (status === 'online') return { text: 'Online', color: 'bg-green-500' }
- if (status === 'away' || status === 'away_manual') return { text: 'Away', color: 'bg-yellow-500' }
- return { text: 'Offline', color: 'bg-gray-400' }
+ if (status === 'active_group') return { text: t('globals.terms.active'), color: 'bg-green-500' }
+ if (status === 'away_manual') return { text: t('globals.terms.away'), color: 'bg-yellow-500' }
+ if (status === 'away_and_reassigning')
+ return { text: t('form.field.awayReassigning'), color: 'bg-orange-500' }
+ return { text: t('globals.terms.offline'), color: 'bg-gray-400' }
})
const teamOptions = computed(() =>
@@ -245,6 +266,9 @@ const form = useForm({
})
const onSubmit = form.handleSubmit((values) => {
+ if (values.availability_status === 'active_group') {
+ values.availability_status = 'offline'
+ }
values.teams = values.teams.map((team) => ({ name: team }))
props.submitForm(values)
})
@@ -260,8 +284,15 @@ watch(
() => props.initialValues,
(newValues) => {
// Hack.
- if (Object.keys(newValues).length) {
+ if (Object.keys(newValues).length > 0) {
setTimeout(() => {
+ if (
+ newValues.availability_status === 'away' ||
+ newValues.availability_status === 'offline' ||
+ newValues.availability_status === 'online'
+ ) {
+ newValues.availability_status = 'active_group'
+ }
form.setValues(newValues)
form.setFieldValue(
'teams',
diff --git a/frontend/src/features/admin/users/formSchema.js b/frontend/src/features/admin/users/formSchema.js
index 6e8cec1..9f64af2 100644
--- a/frontend/src/features/admin/users/formSchema.js
+++ b/frontend/src/features/admin/users/formSchema.js
@@ -45,5 +45,6 @@ export const createFormSchema = (t) => z.object({
})
})
.optional(),
- enabled: z.boolean().optional().default(true)
+ enabled: z.boolean().optional().default(true),
+ availability_status: z.string().optional().default('offline'),
})
diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js
index 212df5e..ff9a035 100644
--- a/frontend/src/stores/user.js
+++ b/frontend/src/stores/user.js
@@ -18,7 +18,6 @@ export const useUserStore = defineStore('user', () => {
teams: [],
permissions: [],
availability_status: 'offline',
- reassign_replies: false
})
const emitter = useEmitter()
@@ -106,22 +105,6 @@ export const useUserStore = defineStore('user', () => {
}
}
- const toggleAssignReplies = async (enabled) => {
- const prev = user.value.reassign_replies
- user.value.reassign_replies = enabled
- try {
- await api.toggleReassignReplies({
- enabled: enabled
- })
- } catch (error) {
- user.value.reassign_replies = prev
- emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
- variant: 'destructive',
- description: handleHTTPError(error).message
- })
- }
- }
-
return {
user,
userID,
@@ -140,7 +123,6 @@ export const useUserStore = defineStore('user', () => {
clearAvatar,
setAvatar,
updateUserAvailability,
- toggleAssignReplies,
can
}
})
\ No newline at end of file
diff --git a/i18n/en.json b/i18n/en.json
index e4a17da..3656c4e 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -73,6 +73,7 @@
"globals.terms.awaitingResponse": "Awaiting Response",
"globals.terms.unassigned": "Unassigned",
"globals.terms.pending": "Pending",
+ "globals.terms.active": "Active",
"globals.messages.badRequest": "Bad request",
"globals.messages.adjustFilters": "Try adjusting filters",
"globals.messages.errorUpdating": "Error updating {name}",
@@ -238,6 +239,8 @@
"navigation.delete": "Delete",
"navigation.reassign_replies": "Reassign replies",
"form.field.name": "Name",
+ "form.field.awayReassigning": "Away and reassigning",
+ "form.field.select": "Select {name}",
"form.field.availabilityStatus": "Availability Status",
"form.field.lastActive": "Last active",
"form.field.lastLogin": "Last login",
@@ -259,7 +262,6 @@
"form.field.default": "Default",
"form.field.channel": "Channel",
"form.field.configure": "Configure",
- "form.field.select": "Select",
"form.field.date": "Date",
"form.field.description": "Description",
"form.field.email": "Email",
diff --git a/i18n/mr.json b/i18n/mr.json
index 8e9fb3f..00a6a40 100644
--- a/i18n/mr.json
+++ b/i18n/mr.json
@@ -73,6 +73,7 @@
"globals.terms.awaitingResponse": "प्रतिसाचाची वाट पाहत आहे",
"globals.terms.unassigned": "नियुक्त नाही",
"globals.terms.pending": "प्रलंबित",
+ "globals.terms.active": "सक्रिय",
"globals.messages.badRequest": "चुकीची विनंती",
"globals.messages.adjustFilters": "फिल्टर्स समायोजित करा",
"globals.messages.errorUpdating": "{name} अद्ययावत करताना त्रुटी",
@@ -238,6 +239,8 @@
"navigation.delete": "हटवा",
"navigation.reassign_replies": "प्रतिसाद पुन्हा नियुक्त करा",
"form.field.name": "नाव",
+ "form.field.awayReassigning": "दूर आणि पुन्हा नियुक्त करत आहे",
+ "form.field.select": "{name} निवडा",
"form.field.availabilityStatus": "उपलब्धता स्थिती",
"form.field.lastActive": "शेवटचे सक्रिय",
"form.field.lastLogin": "शेवटचे लॉगिन",
@@ -259,7 +262,6 @@
"form.field.default": "डिफॉल्ट",
"form.field.channel": "चॅनेल",
"form.field.configure": "कॉन्फिगर करा",
- "form.field.select": "निवडा",
"form.field.date": "तारीख",
"form.field.description": "वर्णन",
"form.field.email": "ईमेल",
diff --git a/internal/autoassigner/autoassigner.go b/internal/autoassigner/autoassigner.go
index 9bd8a0b..31ce0c2 100644
--- a/internal/autoassigner/autoassigner.go
+++ b/internal/autoassigner/autoassigner.go
@@ -156,9 +156,9 @@ func (e *Engine) populateTeamBalancer() error {
balancer := e.roundRobinBalancer[team.ID]
existingUsers := make(map[string]struct{})
for _, user := range users {
- // Skip user if availability status is `away_manual`
- if user.AvailabilityStatus == umodels.AwayManual {
- e.lo.Debug("skipping user with away_manual status", "user_id", user.ID)
+ // Skip user if availability status is `away_manual` or `away_and_reassigning`
+ if user.AvailabilityStatus == umodels.AwayManual || user.AvailabilityStatus == umodels.AwayAndReassigning {
+ e.lo.Debug("user is away, skipping autoasssignment ", "team_id", team.ID, "user_id", user.ID, "availability_status", user.AvailabilityStatus)
continue
}
diff --git a/internal/conversation/queries.sql b/internal/conversation/queries.sql
index ea3f84e..a15bd5a 100644
--- a/internal/conversation/queries.sql
+++ b/internal/conversation/queries.sql
@@ -534,7 +534,7 @@ SET
WHEN EXISTS (
SELECT 1 FROM users
WHERE users.id = conversations.assigned_user_id
- AND users.reassign_replies = TRUE
+ AND users.availability_status = 'away_and_reassigning'
) THEN NULL
ELSE assigned_user_id
END
diff --git a/internal/migrations/v0.6.0.go b/internal/migrations/v0.6.0.go
index 8a259b2..821f338 100644
--- a/internal/migrations/v0.6.0.go
+++ b/internal/migrations/v0.6.0.go
@@ -9,14 +9,25 @@ import (
// V0_6_0 updates the database schema to v0.6.0.
func V0_6_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
_, err := db.Exec(`
- ALTER TABLE users ADD COLUMN IF NOT EXISTS reassign_replies BOOL DEFAULT FALSE NOT NULL;
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ NULL;
`)
if err != nil {
return err
}
_, err = db.Exec(`
- ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ NULL;
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_enum e
+ JOIN pg_type t ON t.oid = e.enumtypid
+ WHERE t.typname = 'user_availability_status'
+ AND e.enumlabel = 'away_and_reassigning'
+ ) THEN
+ ALTER TYPE user_availability_status ADD VALUE 'away_and_reassigning';
+ END IF;
+ END
+ $$;
`)
if err != nil {
return err
diff --git a/internal/user/models/models.go b/internal/user/models/models.go
index 4a22b1e..3cdee8b 100644
--- a/internal/user/models/models.go
+++ b/internal/user/models/models.go
@@ -16,10 +16,11 @@ const (
UserTypeContact = "contact"
// User availability statuses
- Online = "online"
- Offline = "offline"
- Away = "away"
- AwayManual = "away_manual"
+ Online = "online"
+ Offline = "offline"
+ Away = "away"
+ AwayManual = "away_manual"
+ AwayAndReassigning = "away_and_reassigning"
)
type User struct {
@@ -37,7 +38,6 @@ type User struct {
Password string `db:"password" json:"-"`
LastActiveAt null.Time `db:"last_active_at" json:"last_active_at"`
LastLoginAt null.Time `db:"last_login_at" json:"last_login_at"`
- ReassignReplies bool `db:"reassign_replies" json:"reassign_replies"`
Roles pq.StringArray `db:"roles" json:"roles"`
Permissions pq.StringArray `db:"permissions" json:"permissions"`
Meta pq.StringArray `db:"meta" json:"meta"`
diff --git a/internal/user/queries.sql b/internal/user/queries.sql
index 91c8841..0a9af44 100644
--- a/internal/user/queries.sql
+++ b/internal/user/queries.sql
@@ -78,6 +78,7 @@ SET first_name = COALESCE($2, first_name),
avatar_url = COALESCE($6, avatar_url),
password = COALESCE($7, password),
enabled = COALESCE($8, enabled),
+ availability_status = COALESCE($9, availability_status),
updated_at = now()
WHERE id = $1 AND type = 'agent';
diff --git a/internal/user/user.go b/internal/user/user.go
index 30c866f..eebac08 100644
--- a/internal/user/user.go
+++ b/internal/user/user.go
@@ -224,7 +224,7 @@ func (u *Manager) Update(id int, user models.User) error {
u.lo.Debug("setting new password for user", "user_id", id)
}
- if _, err := u.q.UpdateUser.Exec(id, user.FirstName, user.LastName, user.Email, pq.Array(user.Roles), user.AvatarURL, hashedPassword, user.Enabled); err != nil {
+ if _, err := u.q.UpdateUser.Exec(id, user.FirstName, user.LastName, user.Email, pq.Array(user.Roles), user.AvatarURL, hashedPassword, user.Enabled, user.AvailabilityStatus); err != nil {
u.lo.Error("error updating user", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
}
@@ -303,15 +303,6 @@ func (u *Manager) UpdateAvailability(id int, status string) error {
return nil
}
-// ToggleReassignReplies toggles the reassign replies status of an user.
-func (u *Manager) ToggleReassignReplies(id int, reassign bool) error {
- if _, err := u.q.SetReassignReplies.Exec(id, reassign); err != nil {
- u.lo.Error("error updating user reassign replies", "error", err)
- return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
- }
- return nil
-}
-
// UpdateLastActive updates the last active timestamp of an user.
func (u *Manager) UpdateLastActive(id int) error {
if _, err := u.q.UpdateLastActiveAt.Exec(id); err != nil {
diff --git a/schema.sql b/schema.sql
index a565c65..53c5219 100644
--- a/schema.sql
+++ b/schema.sql
@@ -13,7 +13,7 @@ DROP TYPE IF EXISTS "automation_execution_mode" CASCADE; CREATE TYPE "automation
DROP TYPE IF EXISTS "macro_visibility" CASCADE; CREATE TYPE "macro_visibility" AS ENUM ('all', 'team', 'user');
DROP TYPE IF EXISTS "media_disposition" CASCADE; CREATE TYPE "media_disposition" AS ENUM ('inline', 'attachment');
DROP TYPE IF EXISTS "media_store" CASCADE; CREATE TYPE "media_store" AS ENUM ('s3', 'fs');
-DROP TYPE IF EXISTS "user_availability_status" CASCADE; CREATE TYPE "user_availability_status" AS ENUM ('online', 'away', 'away_manual', 'offline');
+DROP TYPE IF EXISTS "user_availability_status" CASCADE; CREATE TYPE "user_availability_status" AS ENUM ('online', 'away', 'away_manual', 'offline', 'away_and_reassigning');
DROP TYPE IF EXISTS "applied_sla_status" CASCADE; CREATE TYPE "applied_sla_status" AS ENUM ('pending', 'breached', 'met', 'partially_met');
DROP TYPE IF EXISTS "sla_metric" CASCADE; CREATE TYPE "sla_metric" AS ENUM ('first_response', 'resolution');
DROP TYPE IF EXISTS "sla_notification_type" CASCADE; CREATE TYPE "sla_notification_type" AS ENUM ('warning', 'breach');
@@ -126,7 +126,6 @@ CREATE TABLE users (
availability_status user_availability_status DEFAULT 'offline' NOT NULL,
last_active_at TIMESTAMPTZ NULL,
last_login_at TIMESTAMPTZ NULL,
- reassign_replies BOOL DEFAULT FALSE NOT NULL,
CONSTRAINT constraint_users_on_country CHECK (LENGTH(country) <= 140),
CONSTRAINT constraint_users_on_phone_number CHECK (LENGTH(phone_number) <= 20),
CONSTRAINT constraint_users_on_email_length CHECK (LENGTH(email) <= 320),