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.GET("/api/v1/users/me/teams", auth(handleGetCurrentUserTeams))
|
||||
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.GET("/api/v1/users/compact", auth(handleGetUsersCompact))
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func handleGetCurrentUserTeams(r *fastglue.Request) error {
|
||||
var (
|
||||
|
@@ -276,7 +276,6 @@ const deleteView = (id) => http.delete(`/api/v1/views/me/${id}`)
|
||||
const getAiPrompts = () => http.get('/api/v1/ai/prompts')
|
||||
const aiCompletion = (data) => http.post('/api/v1/ai/completion', 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 {
|
||||
login,
|
||||
@@ -391,5 +390,4 @@ export default {
|
||||
searchMessages,
|
||||
searchContacts,
|
||||
removeAssignee,
|
||||
toggleReassignReplies,
|
||||
}
|
||||
|
@@ -16,7 +16,8 @@
|
||||
'bg-green-500': userStore.user.availability_status === 'online',
|
||||
'bg-amber-500':
|
||||
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'
|
||||
}"
|
||||
></div>
|
||||
@@ -47,26 +48,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<template
|
||||
v-for="(item, index) in [
|
||||
{
|
||||
label: t('navigation.away'),
|
||||
checked: userStore.user.availability_status === 'away_manual',
|
||||
action: (val) => userStore.updateUserAvailability(val ? 'away' : 'online')
|
||||
},
|
||||
{
|
||||
label: t('navigation.reassign_replies'),
|
||||
checked: userStore.user.reassign_replies,
|
||||
action: (val) => userStore.toggleAssignReplies(val)
|
||||
}
|
||||
]"
|
||||
:key="index"
|
||||
>
|
||||
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
|
||||
<span class="text-muted-foreground">{{ item.label }}</span>
|
||||
<Switch :checked="item.checked" @update:checked="item.action" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- Away switch is checked with 'away_manual' or 'away_and_reassigning' -->
|
||||
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
|
||||
<span class="text-muted-foreground">{{ t('navigation.away') }}</span>
|
||||
<Switch
|
||||
:checked="
|
||||
['away_manual', 'away_and_reassigning'].includes(userStore.user.availability_status)
|
||||
"
|
||||
@update:checked="
|
||||
(val) => {
|
||||
const newStatus = val ? 'away_manual' : 'online'
|
||||
userStore.updateUserAvailability(newStatus)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<!-- Reassign Replies Switch is checked with 'away_and_reassigning' -->
|
||||
<div class="flex items-center gap-2 px-1 text-left text-sm justify-between">
|
||||
<span class="text-muted-foreground">{{ t('navigation.reassign_replies') }}</span>
|
||||
<Switch
|
||||
:checked="userStore.user.availability_status === 'away_and_reassigning'"
|
||||
@update:checked="
|
||||
(val) => {
|
||||
const newStatus = val ? 'away_and_reassigning' : 'away_manual'
|
||||
userStore.updateUserAvailability(newStatus)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit" class="space-y-8">
|
||||
|
||||
<!-- 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">
|
||||
<Avatar class="w-20 h-20">
|
||||
<AvatarImage :src="props.initialValues.avatar_url" :alt="Avatar" />
|
||||
@@ -16,7 +15,7 @@
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
{{ props.initialValues.first_name }} {{ props.initialValues.last_name }}
|
||||
</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 }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -38,7 +37,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<LogIn class="w-5 h-5 text-gray-400" />
|
||||
<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">
|
||||
{{
|
||||
props.initialValues.last_login_at
|
||||
@@ -114,6 +113,35 @@
|
||||
</FormItem>
|
||||
</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">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>{{ t('form.field.setPassword') }}</FormLabel>
|
||||
@@ -148,23 +176,6 @@
|
||||
</FormItem>
|
||||
</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>
|
||||
</form>
|
||||
</template>
|
||||
@@ -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',
|
||||
|
@@ -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'),
|
||||
})
|
||||
|
@@ -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
|
||||
}
|
||||
})
|
@@ -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",
|
||||
|
@@ -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": "ईमेल",
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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"`
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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),
|
||||
|
Reference in New Issue
Block a user