From 3d7567bf23471486f3d3de70ac906b0eb859c77d Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 29 Apr 2026 16:05:27 -0700 Subject: [PATCH] feat: Add a dedicated home for agents and admins --- .../components-next/sidebar/Sidebar.vue | 57 +- .../dashboard/i18n/locale/en/inbox.json | 2 +- .../dashboard/i18n/locale/en/settings.json | 126 ++ .../routes/dashboard/dashboard.routes.js | 2 + .../dashboard/home/HomeCopilotThread.vue | 192 ++++ .../routes/dashboard/home/HomeDashboard.vue | 1009 +++++++++++++++++ .../dashboard/routes/dashboard/home/routes.js | 32 + .../dashboard/settings/profile/Index.vue | 24 +- app/javascript/dashboard/routes/index.js | 4 +- db/schema.rb | 79 +- 10 files changed, 1481 insertions(+), 46 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/home/HomeCopilotThread.vue create mode 100644 app/javascript/dashboard/routes/dashboard/home/HomeDashboard.vue create mode 100644 app/javascript/dashboard/routes/dashboard/home/routes.js diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index cbb3b2099..1fcafc88b 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -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')" /> + + + + {{ unreadInboxCountLabel }} + + diff --git a/app/javascript/dashboard/i18n/locale/en/inbox.json b/app/javascript/dashboard/i18n/locale/en/inbox.json index 385e9e4ce..5b003a9f7 100644 --- a/app/javascript/dashboard/i18n/locale/en/inbox.json +++ b/app/javascript/dashboard/i18n/locale/en/inbox.json @@ -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.", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index bfbd920a7..3ebc432b1 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -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", diff --git a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js index 87bae7d11..8cf80e573 100644 --- a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js +++ b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js @@ -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, diff --git a/app/javascript/dashboard/routes/dashboard/home/HomeCopilotThread.vue b/app/javascript/dashboard/routes/dashboard/home/HomeCopilotThread.vue new file mode 100644 index 000000000..c0fde255c --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/home/HomeCopilotThread.vue @@ -0,0 +1,192 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/home/HomeDashboard.vue b/app/javascript/dashboard/routes/dashboard/home/HomeDashboard.vue new file mode 100644 index 000000000..6be73099f --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/home/HomeDashboard.vue @@ -0,0 +1,1009 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/home/routes.js b/app/javascript/dashboard/routes/dashboard/home/routes.js new file mode 100644 index 000000000..109ab8d81 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/home/routes.js @@ -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, + }, + }, +]; diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index ed674e3bc..e6d66bac7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -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') " /> + { 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}`)); } diff --git a/db/schema.rb b/db/schema.rb index 4ff250ada..0b057fba6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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"