mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
feat: allow admins to set availability status
remove unnecessary column `reassign_replies` instead add a new enum `away_and_reassigning` to enum user availability status
This commit is contained in:
@@ -98,7 +98,6 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
|||||||
g.PUT("/api/v1/users/me", auth(handleUpdateCurrentUser))
|
g.PUT("/api/v1/users/me", auth(handleUpdateCurrentUser))
|
||||||
g.GET("/api/v1/users/me/teams", auth(handleGetCurrentUserTeams))
|
g.GET("/api/v1/users/me/teams", auth(handleGetCurrentUserTeams))
|
||||||
g.PUT("/api/v1/users/me/availability", auth(handleUpdateUserAvailability))
|
g.PUT("/api/v1/users/me/availability", auth(handleUpdateUserAvailability))
|
||||||
g.PUT("/api/v1/users/me/reassign-replies/toggle", auth(handleToggleReassignReplies))
|
|
||||||
g.DELETE("/api/v1/users/me/avatar", auth(handleDeleteAvatar))
|
g.DELETE("/api/v1/users/me/avatar", auth(handleDeleteAvatar))
|
||||||
g.GET("/api/v1/users/compact", auth(handleGetUsersCompact))
|
g.GET("/api/v1/users/compact", auth(handleGetUsersCompact))
|
||||||
g.GET("/api/v1/users", perm(handleGetUsers, "users:manage"))
|
g.GET("/api/v1/users", perm(handleGetUsers, "users:manage"))
|
||||||
|
13
cmd/users.go
13
cmd/users.go
@@ -76,19 +76,6 @@ func handleUpdateUserAvailability(r *fastglue.Request) error {
|
|||||||
return r.SendEnvelope(true)
|
return r.SendEnvelope(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleToggleReassignReplies toggles the reassign replies setting for the current user.
|
|
||||||
func handleToggleReassignReplies(r *fastglue.Request) error {
|
|
||||||
var (
|
|
||||||
app = r.Context.(*App)
|
|
||||||
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
||||||
enabled = r.RequestCtx.PostArgs().GetBool("enabled")
|
|
||||||
)
|
|
||||||
if err := app.user.ToggleReassignReplies(auser.ID, enabled); err != nil {
|
|
||||||
return sendErrorEnvelope(r, err)
|
|
||||||
}
|
|
||||||
return r.SendEnvelope(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGetCurrentUserTeams returns the teams of a user.
|
// handleGetCurrentUserTeams returns the teams of a user.
|
||||||
func handleGetCurrentUserTeams(r *fastglue.Request) error {
|
func handleGetCurrentUserTeams(r *fastglue.Request) error {
|
||||||
var (
|
var (
|
||||||
|
@@ -276,7 +276,6 @@ const deleteView = (id) => http.delete(`/api/v1/views/me/${id}`)
|
|||||||
const getAiPrompts = () => http.get('/api/v1/ai/prompts')
|
const getAiPrompts = () => http.get('/api/v1/ai/prompts')
|
||||||
const aiCompletion = (data) => http.post('/api/v1/ai/completion', data)
|
const aiCompletion = (data) => http.post('/api/v1/ai/completion', data)
|
||||||
const updateAIProvider = (data) => http.put('/api/v1/ai/provider', data)
|
const updateAIProvider = (data) => http.put('/api/v1/ai/provider', data)
|
||||||
const toggleReassignReplies = (data) => http.put('/api/v1/users/me/reassign-replies/toggle', data)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login,
|
login,
|
||||||
@@ -391,5 +390,4 @@ export default {
|
|||||||
searchMessages,
|
searchMessages,
|
||||||
searchContacts,
|
searchContacts,
|
||||||
removeAssignee,
|
removeAssignee,
|
||||||
toggleReassignReplies,
|
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,8 @@
|
|||||||
'bg-green-500': userStore.user.availability_status === 'online',
|
'bg-green-500': userStore.user.availability_status === 'online',
|
||||||
'bg-amber-500':
|
'bg-amber-500':
|
||||||
userStore.user.availability_status === 'away' ||
|
userStore.user.availability_status === 'away' ||
|
||||||
userStore.user.availability_status === 'away_manual',
|
userStore.user.availability_status === 'away_manual' ||
|
||||||
|
userStore.user.availability_status === 'away_and_reassigning',
|
||||||
'bg-gray-400': userStore.user.availability_status === 'offline'
|
'bg-gray-400': userStore.user.availability_status === 'offline'
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
@@ -47,26 +48,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<template
|
<!-- Away switch is checked with 'away_manual' or 'away_and_reassigning' -->
|
||||||
v-for="(item, index) in [
|
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
|
||||||
{
|
<span class="text-muted-foreground">{{ t('navigation.away') }}</span>
|
||||||
label: t('navigation.away'),
|
<Switch
|
||||||
checked: userStore.user.availability_status === 'away_manual',
|
:checked="
|
||||||
action: (val) => userStore.updateUserAvailability(val ? 'away' : 'online')
|
['away_manual', 'away_and_reassigning'].includes(userStore.user.availability_status)
|
||||||
},
|
"
|
||||||
{
|
@update:checked="
|
||||||
label: t('navigation.reassign_replies'),
|
(val) => {
|
||||||
checked: userStore.user.reassign_replies,
|
const newStatus = val ? 'away_manual' : 'online'
|
||||||
action: (val) => userStore.toggleAssignReplies(val)
|
userStore.updateUserAvailability(newStatus)
|
||||||
}
|
}
|
||||||
]"
|
"
|
||||||
:key="index"
|
/>
|
||||||
>
|
</div>
|
||||||
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
|
<!-- Reassign Replies Switch is checked with 'away_and_reassigning' -->
|
||||||
<span class="text-muted-foreground">{{ item.label }}</span>
|
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
|
||||||
<Switch :checked="item.checked" @update:checked="item.action" />
|
<span class="text-muted-foreground">{{ t('navigation.reassign_replies') }}</span>
|
||||||
</div>
|
<Switch
|
||||||
</template>
|
:checked="userStore.user.availability_status === 'away_and_reassigning'"
|
||||||
|
@update:checked="
|
||||||
|
(val) => {
|
||||||
|
const newStatus = val ? 'away_and_reassigning' : 'away_manual'
|
||||||
|
userStore.updateUserAvailability(newStatus)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="onSubmit" class="space-y-8">
|
<form @submit.prevent="onSubmit" class="space-y-8">
|
||||||
|
|
||||||
<!-- Summary Section -->
|
<!-- Summary Section -->
|
||||||
<div class="bg-muted/30 box py-6 px-3">
|
<div class="bg-muted/30 box py-6 px-3" v-if="!isNewForm">
|
||||||
<div class="flex items-start gap-6">
|
<div class="flex items-start gap-6">
|
||||||
<Avatar class="w-20 h-20">
|
<Avatar class="w-20 h-20">
|
||||||
<AvatarImage :src="props.initialValues.avatar_url" :alt="Avatar" />
|
<AvatarImage :src="props.initialValues.avatar_url" :alt="Avatar" />
|
||||||
@@ -16,7 +15,7 @@
|
|||||||
<h3 class="text-lg font-semibold text-gray-900">
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
{{ props.initialValues.first_name }} {{ props.initialValues.last_name }}
|
{{ props.initialValues.first_name }} {{ props.initialValues.last_name }}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge :class="['p-1 rounded-full text-xs font-medium', availabilityStatus.color]">
|
<Badge :class="['px-2 rounded-full text-xs font-medium', availabilityStatus.color]">
|
||||||
{{ availabilityStatus.text }}
|
{{ availabilityStatus.text }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<LogIn class="w-5 h-5 text-gray-400" />
|
<LogIn class="w-5 h-5 text-gray-400" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-500"> {{ $t('form.field.lastLogin') }}</p>
|
<p class="text-sm text-gray-500">{{ $t('form.field.lastLogin') }}</p>
|
||||||
<p class="text-sm font-medium text-gray-700">
|
<p class="text-sm font-medium text-gray-700">
|
||||||
{{
|
{{
|
||||||
props.initialValues.last_login_at
|
props.initialValues.last_login_at
|
||||||
@@ -114,6 +113,35 @@
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="availability_status" v-if="!isNewForm">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{{ t('form.field.availabilityStatus') }}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select v-bind="componentField" v-model="componentField.modelValue">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
:placeholder="
|
||||||
|
t('form.field.select', {
|
||||||
|
name: t('form.field.availabilityStatus')
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="active_group">{{ t('globals.terms.active') }}</SelectItem>
|
||||||
|
<SelectItem value="away_manual">{{ t('globals.terms.away') }}</SelectItem>
|
||||||
|
<SelectItem value="away_and_reassigning">
|
||||||
|
{{ t('form.field.awayReassigning') }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ field }" name="new_password" v-if="!isNewForm">
|
<FormField v-slot="{ field }" name="new_password" v-if="!isNewForm">
|
||||||
<FormItem v-auto-animate>
|
<FormItem v-auto-animate>
|
||||||
<FormLabel>{{ t('form.field.setPassword') }}</FormLabel>
|
<FormLabel>{{ t('form.field.setPassword') }}</FormLabel>
|
||||||
@@ -148,23 +176,6 @@
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
|
||||||
v-slot="{ value, handleChange }"
|
|
||||||
type="checkbox"
|
|
||||||
name="reassign_replies"
|
|
||||||
v-if="!isNewForm"
|
|
||||||
>
|
|
||||||
<FormItem class="flex flex-row items-start gap-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox :checked="value" @update:checked="handleChange" />
|
|
||||||
</FormControl>
|
|
||||||
<div class="space-y-1 leading-none">
|
|
||||||
<FormLabel> {{ $t('navigation.reassign_replies') }} </FormLabel>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<Button type="submit" :isLoading="isLoading"> {{ submitLabel }} </Button>
|
<Button type="submit" :isLoading="isLoading"> {{ submitLabel }} </Button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@@ -182,6 +193,14 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import { Clock, LogIn } from 'lucide-vue-next'
|
import { Clock, LogIn } from 'lucide-vue-next'
|
||||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
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 { SelectTag } from '@/components/ui/select'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -228,9 +247,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const availabilityStatus = computed(() => {
|
const availabilityStatus = computed(() => {
|
||||||
const status = form.values.availability_status
|
const status = form.values.availability_status
|
||||||
if (status === 'online') return { text: 'Online', color: 'bg-green-500' }
|
if (status === 'active_group') return { text: t('globals.terms.active'), color: 'bg-green-500' }
|
||||||
if (status === 'away' || status === 'away_manual') return { text: 'Away', color: 'bg-yellow-500' }
|
if (status === 'away_manual') return { text: t('globals.terms.away'), color: 'bg-yellow-500' }
|
||||||
return { text: 'Offline', color: 'bg-gray-400' }
|
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(() =>
|
const teamOptions = computed(() =>
|
||||||
@@ -245,6 +266,9 @@ const form = useForm({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((values) => {
|
const onSubmit = form.handleSubmit((values) => {
|
||||||
|
if (values.availability_status === 'active_group') {
|
||||||
|
values.availability_status = 'offline'
|
||||||
|
}
|
||||||
values.teams = values.teams.map((team) => ({ name: team }))
|
values.teams = values.teams.map((team) => ({ name: team }))
|
||||||
props.submitForm(values)
|
props.submitForm(values)
|
||||||
})
|
})
|
||||||
@@ -260,8 +284,15 @@ watch(
|
|||||||
() => props.initialValues,
|
() => props.initialValues,
|
||||||
(newValues) => {
|
(newValues) => {
|
||||||
// Hack.
|
// Hack.
|
||||||
if (Object.keys(newValues).length) {
|
if (Object.keys(newValues).length > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (
|
||||||
|
newValues.availability_status === 'away' ||
|
||||||
|
newValues.availability_status === 'offline' ||
|
||||||
|
newValues.availability_status === 'online'
|
||||||
|
) {
|
||||||
|
newValues.availability_status = 'active_group'
|
||||||
|
}
|
||||||
form.setValues(newValues)
|
form.setValues(newValues)
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
'teams',
|
'teams',
|
||||||
|
@@ -45,5 +45,6 @@ export const createFormSchema = (t) => z.object({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
enabled: z.boolean().optional().default(true)
|
enabled: z.boolean().optional().default(true),
|
||||||
|
availability_status: z.string().optional().default('offline'),
|
||||||
})
|
})
|
||||||
|
@@ -18,7 +18,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
teams: [],
|
teams: [],
|
||||||
permissions: [],
|
permissions: [],
|
||||||
availability_status: 'offline',
|
availability_status: 'offline',
|
||||||
reassign_replies: false
|
|
||||||
})
|
})
|
||||||
const emitter = useEmitter()
|
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 {
|
return {
|
||||||
user,
|
user,
|
||||||
userID,
|
userID,
|
||||||
@@ -140,7 +123,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
clearAvatar,
|
clearAvatar,
|
||||||
setAvatar,
|
setAvatar,
|
||||||
updateUserAvailability,
|
updateUserAvailability,
|
||||||
toggleAssignReplies,
|
|
||||||
can
|
can
|
||||||
}
|
}
|
||||||
})
|
})
|
@@ -73,6 +73,7 @@
|
|||||||
"globals.terms.awaitingResponse": "Awaiting Response",
|
"globals.terms.awaitingResponse": "Awaiting Response",
|
||||||
"globals.terms.unassigned": "Unassigned",
|
"globals.terms.unassigned": "Unassigned",
|
||||||
"globals.terms.pending": "Pending",
|
"globals.terms.pending": "Pending",
|
||||||
|
"globals.terms.active": "Active",
|
||||||
"globals.messages.badRequest": "Bad request",
|
"globals.messages.badRequest": "Bad request",
|
||||||
"globals.messages.adjustFilters": "Try adjusting filters",
|
"globals.messages.adjustFilters": "Try adjusting filters",
|
||||||
"globals.messages.errorUpdating": "Error updating {name}",
|
"globals.messages.errorUpdating": "Error updating {name}",
|
||||||
@@ -238,6 +239,8 @@
|
|||||||
"navigation.delete": "Delete",
|
"navigation.delete": "Delete",
|
||||||
"navigation.reassign_replies": "Reassign replies",
|
"navigation.reassign_replies": "Reassign replies",
|
||||||
"form.field.name": "Name",
|
"form.field.name": "Name",
|
||||||
|
"form.field.awayReassigning": "Away and reassigning",
|
||||||
|
"form.field.select": "Select {name}",
|
||||||
"form.field.availabilityStatus": "Availability Status",
|
"form.field.availabilityStatus": "Availability Status",
|
||||||
"form.field.lastActive": "Last active",
|
"form.field.lastActive": "Last active",
|
||||||
"form.field.lastLogin": "Last login",
|
"form.field.lastLogin": "Last login",
|
||||||
@@ -259,7 +262,6 @@
|
|||||||
"form.field.default": "Default",
|
"form.field.default": "Default",
|
||||||
"form.field.channel": "Channel",
|
"form.field.channel": "Channel",
|
||||||
"form.field.configure": "Configure",
|
"form.field.configure": "Configure",
|
||||||
"form.field.select": "Select",
|
|
||||||
"form.field.date": "Date",
|
"form.field.date": "Date",
|
||||||
"form.field.description": "Description",
|
"form.field.description": "Description",
|
||||||
"form.field.email": "Email",
|
"form.field.email": "Email",
|
||||||
|
@@ -73,6 +73,7 @@
|
|||||||
"globals.terms.awaitingResponse": "प्रतिसाचाची वाट पाहत आहे",
|
"globals.terms.awaitingResponse": "प्रतिसाचाची वाट पाहत आहे",
|
||||||
"globals.terms.unassigned": "नियुक्त नाही",
|
"globals.terms.unassigned": "नियुक्त नाही",
|
||||||
"globals.terms.pending": "प्रलंबित",
|
"globals.terms.pending": "प्रलंबित",
|
||||||
|
"globals.terms.active": "सक्रिय",
|
||||||
"globals.messages.badRequest": "चुकीची विनंती",
|
"globals.messages.badRequest": "चुकीची विनंती",
|
||||||
"globals.messages.adjustFilters": "फिल्टर्स समायोजित करा",
|
"globals.messages.adjustFilters": "फिल्टर्स समायोजित करा",
|
||||||
"globals.messages.errorUpdating": "{name} अद्ययावत करताना त्रुटी",
|
"globals.messages.errorUpdating": "{name} अद्ययावत करताना त्रुटी",
|
||||||
@@ -238,6 +239,8 @@
|
|||||||
"navigation.delete": "हटवा",
|
"navigation.delete": "हटवा",
|
||||||
"navigation.reassign_replies": "प्रतिसाद पुन्हा नियुक्त करा",
|
"navigation.reassign_replies": "प्रतिसाद पुन्हा नियुक्त करा",
|
||||||
"form.field.name": "नाव",
|
"form.field.name": "नाव",
|
||||||
|
"form.field.awayReassigning": "दूर आणि पुन्हा नियुक्त करत आहे",
|
||||||
|
"form.field.select": "{name} निवडा",
|
||||||
"form.field.availabilityStatus": "उपलब्धता स्थिती",
|
"form.field.availabilityStatus": "उपलब्धता स्थिती",
|
||||||
"form.field.lastActive": "शेवटचे सक्रिय",
|
"form.field.lastActive": "शेवटचे सक्रिय",
|
||||||
"form.field.lastLogin": "शेवटचे लॉगिन",
|
"form.field.lastLogin": "शेवटचे लॉगिन",
|
||||||
@@ -259,7 +262,6 @@
|
|||||||
"form.field.default": "डिफॉल्ट",
|
"form.field.default": "डिफॉल्ट",
|
||||||
"form.field.channel": "चॅनेल",
|
"form.field.channel": "चॅनेल",
|
||||||
"form.field.configure": "कॉन्फिगर करा",
|
"form.field.configure": "कॉन्फिगर करा",
|
||||||
"form.field.select": "निवडा",
|
|
||||||
"form.field.date": "तारीख",
|
"form.field.date": "तारीख",
|
||||||
"form.field.description": "वर्णन",
|
"form.field.description": "वर्णन",
|
||||||
"form.field.email": "ईमेल",
|
"form.field.email": "ईमेल",
|
||||||
|
@@ -156,9 +156,9 @@ func (e *Engine) populateTeamBalancer() error {
|
|||||||
balancer := e.roundRobinBalancer[team.ID]
|
balancer := e.roundRobinBalancer[team.ID]
|
||||||
existingUsers := make(map[string]struct{})
|
existingUsers := make(map[string]struct{})
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
// Skip user if availability status is `away_manual`
|
// Skip user if availability status is `away_manual` or `away_and_reassigning`
|
||||||
if user.AvailabilityStatus == umodels.AwayManual {
|
if user.AvailabilityStatus == umodels.AwayManual || user.AvailabilityStatus == umodels.AwayAndReassigning {
|
||||||
e.lo.Debug("skipping user with away_manual status", "user_id", user.ID)
|
e.lo.Debug("user is away, skipping autoasssignment ", "team_id", team.ID, "user_id", user.ID, "availability_status", user.AvailabilityStatus)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -534,7 +534,7 @@ SET
|
|||||||
WHEN EXISTS (
|
WHEN EXISTS (
|
||||||
SELECT 1 FROM users
|
SELECT 1 FROM users
|
||||||
WHERE users.id = conversations.assigned_user_id
|
WHERE users.id = conversations.assigned_user_id
|
||||||
AND users.reassign_replies = TRUE
|
AND users.availability_status = 'away_and_reassigning'
|
||||||
) THEN NULL
|
) THEN NULL
|
||||||
ELSE assigned_user_id
|
ELSE assigned_user_id
|
||||||
END
|
END
|
||||||
|
@@ -9,14 +9,25 @@ import (
|
|||||||
// V0_6_0 updates the database schema to v0.6.0.
|
// 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 {
|
func V0_6_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
||||||
_, err := db.Exec(`
|
_, 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`
|
_, 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@@ -16,10 +16,11 @@ const (
|
|||||||
UserTypeContact = "contact"
|
UserTypeContact = "contact"
|
||||||
|
|
||||||
// User availability statuses
|
// User availability statuses
|
||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
AwayManual = "away_manual"
|
AwayManual = "away_manual"
|
||||||
|
AwayAndReassigning = "away_and_reassigning"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -37,7 +38,6 @@ type User struct {
|
|||||||
Password string `db:"password" json:"-"`
|
Password string `db:"password" json:"-"`
|
||||||
LastActiveAt null.Time `db:"last_active_at" json:"last_active_at"`
|
LastActiveAt null.Time `db:"last_active_at" json:"last_active_at"`
|
||||||
LastLoginAt null.Time `db:"last_login_at" json:"last_login_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"`
|
Roles pq.StringArray `db:"roles" json:"roles"`
|
||||||
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
||||||
Meta pq.StringArray `db:"meta" json:"meta"`
|
Meta pq.StringArray `db:"meta" json:"meta"`
|
||||||
|
@@ -78,6 +78,7 @@ SET first_name = COALESCE($2, first_name),
|
|||||||
avatar_url = COALESCE($6, avatar_url),
|
avatar_url = COALESCE($6, avatar_url),
|
||||||
password = COALESCE($7, password),
|
password = COALESCE($7, password),
|
||||||
enabled = COALESCE($8, enabled),
|
enabled = COALESCE($8, enabled),
|
||||||
|
availability_status = COALESCE($9, availability_status),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1 AND type = 'agent';
|
WHERE id = $1 AND type = 'agent';
|
||||||
|
|
||||||
|
@@ -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)
|
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)
|
u.lo.Error("error updating user", "error", err)
|
||||||
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}"), nil)
|
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
|
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.
|
// UpdateLastActive updates the last active timestamp of an user.
|
||||||
func (u *Manager) UpdateLastActive(id int) error {
|
func (u *Manager) UpdateLastActive(id int) error {
|
||||||
if _, err := u.q.UpdateLastActiveAt.Exec(id); err != nil {
|
if _, err := u.q.UpdateLastActiveAt.Exec(id); err != nil {
|
||||||
|
@@ -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 "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_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 "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 "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_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');
|
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,
|
availability_status user_availability_status DEFAULT 'offline' NOT NULL,
|
||||||
last_active_at TIMESTAMPTZ NULL,
|
last_active_at TIMESTAMPTZ NULL,
|
||||||
last_login_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_country CHECK (LENGTH(country) <= 140),
|
||||||
CONSTRAINT constraint_users_on_phone_number CHECK (LENGTH(phone_number) <= 20),
|
CONSTRAINT constraint_users_on_phone_number CHECK (LENGTH(phone_number) <= 20),
|
||||||
CONSTRAINT constraint_users_on_email_length CHECK (LENGTH(email) <= 320),
|
CONSTRAINT constraint_users_on_email_length CHECK (LENGTH(email) <= 320),
|
||||||
|
Reference in New Issue
Block a user