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:
Abhinav Raut
2025-04-05 18:12:42 +05:30
parent b4f2186150
commit 1821647695
16 changed files with 119 additions and 106 deletions

View File

@@ -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"))

View File

@@ -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 (

View File

@@ -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,
} }

View File

@@ -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 />

View File

@@ -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',

View File

@@ -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'),
}) })

View File

@@ -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
} }
}) })

View File

@@ -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",

View File

@@ -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": "ईमेल",

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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"`

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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),