mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-02 04:53:41 +00:00
feat: max active conversation auto assignement limits to agents
This commit is contained in:
36
cmd/teams.go
36
cmd/teams.go
@@ -52,15 +52,16 @@ func handleGetTeam(r *fastglue.Request) error {
|
||||
// handleCreateTeam creates a new team.
|
||||
func handleCreateTeam(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
name = string(r.RequestCtx.PostArgs().Peek("name"))
|
||||
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
|
||||
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
|
||||
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
|
||||
businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
|
||||
slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id")))
|
||||
app = r.Context.(*App)
|
||||
name = string(r.RequestCtx.PostArgs().Peek("name"))
|
||||
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
|
||||
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
|
||||
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
|
||||
businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
|
||||
slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id")))
|
||||
maxAutoAssignedConversations, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("max_auto_assigned_conversations")))
|
||||
)
|
||||
if err := app.team.Create(name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji); err != nil {
|
||||
if err := app.team.Create(name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji, maxAutoAssignedConversations); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
return r.SendEnvelope("Team created successfully.")
|
||||
@@ -69,19 +70,20 @@ func handleCreateTeam(r *fastglue.Request) error {
|
||||
// handleUpdateTeam updates an existing team.
|
||||
func handleUpdateTeam(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
name = string(r.RequestCtx.PostArgs().Peek("name"))
|
||||
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
|
||||
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
|
||||
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
|
||||
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
||||
businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
|
||||
slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id")))
|
||||
app = r.Context.(*App)
|
||||
name = string(r.RequestCtx.PostArgs().Peek("name"))
|
||||
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
|
||||
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
|
||||
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
|
||||
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
||||
businessHrsID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
|
||||
slaPolicyID, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("sla_policy_id")))
|
||||
maxAutoAssignedConversations, _ = strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("max_auto_assigned_conversations")))
|
||||
)
|
||||
if id < 1 {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid team `id`", nil, envelope.InputError)
|
||||
}
|
||||
if err := app.team.Update(id, name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji); err != nil {
|
||||
if err := app.team.Update(id, name, timezone, conversationAssignmentType, null.NewInt(businessHrsID, businessHrsID != 0), null.NewInt(slaPolicyID, slaPolicyID != 0), emoji, maxAutoAssignedConversations); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
return r.SendEnvelope("Team updated successfully.")
|
||||
|
||||
@@ -20,13 +20,14 @@
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Name" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>Select an unique name.</FormDescription>
|
||||
<FormDescription>Select an unique name for the team.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField name="conversation_assignment_type" v-slot="{ componentField }">
|
||||
<FormItem>
|
||||
<FormLabel>Auto assignment type</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
@@ -49,6 +50,21 @@
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="max_auto_assigned_conversations">
|
||||
<FormItem>
|
||||
<FormLabel>Maximum auto-assigned conversations</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="0" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Maximum number of active conversations that can be auto-assigned to an agent at once.
|
||||
Conversations in "Resolved" or "Closed" states do not count toward this limit. Set to 0
|
||||
for unlimited.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="timezone">
|
||||
<FormItem>
|
||||
<FormLabel>Timezone</FormLabel>
|
||||
@@ -105,7 +121,11 @@
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem v-for="sla in slaStore.options" :key="sla.value" :value="parseInt(sla.value)">
|
||||
<SelectItem
|
||||
v-for="sla in slaStore.options"
|
||||
:key="sla.value"
|
||||
:value="parseInt(sla.value)"
|
||||
>
|
||||
{{ sla.label }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
|
||||
@@ -10,6 +10,7 @@ export const teamFormSchema = z.object({
|
||||
}),
|
||||
emoji: z.string({ required_error: 'Emoji is required.' }),
|
||||
conversation_assignment_type: z.string({ required_error: 'Conversation assignment type is required.' }),
|
||||
max_auto_assigned_conversations: z.coerce.number().optional().default(0),
|
||||
timezone: z.string({ required_error: 'Timezone is required.' }),
|
||||
business_hours_id: z.number().optional().nullable(),
|
||||
sla_policy_id: z.number().optional().nullable(),
|
||||
|
||||
@@ -47,7 +47,7 @@ export function useConversationFilters () {
|
||||
}))
|
||||
|
||||
const newConversationFilters = computed(() => ({
|
||||
email: {
|
||||
contact_email: {
|
||||
label: 'Email',
|
||||
type: FIELD_TYPE.TEXT,
|
||||
operators: FIELD_OPERATORS.TEXT
|
||||
|
||||
@@ -27,6 +27,7 @@ const (
|
||||
type conversationStore interface {
|
||||
GetUnassignedConversations() ([]models.Conversation, error)
|
||||
UpdateConversationUserAssignee(conversationUUID string, userID int, user umodels.User) error
|
||||
ActiveUserConversationsCount(userID int) (int, error)
|
||||
}
|
||||
|
||||
type teamStore interface {
|
||||
@@ -37,10 +38,10 @@ type teamStore interface {
|
||||
// Engine represents a manager for assigning unassigned conversations
|
||||
// to team agents in a round-robin pattern.
|
||||
type Engine struct {
|
||||
// TODO: Implement a persistent store for the balancer.
|
||||
roundRobinBalancer map[int]*balance.Balance
|
||||
// Mutex to protect the balancer map
|
||||
balanceMu sync.Mutex
|
||||
balanceMu sync.Mutex
|
||||
teamMaxAutoAssignments map[int]int
|
||||
|
||||
systemUser umodels.User
|
||||
conversationStore conversationStore
|
||||
@@ -55,10 +56,11 @@ type Engine struct {
|
||||
// conversation manager, and logger.
|
||||
func New(teamStore teamStore, conversationStore conversationStore, systemUser umodels.User, lo *logf.Logger) (*Engine, error) {
|
||||
var e = Engine{
|
||||
conversationStore: conversationStore,
|
||||
teamStore: teamStore,
|
||||
systemUser: systemUser,
|
||||
lo: lo,
|
||||
conversationStore: conversationStore,
|
||||
teamStore: teamStore,
|
||||
systemUser: systemUser,
|
||||
lo: lo,
|
||||
teamMaxAutoAssignments: make(map[int]int),
|
||||
}
|
||||
balancer, err := e.populateTeamBalancer()
|
||||
if err != nil {
|
||||
@@ -136,6 +138,7 @@ func (e *Engine) populateTeamBalancer() (map[int]*balance.Balance, error) {
|
||||
|
||||
for _, team := range teams {
|
||||
if team.ConversationAssignmentType != AssignmentTypeRoundRobin {
|
||||
e.lo.Warn("unsupported conversation assignment type", "team_id", team.ID, "type", team.ConversationAssignmentType)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -151,6 +154,9 @@ func (e *Engine) populateTeamBalancer() (map[int]*balance.Balance, error) {
|
||||
}
|
||||
balancer[team.ID].Add(strconv.Itoa(user.ID), 1)
|
||||
}
|
||||
|
||||
// Set max auto assigned conversations for the team.
|
||||
e.teamMaxAutoAssignments[team.ID] = team.MaxAutoAssignedConversations
|
||||
}
|
||||
return balancer, nil
|
||||
}
|
||||
@@ -169,9 +175,11 @@ func (e *Engine) assignConversations() error {
|
||||
|
||||
for _, conversation := range unassignedConversations {
|
||||
// Get user from the pool.
|
||||
userIDStr, err := e.getUserFromPool(conversation)
|
||||
userIDStr, err := e.getUserFromPool(conversation.AssignedTeamID.Int)
|
||||
if err != nil {
|
||||
e.lo.Error("error fetching user from balancer pool", "conversation_uuid", conversation.UUID, "error", err)
|
||||
if err != ErrTeamNotFound {
|
||||
e.lo.Error("error fetching user from balancer pool", "conversation_uuid", conversation.UUID, "error", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -182,7 +190,20 @@ func (e *Engine) assignConversations() error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Assign conversation.
|
||||
// Get active conversations count for the user.
|
||||
activeConversationsCount, err := e.conversationStore.ActiveUserConversationsCount(userID)
|
||||
if err != nil {
|
||||
e.lo.Error("error fetching active conversations count for user", "user_id", userID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if user has reached the max auto assigned conversations limit.
|
||||
if activeConversationsCount >= e.teamMaxAutoAssignments[conversation.AssignedTeamID.Int] {
|
||||
e.lo.Debug("user has reached max auto assigned conversations limit, skipping auto assignment", "user_id", userID, "user_active_conversations_count", activeConversationsCount, "max_auto_assigned_conversations", e.teamMaxAutoAssignments[conversation.AssignedTeamID.Int])
|
||||
continue
|
||||
}
|
||||
|
||||
// Assign conversation to user.
|
||||
if err := e.conversationStore.UpdateConversationUserAssignee(conversation.UUID, userID, e.systemUser); err != nil {
|
||||
e.lo.Error("error assigning conversation", "conversation_uuid", conversation.UUID, "error", err)
|
||||
continue
|
||||
@@ -192,13 +213,12 @@ func (e *Engine) assignConversations() error {
|
||||
}
|
||||
|
||||
// getUserFromPool returns user ID from the team balancer pool.
|
||||
func (e *Engine) getUserFromPool(conversation models.Conversation) (string, error) {
|
||||
func (e *Engine) getUserFromPool(assignedTeamID int) (string, error) {
|
||||
e.balanceMu.Lock()
|
||||
defer e.balanceMu.Unlock()
|
||||
|
||||
pool, ok := e.roundRobinBalancer[conversation.AssignedTeamID.Int]
|
||||
pool, ok := e.roundRobinBalancer[assignedTeamID]
|
||||
if !ok {
|
||||
e.lo.Warn("team not found in balancer", "team_id", conversation.AssignedTeamID.Int)
|
||||
return "", ErrTeamNotFound
|
||||
}
|
||||
return pool.Get(), nil
|
||||
|
||||
@@ -28,7 +28,6 @@ func (e *Engine) evalConversationRules(rules []models.Rule, conversation cmodels
|
||||
for idx, group := range rule.Groups {
|
||||
if len(group.Rules) == 0 {
|
||||
e.lo.Debug("no rules found in group, skipping rule group evaluation", "group_num", idx+1, "conversation_uuid", conversation.UUID)
|
||||
groupEvalResults = append(groupEvalResults, true)
|
||||
continue
|
||||
}
|
||||
result := e.evaluateGroup(group.Rules, group.LogicalOp, conversation)
|
||||
@@ -56,6 +55,7 @@ func (e *Engine) evalConversationRules(rules []models.Rule, conversation cmodels
|
||||
// evaluateFinalResult computes the final result of multiple group evaluations
|
||||
// based on the specified logical operator (AND/OR).
|
||||
func evaluateFinalResult(results []bool, operator string) bool {
|
||||
fmt.Println("GROUP RESULTS: ", results)
|
||||
if operator == models.OperatorAnd {
|
||||
for _, result := range results {
|
||||
if !result {
|
||||
|
||||
@@ -41,8 +41,8 @@ const (
|
||||
ConversationAssignedTeam = "assigned_team"
|
||||
ConversationHoursSinceCreated = "hours_since_created"
|
||||
ConversationHoursSinceResolved = "hours_since_resolved"
|
||||
ContactEmail = "contact_email"
|
||||
ConversationInbox = "inbox"
|
||||
ContactEmail = "contact_email"
|
||||
|
||||
EventConversationUserAssigned = "conversation.user.assigned"
|
||||
EventConversationTeamAssigned = "conversation.team.assigned"
|
||||
|
||||
@@ -170,6 +170,7 @@ type queries struct {
|
||||
GetUnassignedConversations *sqlx.Stmt `query:"get-unassigned-conversations"`
|
||||
GetConversations string `query:"get-conversations"`
|
||||
GetConversationParticipants *sqlx.Stmt `query:"get-conversation-participants"`
|
||||
GetUserActiveConversationsCount *sqlx.Stmt `query:"get-user-active-conversations-count"`
|
||||
UpdateConversationFirstReplyAt *sqlx.Stmt `query:"update-conversation-first-reply-at"`
|
||||
UpdateConversationAssigneeLastSeen *sqlx.Stmt `query:"update-conversation-assignee-last-seen"`
|
||||
UpdateConversationAssignedUser *sqlx.Stmt `query:"update-conversation-assigned-user"`
|
||||
@@ -354,6 +355,16 @@ func (c *Manager) ReOpenConversation(conversationUUID string, actor umodels.User
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActiveUserConversationsCount returns the count of active conversations for a user. i.e. conversations not closed or resolved status.
|
||||
func (c *Manager) ActiveUserConversationsCount(userID int) (int, error) {
|
||||
var count int
|
||||
if err := c.q.GetUserActiveConversationsCount.Get(&count, userID); err != nil {
|
||||
c.lo.Error("error fetching active conversation count", "error", err)
|
||||
return count, envelope.NewError(envelope.GeneralError, "Error fetching active conversation count", nil)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// UpdateConversationLastMessage updates the last message details for a conversation.
|
||||
func (c *Manager) UpdateConversationLastMessage(convesationID int, conversationUUID, lastMessage string, lastMessageAt time.Time) error {
|
||||
if _, err := c.q.UpdateConversationLastMessage.Exec(convesationID, conversationUUID, lastMessage, lastMessageAt); err != nil {
|
||||
|
||||
@@ -184,6 +184,9 @@ SET status_id = (SELECT id FROM conversation_statuses WHERE name = $2),
|
||||
updated_at = now()
|
||||
WHERE uuid = $1;
|
||||
|
||||
-- name: get-user-active-conversations-count
|
||||
SELECT COUNT(*) FROM conversations WHERE status_id IN (SELECT id FROM conversation_statuses WHERE name NOT IN ('Resolved', 'Closed')) and assigned_user_id = $1;
|
||||
|
||||
-- name: update-conversation-priority
|
||||
UPDATE conversations
|
||||
SET priority_id = (SELECT id FROM conversation_priorities WHERE name = $2),
|
||||
|
||||
@@ -10,15 +10,16 @@ import (
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Emoji null.String `db:"emoji" json:"emoji"`
|
||||
Name string `db:"name" json:"name"`
|
||||
ConversationAssignmentType string `db:"conversation_assignment_type" json:"conversation_assignment_type,omitempty"`
|
||||
Timezone string `db:"timezone" json:"timezone,omitempty"`
|
||||
BusinessHoursID null.Int `db:"business_hours_id" json:"business_hours_id,omitempty"`
|
||||
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id,omitempty"`
|
||||
ID int `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Emoji null.String `db:"emoji" json:"emoji"`
|
||||
Name string `db:"name" json:"name"`
|
||||
ConversationAssignmentType string `db:"conversation_assignment_type" json:"conversation_assignment_type,omitempty"`
|
||||
Timezone string `db:"timezone" json:"timezone,omitempty"`
|
||||
BusinessHoursID null.Int `db:"business_hours_id" json:"business_hours_id,omitempty"`
|
||||
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id,omitempty"`
|
||||
MaxAutoAssignedConversations int `db:"max_auto_assigned_conversations" json:"max_auto_assigned_conversations"`
|
||||
}
|
||||
|
||||
type Teams []Team
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
-- name: get-teams
|
||||
SELECT id, emoji, created_at, updated_at, name, conversation_assignment_type, timezone from teams order by updated_at desc;
|
||||
SELECT id, emoji, created_at, updated_at, name, conversation_assignment_type, timezone, max_auto_assigned_conversations from teams order by updated_at desc;
|
||||
|
||||
-- name: get-teams-compact
|
||||
SELECT id, name, emoji from teams order by name;
|
||||
|
||||
-- name: get-user-teams
|
||||
SELECT id, emoji, created_at, updated_at, name, conversation_assignment_type, timezone from teams WHERE id IN (SELECT team_id FROM team_members WHERE user_id = $1) order by updated_at desc;
|
||||
SELECT id, emoji, created_at, updated_at, name, conversation_assignment_type, timezone, max_auto_assigned_conversations from teams WHERE id IN (SELECT team_id FROM team_members WHERE user_id = $1) order by updated_at desc;
|
||||
|
||||
-- name: get-team
|
||||
SELECT id, emoji, name, conversation_assignment_type, timezone, business_hours_id, sla_policy_id from teams where id = $1;
|
||||
SELECT id, emoji, name, conversation_assignment_type, timezone, business_hours_id, sla_policy_id, max_auto_assigned_conversations from teams where id = $1;
|
||||
|
||||
-- name: get-team-members
|
||||
SELECT u.id, t.id as team_id
|
||||
@@ -18,10 +18,10 @@ JOIN teams t ON t.id = tm.team_id
|
||||
WHERE t.id = $1;
|
||||
|
||||
-- name: insert-team
|
||||
INSERT INTO teams (name, timezone, conversation_assignment_type, business_hours_id, sla_policy_id, emoji) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id;
|
||||
INSERT INTO teams (name, timezone, conversation_assignment_type, business_hours_id, sla_policy_id, emoji, max_auto_assigned_conversations) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id;
|
||||
|
||||
-- name: update-team
|
||||
UPDATE teams set name = $2, timezone = $3, conversation_assignment_type = $4, business_hours_id = $5, sla_policy_id = $6, emoji = $7, updated_at = now() where id = $1;
|
||||
UPDATE teams set name = $2, timezone = $3, conversation_assignment_type = $4, business_hours_id = $5, sla_policy_id = $6, emoji = $7, max_auto_assigned_conversations = $8, updated_at = now() where id = $1;
|
||||
|
||||
-- name: upsert-user-teams
|
||||
WITH delete_old_teams AS (
|
||||
|
||||
@@ -101,8 +101,8 @@ func (u *Manager) Get(id int) (models.Team, error) {
|
||||
}
|
||||
|
||||
// Create creates a new team.
|
||||
func (u *Manager) Create(name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string) error {
|
||||
if _, err := u.q.InsertTeam.Exec(name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji); err != nil {
|
||||
func (u *Manager) Create(name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string, maxAutoAssignedConversations int) error {
|
||||
if _, err := u.q.InsertTeam.Exec(name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji, maxAutoAssignedConversations); err != nil {
|
||||
if dbutil.IsUniqueViolationError(err) {
|
||||
return envelope.NewError(envelope.GeneralError, "Team with the same name already exists", nil)
|
||||
}
|
||||
@@ -113,8 +113,8 @@ func (u *Manager) Create(name, timezone, conversationAssignmentType string, busi
|
||||
}
|
||||
|
||||
// Update updates an existing team.
|
||||
func (u *Manager) Update(id int, name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string) error {
|
||||
if _, err := u.q.UpdateTeam.Exec(id, name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji); err != nil {
|
||||
func (u *Manager) Update(id int, name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string, maxAutoAssignedConversations int) error {
|
||||
if _, err := u.q.UpdateTeam.Exec(id, name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji, maxAutoAssignedConversations); err != nil {
|
||||
u.lo.Error("error updating team", "error", err)
|
||||
return envelope.NewError(envelope.GeneralError, "Error updating team", nil)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ CREATE TABLE teams (
|
||||
"name" TEXT NOT NULL,
|
||||
emoji TEXT NULL,
|
||||
conversation_assignment_type conversation_assignment_type NOT NULL,
|
||||
max_auto_assigned_conversations INT DEFAULT 0 NOT NULL,
|
||||
business_hours_id INT REFERENCES business_hours(id) ON DELETE SET NULL ON UPDATE CASCADE NULL,
|
||||
sla_policy_id INT REFERENCES sla_policies(id) ON DELETE SET NULL ON UPDATE CASCADE NULL,
|
||||
timezone TEXT NULL,
|
||||
|
||||
Reference in New Issue
Block a user