Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2921db5bd4 | ||
|
|
3d7567bf23 |
@@ -4,6 +4,7 @@ import { provideSidebarContext, useSidebarResize } from './provider';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useKbd } from 'dashboard/composables/utils/useKbd';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||
@@ -40,6 +41,16 @@ const { accountScopedRoute, isOnChatwootCloud } = useAccount();
|
||||
const store = useStore();
|
||||
const searchShortcut = useKbd([`$mod`, 'k']);
|
||||
const { t } = useI18n();
|
||||
const { uiSettings } = useUISettings();
|
||||
const isHomeDashboardEnabled = computed(
|
||||
() => !!uiSettings.value?.home_dashboard_enabled
|
||||
);
|
||||
const unreadInboxCount = useMapGetter('notifications/getUnreadCount');
|
||||
const unreadInboxCountLabel = computed(() => {
|
||||
const count = unreadInboxCount.value;
|
||||
if (!count) return '';
|
||||
return count < 100 ? String(count) : '99+';
|
||||
});
|
||||
|
||||
const isACustomBrandedInstance = useMapGetter(
|
||||
'globalConfig/isACustomBrandedInstance'
|
||||
@@ -212,16 +223,28 @@ const reportRoutes = computed(() => newReportRoutes());
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: 'Inbox',
|
||||
label: t('SIDEBAR.INBOX'),
|
||||
icon: 'i-lucide-inbox',
|
||||
to: accountScopedRoute('inbox_view'),
|
||||
activeOn: ['inbox_view', 'inbox_view_conversation'],
|
||||
getterKeys: {
|
||||
count: 'notifications/getUnreadCount',
|
||||
},
|
||||
},
|
||||
...(isHomeDashboardEnabled.value
|
||||
? [
|
||||
{
|
||||
name: 'Home',
|
||||
label: t('SIDEBAR.HOME'),
|
||||
icon: 'i-lucide-gauge',
|
||||
to: accountScopedRoute('home_dashboard'),
|
||||
activeOn: ['home_dashboard', 'home_dashboard_admin'],
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Inbox',
|
||||
label: t('SIDEBAR.INBOX'),
|
||||
icon: 'i-lucide-inbox',
|
||||
to: accountScopedRoute('inbox_view'),
|
||||
activeOn: ['inbox_view', 'inbox_view_conversation'],
|
||||
getterKeys: {
|
||||
count: 'notifications/getUnreadCount',
|
||||
},
|
||||
},
|
||||
]),
|
||||
{
|
||||
name: 'Conversation',
|
||||
label: t('SIDEBAR.CONVERSATIONS'),
|
||||
@@ -858,6 +881,20 @@ const menuItems = computed(() => {
|
||||
:is-collapsed="isEffectivelyCollapsed"
|
||||
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
||||
/>
|
||||
<RouterLink
|
||||
v-if="isHomeDashboardEnabled"
|
||||
:to="accountScopedRoute('inbox_view')"
|
||||
:title="t('SIDEBAR.INBOX')"
|
||||
class="size-8 rounded-lg hover:bg-n-alpha-1 flex-shrink-0 grid place-content-center relative text-n-slate-11"
|
||||
>
|
||||
<span class="i-lucide-bell size-4" />
|
||||
<span
|
||||
v-if="unreadInboxCountLabel"
|
||||
class="min-h-2 min-w-2 p-0.5 px-1 bg-n-ruby-9 rounded-lg absolute -top-1 -right-1.5 grid place-items-center text-[9px] leading-none text-n-ruby-3"
|
||||
>
|
||||
{{ unreadInboxCountLabel }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Resize Handle (desktop only) -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"INBOX": {
|
||||
"LIST": {
|
||||
"TITLE": "My Inbox",
|
||||
"TITLE": "Notifications",
|
||||
"DISPLAY_DROPDOWN": "Display",
|
||||
"LOADING": "Fetching notifications",
|
||||
"404": "There are no active notifications in this group.",
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
"UPDATE_SUCCESS": "Your Language settings have been updated successfully",
|
||||
"UPDATE_ERROR": "There is an error while updating the language settings, please try again",
|
||||
"USE_ACCOUNT_DEFAULT": "Use account default"
|
||||
},
|
||||
"HOME_DASHBOARD": {
|
||||
"TITLE": "Home dashboard",
|
||||
"NOTE": "Show a Home dashboard with team workload, KPIs, and a Captain prompt as your landing page. Hides the My Inbox sidebar item."
|
||||
}
|
||||
},
|
||||
"MESSAGE_SIGNATURE_SECTION": {
|
||||
@@ -228,6 +232,128 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"HOME_DASHBOARD_PAGE": {
|
||||
"ROLE_SWITCH": {
|
||||
"AGENT": "Agent",
|
||||
"ADMIN": "Admin"
|
||||
},
|
||||
"GREETING": {
|
||||
"MORNING": "Good morning",
|
||||
"AFTERNOON": "Good afternoon",
|
||||
"EVENING": "Good evening",
|
||||
"LATE": "Working late",
|
||||
"FALLBACK_NAME": "there",
|
||||
"GREETING_LINE": "{greeting}, ",
|
||||
"NAME_LINE": "{name}."
|
||||
},
|
||||
"TEAM_SUMMARY": {
|
||||
"PREFIX": "Your team has ",
|
||||
"OPEN_CONVERSATIONS": "{count} open conversations",
|
||||
"MID": ", ",
|
||||
"NEARING_SLA": "{count} nearing SLA",
|
||||
"AND": ", and ",
|
||||
"AGENTS_ONLINE": "{count} agents online",
|
||||
"PERIOD": "."
|
||||
},
|
||||
"AGENT_SUMMARY": {
|
||||
"PREFIX": "You have ",
|
||||
"OPEN_CONVERSATIONS": "{count} open conversations",
|
||||
"MID": ", ",
|
||||
"UNATTENDED": "{count} unattended",
|
||||
"AND": ", and ",
|
||||
"NEARING_SLA": "{count} close to SLA breach",
|
||||
"PERIOD": "."
|
||||
},
|
||||
"KPI": {
|
||||
"OPEN_CONVERSATIONS": "Open conversations",
|
||||
"AVG_FIRST_RESPONSE": "Avg first response",
|
||||
"FIRST_RESPONSE": "First response",
|
||||
"RESOLVED_TODAY": "Resolved today",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"SLA_BREACHES_TODAY": "SLA breaches today",
|
||||
"TEAM_CSAT_TODAY": "Team CSAT today",
|
||||
"SEE_ALL": "See all"
|
||||
},
|
||||
"AI": {
|
||||
"SHORTCUT": "⌘K"
|
||||
},
|
||||
"TEAM_WORKLOAD": {
|
||||
"TITLE": "Team workload",
|
||||
"SORT_BY": "Sort by",
|
||||
"HEADERS": {
|
||||
"AGENT": "Agent",
|
||||
"WORKLOAD": "Workload",
|
||||
"UNATTENDED_OPEN": "Unattended / Open",
|
||||
"FRT": "FRT",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"OFF_SHIFT": "Off shift",
|
||||
"CAPACITY": "{percent}% capacity",
|
||||
"YOU": "YOU"
|
||||
},
|
||||
"ATTENTION": {
|
||||
"TITLE": "Needs your attention",
|
||||
"SUBTITLE": "3 items, surfaced by Captain"
|
||||
},
|
||||
"SUGGESTIONS_ADMIN": {
|
||||
"OVERLOADED": "Who's overloaded right now?",
|
||||
"HIGH_VOLUME": "What's driving the {count} open conversations today?",
|
||||
"CSAT_OUTLIERS": "Show CSAT outliers this week",
|
||||
"TEAM_PERFORMANCE": "Summarize team performance today",
|
||||
"COACH_AGENT": "Draft a coaching note for {name}",
|
||||
"QUIET_DAY": "What did the team accomplish today?",
|
||||
"FIND_PATTERNS": "Trending customer questions"
|
||||
},
|
||||
"SUGGESTIONS_AGENT": {
|
||||
"SUMMARIZE_QUEUE": "Summarize my queue",
|
||||
"TRIAGE_UNATTENDED": "Help me triage my {count} unattended",
|
||||
"NEAR_SLA": "Which of my conversations are close to SLA breach?",
|
||||
"URGENT": "What's most urgent right now?",
|
||||
"SUMMARIZE_CONVERSATION": "Summarize my conversation with {name}",
|
||||
"CATCH_UP": "Catch me up on what I missed",
|
||||
"SLOW_DAY": "What can I work on?"
|
||||
},
|
||||
"AI_PLACEHOLDERS_ADMIN": {
|
||||
"DEFAULT": "Ask Captain about your team…",
|
||||
"SUMMARIZE": "Summarize the three most urgent tickets…",
|
||||
"DRAFT_REPLY": "Draft a refund reply for Aria Chen…",
|
||||
"WHY_CSAT": "Why did CSAT slip today?",
|
||||
"FIND_TICKETS": "Find tickets mentioning rate limit this week…"
|
||||
},
|
||||
"AI_PLACEHOLDERS_AGENT": {
|
||||
"DEFAULT": "Ask anything about your queue…",
|
||||
"SUMMARIZE": "Summarize the three most urgent tickets…",
|
||||
"DRAFT_REPLY": "Draft a refund reply for Aria Chen…",
|
||||
"WHY_CSAT": "Why did CSAT slip today?",
|
||||
"FIND_TICKETS": "Find tickets mentioning rate limit this week…"
|
||||
},
|
||||
"SORT_OPTIONS_ADMIN": {
|
||||
"CAPACITY": "Capacity",
|
||||
"FIRST_RESPONSE": "First response",
|
||||
"CSAT_TODAY": "CSAT today",
|
||||
"RESOLVED_TODAY": "Resolved today"
|
||||
},
|
||||
"SORT_OPTIONS_AGENT": {
|
||||
"PRIORITY": "Priority",
|
||||
"SLA_BREACH": "SLA breach",
|
||||
"LATEST": "Latest activity",
|
||||
"OLDEST": "Oldest first",
|
||||
"CUSTOMER": "Customer name"
|
||||
},
|
||||
"TOP_CONVERSATIONS": {
|
||||
"TITLE": "Top conversations",
|
||||
"SLA_LEFT": "{time} left",
|
||||
"EMPTY": "Nothing in your queue.",
|
||||
"LOADING": "Loading…"
|
||||
},
|
||||
"COPILOT_THREAD": {
|
||||
"BACK": "Back to home",
|
||||
"SEND": "Send"
|
||||
},
|
||||
"RECENT_THREADS": {
|
||||
"TITLE": "Recent threads"
|
||||
}
|
||||
},
|
||||
"SIDEBAR_ITEMS": {
|
||||
"CHANGE_AVAILABILITY_STATUS": "Change",
|
||||
"CHANGE_ACCOUNTS": "Switch account",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { routes as contactRoutes } from './contacts/routes';
|
||||
import { routes as companyRoutes } from './companies/routes';
|
||||
import { routes as notificationRoutes } from './notifications/routes';
|
||||
import { routes as inboxRoutes } from './inbox/routes';
|
||||
import { routes as homeRoutes } from './home/routes';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||
import campaignsRoutes from './campaigns/campaigns.routes';
|
||||
@@ -21,6 +22,7 @@ export default {
|
||||
component: AppContainer,
|
||||
children: [
|
||||
...captainRoutes,
|
||||
...homeRoutes,
|
||||
...inboxRoutes,
|
||||
...conversation.routes,
|
||||
...settings.routes,
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import CopilotMessagesAPI from 'dashboard/api/captain/copilotMessages';
|
||||
import CopilotAgentMessage from 'dashboard/components-next/copilot/CopilotAgentMessage.vue';
|
||||
import CopilotAssistantMessage from 'dashboard/components-next/copilot/CopilotAssistantMessage.vue';
|
||||
import CopilotThinkingGroup from 'dashboard/components-next/copilot/CopilotThinkingGroup.vue';
|
||||
import CopilotLoader from 'dashboard/components-next/copilot/CopilotLoader.vue';
|
||||
import CopilotInput from 'dashboard/components-next/copilot/CopilotInput.vue';
|
||||
import ToggleCopilotAssistant from 'dashboard/components-next/copilot/ToggleCopilotAssistant.vue';
|
||||
|
||||
const props = defineProps({
|
||||
threadId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { accountScopedRoute } = useAccount();
|
||||
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
|
||||
const messages = computed(() =>
|
||||
store.getters['copilotMessages/getMessagesByThreadId'](props.threadId)
|
||||
);
|
||||
|
||||
const activeAssistant = computed(() => {
|
||||
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
||||
if (preferredId) {
|
||||
const found = assistants.value.find(a => a.id === preferredId);
|
||||
if (found) return found;
|
||||
}
|
||||
return assistants.value[0];
|
||||
});
|
||||
|
||||
const setAssistant = async assistant => {
|
||||
await updateUISettings({
|
||||
preferred_captain_assistant_id: assistant.id,
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessage = async message => {
|
||||
try {
|
||||
await store.dispatch('copilotMessages/create', {
|
||||
assistant_id: activeAssistant.value?.id,
|
||||
threadId: props.threadId,
|
||||
message,
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
router.push(accountScopedRoute('home_dashboard'));
|
||||
};
|
||||
|
||||
const groupedMessages = computed(() => {
|
||||
const result = [];
|
||||
let thinkingGroup = [];
|
||||
messages.value.forEach(message => {
|
||||
if (message.message_type === 'assistant_thinking') {
|
||||
thinkingGroup.push(message);
|
||||
} else {
|
||||
if (thinkingGroup.length) {
|
||||
result.push({
|
||||
id: thinkingGroup[0].id,
|
||||
message_type: 'thinking_group',
|
||||
messages: thinkingGroup,
|
||||
});
|
||||
thinkingGroup = [];
|
||||
}
|
||||
result.push(message);
|
||||
}
|
||||
});
|
||||
if (thinkingGroup.length) {
|
||||
result.push({
|
||||
id: thinkingGroup[0].id,
|
||||
message_type: 'thinking_group',
|
||||
messages: thinkingGroup,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const isLastMessageFromAssistant = computed(() => {
|
||||
const last = groupedMessages.value[groupedMessages.value.length - 1];
|
||||
return last?.message_type === 'assistant';
|
||||
});
|
||||
|
||||
const chatContainer = ref(null);
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (chatContainer.value) {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => messages.value, scrollToBottom, { deep: true });
|
||||
|
||||
onMounted(async () => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
try {
|
||||
const { data } = await CopilotMessagesAPI.get(props.threadId);
|
||||
const payload = data?.payload || [];
|
||||
payload.forEach(message => {
|
||||
store.dispatch('copilotMessages/upsert', message);
|
||||
});
|
||||
scrollToBottom();
|
||||
} catch {
|
||||
// best-effort fetch; rely on websocket if API errors
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col w-full h-full bg-n-surface-1 text-n-slate-12">
|
||||
<div
|
||||
class="flex items-center gap-2 px-6 py-3 border-b border-n-weak flex-shrink-0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1.5 h-8 px-2.5 rounded-md text-[13px] font-medium text-n-slate-11 hover:text-n-slate-12 hover:bg-n-alpha-2 transition-colors bg-transparent border-0 cursor-pointer"
|
||||
@click="goHome"
|
||||
>
|
||||
<span class="i-lucide-chevron-left size-4" />
|
||||
{{ $t('HOME_DASHBOARD_PAGE.COPILOT_THREAD.BACK') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref="chatContainer"
|
||||
class="flex-1 min-h-0 w-full overflow-y-auto text-sm leading-6 tracking-tight"
|
||||
>
|
||||
<div class="w-full max-w-[820px] mx-auto px-6 py-6">
|
||||
<div
|
||||
v-if="groupedMessages.length"
|
||||
class="space-y-6 flex flex-col w-full"
|
||||
>
|
||||
<template v-for="(item, index) in groupedMessages" :key="item.id">
|
||||
<CopilotAgentMessage
|
||||
v-if="item.message_type === 'user'"
|
||||
:message="item.message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="item.message_type === 'assistant'"
|
||||
:message="item.message"
|
||||
:is-last-message="index === groupedMessages.length - 1"
|
||||
conversation-inbox-type=""
|
||||
/>
|
||||
<CopilotThinkingGroup
|
||||
v-else
|
||||
:messages="item.messages"
|
||||
:default-collapsed="isLastMessageFromAssistant"
|
||||
/>
|
||||
</template>
|
||||
<CopilotLoader v-if="!isLastMessageFromAssistant" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center h-full text-sm text-n-slate-10"
|
||||
>
|
||||
{{ $t('HOME_DASHBOARD_PAGE.TOP_CONVERSATIONS.LOADING') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex-shrink-0">
|
||||
<div class="w-full max-w-[820px] mx-auto px-6 pt-px pb-4">
|
||||
<div class="flex items-center justify-between gap-2 mb-1">
|
||||
<ToggleCopilotAssistant
|
||||
v-if="assistants.length > 1"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant || {}"
|
||||
@set-assistant="setAssistant"
|
||||
/>
|
||||
<div v-else />
|
||||
</div>
|
||||
<CopilotInput
|
||||
v-if="assistants.length"
|
||||
class="w-full"
|
||||
@send="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import { ROLES } from 'dashboard/constants/permissions.js';
|
||||
import HomeDashboard from './HomeDashboard.vue';
|
||||
import HomeCopilotThread from './HomeCopilotThread.vue';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/home'),
|
||||
name: 'home_dashboard',
|
||||
component: HomeDashboard,
|
||||
meta: {
|
||||
permissions: ROLES,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/home/admin'),
|
||||
name: 'home_dashboard_admin',
|
||||
component: HomeDashboard,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/home/copilot/:threadId'),
|
||||
name: 'home_copilot_thread',
|
||||
component: HomeCopilotThread,
|
||||
props: true,
|
||||
meta: {
|
||||
permissions: ROLES,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -13,6 +13,7 @@ import UserBasicDetails from './UserBasicDetails.vue';
|
||||
import MessageSignature from './MessageSignature.vue';
|
||||
import FontSize from './FontSize.vue';
|
||||
import UserLanguageSelect from './UserLanguageSelect.vue';
|
||||
import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue';
|
||||
import HotKeyCard from './HotKeyCard.vue';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import NotificationPreferences from './NotificationPreferences.vue';
|
||||
@@ -43,13 +44,16 @@ export default {
|
||||
AccessToken,
|
||||
MfaSettingsCard,
|
||||
BaseSettingsHeader,
|
||||
SettingsToggleSection,
|
||||
},
|
||||
setup() {
|
||||
const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
|
||||
const { uiSettings, isEditorHotKeyEnabled, updateUISettings } =
|
||||
useUISettings();
|
||||
const { currentFontSize, updateFontSize } = useFontSize();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
currentFontSize,
|
||||
updateFontSize,
|
||||
isEditorHotKeyEnabled,
|
||||
@@ -103,6 +107,14 @@ export default {
|
||||
isMfaEnabled() {
|
||||
return parseBoolean(window.chatwootConfig?.isMfaEnabled);
|
||||
},
|
||||
homeDashboardEnabled: {
|
||||
get() {
|
||||
return !!this.uiSettings?.home_dashboard_enabled;
|
||||
},
|
||||
set(value) {
|
||||
this.updateUISettings({ home_dashboard_enabled: value });
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.currentUserId) {
|
||||
@@ -248,6 +260,16 @@ export default {
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.NOTE')
|
||||
"
|
||||
/>
|
||||
<SettingsToggleSection
|
||||
v-model="homeDashboardEnabled"
|
||||
class="w-full"
|
||||
:header="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.HOME_DASHBOARD.TITLE')
|
||||
"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.HOME_DASHBOARD.NOTE')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
<SectionLayout
|
||||
|
||||
@@ -39,7 +39,9 @@ export const validateAuthenticateRoutePermission = async (to, next) => {
|
||||
isActive;
|
||||
|
||||
if (to.name === 'no_accounts' || !to.name) {
|
||||
const target = needsOnboarding ? 'onboarding' : 'dashboard';
|
||||
const homeDashboardEnabled = !!user?.ui_settings?.home_dashboard_enabled;
|
||||
const landingTarget = homeDashboardEnabled ? 'home' : 'dashboard';
|
||||
const target = needsOnboarding ? 'onboarding' : landingTarget;
|
||||
return next(frontendURL(`accounts/${routeAccountId}/${target}`));
|
||||
}
|
||||
|
||||
|
||||
+46
-33
@@ -52,6 +52,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.boolean "auto_offline", default: true, null: false
|
||||
t.bigint "custom_role_id"
|
||||
t.bigint "agent_capacity_policy_id"
|
||||
t.integer "status", default: 0, null: false
|
||||
t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true
|
||||
t.index ["account_id"], name: "index_account_users_on_account_id"
|
||||
t.index ["agent_capacity_policy_id"], name: "index_account_users_on_agent_capacity_policy_id"
|
||||
@@ -71,6 +72,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.jsonb "limits", default: {}
|
||||
t.jsonb "custom_attributes", default: {}
|
||||
t.integer "status", default: 0
|
||||
t.integer "contactable_contacts_count", default: 0
|
||||
t.jsonb "internal_attributes", default: {}, null: false
|
||||
t.jsonb "settings", default: {}
|
||||
t.index ["status"], name: "index_accounts_on_status"
|
||||
@@ -381,11 +383,11 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.integer "sync_status"
|
||||
t.datetime "last_synced_at"
|
||||
t.datetime "last_sync_attempted_at"
|
||||
t.index ["account_id", "sync_status"], name: "index_captain_documents_on_account_id_and_sync_status"
|
||||
t.index ["account_id"], name: "index_captain_documents_on_account_id"
|
||||
t.index ["assistant_id", "external_link"], name: "index_captain_documents_on_assistant_id_and_external_link", unique: true
|
||||
t.index ["assistant_id"], name: "index_captain_documents_on_assistant_id"
|
||||
t.index ["status"], name: "index_captain_documents_on_status"
|
||||
t.index ["account_id", "sync_status"], name: "index_captain_documents_on_account_id_and_sync_status"
|
||||
end
|
||||
|
||||
create_table "captain_inboxes", force: :cascade do |t|
|
||||
@@ -611,13 +613,13 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.bigint "account_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "contacts_count"
|
||||
t.integer "contacts_count", default: 0, null: false
|
||||
t.index ["account_id", "domain"], name: "index_companies_on_account_and_domain", unique: true, where: "(domain IS NOT NULL)"
|
||||
t.index ["account_id"], name: "index_companies_on_account_id"
|
||||
t.index ["name", "account_id"], name: "index_companies_on_name_and_account_id"
|
||||
end
|
||||
|
||||
create_table "contact_inboxes", force: :cascade do |t|
|
||||
create_table "contact_inboxes", id: :bigint, default: -> { "nextval('contact_inboxes2_id_seq'::regclass)" }, force: :cascade do |t|
|
||||
t.bigint "contact_id"
|
||||
t.bigint "inbox_id"
|
||||
t.text "source_id", null: false
|
||||
@@ -625,14 +627,14 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "hmac_verified", default: false
|
||||
t.string "pubsub_token"
|
||||
t.index ["contact_id"], name: "index_contact_inboxes_on_contact_id"
|
||||
t.index ["inbox_id", "source_id"], name: "index_contact_inboxes_on_inbox_id_and_source_id", unique: true
|
||||
t.index ["inbox_id"], name: "index_contact_inboxes_on_inbox_id"
|
||||
t.index ["pubsub_token"], name: "index_contact_inboxes_on_pubsub_token", unique: true
|
||||
t.index ["source_id"], name: "index_contact_inboxes_on_source_id"
|
||||
t.index ["contact_id"], name: "contact_inboxes2_contact_id_idx"
|
||||
t.index ["inbox_id", "source_id"], name: "contact_inboxes2_inbox_id_source_id_idx", unique: true
|
||||
t.index ["inbox_id"], name: "contact_inboxes2_inbox_id_idx"
|
||||
t.index ["pubsub_token"], name: "contact_inboxes2_pubsub_token_idx", unique: true
|
||||
t.index ["source_id"], name: "contact_inboxes2_source_id_idx"
|
||||
end
|
||||
|
||||
create_table "contacts", id: :serial, force: :cascade do |t|
|
||||
create_table "contacts", id: :integer, default: -> { "nextval('contacts2_id_seq'::regclass)" }, force: :cascade do |t|
|
||||
t.string "name", default: ""
|
||||
t.string "email"
|
||||
t.string "phone_number"
|
||||
@@ -643,25 +645,27 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.string "identifier"
|
||||
t.jsonb "custom_attributes", default: {}
|
||||
t.datetime "last_activity_at", precision: nil
|
||||
t.integer "contact_type", default: 0
|
||||
t.boolean "resolved", default: false, null: false
|
||||
t.integer "contact_type", default: 0, null: false
|
||||
t.string "middle_name", default: ""
|
||||
t.string "last_name", default: ""
|
||||
t.string "location", default: ""
|
||||
t.string "country_code", default: ""
|
||||
t.boolean "blocked", default: false, null: false
|
||||
t.bigint "company_id"
|
||||
t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id"
|
||||
t.index "lower((email)::text), account_id", name: "contacts2_lower_account_id_idx"
|
||||
t.index ["account_id", "contact_type"], name: "index_contacts_on_account_id_and_contact_type"
|
||||
t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["account_id", "email", "phone_number", "identifier"], name: "contacts2_account_id_email_phone_number_identifier_idx", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["account_id", "last_activity_at"], name: "index_contacts_on_account_id_and_last_activity_at", order: { last_activity_at: "DESC NULLS LAST" }
|
||||
t.index ["account_id"], name: "index_contacts_on_account_id"
|
||||
t.index ["account_id"], name: "index_resolved_contact_account_id", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["account_id", "resolved"], name: "index_contacts_on_account_id_and_resolved"
|
||||
t.index ["account_id"], name: "contacts2_account_id_idx"
|
||||
t.index ["account_id"], name: "contacts2_account_id_idx1", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["blocked"], name: "index_contacts_on_blocked"
|
||||
t.index ["company_id"], name: "index_contacts_on_company_id"
|
||||
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
|
||||
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
|
||||
t.index ["name", "email", "phone_number", "identifier"], name: "index_contacts_on_name_email_phone_number_identifier", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["phone_number", "account_id"], name: "index_contacts_on_phone_number_and_account_id"
|
||||
t.index ["email", "account_id"], name: "contacts2_email_account_id_idx", unique: true
|
||||
t.index ["identifier", "account_id"], name: "contacts2_identifier_account_id_idx", unique: true
|
||||
t.index ["name", "email", "phone_number", "identifier"], name: "contacts2_name_email_phone_number_identifier_idx", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["phone_number", "account_id"], name: "contacts2_phone_number_account_id_idx"
|
||||
end
|
||||
|
||||
create_table "conversation_participants", force: :cascade do |t|
|
||||
@@ -689,7 +693,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.datetime "agent_last_seen_at", precision: nil
|
||||
t.jsonb "additional_attributes", default: {}
|
||||
t.bigint "contact_inbox_id"
|
||||
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
|
||||
t.uuid "uuid", default: -> { "public.gen_random_uuid()" }, null: false
|
||||
t.string "identifier"
|
||||
t.datetime "last_activity_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||
t.bigint "team_id"
|
||||
@@ -703,6 +707,12 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.datetime "waiting_since"
|
||||
t.text "cached_label_list"
|
||||
t.bigint "assignee_agent_bot_id"
|
||||
t.bigint "last_message_id"
|
||||
t.bigint "last_incoming_message_id"
|
||||
t.bigint "last_non_activity_message_id"
|
||||
t.text "cached_summary"
|
||||
t.datetime "cached_summary_at"
|
||||
t.index ["account_id", "created_at", "inbox_id"], name: "index_conversations_on_account_created_inbox"
|
||||
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
|
||||
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
|
||||
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"
|
||||
@@ -714,6 +724,9 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.index ["first_reply_created_at"], name: "index_conversations_on_first_reply_created_at"
|
||||
t.index ["identifier", "account_id"], name: "index_conversations_on_identifier_and_account_id"
|
||||
t.index ["inbox_id"], name: "index_conversations_on_inbox_id"
|
||||
t.index ["last_incoming_message_id"], name: "index_conversations_on_last_incoming_message_id"
|
||||
t.index ["last_message_id"], name: "index_conversations_on_last_message_id"
|
||||
t.index ["last_non_activity_message_id"], name: "index_conversations_on_last_non_activity_message_id"
|
||||
t.index ["priority"], name: "index_conversations_on_priority"
|
||||
t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id"
|
||||
t.index ["status", "priority"], name: "index_conversations_on_status_and_priority"
|
||||
@@ -780,7 +793,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.string "regex_pattern"
|
||||
t.string "regex_cue"
|
||||
t.index ["account_id"], name: "index_custom_attribute_definitions_on_account_id"
|
||||
t.index ["attribute_key", "attribute_model", "account_id"], name: "attribute_key_model_index", unique: true
|
||||
end
|
||||
|
||||
create_table "custom_filters", force: :cascade do |t|
|
||||
@@ -963,7 +975,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.integer "visibility", default: 0
|
||||
t.bigint "created_by_id"
|
||||
t.bigint "updated_by_id"
|
||||
t.jsonb "actions", default: {}, null: false
|
||||
t.jsonb "actions", default: "{}", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_macros_on_account_id"
|
||||
@@ -982,7 +994,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.index ["user_id"], name: "index_mentions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "messages", id: :serial, force: :cascade do |t|
|
||||
create_table "messages", id: :integer, default: -> { "nextval('messages2_id_seq'::regclass)" }, force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.integer "account_id", null: false
|
||||
t.integer "inbox_id", null: false
|
||||
@@ -1001,18 +1013,18 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.jsonb "additional_attributes", default: {}
|
||||
t.text "processed_message_content"
|
||||
t.jsonb "sentiment", default: {}
|
||||
t.index "((additional_attributes -> 'campaign_id'::text))", name: "index_messages_on_additional_attributes_campaign_id", using: :gin
|
||||
t.index "((additional_attributes -> 'campaign_id'::text))", name: "messages2_expr_idx", using: :gin
|
||||
t.index ["account_id", "content_type", "created_at"], name: "idx_messages_account_content_created"
|
||||
t.index ["account_id", "created_at", "message_type"], name: "index_messages_on_account_created_type"
|
||||
t.index ["account_id", "inbox_id"], name: "index_messages_on_account_id_and_inbox_id"
|
||||
t.index ["account_id"], name: "index_messages_on_account_id"
|
||||
t.index ["content"], name: "index_messages_on_content", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["conversation_id", "account_id", "message_type", "created_at"], name: "index_messages_on_conversation_account_type_created"
|
||||
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
|
||||
t.index ["created_at"], name: "index_messages_on_created_at"
|
||||
t.index ["inbox_id"], name: "index_messages_on_inbox_id"
|
||||
t.index ["sender_type", "sender_id"], name: "index_messages_on_sender_type_and_sender_id"
|
||||
t.index ["source_id"], name: "index_messages_on_source_id"
|
||||
t.index ["account_id", "inbox_id"], name: "messages2_account_id_inbox_id_idx"
|
||||
t.index ["account_id"], name: "messages2_account_id_idx"
|
||||
t.index ["content"], name: "messages2_content_idx", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["conversation_id", "account_id", "message_type", "created_at"], name: "messages2_conversation_id_account_id_message_type_created_a_idx"
|
||||
t.index ["conversation_id"], name: "messages2_conversation_id_idx"
|
||||
t.index ["created_at"], name: "messages2_created_at_idx"
|
||||
t.index ["inbox_id"], name: "messages2_inbox_id_idx"
|
||||
t.index ["sender_type", "sender_id"], name: "messages2_sender_type_sender_id_idx"
|
||||
t.index ["source_id"], name: "messages2_source_id_idx"
|
||||
end
|
||||
|
||||
create_table "notes", force: :cascade do |t|
|
||||
@@ -1144,6 +1156,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.float "value_in_business_hours"
|
||||
t.datetime "event_start_time", precision: nil
|
||||
t.datetime "event_end_time", precision: nil
|
||||
t.index ["account_id", "name", "created_at", "inbox_id"], name: "index_reporting_events_on_account_name_created_inbox"
|
||||
t.index ["account_id", "name", "created_at"], name: "reporting_events__account_id__name__created_at"
|
||||
t.index ["account_id", "name", "inbox_id", "created_at"], name: "index_reporting_events_for_response_distribution"
|
||||
t.index ["account_id"], name: "index_reporting_events_on_account_id"
|
||||
@@ -1277,7 +1290,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_28_120000) do
|
||||
t.text "message_signature"
|
||||
t.string "otp_secret"
|
||||
t.integer "consumed_timestep"
|
||||
t.boolean "otp_required_for_login", default: false
|
||||
t.boolean "otp_required_for_login", default: false, null: false
|
||||
t.text "otp_backup_codes"
|
||||
t.index ["email"], name: "index_users_on_email"
|
||||
t.index ["otp_required_for_login"], name: "index_users_on_otp_required_for_login"
|
||||
|
||||
Reference in New Issue
Block a user