diff --git a/frontend/apps/widget/src/App.vue b/frontend/apps/widget/src/App.vue index ab85e987..1eb3526c 100644 --- a/frontend/apps/widget/src/App.vue +++ b/frontend/apps/widget/src/App.vue @@ -26,22 +26,21 @@ const userStore = useUserStore() // Initialize unread count tracking and sending to parent window. useUnreadCount() +const widgetConfig = getCurrentInstance().appContext.config.globalProperties.$widgetConfig +if (widgetConfig) { + widgetStore.updateConfig(widgetConfig) + applyCSSColor('--primary', widgetConfig.colors?.primary) +} + onMounted(async () => { - // Use pre-fetched widget config from main.js - const widgetConfig = getCurrentInstance().appContext.config.globalProperties.$widgetConfig - if (widgetConfig) { - widgetStore.updateConfig(widgetConfig) - // Apply custom primary color from config - applyCSSColor('--primary', widgetConfig.colors?.primary) - } - initializeWebSocket() - widgetStore.setOpen(true) - setupParentMessageListeners() - await chatStore.fetchConversations() + // Only fetch conversations if not directly navigating to a conversation + if (!widgetStore.config?.direct_to_conversation) { + await chatStore.fetchConversations() + } // Notify parent window that Vue app is ready window.parent.postMessage( diff --git a/frontend/apps/widget/src/components/ChatMessages.vue b/frontend/apps/widget/src/components/ChatMessages.vue index e739d319..05112e4f 100644 --- a/frontend/apps/widget/src/components/ChatMessages.vue +++ b/frontend/apps/widget/src/components/ChatMessages.vue @@ -245,8 +245,10 @@ const scrollToBottom = () => { } onMounted(() => { - // Update last seen timestamp when conversation is opened - chatStore.updateCurrentConversationLastSeen() + // Update last seen timestamp only when widget is actually visible. + if (widgetStore.isOpen) { + chatStore.updateCurrentConversationLastSeen() + } // Check initial scroll position checkIfAtBottom() @@ -309,4 +311,18 @@ watch( } } ) + +// Watch for widget becoming visible - scroll to bottom and update last seen +// This handles the direct_to_conversation case where messages load while widget is hidden +watch( + () => widgetStore.isOpen, + (isOpen) => { + if (isOpen && chatStore.currentConversation?.uuid) { + chatStore.updateCurrentConversationLastSeen() + setTimeout(() => { + scrollToBottom() + }, 50) + } + } +) diff --git a/frontend/apps/widget/src/components/ChatTitle.vue b/frontend/apps/widget/src/components/ChatTitle.vue index a06a20ba..ee70c8cd 100644 --- a/frontend/apps/widget/src/components/ChatTitle.vue +++ b/frontend/apps/widget/src/components/ChatTitle.vue @@ -46,10 +46,12 @@ import { computed } from 'vue' import { Avatar, AvatarFallback, AvatarImage } from '@shared-ui/components/ui/avatar' import { getRelativeTime } from '@shared-ui/utils/datetime.js' import { useBusinessHours } from '@widget/composables/useBusinessHours.js' +import { useI18n } from 'vue-i18n' const chatStore = useChatStore() const widgetStore = useWidgetStore() const { resolveBusinessHours, getBusinessHoursStatus } = useBusinessHours() +const { t } = useI18n() const businessHoursStatus = computed(() => { const config = widgetStore.config @@ -84,6 +86,17 @@ const businessHoursStatus = computed(() => { }) const chatTitle = computed(() => { + // Show loading state while conversation is being fetched + if (chatStore.isLoadingConversation) { + return { + name: t('globals.terms.loading'), + avatarUrl: '', + avatarFallback: '', + availability_status: null, + hasAssignee: false + } + } + const config = widgetStore.config const assignee = chatStore.currentConversation?.assignee if (assignee?.id && assignee?.id > 0) { diff --git a/frontend/apps/widget/src/components/RecentConversationCard.vue b/frontend/apps/widget/src/components/RecentConversationCard.vue index b6207d58..98026fa2 100644 --- a/frontend/apps/widget/src/components/RecentConversationCard.vue +++ b/frontend/apps/widget/src/components/RecentConversationCard.vue @@ -60,9 +60,7 @@ const authorDisplayName = computed(() => { }) const continueConversation = async () => { - // Load the conversation details and messages - await chatStore.loadConversation(props.conversation.uuid) - // Navigate to chat view widgetStore.navigateToChat() + await chatStore.loadConversation(props.conversation.uuid) } diff --git a/frontend/apps/widget/src/composables/useBusinessHours.js b/frontend/apps/widget/src/composables/useBusinessHours.js index 05056ecf..f394bdc8 100644 --- a/frontend/apps/widget/src/composables/useBusinessHours.js +++ b/frontend/apps/widget/src/composables/useBusinessHours.js @@ -7,12 +7,6 @@ import { useI18n } from 'vue-i18n' export function useBusinessHours() { const { t } = useI18n() - /** - * Get the business hours by ID from a list - * @param {number} businessHoursId - Business hours ID - * @param {Array} businessHoursList - List of business hours objects - * @returns {Object|null} Business hours object or null - */ function getBusinessHoursById(businessHoursId, businessHoursList) { if (!businessHoursId || !businessHoursList) { return null @@ -21,16 +15,6 @@ export function useBusinessHours() { return businessHoursList.find(bh => bh.id === businessHoursId) } - /** - * Determine which business hours to use based on configuration - * @param {Object} options - Configuration options - * @param {boolean} options.showOfficeHours - Whether to show office hours - * @param {boolean} options.showAfterAssignment - Whether to show team hours after assignment - * @param {number|null} options.assignedBusinessHoursId - Business hours ID from assignment - * @param {number|null} options.defaultBusinessHoursId - Default business hours ID - * @param {Array} options.businessHoursList - List of available business hours - * @returns {Object|null} Business hours object or null - */ function resolveBusinessHours(options) { const { showOfficeHours, @@ -57,13 +41,6 @@ export function useBusinessHours() { return getBusinessHoursById(businessHoursId, businessHoursList) } - /** - * Check if a given time is within business hours - * @param {Object} businessHours - Business hours object - * @param {Date} date - Date to check - * @param {number} utcOffset - UTC offset in minutes - * @returns {boolean} True if within business hours - */ function isWithinBusinessHours(businessHours, date, utcOffset = 0) { if (!businessHours || businessHours.is_always_open) { return true @@ -93,12 +70,6 @@ export function useBusinessHours() { return currentTime >= schedule.open && currentTime <= schedule.close } - /** - * Check if a date is a holiday - * @param {Object} businessHours - Business hours object - * @param {Date} date - Date to check - * @returns {boolean} True if it's a holiday - */ function isHoliday(businessHours, date) { if (!businessHours.holidays || businessHours.holidays.length === 0) { return false @@ -107,13 +78,6 @@ export function useBusinessHours() { return businessHours.holidays.some(holiday => holiday.date === dateStr) } - /** - * Get the next working time - * @param {Object} businessHours - Business hours object - * @param {Date} fromDate - Date to start from - * @param {number} utcOffset - UTC offset in minutes - * @returns {Date|null} Next working time or null - */ function getNextWorkingTime(businessHours, fromDate, utcOffset = 0) { if (!businessHours || businessHours.is_always_open) { return fromDate @@ -140,14 +104,18 @@ export function useBusinessHours() { const [openHour, openMinute] = schedule.open.split(':').map(Number) let nextWorking = setMinutes(setHours(localDate, openHour), openMinute) - // If it's the same day and current time is before opening time + // Handle same-day logic if (i === 0) { const currentTime = format(localDate, 'HH:mm') + // Currently within business hours + if (currentTime >= schedule.open && currentTime < schedule.close) { + return new Date(localDate.getTime() - (utcOffset * 60000)) + } + // Before opening time today if (currentTime < schedule.open) { - // Convert back from business timezone to user timezone return new Date(nextWorking.getTime() - (utcOffset * 60000)) } - // If it's the same day but past opening time, continue to next day + // Past closing time, check next day continue } @@ -159,29 +127,11 @@ export function useBusinessHours() { return null } - /** - * Get day name from day number - * @param {number} dayNum - Day number (0 = Sunday, 1 = Monday, etc.) - * @returns {string} Day name - */ + // Returns English day name to match backend hours keys function getDayName(dayNum) { - const days = [ - t('globals.days.sunday'), - t('globals.days.monday'), - t('globals.days.tuesday'), - t('globals.days.wednesday'), - t('globals.days.thursday'), - t('globals.days.friday'), - t('globals.days.saturday') - ] - return days[dayNum] + return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayNum] } - /** - * Format the next working time for display - * @param {Date} nextWorkingTime - Next working time - * @returns {string} Formatted string - */ function formatNextWorkingTime(nextWorkingTime) { if (!nextWorkingTime) { return '' @@ -198,13 +148,6 @@ export function useBusinessHours() { }) } } - /** - * Get business hours status message and whether it's within business hours - * @param {Object} businessHours - Business hours object - * @param {number} utcOffset - UTC offset in minutes - * @param {string} withinHoursMessage - Message to show when within hours - * @returns {Object|null} { status: string|null, isWithin: boolean } or null - */ function getBusinessHoursStatus(businessHours, utcOffset = 0, withinHoursMessage = '') { if (!businessHours) { return null