mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-02 21:13:47 +00:00
Consider visitors and contacts same, as the only difference is that visitors are not authenticated.
Show a warning in sidebar when a conversation with a visitor is opened. Move string.js utils to shared-ui utils Modify user fetch queries to allow optional filter by multiple user types not just one. Fix capitalizaiton for live chat en translations
This commit is contained in:
29
cmd/chat.go
29
cmd/chat.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
@@ -613,7 +614,7 @@ func handleChatSendMessage(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
// Fetch sender.
|
||||
sender, err := app.user.Get(senderID, "", "")
|
||||
sender, err := app.user.Get(senderID, "", []string{})
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching sender user", "sender_id", senderID, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
@@ -777,7 +778,7 @@ func handleWidgetMediaUpload(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
// Get sender user for ProcessIncomingMessage
|
||||
sender, err := app.user.Get(senderID, "", "")
|
||||
sender, err := app.user.Get(senderID, "", []string{})
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching sender user", "sender_id", senderID, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
@@ -847,22 +848,30 @@ func buildConversationResponseWithBusinessHours(app *App, conversation cmodels.C
|
||||
// resolveUserIDFromClaims resolves the actual user ID from JWT claims,
|
||||
// handling both regular user_id and external_user_id cases
|
||||
func resolveUserIDFromClaims(app *App, claims Claims) (int, error) {
|
||||
// If UserID is already set and valid, use it directly
|
||||
if claims.UserID > 0 {
|
||||
return claims.UserID, nil
|
||||
}
|
||||
|
||||
// If UserID is not set but ExternalUserID is available, resolve it
|
||||
if claims.ExternalUserID != "" {
|
||||
user, err := app.user.Get(claims.UserID, "", []string{})
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching user by user ID", "user_id", claims.UserID, "error", err)
|
||||
return 0, errors.New("error fetching user")
|
||||
}
|
||||
if !user.Enabled {
|
||||
return 0, errors.New("user is disabled")
|
||||
}
|
||||
return user.ID, nil
|
||||
} else if claims.ExternalUserID != "" {
|
||||
user, err := app.user.GetByExternalID(claims.ExternalUserID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching user by external ID", "external_user_id", claims.ExternalUserID, "error", err)
|
||||
return 0, fmt.Errorf("user not found for external_user_id %s: %w", claims.ExternalUserID, err)
|
||||
return 0, errors.New("error fetching user")
|
||||
}
|
||||
if !user.Enabled {
|
||||
return 0, errors.New("user is disabled")
|
||||
}
|
||||
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no valid user ID found in JWT claims")
|
||||
return 0, errors.New("error fetching user")
|
||||
}
|
||||
|
||||
// verifyJWT verifies and validates a JWT token with proper signature verification
|
||||
|
||||
@@ -276,11 +276,16 @@ func handleBlockContact(r *fastglue.Request) error {
|
||||
|
||||
app.lo.Info("setting contact block status", "contact_id", contactID, "enabled", req.Enabled, "actor_id", auser.ID)
|
||||
|
||||
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, req.Enabled); err != nil {
|
||||
contact, err := app.user.GetContact(contactID, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
contact, err := app.user.GetContact(contactID, "")
|
||||
if err := app.user.ToggleEnabled(contactID, contact.Type, req.Enabled); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
contact, err = app.user.GetContact(contactID, "")
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
@@ -26,10 +26,6 @@ const (
|
||||
maxAvatarSizeMB = 2
|
||||
)
|
||||
|
||||
type updateAvailabilityRequest struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type resetPasswordRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ import {
|
||||
} from '@shared-ui/components/ui/select'
|
||||
import { SelectTag } from '@shared-ui/components/ui/select'
|
||||
import { useConversationFilters } from '../../../composables/useConversationFilters'
|
||||
import { getTextFromHTML } from '../../../utils/strings.js'
|
||||
import { getTextFromHTML } from '@shared-ui/utils/string'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Editor from '@main/components/editor/TextEditor.vue'
|
||||
import SelectComboBox from '@main/components/combobox/SelectCombobox.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod'
|
||||
import { isGoDuration } from '../../../utils/strings'
|
||||
import { isGoDuration } from '@shared-ui/utils/string'
|
||||
|
||||
export const createFormSchema = (t) => z.object({
|
||||
name: z.string().min(1, t('globals.messages.required')),
|
||||
|
||||
@@ -158,7 +158,7 @@ import ActionBuilder from '@/features/admin/macros/ActionBuilder.vue'
|
||||
import { useConversationFilters } from '../../../composables/useConversationFilters.js'
|
||||
import { useUsersStore } from '../../../stores/users.js'
|
||||
import { useTeamStore } from '../../../stores/team.js'
|
||||
import { getTextFromHTML } from '../../../utils/strings.js'
|
||||
import { getTextFromHTML } from '@shared-ui/utils/string'
|
||||
import { createFormSchema } from './formSchema.js'
|
||||
import SelectComboBox from '@main/components/combobox/SelectCombobox.vue'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod'
|
||||
import { getTextFromHTML } from '../../../utils/strings.js'
|
||||
import { getTextFromHTML } from '@shared-ui/utils/string'
|
||||
|
||||
const actionSchema = () => z.array(
|
||||
z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
import { isGoDuration } from '../../../utils/strings';
|
||||
import { isGoDuration } from '@shared-ui/utils/string';
|
||||
|
||||
export const createFormSchema = (t) => z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod'
|
||||
import { isGoHourMinuteDuration } from '../../../utils/strings'
|
||||
import { isGoHourMinuteDuration } from '@shared-ui/utils/string'
|
||||
|
||||
export const createFormSchema = (t) =>
|
||||
z
|
||||
|
||||
@@ -153,7 +153,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useEmitter } from '../../composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '../../constants/emitterEvents.js'
|
||||
import { handleHTTPError } from '../../utils/http'
|
||||
import { getInitials } from '../../utils/strings'
|
||||
import { getInitials } from '@shared-ui/utils/string'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import api from '../../api'
|
||||
|
||||
|
||||
@@ -73,10 +73,15 @@
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div class="space-y-1 overflow-hidden">
|
||||
<h4 class="text-sm font-semibold truncate">
|
||||
{{ contact.first_name }} {{ contact.last_name }}
|
||||
</h4>
|
||||
<div class="space-y-1 overflow-hidden flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-sm font-semibold truncate">
|
||||
{{ contact.first_name }} {{ contact.last_name }}
|
||||
</h4>
|
||||
<Badge v-if="contact.type" variant="secondary" class="text-xs px-1.5 py-0">
|
||||
{{ contact.type.titleCase() }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{{ contact.email }}
|
||||
</p>
|
||||
@@ -163,6 +168,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { Card } from '@shared-ui/components/ui/card'
|
||||
import { Skeleton } from '@shared-ui/components/ui/skeleton'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@shared-ui/components/ui/avatar'
|
||||
import { Badge } from '@shared-ui/components/ui/badge'
|
||||
import {
|
||||
Pagination,
|
||||
PaginationEllipsis,
|
||||
|
||||
@@ -142,7 +142,7 @@ import AttachmentsPreview from '@/features/conversation/message/attachment/Attac
|
||||
import MacroActionsPreview from '@/features/conversation/MacroActionsPreview.vue'
|
||||
import ReplyBoxMenuBar from '@/features/conversation/ReplyBoxMenuBar.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { validateEmail } from '../../utils/strings'
|
||||
import { validateEmail } from '@shared-ui/utils/string'
|
||||
import { useMacroStore } from '../../stores/macro'
|
||||
|
||||
const messageType = defineModel('messageType', { default: 'reply' })
|
||||
|
||||
@@ -88,7 +88,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@shared-ui/components/u
|
||||
import { Spinner } from '@shared-ui/components/ui/spinner'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@shared-ui/components/ui/avatar'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { revertCIDToImageSrc } from '@/utils/strings'
|
||||
import { revertCIDToImageSrc } from '@shared-ui/utils/string'
|
||||
import { formatMessageTimestamp, formatFullTimestamp } from '@/utils/datetime'
|
||||
import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue'
|
||||
import MessageEnvelope from './MessageEnvelope.vue'
|
||||
|
||||
@@ -64,6 +64,13 @@
|
||||
{{ conversation.contact.external_user_id }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="conversation?.contact?.type === 'visitor'"
|
||||
class="text-sm text-amber-600 flex gap-2 items-center bg-amber-50 dark:bg-amber-900/20 p-2 rounded"
|
||||
>
|
||||
<AlertCircle size="16" class="flex-shrink-0" />
|
||||
<span>{{ t('contact.identityNotVerified') }} ({{ t('globals.terms.visitor', 1) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -72,7 +79,7 @@ import { computed } from 'vue'
|
||||
import { ViewVerticalIcon } from '@radix-icons/vue'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@shared-ui/components/ui/avatar'
|
||||
import { Mail, Phone, ExternalLink } from 'lucide-vue-next'
|
||||
import { Mail, Phone, ExternalLink, AlertCircle, IdCard } from 'lucide-vue-next'
|
||||
import countries from '@/constants/countries.js'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
|
||||
@@ -6,7 +6,7 @@ import router from './router'
|
||||
import mitt from 'mitt'
|
||||
import api from './api'
|
||||
import '@shared-ui/assets/styles/main.scss'
|
||||
import './utils/strings.js'
|
||||
import '@shared-ui/utils/string'
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html'
|
||||
import Root from './Root.vue'
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Adds titleCase property to string.
|
||||
String.prototype.titleCase = function () {
|
||||
return this.toLowerCase()
|
||||
.split(' ')
|
||||
.map(function (word) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the `src` attribute of all <img> tags with the class `inline-image`
|
||||
* to use the value of the `title` attribute as a Content-ID (cid).
|
||||
* The resulting `src` will be in the format `cid:content_id`
|
||||
*
|
||||
* @param {string} htmlString - The input HTML string.
|
||||
* @returns {string} - The updated HTML string with `src` replaced by `cid:title`.
|
||||
*/
|
||||
export function transformImageSrcToCID (htmlString) {
|
||||
return htmlString.replace(/(<img\s+class="inline-image"[^>]*?src=")[^"]*(".*?title=")([^"]*)("[^>]*?>)/g, '$1cid:$3$2$3$4');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the `src` attribute of all <img> tags with the class `inline-image`
|
||||
* from the `cid:filename` format to `/uploads/filename`, where the filename is stored in the `title` attribute.
|
||||
*
|
||||
* @param {string} htmlString - The input HTML string.
|
||||
* @returns {string} - The updated HTML string with `cid:title` replaced by `/uploads/title`.
|
||||
*/
|
||||
export function revertCIDToImageSrc (htmlString) {
|
||||
return htmlString.replace(/(<img\s+class="inline-image"[^>]*?src=")cid:([^"]*)(".*?title=")\2("[^>]*?>)/g, '$1/uploads/$2$3$2$4');
|
||||
}
|
||||
|
||||
export function validateEmail (email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
export const isGoDuration = (value) => {
|
||||
if (value === '') return false
|
||||
const regex = /^(\d+h)?(\d+m)?(\d+s)?$/
|
||||
return regex.test(value)
|
||||
}
|
||||
|
||||
export const isGoHourMinuteDuration = (value) => {
|
||||
const regex = /^([0-9]+h|[0-9]+m)$/
|
||||
return regex.test(value)
|
||||
}
|
||||
|
||||
const template = document.createElement('template')
|
||||
export function getTextFromHTML (htmlString) {
|
||||
try {
|
||||
template.innerHTML = htmlString
|
||||
const text = template.content.textContent || template.content.innerText || ''
|
||||
template.innerHTML = ''
|
||||
return text.trim()
|
||||
} catch (error) {
|
||||
console.error('Error converting HTML to text:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function getInitials (firstName = '', lastName = '') {
|
||||
const firstInitial = firstName.charAt(0).toUpperCase() || ''
|
||||
const lastInitial = lastName.charAt(0).toUpperCase() || ''
|
||||
return `${firstInitial}${lastInitial}`
|
||||
}
|
||||
@@ -5,8 +5,14 @@
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
<p>Add email inboxes by configuring IMAP and SMTP settings.</p>
|
||||
<p>Each added inbox creates a new email channel for receiving customer emails.</p>
|
||||
<div class="flex flex-col justify-evenly h-full gap-4">
|
||||
<p class="font-semibold text-2xl">{{ $t('admin.inbox.help.title') }}</p>
|
||||
<p>{{ $t('admin.inbox.help.description') }}</p>
|
||||
<ul class="list-disc list-inside space-y-2">
|
||||
<li>{{ $t('admin.inbox.help.email') }}</li>
|
||||
<li>{{ $t('admin.inbox.help.livechat') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</AdminPageWithHelp>
|
||||
</template>
|
||||
|
||||
@@ -64,7 +64,7 @@ import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { handleHTTPError } from '../../utils/http'
|
||||
import api from '../../api'
|
||||
import { validateEmail } from '../../utils/strings'
|
||||
import { validateEmail } from '@shared-ui/utils/string'
|
||||
import { useTemporaryClass } from '../../composables/useTemporaryClass'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Error } from '@shared-ui/components/ui/error'
|
||||
|
||||
@@ -123,7 +123,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { handleHTTPError } from '../../utils/http'
|
||||
import api from '../../api'
|
||||
import { validateEmail } from '../../utils/strings'
|
||||
import { validateEmail } from '@shared-ui/utils/string'
|
||||
import { useTemporaryClass } from '../../composables/useTemporaryClass'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Error } from '@shared-ui/components/ui/error'
|
||||
|
||||
@@ -20,10 +20,13 @@
|
||||
:label="t('globals.messages.upload')"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-foreground">
|
||||
{{ contact.first_name }} {{ contact.last_name }}
|
||||
</h2>
|
||||
<Badge v-if="contact.type" variant="secondary">
|
||||
{{ contact.type.titleCase() }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
@@ -93,6 +96,7 @@ import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { AvatarUpload } from '@shared-ui/components/ui/avatar'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Badge } from '@shared-ui/components/ui/badge'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// Adds titleCase property to string.
|
||||
String.prototype.titleCase = function () {
|
||||
return this.toLowerCase()
|
||||
.split(' ')
|
||||
.map(function (word) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function convertTextToHtml (text) {
|
||||
const div = document.createElement('div')
|
||||
div.innerText = text
|
||||
@@ -9,3 +19,49 @@ export function parseJWT (token) {
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
return JSON.parse(atob(base64))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the `src` attribute of all <img> tags with the class `inline-image`
|
||||
* from the `cid:filename` format to `/uploads/filename`, where the filename is stored in the `title` attribute.
|
||||
*
|
||||
* @param {string} htmlString - The input HTML string.
|
||||
* @returns {string} - The updated HTML string with `cid:title` replaced by `/uploads/title`.
|
||||
*/
|
||||
export function revertCIDToImageSrc (htmlString) {
|
||||
return htmlString.replace(/(<img\s+class="inline-image"[^>]*?src=")cid:([^"]*)(".*?title=")\2("[^>]*?>)/g, '$1/uploads/$2$3$2$4');
|
||||
}
|
||||
|
||||
export function validateEmail (email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
export const isGoDuration = (value) => {
|
||||
if (value === '') return false
|
||||
const regex = /^(\d+h)?(\d+m)?(\d+s)?$/
|
||||
return regex.test(value)
|
||||
}
|
||||
|
||||
export const isGoHourMinuteDuration = (value) => {
|
||||
const regex = /^([0-9]+h|[0-9]+m)$/
|
||||
return regex.test(value)
|
||||
}
|
||||
|
||||
const template = document.createElement('template')
|
||||
export function getTextFromHTML (htmlString) {
|
||||
try {
|
||||
template.innerHTML = htmlString
|
||||
const text = template.content.textContent || template.content.innerText || ''
|
||||
template.innerHTML = ''
|
||||
return text.trim()
|
||||
} catch (error) {
|
||||
console.error('Error converting HTML to text:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function getInitials (firstName = '', lastName = '') {
|
||||
const firstInitial = firstName.charAt(0).toUpperCase() || ''
|
||||
const lastInitial = lastName.charAt(0).toUpperCase() || ''
|
||||
return `${firstInitial}${lastInitial}`
|
||||
}
|
||||
|
||||
78
i18n/en.json
78
i18n/en.json
@@ -3,6 +3,7 @@
|
||||
"_.name": "English (en)",
|
||||
"globals.terms.user": "User | Users",
|
||||
"globals.terms.contact": "Contact | Contacts",
|
||||
"globals.terms.visitor": "Visitor | Visitors",
|
||||
"globals.terms.agent": "Agent | Agents",
|
||||
"globals.terms.team": "Team | Teams",
|
||||
"globals.terms.message": "Message | Messages",
|
||||
@@ -525,83 +526,87 @@
|
||||
"admin.inbox.chooseChannel": "Choose a channel",
|
||||
"admin.inbox.configureChannel": "Configure channel",
|
||||
"admin.inbox.createEmailInbox": "Create Email Inbox",
|
||||
"admin.inbox.livechatConfig": "Live Chat Configuration",
|
||||
"admin.inbox.livechatConfig": "Live chat configuration",
|
||||
"admin.inbox.help.title": "Manage inboxes",
|
||||
"admin.inbox.help.description": "Configure and manage different communication channels for customer interactions",
|
||||
"admin.inbox.help.email": "Configure IMAP/SMTP settings to set up email support and receive customer emails",
|
||||
"admin.inbox.help.livechat": "Create live chat widgets that can be embedded on your website for real-time customer support",
|
||||
"admin.inbox.livechat.tabs.general": "General",
|
||||
"admin.inbox.livechat.tabs.appearance": "Appearance",
|
||||
"admin.inbox.livechat.tabs.messages": "Messages",
|
||||
"admin.inbox.livechat.tabs.features": "Features",
|
||||
"admin.inbox.livechat.tabs.security": "Security",
|
||||
"admin.inbox.livechat.tabs.prechat": "Pre-Chat Form",
|
||||
"admin.inbox.livechat.tabs.prechat": "Pre-chat form",
|
||||
"admin.inbox.livechat.tabs.users": "Users",
|
||||
"admin.inbox.livechat.logoUrl": "Logo URL",
|
||||
"admin.inbox.livechat.logoUrl.description": "URL of the logo to display in the chat widget",
|
||||
"admin.inbox.livechat.secretKey": "Secret Key",
|
||||
"admin.inbox.livechat.secretKey": "Secret key",
|
||||
"admin.inbox.livechat.secretKey.description": "Set a secret key to secure the chat widget.",
|
||||
"admin.inbox.livechat.launcher": "Launcher",
|
||||
"admin.inbox.livechat.launcher.position": "Position",
|
||||
"admin.inbox.livechat.launcher.position.left": "Left",
|
||||
"admin.inbox.livechat.launcher.position.right": "Right",
|
||||
"admin.inbox.livechat.launcher.logo": "Launcher Logo",
|
||||
"admin.inbox.livechat.launcher.spacing.side": "Side Spacing",
|
||||
"admin.inbox.livechat.launcher.logo": "Launcher logo",
|
||||
"admin.inbox.livechat.launcher.spacing.side": "Side spacing",
|
||||
"admin.inbox.livechat.launcher.spacing.side.description": "Distance from the side of the screen in pixels",
|
||||
"admin.inbox.livechat.launcher.spacing.bottom": "Bottom Spacing",
|
||||
"admin.inbox.livechat.launcher.spacing.bottom": "Bottom spacing",
|
||||
"admin.inbox.livechat.launcher.spacing.bottom.description": "Distance from the bottom of the screen in pixels",
|
||||
"admin.inbox.livechat.messages": "Messages",
|
||||
"admin.inbox.livechat.greetingMessage": "Greeting Message",
|
||||
"admin.inbox.livechat.introductionMessage": "Introduction Message",
|
||||
"admin.inbox.livechat.chatIntroduction": "Chat Introduction",
|
||||
"admin.inbox.livechat.greetingMessage": "Greeting message",
|
||||
"admin.inbox.livechat.introductionMessage": "Introduction message",
|
||||
"admin.inbox.livechat.chatIntroduction": "Chat introduction",
|
||||
"admin.inbox.livechat.chatIntroduction.description": "Default: Ask us anything, or share your feedback.",
|
||||
"admin.inbox.livechat.chatReplyExpectationMessage": "Chat reply expectation message",
|
||||
"admin.inbox.livechat.chatReplyExpectationMessage.description": "Message shown to customers during business hours about expected reply times",
|
||||
"admin.inbox.livechat.officeHours": "Office Hours",
|
||||
"admin.inbox.livechat.showOfficeHoursInChat": "Show Office Hours in Chat",
|
||||
"admin.inbox.livechat.officeHours": "Office hours",
|
||||
"admin.inbox.livechat.showOfficeHoursInChat": "Show office hours in chat",
|
||||
"admin.inbox.livechat.showOfficeHoursInChat.description": "Show when the team will be next available",
|
||||
"admin.inbox.livechat.showOfficeHoursAfterAssignment": "Show Office Hours After Team Assignment",
|
||||
"admin.inbox.livechat.showOfficeHoursAfterAssignment": "Show office hours after team assignment",
|
||||
"admin.inbox.livechat.showOfficeHoursAfterAssignment.description": "Show office hours after conversation is assigned to a team",
|
||||
"admin.inbox.livechat.showPoweredBy": "Show Powered By",
|
||||
"admin.inbox.livechat.showPoweredBy": "Show powered by",
|
||||
"admin.inbox.livechat.showPoweredBy.description": "Show \"Powered by Libredesk\" in the chat widget",
|
||||
"admin.inbox.livechat.noticeBanner": "Notice Banner",
|
||||
"admin.inbox.livechat.noticeBanner.enabled": "Enable Notice Banner",
|
||||
"admin.inbox.livechat.noticeBanner.text": "Notice Banner Text",
|
||||
"admin.inbox.livechat.noticeBanner": "Notice banner",
|
||||
"admin.inbox.livechat.noticeBanner.enabled": "Enable notice banner",
|
||||
"admin.inbox.livechat.noticeBanner.text": "Notice banner text",
|
||||
"admin.inbox.livechat.colors": "Colors",
|
||||
"admin.inbox.livechat.colors.primary": "Primary Color",
|
||||
"admin.inbox.livechat.colors.background": "Background Color",
|
||||
"admin.inbox.livechat.darkMode": "Dark Mode",
|
||||
"admin.inbox.livechat.colors.primary": "Primary color",
|
||||
"admin.inbox.livechat.colors.background": "Background color",
|
||||
"admin.inbox.livechat.darkMode": "Dark mode",
|
||||
"admin.inbox.livechat.darkMode.description": "Enable dark mode for the chat widget",
|
||||
"admin.inbox.livechat.features": "Features",
|
||||
"admin.inbox.livechat.features.fileUpload": "File Upload",
|
||||
"admin.inbox.livechat.features.fileUpload": "File upload",
|
||||
"admin.inbox.livechat.features.fileUpload.description": "Allow users to upload files in chat",
|
||||
"admin.inbox.livechat.features.emoji": "Emoji Support",
|
||||
"admin.inbox.livechat.features.emoji": "Emoji support",
|
||||
"admin.inbox.livechat.features.emoji.description": "Allow users to use emojis in chat",
|
||||
"admin.inbox.livechat.features.allowCloseConversation": "Allow Close Conversation",
|
||||
"admin.inbox.livechat.features.allowCloseConversation": "Allow close conversation",
|
||||
"admin.inbox.livechat.features.allowCloseConversation.description": "Allow users to close their own conversations",
|
||||
"admin.inbox.livechat.externalLinks": "External Links",
|
||||
"admin.inbox.livechat.externalLinks.add": "Add External Link",
|
||||
"admin.inbox.livechat.externalLinks": "External links",
|
||||
"admin.inbox.livechat.externalLinks.add": "Add external link",
|
||||
"admin.inbox.livechat.externalLinks.description": "Add helpful links that will be displayed in the chat widget",
|
||||
"admin.inbox.livechat.trustedDomains": "Trusted Domains",
|
||||
"admin.inbox.livechat.trustedDomains.list": "Domain List",
|
||||
"admin.inbox.livechat.trustedDomains": "Trusted domains",
|
||||
"admin.inbox.livechat.trustedDomains.list": "Domain list",
|
||||
"admin.inbox.livechat.trustedDomains.description": "Specify your trusted domains and subdomains, one per line. Use an asterisk wildcard to trust all subdomains: *.example.com. Leaving this field empty will allowing widget to be embedded on any domain.",
|
||||
"admin.inbox.livechat.userSettings": "User Settings",
|
||||
"admin.inbox.livechat.userSettings": "User settings",
|
||||
"admin.inbox.livechat.userSettings.visitors": "Visitors",
|
||||
"admin.inbox.livechat.userSettings.users": "Users",
|
||||
"admin.inbox.livechat.startConversationButtonText": "Start Conversation Button Text",
|
||||
"admin.inbox.livechat.allowStartConversation": "Allow Start Conversation",
|
||||
"admin.inbox.livechat.startConversationButtonText": "Start conversation button text",
|
||||
"admin.inbox.livechat.allowStartConversation": "Allow start conversation",
|
||||
"admin.inbox.livechat.allowStartConversation.visitors.description": "Allow visitors to start new conversations",
|
||||
"admin.inbox.livechat.allowStartConversation.users.description": "Allow users users to start new conversations",
|
||||
"admin.inbox.livechat.preventMultipleConversations": "Prevent Multiple Conversations",
|
||||
"admin.inbox.livechat.preventMultipleConversations": "Prevent multiple conversations",
|
||||
"admin.inbox.livechat.preventMultipleConversations.visitors.description": "Prevent visitors from starting multiple conversations simultaneously",
|
||||
"admin.inbox.livechat.preventMultipleConversations.users.description": "Prevent users users from starting multiple conversations simultaneously",
|
||||
"admin.inbox.livechat.preventReplyingToClosedConversations": "Prevent Replying to Closed Conversations",
|
||||
"admin.inbox.livechat.preventReplyingToClosedConversations": "Prevent replying to closed conversations",
|
||||
"admin.inbox.livechat.preventReplyingToClosedConversations.visitors.description": "Prevent visitors from replying to closed conversations",
|
||||
"admin.inbox.livechat.preventReplyingToClosedConversations.users.description": "Prevent users users from replying to closed conversations",
|
||||
"admin.inbox.livechat.prechatForm.enabled": "Enable Pre-Chat Form",
|
||||
"admin.inbox.livechat.prechatForm.enabled": "Enable pre-chat form",
|
||||
"admin.inbox.livechat.prechatForm.enabled.description": "Show a form to collect information before chat starts",
|
||||
"admin.inbox.livechat.prechatForm.title": "Form Title",
|
||||
"admin.inbox.livechat.prechatForm.title": "Form title",
|
||||
"admin.inbox.livechat.prechatForm.title.description": "Title displayed above the pre-chat form",
|
||||
"admin.inbox.livechat.prechatForm.fields": "Form Fields",
|
||||
"admin.inbox.livechat.prechatForm.addField": "Add Field",
|
||||
"admin.inbox.livechat.prechatForm.fields": "Form fields",
|
||||
"admin.inbox.livechat.prechatForm.addField": "Add field",
|
||||
"admin.inbox.livechat.prechatForm.noFields": "No fields configured. Add fields from custom attributes.",
|
||||
"admin.inbox.livechat.prechatForm.availableFields": "Available Custom Attributes",
|
||||
"admin.inbox.livechat.prechatForm.availableFields": "Available custom attributes",
|
||||
"admin.inbox.livechat.conversationContinuity": "Conversation continuity email inbox",
|
||||
"admin.inbox.livechat.conversationContinuity.description": "When contacts go offline, replies will be sent from this email inbox. The contacts can continue the same conversation by replying to the email or in the chat widget when they return to your site.",
|
||||
"admin.inbox.livechat.continuityEmailFooter": "Reply directly to this email to continue the conversation.",
|
||||
@@ -766,6 +771,7 @@
|
||||
"contact.blockConfirm": "Are you sure you want to block this contact? They will no longer be able to interact with you.",
|
||||
"contact.unblockConfirm": "Are you sure you want to unblock this contact? They will be able to interact with you again.",
|
||||
"contact.alreadyExistsWithEmail": "Another contact with same email already exists",
|
||||
"contact.identityNotVerified": "Identity not verified",
|
||||
"contact.notes.empty": "No notes yet",
|
||||
"contact.notes.help": "Add note for this contact to keep track of important information and conversations.",
|
||||
"globals.days.sunday": "Sunday",
|
||||
|
||||
@@ -183,8 +183,7 @@ func (al *Manager) UserAvailability(actorID int, actorEmail, status, ip, targetE
|
||||
|
||||
// create creates a new activity log in DB.
|
||||
func (m *Manager) create(activityType, activityDescription string, actorID int, targetModelType string, targetModelID int, ip string) error {
|
||||
var activityLog models.ActivityLog
|
||||
if err := m.q.InsertActivity.Get(&activityLog, activityType, activityDescription, actorID, targetModelType, targetModelID, ip); err != nil {
|
||||
if _, err := m.q.InsertActivity.Exec(activityType, activityDescription, actorID, targetModelType, targetModelID, ip); err != nil {
|
||||
m.lo.Error("error inserting activity log", "error", err)
|
||||
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.activityLog}"), nil)
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ type teamStore interface {
|
||||
}
|
||||
|
||||
type userStore interface {
|
||||
Get(int, string, string) (umodels.User, error)
|
||||
Get(int, string, []string) (umodels.User, error)
|
||||
GetAgent(int, string) (umodels.User, error)
|
||||
GetContact(int, string) (umodels.User, error)
|
||||
GetVisitor(int) (umodels.User, error)
|
||||
@@ -1460,7 +1460,7 @@ func (m *Manager) BuildWidgetConversationResponse(conversation models.Conversati
|
||||
if cachedUser, ok := userCache[msg.SenderID]; ok {
|
||||
user = cachedUser
|
||||
} else {
|
||||
user, err = m.userStore.Get(msg.SenderID, "", "")
|
||||
user, err = m.userStore.Get(msg.SenderID, "", []string{})
|
||||
if err != nil {
|
||||
m.lo.Error("error fetching message sender user", "sender_id", msg.SenderID, "conversation_uuid", conversation.UUID, "error", err)
|
||||
} else {
|
||||
|
||||
@@ -424,12 +424,7 @@ func (m *Manager) QueueReply(media []mmodels.Media, inboxID, senderID, contactID
|
||||
return models.Message{}, envelope.NewError(envelope.InputError, m.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil)
|
||||
}
|
||||
|
||||
sourceID, err := stringutil.GenerateEmailMessageID(conversationUUID, inboxRecord.From)
|
||||
if err != nil {
|
||||
m.lo.Error("error generating source message id", "error", err)
|
||||
return message, envelope.NewError(envelope.GeneralError, m.i18n.T("conversation.errorGeneratingMessageID"), nil)
|
||||
}
|
||||
|
||||
var sourceID string
|
||||
switch inboxRecord.Channel {
|
||||
case inbox.ChannelEmail:
|
||||
// Add `to`, `cc`, and `bcc` recipients to meta map.
|
||||
@@ -746,7 +741,7 @@ func (m *Manager) ProcessIncomingMessage(in models.IncomingMessage) (models.Mess
|
||||
}
|
||||
} else {
|
||||
// Conversation found validate sender email matches contact email
|
||||
contact, err := m.userStore.Get(conversation.ContactID, "", "")
|
||||
contact, err := m.userStore.Get(conversation.ContactID, "", []string{})
|
||||
if err != nil {
|
||||
m.lo.Error("error fetching conversation contact", "contact_id", conversation.ContactID, "error", err)
|
||||
return models.Message{}, fmt.Errorf("fetching conversation contact: %w", err)
|
||||
|
||||
@@ -166,7 +166,7 @@ type Conversation struct {
|
||||
InboxName string `db:"inbox_name" json:"inbox_name"`
|
||||
InboxChannel string `db:"inbox_channel" json:"inbox_channel"`
|
||||
Tags null.JSON `db:"tags" json:"tags"`
|
||||
Meta pq.StringArray `db:"meta" json:"meta"`
|
||||
Meta json.RawMessage `db:"meta" json:"meta"`
|
||||
CustomAttributes json.RawMessage `db:"custom_attributes" json:"custom_attributes"`
|
||||
LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"`
|
||||
LastMessage null.String `db:"last_message" json:"last_message"`
|
||||
|
||||
@@ -11,7 +11,7 @@ VALUES($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *
|
||||
|
||||
-- name: get-inbox
|
||||
SELECT id, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from" FROM inboxes where id = $1 and deleted_at is NULL;
|
||||
SELECT id, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from", secret FROM inboxes where id = $1 and deleted_at is NULL;
|
||||
|
||||
-- name: update
|
||||
UPDATE inboxes
|
||||
|
||||
@@ -29,7 +29,7 @@ func (u *Manager) MonitorAgentAvailability(ctx context.Context) {
|
||||
|
||||
// GetAgent retrieves an agent by ID and also caches it for future requests.
|
||||
func (u *Manager) GetAgent(id int, email string) (models.User, error) {
|
||||
agent, err := u.Get(id, email, models.UserTypeAgent)
|
||||
agent, err := u.Get(id, email, []string{models.UserTypeAgent})
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func (u *Manager) CreateAgent(firstName, lastName, email string, roles []string)
|
||||
u.lo.Error("error creating user", "error", err)
|
||||
return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}"), nil)
|
||||
}
|
||||
return u.Get(id, "", models.UserTypeAgent)
|
||||
return u.Get(id, "", []string{models.UserTypeAgent})
|
||||
}
|
||||
|
||||
// UpdateAgent updates an agent with individual field parameters
|
||||
@@ -162,5 +162,5 @@ func (u *Manager) markInactiveAgentsOffline() {
|
||||
// GetAllAgents returns a list of all agents.
|
||||
func (u *Manager) GetAgents() ([]models.UserCompact, error) {
|
||||
// Some dirty hack.
|
||||
return u.GetAllUsers(1, 999999999, models.UserTypeAgent, "desc", "users.updated_at", "")
|
||||
return u.GetAllUsers(1, 999999999, []string{models.UserTypeAgent}, "desc", "users.updated_at", "")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (u *Manager) UpdateContact(id int, user models.User) error {
|
||||
|
||||
// GetContact retrieves a contact by ID.
|
||||
func (u *Manager) GetContact(id int, email string) (models.User, error) {
|
||||
return u.Get(id, email, models.UserTypeContact)
|
||||
return u.Get(id, email, []string{models.UserTypeContact, models.UserTypeVisitor})
|
||||
}
|
||||
|
||||
// GetAllContacts returns a list of all contacts.
|
||||
@@ -61,5 +61,5 @@ func (u *Manager) GetContacts(page, pageSize int, order, orderBy string, filters
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
return u.GetAllUsers(page, pageSize, models.UserTypeContact, order, orderBy, filtersJSON)
|
||||
return u.GetAllUsers(page, pageSize, []string{models.UserTypeContact, models.UserTypeVisitor}, order, orderBy, filtersJSON)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ type User struct {
|
||||
PhoneNumber null.String `db:"phone_number" json:"phone_number"`
|
||||
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
Password string `db:"password" json:"-"`
|
||||
Password null.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"`
|
||||
Roles pq.StringArray `db:"roles" json:"roles"`
|
||||
|
||||
@@ -61,7 +61,7 @@ LEFT JOIN LATERAL unnest(r.permissions) AS p ON true
|
||||
WHERE u.deleted_at IS NULL
|
||||
AND ($1 = 0 OR u.id = $1)
|
||||
AND ($2 = '' OR u.email = $2)
|
||||
AND ($3 = '' OR u.type::text = $3)
|
||||
AND (cardinality($3::text[]) = 0 OR u.type::text = ANY($3::text[]))
|
||||
GROUP BY u.id;
|
||||
|
||||
-- name: set-user-password
|
||||
@@ -192,7 +192,7 @@ SET first_name = COALESCE($2, first_name),
|
||||
phone_number = $6,
|
||||
phone_number_country_code = $7,
|
||||
updated_at = now()
|
||||
WHERE id = $1 and type = 'contact';
|
||||
WHERE id = $1 and type in ('contact', 'visitor');
|
||||
|
||||
-- name: get-notes
|
||||
SELECT
|
||||
@@ -301,7 +301,7 @@ SELECT
|
||||
u.availability_status,
|
||||
u.last_active_at,
|
||||
u.last_login_at,
|
||||
u.phone_number_calling_code,
|
||||
u.phone_number_country_code,
|
||||
u.phone_number,
|
||||
u.external_user_id,
|
||||
u.custom_attributes,
|
||||
|
||||
@@ -111,22 +111,22 @@ func New(i18n *i18n.I18n, opts Opts) (*Manager, error) {
|
||||
// VerifyPassword authenticates an user by email and password, returning the user if successful.
|
||||
func (u *Manager) VerifyPassword(email string, password []byte) (models.User, error) {
|
||||
var user models.User
|
||||
if err := u.q.GetUser.Get(&user, 0, email, models.UserTypeAgent); err != nil {
|
||||
if err := u.q.GetUser.Get(&user, 0, email, pq.Array([]string{models.UserTypeAgent})); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return user, envelope.NewError(envelope.InputError, u.i18n.T("user.invalidEmailPassword"), nil)
|
||||
}
|
||||
u.lo.Error("error fetching user from db", "error", err)
|
||||
return user, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil)
|
||||
}
|
||||
if err := u.verifyPassword(password, user.Password); err != nil {
|
||||
if err := u.verifyPassword(password, user.Password.String); err != nil {
|
||||
return user, envelope.NewError(envelope.InputError, u.i18n.T("user.invalidEmailPassword"), nil)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetAllUsers returns a list of all users.
|
||||
func (u *Manager) GetAllUsers(page, pageSize int, userType, order, orderBy string, filtersJSON string) ([]models.UserCompact, error) {
|
||||
query, qArgs, err := u.makeUserListQuery(page, pageSize, userType, order, orderBy, filtersJSON)
|
||||
func (u *Manager) GetAllUsers(page, pageSize int, userTypes []string, order, orderBy string, filtersJSON string) ([]models.UserCompact, error) {
|
||||
query, qArgs, err := u.makeUserListQuery(page, pageSize, userTypes, order, orderBy, filtersJSON)
|
||||
if err != nil {
|
||||
u.lo.Error("error creating user list query", "error", err)
|
||||
return nil, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil)
|
||||
@@ -152,14 +152,14 @@ func (u *Manager) GetAllUsers(page, pageSize int, userType, order, orderBy strin
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Get retrieves an user by ID or email.
|
||||
func (u *Manager) Get(id int, email, type_ string) (models.User, error) {
|
||||
// Get retrieves an user by ID or email or type. At least one of ID or email must be provided.
|
||||
func (u *Manager) Get(id int, email string, userType []string) (models.User, error) {
|
||||
if id == 0 && email == "" {
|
||||
return models.User{}, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.user}"), nil)
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := u.q.GetUser.Get(&user, id, email, type_); err != nil {
|
||||
if err := u.q.GetUser.Get(&user, id, email, pq.Array(userType)); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return user, envelope.NewError(envelope.NotFoundError, u.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.user}"), nil)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (u *Manager) Get(id int, email, type_ string) (models.User, error) {
|
||||
|
||||
// GetSystemUser retrieves the system user.
|
||||
func (u *Manager) GetSystemUser() (models.User, error) {
|
||||
return u.Get(0, models.SystemUserEmail, models.UserTypeAgent)
|
||||
return u.Get(0, models.SystemUserEmail, []string{models.UserTypeAgent})
|
||||
}
|
||||
|
||||
// GetByExternalID retrieves a user by external user ID.
|
||||
@@ -459,9 +459,9 @@ func updateSystemUserPassword(db *sqlx.DB, hashedPassword []byte) error {
|
||||
}
|
||||
|
||||
// makeUserListQuery generates a query to fetch users based on the provided filters.
|
||||
func (u *Manager) makeUserListQuery(page, pageSize int, typ, order, orderBy, filtersJSON string) (string, []interface{}, error) {
|
||||
func (u *Manager) makeUserListQuery(page, pageSize int, userTypes []string, order, orderBy, filtersJSON string) (string, []interface{}, error) {
|
||||
var qArgs []any
|
||||
qArgs = append(qArgs, pq.Array([]string{typ}))
|
||||
qArgs = append(qArgs, pq.Array(userTypes))
|
||||
return dbutil.BuildPaginatedQuery(u.q.GetUsersCompact, qArgs, dbutil.PaginationOptions{
|
||||
Order: order,
|
||||
OrderBy: orderBy,
|
||||
|
||||
@@ -28,5 +28,5 @@ func (u *Manager) CreateVisitor(user *models.User) error {
|
||||
|
||||
// GetVisitor retrieves a visitor user by ID
|
||||
func (u *Manager) GetVisitor(id int) (models.User, error) {
|
||||
return u.Get(id, "", models.UserTypeVisitor)
|
||||
return u.Get(id, "", []string{models.UserTypeVisitor})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ DROP TYPE IF EXISTS "message_status" CASCADE; CREATE TYPE "message_status" AS EN
|
||||
DROP TYPE IF EXISTS "content_type" CASCADE; CREATE TYPE "content_type" AS ENUM ('text','html');
|
||||
DROP TYPE IF EXISTS "conversation_assignment_type" CASCADE; CREATE TYPE "conversation_assignment_type" AS ENUM ('Round robin','Manual');
|
||||
DROP TYPE IF EXISTS "template_type" CASCADE; CREATE TYPE "template_type" AS ENUM ('email_outgoing', 'email_notification');
|
||||
-- Visitors are unauthenticated contacts.
|
||||
DROP TYPE IF EXISTS "user_type" CASCADE; CREATE TYPE "user_type" AS ENUM ('agent', 'contact', 'visitor');
|
||||
DROP TYPE IF EXISTS "ai_provider" CASCADE; CREATE TYPE "ai_provider" AS ENUM ('openai');
|
||||
DROP TYPE IF EXISTS "automation_execution_mode" CASCADE; CREATE TYPE "automation_execution_mode" AS ENUM ('all', 'first_match');
|
||||
|
||||
Reference in New Issue
Block a user