feat: Migrate availability mixins to composable and helper (#11596)
# Pull Request Template ## Description **This PR includes:** * Refactored two legacy mixins (`availability.js`, `nextAvailability.js`) into a Vue 3 composable (`useAvailability`), helper module and component based rendering logic. * Fixed an issue where the widget wouldn't load if business hours were enabled but all days were unchecked. * Fixed translation issue [[#11280](https://github.com/chatwoot/chatwoot/issues/11280)](https://github.com/chatwoot/chatwoot/issues/11280). * Reduced code complexity and size. * Added test coverage for both the composable and helper functions. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/2bc3ed694b4349419505e275d14d0b98?sid=22d585e4-0dc7-4242-bcb6-e3edc16e3aee ### Story <img width="995" height="442" alt="image" src="https://github.com/user-attachments/assets/d6340738-07db-41d5-86fa-a8ecf734cc70" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Fixes https://github.com/chatwoot/chatwoot/issues/12012 --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import './design-system/histoire.scss';
|
||||
import { defineSetupVue3 } from '@histoire/plugin-vue';
|
||||
import i18nMessages from 'dashboard/i18n';
|
||||
import dashboardI18n from 'dashboard/i18n';
|
||||
import widgetI18n from 'widget/i18n';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { vResizeObserver } from '@vueuse/components';
|
||||
import store from 'dashboard/store';
|
||||
@@ -9,10 +10,30 @@ import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
|
||||
import { directive as onClickaway } from 'vue3-click-away';
|
||||
|
||||
function mergeMessages(...sources) {
|
||||
return sources.reduce((acc, src) => {
|
||||
Object.keys(src).forEach(key => {
|
||||
if (
|
||||
acc[key] &&
|
||||
typeof acc[key] === 'object' &&
|
||||
typeof src[key] === 'object'
|
||||
) {
|
||||
acc[key] = mergeMessages(acc[key], src[key]);
|
||||
} else {
|
||||
acc[key] = src[key];
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
|
||||
locale: 'en',
|
||||
messages: i18nMessages,
|
||||
messages: mergeMessages(
|
||||
structuredClone(dashboardI18n),
|
||||
structuredClone(widgetI18n)
|
||||
),
|
||||
});
|
||||
|
||||
export const setupVue3 = defineSetupVue3(({ app }) => {
|
||||
|
||||
@@ -4,12 +4,10 @@ import { setHeader } from 'widget/helpers/axios';
|
||||
import addHours from 'date-fns/addHours';
|
||||
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||
import configMixin from './mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import { getLocale } from './helpers/urlParamsHelper';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import routerMixin from './mixins/routerMixin';
|
||||
import {
|
||||
getExtraSpaceToScroll,
|
||||
loadedEventConfig,
|
||||
@@ -20,6 +18,8 @@ import {
|
||||
ON_UNREAD_MESSAGE_CLICK,
|
||||
} from './constants/widgetBusEvents';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
@@ -28,10 +28,13 @@ export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
mixins: [availabilityMixin, configMixin, routerMixin],
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
return { prefersDarkMode };
|
||||
const router = useRouter();
|
||||
const { isInWorkingHours } = useAvailability();
|
||||
|
||||
return { prefersDarkMode, router, isInWorkingHours };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -157,15 +160,17 @@ export default {
|
||||
this.setUnreadView();
|
||||
});
|
||||
emitter.on(ON_UNREAD_MESSAGE_CLICK, () => {
|
||||
this.replaceRoute('messages').then(() => this.unsetUnreadView());
|
||||
this.router
|
||||
.replace({ name: 'messages' })
|
||||
.then(() => this.unsetUnreadView());
|
||||
});
|
||||
},
|
||||
registerCampaignEvents() {
|
||||
emitter.on(ON_CAMPAIGN_MESSAGE_CLICK, () => {
|
||||
if (this.shouldShowPreChatForm) {
|
||||
this.replaceRoute('prechat-form');
|
||||
this.router.replace({ name: 'prechat-form' });
|
||||
} else {
|
||||
this.replaceRoute('messages');
|
||||
this.router.replace({ name: 'messages' });
|
||||
emitter.emit('execute-campaign', {
|
||||
campaignId: this.activeCampaign.id,
|
||||
});
|
||||
@@ -176,7 +181,7 @@ export default {
|
||||
const { customAttributes, campaignId } = campaignDetails;
|
||||
const { websiteToken } = window.chatwootWebChannel;
|
||||
this.executeCampaign({ campaignId, websiteToken, customAttributes });
|
||||
this.replaceRoute('messages');
|
||||
this.router.replace({ name: 'messages' });
|
||||
});
|
||||
emitter.on('snooze-campaigns', () => {
|
||||
const expireBy = addHours(new Date(), 1);
|
||||
@@ -192,7 +197,7 @@ export default {
|
||||
!messageCount &&
|
||||
!shouldSnoozeCampaign;
|
||||
if (this.isIFrame && isCampaignReadyToExecute) {
|
||||
this.replaceRoute('campaigns').then(() => {
|
||||
this.router.replace({ name: 'campaigns' }).then(() => {
|
||||
this.setIframeHeight(true);
|
||||
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
|
||||
});
|
||||
@@ -207,7 +212,7 @@ export default {
|
||||
unreadMessageCount > 0 &&
|
||||
!this.isWidgetOpen
|
||||
) {
|
||||
this.replaceRoute('unread-messages').then(() => {
|
||||
this.router.replace({ name: 'unread-messages' }).then(() => {
|
||||
this.setIframeHeight(true);
|
||||
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
|
||||
});
|
||||
@@ -263,7 +268,7 @@ export default {
|
||||
this.initCampaigns({
|
||||
currentURL: referrerURL,
|
||||
websiteToken,
|
||||
isInBusinessHours: this.isInBusinessHours,
|
||||
isInBusinessHours: this.isInWorkingHours,
|
||||
});
|
||||
window.referrerURL = referrerURL;
|
||||
this.setReferrerHost(referrerHost);
|
||||
@@ -314,12 +319,12 @@ export default {
|
||||
['unread-messages', 'campaigns'].includes(this.$route.name);
|
||||
|
||||
if (shouldShowMessageView) {
|
||||
this.replaceRoute('messages');
|
||||
this.router.replace({ name: 'messages' });
|
||||
}
|
||||
if (shouldShowHomeView) {
|
||||
this.$store.dispatch('conversation/setUserLastSeen');
|
||||
this.unsetUnreadView();
|
||||
this.replaceRoute('home');
|
||||
this.router.replace({ name: 'home' });
|
||||
}
|
||||
if (!message.isOpen) {
|
||||
this.resetCampaign();
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import GroupedAvatars from 'widget/components/GroupedAvatars.vue';
|
||||
import AvailabilityText from './AvailabilityText.vue';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
|
||||
const props = defineProps({
|
||||
agents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAvatars: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textClasses: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const availableMessage = useMapGetter('appConfig/getAvailableMessage');
|
||||
const unavailableMessage = useMapGetter('appConfig/getUnavailableMessage');
|
||||
|
||||
const {
|
||||
currentTime,
|
||||
hasOnlineAgents,
|
||||
isOnline,
|
||||
inboxConfig,
|
||||
isInWorkingHours,
|
||||
} = useAvailability(props.agents);
|
||||
|
||||
const workingHours = computed(() => inboxConfig.value.workingHours || []);
|
||||
const workingHoursEnabled = computed(
|
||||
() => inboxConfig.value.workingHoursEnabled || false
|
||||
);
|
||||
const utcOffset = computed(
|
||||
() => inboxConfig.value.utcOffset || inboxConfig.value.timezone || 'UTC'
|
||||
);
|
||||
const replyTime = computed(
|
||||
() => inboxConfig.value.replyTime || 'in_a_few_minutes'
|
||||
);
|
||||
|
||||
// If online or in working hours
|
||||
const isAvailable = computed(
|
||||
() => isOnline.value || (workingHoursEnabled.value && isInWorkingHours.value)
|
||||
);
|
||||
|
||||
const headerText = computed(() =>
|
||||
isAvailable.value
|
||||
? availableMessage.value || t('TEAM_AVAILABILITY.ONLINE')
|
||||
: unavailableMessage.value || t('TEAM_AVAILABILITY.OFFLINE')
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div v-if="showHeader" class="font-medium text-n-slate-12">
|
||||
{{ headerText }}
|
||||
</div>
|
||||
|
||||
<AvailabilityText
|
||||
:time="currentTime"
|
||||
:utc-offset="utcOffset"
|
||||
:working-hours="workingHours"
|
||||
:working-hours-enabled="workingHoursEnabled"
|
||||
:has-online-agents="hasOnlineAgents"
|
||||
:reply-time="replyTime"
|
||||
:is-online="isOnline"
|
||||
:is-in-working-hours="isInWorkingHours"
|
||||
:class="textClasses"
|
||||
class="text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GroupedAvatars v-if="showAvatars && isOnline" :users="agents" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,217 @@
|
||||
<script setup>
|
||||
import AvailabilityText from './AvailabilityText.vue';
|
||||
|
||||
// Base time for consistent testing: Monday, July 15, 2024, 10:00:00 UTC
|
||||
const baseTime = new Date('2024-07-15T10:00:00.000Z');
|
||||
const utcOffset = '+00:00'; // UTC
|
||||
|
||||
const defaultProps = {
|
||||
time: baseTime,
|
||||
utcOffset,
|
||||
workingHours: [
|
||||
{
|
||||
dayOfWeek: 0,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Sunday
|
||||
{
|
||||
dayOfWeek: 1,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Monday (current day)
|
||||
{
|
||||
dayOfWeek: 2,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Tuesday
|
||||
{
|
||||
dayOfWeek: 3,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Wednesday
|
||||
{
|
||||
dayOfWeek: 4,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Thursday
|
||||
{
|
||||
dayOfWeek: 5,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Friday
|
||||
{
|
||||
dayOfWeek: 6,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: true,
|
||||
}, // Saturday (closed)
|
||||
],
|
||||
workingHoursEnabled: true,
|
||||
replyTime: 'in_a_few_minutes',
|
||||
isOnline: true,
|
||||
isInWorkingHours: true,
|
||||
};
|
||||
|
||||
const createVariant = (
|
||||
title,
|
||||
propsOverride = {},
|
||||
isOnlineOverride = null,
|
||||
isInWorkingHoursOverride = null
|
||||
) => {
|
||||
const props = { ...defaultProps, ...propsOverride };
|
||||
if (isOnlineOverride !== null) props.isOnline = isOnlineOverride;
|
||||
if (isInWorkingHoursOverride !== null)
|
||||
props.isInWorkingHours = isInWorkingHoursOverride;
|
||||
|
||||
// Adjust time for specific scenarios
|
||||
if (title.includes('Back Tomorrow')) {
|
||||
// Set time to just after closing on Monday to trigger 'Back Tomorrow' (Tuesday)
|
||||
props.time = new Date('2024-07-15T17:01:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Multiple Days Away')) {
|
||||
// Set time to Friday evening to trigger 'Back on Sunday' (as Saturday is closed)
|
||||
props.time = new Date('2024-07-19T18:00:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Same Day - In Minutes')) {
|
||||
// Monday 16:50, next slot is 17:00 (in 10 minutes)
|
||||
// To make this specific, let's assume the next slot is within the hour
|
||||
// For this, we need to be outside working hours but a slot is available soon.
|
||||
// Let's say current time is 8:50 AM, office opens at 9:00 AM.
|
||||
props.time = new Date('2024-07-15T08:50:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Same Day - In Hours')) {
|
||||
// Monday 07:30 AM, office opens at 9:00 AM (in 1.5 hours, rounds to 2 hours)
|
||||
props.time = new Date('2024-07-15T07:30:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('in 1 hour')) {
|
||||
// Monday 08:00 AM, office opens at 9:00 AM (exactly in 1 hour)
|
||||
// At exactly 1 hour difference, remainingMinutes = 0
|
||||
props.time = new Date('2024-07-15T08:00:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Same Day - At Time')) {
|
||||
// Monday 05:00 AM, office opens at 9:00 AM (at 9:00 AM)
|
||||
props.time = new Date('2024-07-15T05:00:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
props,
|
||||
};
|
||||
};
|
||||
|
||||
const variants = [
|
||||
createVariant(
|
||||
'Working Hours Disabled - Online',
|
||||
{ workingHoursEnabled: false },
|
||||
true,
|
||||
true
|
||||
),
|
||||
createVariant(
|
||||
'Working Hours Disabled - Offline',
|
||||
{ workingHoursEnabled: false },
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'All Day Closed - Offline',
|
||||
{
|
||||
workingHours: defaultProps.workingHours.map(wh => ({
|
||||
...wh,
|
||||
closedAllDay: true,
|
||||
})),
|
||||
},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant('Online and In Working Hours', {}, true, true),
|
||||
createVariant(
|
||||
'No Next Slot Available (e.g., all future slots closed or empty workingHours)',
|
||||
{ workingHours: [] }, // No working hours defined
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Tomorrow',
|
||||
{},
|
||||
false,
|
||||
false // Time will be adjusted by createVariant
|
||||
),
|
||||
createVariant('Back Multiple Days Away (e.g., on Sunday)', {}, false, false),
|
||||
createVariant(
|
||||
'Back Same Day - In Minutes (e.g., in 10 minutes)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Same Day - In Hours (e.g., in 2 hours)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Same Day - Exactly an Hour (e.g., in 1 hour)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Same Day - At Time (e.g., at 09:00 AM)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Widget/Components/Availability/AvailabilityText"
|
||||
:layout="{ type: 'grid', width: 300 }"
|
||||
>
|
||||
<Variant v-for="(variant, i) in variants" :key="i" :title="variant.title">
|
||||
<AvailabilityText
|
||||
:time="variant.props.time"
|
||||
:utc-offset="variant.props.utcOffset"
|
||||
:working-hours="variant.props.workingHours"
|
||||
:working-hours-enabled="variant.props.workingHoursEnabled"
|
||||
:reply-time="variant.props.replyTime"
|
||||
:is-online="variant.props.isOnline"
|
||||
:is-in-working-hours="variant.props.isInWorkingHours"
|
||||
class="text-n-slate-11"
|
||||
/>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,178 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getTime } from 'dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js';
|
||||
import { findNextAvailableSlotDetails } from 'widget/helpers/availabilityHelpers';
|
||||
|
||||
const props = defineProps({
|
||||
time: {
|
||||
type: Date,
|
||||
required: true,
|
||||
},
|
||||
utcOffset: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workingHours: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
workingHoursEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
replyTime: {
|
||||
type: String,
|
||||
default: 'in_a_few_minutes',
|
||||
},
|
||||
isOnline: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isInWorkingHours: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MINUTE_ROUNDING_INTERVAL = 5;
|
||||
const HOUR_THRESHOLD_FOR_EXACT_TIME = 3;
|
||||
const MINUTES_IN_HOUR = 60;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dayNames = computed(() => [
|
||||
t('DAY_NAMES.SUNDAY'),
|
||||
t('DAY_NAMES.MONDAY'),
|
||||
t('DAY_NAMES.TUESDAY'),
|
||||
t('DAY_NAMES.WEDNESDAY'),
|
||||
t('DAY_NAMES.THURSDAY'),
|
||||
t('DAY_NAMES.FRIDAY'),
|
||||
t('DAY_NAMES.SATURDAY'),
|
||||
]);
|
||||
|
||||
// Check if all days in working hours are closed
|
||||
const allDayClosed = computed(() => {
|
||||
if (!props.workingHours.length) return false;
|
||||
return props.workingHours.every(slot => slot.closedAllDay);
|
||||
});
|
||||
|
||||
const replyTimeMessage = computed(() => {
|
||||
const replyTimeKey = `REPLY_TIME.${props.replyTime.toUpperCase()}`;
|
||||
return t(replyTimeKey);
|
||||
});
|
||||
|
||||
const nextSlot = computed(() => {
|
||||
if (
|
||||
!props.workingHoursEnabled ||
|
||||
allDayClosed.value ||
|
||||
(props.isInWorkingHours && props.isOnline)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slot = findNextAvailableSlotDetails(
|
||||
props.time,
|
||||
props.utcOffset,
|
||||
props.workingHours
|
||||
);
|
||||
if (!slot) return null;
|
||||
|
||||
return {
|
||||
...slot,
|
||||
hoursUntilOpen: Math.floor(slot.minutesUntilOpen / MINUTES_IN_HOUR),
|
||||
remainingMinutes: slot.minutesUntilOpen % MINUTES_IN_HOUR,
|
||||
};
|
||||
});
|
||||
|
||||
const roundedMinutesUntilOpen = computed(() => {
|
||||
if (!nextSlot.value) return 0;
|
||||
return (
|
||||
Math.ceil(nextSlot.value.remainingMinutes / MINUTE_ROUNDING_INTERVAL) *
|
||||
MINUTE_ROUNDING_INTERVAL
|
||||
);
|
||||
});
|
||||
|
||||
const adjustedHoursUntilOpen = computed(() => {
|
||||
if (!nextSlot.value) return 0;
|
||||
return nextSlot.value.remainingMinutes > 0
|
||||
? nextSlot.value.hoursUntilOpen + 1
|
||||
: nextSlot.value.hoursUntilOpen;
|
||||
});
|
||||
|
||||
const formattedOpeningTime = computed(() => {
|
||||
if (!nextSlot.value) return '';
|
||||
return getTime(
|
||||
nextSlot.value.config.openHour || 0,
|
||||
nextSlot.value.config.openMinutes || 0
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<!-- 1. If currently in working hours, show reply time -->
|
||||
<template v-if="isInWorkingHours">
|
||||
{{ replyTimeMessage }}
|
||||
</template>
|
||||
|
||||
<!-- 2. Else, if working hours are disabled, show based on online status -->
|
||||
<template v-else-if="!workingHoursEnabled">
|
||||
{{
|
||||
isOnline
|
||||
? replyTimeMessage
|
||||
: t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE')
|
||||
}}
|
||||
</template>
|
||||
|
||||
<!-- 3. Else (not in working hours, but working hours ARE enabled) -->
|
||||
<!-- Check if all configured slots are 'closedAllDay' -->
|
||||
<template v-else-if="allDayClosed">
|
||||
{{ t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE') }}
|
||||
</template>
|
||||
|
||||
<!-- 4. Else (not in WH, WH enabled, not allDayClosed), calculate next slot -->
|
||||
<template v-else-if="!nextSlot">
|
||||
{{ t('REPLY_TIME.BACK_IN_SOME_TIME') }}
|
||||
</template>
|
||||
|
||||
<!-- Tomorrow -->
|
||||
<template v-else-if="nextSlot.daysUntilOpen === 1">
|
||||
{{ t('REPLY_TIME.BACK_TOMORROW') }}
|
||||
</template>
|
||||
|
||||
<!-- Multiple days away (eg: on Monday) -->
|
||||
<template v-else-if="nextSlot.daysUntilOpen > 1">
|
||||
{{
|
||||
t('REPLY_TIME.BACK_ON_DAY', {
|
||||
day: dayNames[nextSlot.config.dayOfWeek],
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
|
||||
<!-- Same day - less than 1 hour (eg: in 5 minutes) -->
|
||||
<template v-else-if="nextSlot.hoursUntilOpen === 0">
|
||||
{{
|
||||
t('REPLY_TIME.BACK_IN_MINUTES', {
|
||||
time: `${roundedMinutesUntilOpen}`,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
|
||||
<!-- Same day - less than 3 hours (eg: in 2 hours) -->
|
||||
<template
|
||||
v-else-if="nextSlot.hoursUntilOpen < HOUR_THRESHOLD_FOR_EXACT_TIME"
|
||||
>
|
||||
{{ t('REPLY_TIME.BACK_IN_HOURS', adjustedHoursUntilOpen) }}
|
||||
</template>
|
||||
|
||||
<!-- Same day - 3+ hours away (eg: at 10:00 AM) -->
|
||||
<template v-else>
|
||||
{{
|
||||
t('REPLY_TIME.BACK_AT_TIME', {
|
||||
time: formattedOpeningTime,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
@@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue';
|
||||
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { sendEmailTranscript } from 'widget/api/conversation';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { IFrameHelper } from '../helpers/utils';
|
||||
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
@@ -17,7 +17,10 @@ export default {
|
||||
CustomButton,
|
||||
FooterReplyTo,
|
||||
},
|
||||
mixins: [routerMixin],
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
return { router };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inReplyTo: null,
|
||||
@@ -77,7 +80,7 @@ export default {
|
||||
this.inReplyTo = null;
|
||||
},
|
||||
startNewConversation() {
|
||||
this.replaceRoute('prechat-form');
|
||||
this.router.replace({ name: 'prechat-form' });
|
||||
IFrameHelper.sendMessage({
|
||||
event: 'onEvent',
|
||||
eventIdentifier: CHATWOOT_ON_START_CONVERSATION,
|
||||
|
||||
@@ -1,55 +1,26 @@
|
||||
<script>
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
<script setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import AvailabilityContainer from 'widget/components/Availability/AvailabilityContainer.vue';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
|
||||
export default {
|
||||
name: 'ChatHeader',
|
||||
components: {
|
||||
FluentIcon,
|
||||
HeaderActions,
|
||||
},
|
||||
mixins: [nextAvailabilityTime, availabilityMixin, routerMixin],
|
||||
props: {
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showPopoutButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isOnline() {
|
||||
const { workingHoursEnabled } = this.channelConfig;
|
||||
const anyAgentOnline = this.availableAgents.length > 0;
|
||||
const props = defineProps({
|
||||
avatarUrl: { type: String, default: '' },
|
||||
title: { type: String, default: '' },
|
||||
showPopoutButton: { type: Boolean, default: false },
|
||||
showBackButton: { type: Boolean, default: false },
|
||||
availableAgents: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
if (workingHoursEnabled) {
|
||||
return this.isInBetweenTheWorkingHours;
|
||||
}
|
||||
return anyAgentOnline;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onBackButtonClick() {
|
||||
this.replaceRoute('home');
|
||||
},
|
||||
},
|
||||
const availableAgents = toRef(props, 'availableAgents');
|
||||
|
||||
const router = useRouter();
|
||||
const { isOnline } = useAvailability(availableAgents);
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
router.replace({ name: 'home' });
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -79,9 +50,12 @@ export default {
|
||||
${isOnline ? 'bg-n-teal-10' : 'hidden'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs leading-3 text-n-slate-11">
|
||||
{{ replyWaitMessage }}
|
||||
</div>
|
||||
<AvailabilityContainer
|
||||
:agents="availableAgents"
|
||||
:show-header="false"
|
||||
:show-avatars="false"
|
||||
text-classes="text-xs leading-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HeaderActions :show-popout-button="showPopoutButton" />
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import { getRegexp } from 'shared/helpers/Validators';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import { FormKit, createInput } from '@formkit/vue';
|
||||
import PhoneInput from 'widget/components/Form/PhoneInput.vue';
|
||||
@@ -17,7 +16,7 @@ export default {
|
||||
Spinner,
|
||||
FormKit,
|
||||
},
|
||||
mixins: [routerMixin, configMixin],
|
||||
mixins: [configMixin],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
|
||||
@@ -1,74 +1,27 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
<script setup>
|
||||
import { IFrameHelper } from 'widget/helpers/utils';
|
||||
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
|
||||
import GroupedAvatars from 'widget/components/GroupedAvatars.vue';
|
||||
import AvailabilityContainer from 'widget/components/Availability/AvailabilityContainer.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
|
||||
export default {
|
||||
name: 'TeamAvailability',
|
||||
components: {
|
||||
GroupedAvatars,
|
||||
},
|
||||
mixins: [configMixin, nextAvailabilityTime, availabilityMixin],
|
||||
props: {
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
hasConversation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['startConversation'],
|
||||
const props = defineProps({
|
||||
availableAgents: { type: Array, default: () => [] },
|
||||
hasConversation: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
availableMessage: 'appConfig/getAvailableMessage',
|
||||
unavailableMessage: 'appConfig/getUnavailableMessage',
|
||||
}),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
agentAvatars() {
|
||||
return this.availableAgents.map(agent => ({
|
||||
name: agent.name,
|
||||
avatar: agent.avatar_url,
|
||||
id: agent.id,
|
||||
}));
|
||||
},
|
||||
headerMessage() {
|
||||
return this.isOnline
|
||||
? this.availableMessage || this.$t('TEAM_AVAILABILITY.ONLINE')
|
||||
: this.unavailableMessage || this.$t('TEAM_AVAILABILITY.OFFLINE');
|
||||
},
|
||||
isOnline() {
|
||||
const { workingHoursEnabled } = this.channelConfig;
|
||||
const anyAgentOnline = this.availableAgents.length > 0;
|
||||
const emit = defineEmits(['startConversation']);
|
||||
|
||||
if (workingHoursEnabled) {
|
||||
return this.isInBetweenTheWorkingHours;
|
||||
}
|
||||
return anyAgentOnline;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startConversation() {
|
||||
this.$emit('startConversation');
|
||||
if (!this.hasConversation) {
|
||||
IFrameHelper.sendMessage({
|
||||
event: 'onEvent',
|
||||
eventIdentifier: CHATWOOT_ON_START_CONVERSATION,
|
||||
data: { hasConversation: false },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
const widgetColor = useMapGetter('appConfig/getWidgetColor');
|
||||
|
||||
const startConversation = () => {
|
||||
emit('startConversation');
|
||||
if (!props.hasConversation) {
|
||||
IFrameHelper.sendMessage({
|
||||
event: 'onEvent',
|
||||
eventIdentifier: CHATWOOT_ON_START_CONVERSATION,
|
||||
data: { hasConversation: false },
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -76,17 +29,8 @@ export default {
|
||||
<div
|
||||
class="flex flex-col gap-3 w-full shadow outline-1 outline outline-n-container rounded-xl bg-n-background dark:bg-n-solid-2 px-5 py-4"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="font-medium text-n-slate-12 line-clamp-2">
|
||||
{{ headerMessage }}
|
||||
</div>
|
||||
<div class="text-n-slate-11">
|
||||
{{ replyWaitMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<GroupedAvatars v-if="isOnline" :users="availableAgents" />
|
||||
</div>
|
||||
<AvailabilityContainer :agents="availableAgents" show-header show-avatars />
|
||||
|
||||
<button
|
||||
class="inline-flex items-center gap-1 font-medium text-n-slate-12"
|
||||
:style="{ color: widgetColor }"
|
||||
|
||||
125
app/javascript/widget/composables/specs/useAvailability.spec.js
Normal file
125
app/javascript/widget/composables/specs/useAvailability.spec.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ref } from 'vue';
|
||||
import { useAvailability } from '../useAvailability';
|
||||
|
||||
const mockIsOnline = vi.fn();
|
||||
const mockIsInWorkingHours = vi.fn();
|
||||
const mockUseCamelCase = vi.fn(obj => obj);
|
||||
|
||||
vi.mock('widget/helpers/availabilityHelpers', () => ({
|
||||
isOnline: (...args) => mockIsOnline(...args),
|
||||
isInWorkingHours: (...args) => mockIsInWorkingHours(...args),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/useTransformKeys', () => ({
|
||||
useCamelCase: obj => mockUseCamelCase(obj),
|
||||
}));
|
||||
|
||||
describe('useAvailability', () => {
|
||||
const originalWindow = window.chatwootWebChannel;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset mocks to return true by default
|
||||
mockIsOnline.mockReturnValue(true);
|
||||
mockIsInWorkingHours.mockReturnValue(true);
|
||||
mockUseCamelCase.mockImplementation(obj => obj);
|
||||
|
||||
window.chatwootWebChannel = {
|
||||
workingHours: [],
|
||||
workingHoursEnabled: false,
|
||||
timezone: 'UTC',
|
||||
utcOffset: 'UTC',
|
||||
replyTime: 'in_a_few_minutes',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.chatwootWebChannel = originalWindow;
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const { availableAgents, hasOnlineAgents, isInWorkingHours, isOnline } =
|
||||
useAvailability();
|
||||
|
||||
expect(availableAgents.value).toEqual([]);
|
||||
expect(hasOnlineAgents.value).toBe(false);
|
||||
expect(isInWorkingHours.value).toBe(true);
|
||||
expect(isOnline.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with agents', () => {
|
||||
it('should handle agents array', () => {
|
||||
const agents = [{ id: 1 }, { id: 2 }];
|
||||
const { availableAgents, hasOnlineAgents } = useAvailability(agents);
|
||||
|
||||
expect(availableAgents.value).toEqual(agents);
|
||||
expect(hasOnlineAgents.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle reactive agents', () => {
|
||||
const agents = ref([{ id: 1 }]);
|
||||
const { hasOnlineAgents } = useAvailability(agents);
|
||||
|
||||
expect(hasOnlineAgents.value).toBe(true);
|
||||
|
||||
agents.value = [];
|
||||
expect(hasOnlineAgents.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('working hours', () => {
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
beforeEach(() => {
|
||||
window.chatwootWebChannel = {
|
||||
workingHours,
|
||||
workingHoursEnabled: true,
|
||||
utcOffset: '+05:30',
|
||||
};
|
||||
});
|
||||
|
||||
it('should check working hours', () => {
|
||||
mockIsInWorkingHours.mockReturnValueOnce(true);
|
||||
const { isInWorkingHours } = useAvailability();
|
||||
const result = isInWorkingHours.value;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockIsInWorkingHours).toHaveBeenCalledWith(
|
||||
expect.any(Date),
|
||||
'+05:30',
|
||||
workingHours
|
||||
);
|
||||
});
|
||||
|
||||
it('should determine online status based on working hours and agents', () => {
|
||||
mockIsOnline.mockReturnValueOnce(true);
|
||||
const { isOnline } = useAvailability([{ id: 1 }]);
|
||||
const result = isOnline.value;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockIsOnline).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.any(Date),
|
||||
'+05:30',
|
||||
workingHours,
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config changes', () => {
|
||||
it('should react to window.chatwootWebChannel changes', () => {
|
||||
const { inboxConfig } = useAvailability();
|
||||
|
||||
window.chatwootWebChannel = {
|
||||
...window.chatwootWebChannel,
|
||||
replyTime: 'in_a_day',
|
||||
};
|
||||
|
||||
expect(inboxConfig.value.replyTime).toBe('in_a_day');
|
||||
});
|
||||
});
|
||||
});
|
||||
69
app/javascript/widget/composables/useAvailability.js
Normal file
69
app/javascript/widget/composables/useAvailability.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { computed, toRef } from 'vue';
|
||||
import {
|
||||
isOnline as checkIsOnline,
|
||||
isInWorkingHours as checkInWorkingHours,
|
||||
} from 'widget/helpers/availabilityHelpers';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
|
||||
const DEFAULT_TIMEZONE = 'UTC';
|
||||
const DEFAULT_REPLY_TIME = 'in_a_few_minutes';
|
||||
|
||||
/**
|
||||
* Composable for availability-related logic
|
||||
* @param {Ref|Array} agents - Available agents (can be ref or raw array)
|
||||
* @returns {Object} Availability utilities and computed properties
|
||||
*/
|
||||
export function useAvailability(agents = []) {
|
||||
const availableAgents = toRef(agents);
|
||||
|
||||
const channelConfig = computed(() => window.chatwootWebChannel || {});
|
||||
|
||||
const inboxConfig = computed(() => ({
|
||||
workingHours: channelConfig.value.workingHours?.map(useCamelCase) || [],
|
||||
workingHoursEnabled: channelConfig.value.workingHoursEnabled || false,
|
||||
timezone: channelConfig.value.timezone || DEFAULT_TIMEZONE,
|
||||
utcOffset:
|
||||
channelConfig.value.utcOffset ||
|
||||
channelConfig.value.timezone ||
|
||||
DEFAULT_TIMEZONE,
|
||||
replyTime: channelConfig.value.replyTime || DEFAULT_REPLY_TIME,
|
||||
}));
|
||||
|
||||
const currentTime = computed(() => new Date());
|
||||
|
||||
const hasOnlineAgents = computed(() => {
|
||||
const agentList = availableAgents.value || [];
|
||||
return Array.isArray(agentList) ? agentList.length > 0 : false;
|
||||
});
|
||||
|
||||
const isInWorkingHours = computed(() =>
|
||||
checkInWorkingHours(
|
||||
currentTime.value,
|
||||
inboxConfig.value.utcOffset,
|
||||
inboxConfig.value.workingHours
|
||||
)
|
||||
);
|
||||
|
||||
// Check if online (considering both working hours and agents)
|
||||
const isOnline = computed(() =>
|
||||
checkIsOnline(
|
||||
inboxConfig.value.workingHoursEnabled,
|
||||
currentTime.value,
|
||||
inboxConfig.value.utcOffset,
|
||||
inboxConfig.value.workingHours,
|
||||
hasOnlineAgents.value
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
channelConfig,
|
||||
inboxConfig,
|
||||
|
||||
currentTime,
|
||||
availableAgents,
|
||||
hasOnlineAgents,
|
||||
|
||||
isOnline,
|
||||
isInWorkingHours,
|
||||
};
|
||||
}
|
||||
289
app/javascript/widget/helpers/availabilityHelpers.js
Normal file
289
app/javascript/widget/helpers/availabilityHelpers.js
Normal file
@@ -0,0 +1,289 @@
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
|
||||
// Constants
|
||||
const DAYS_IN_WEEK = 7;
|
||||
const MINUTES_IN_HOUR = 60;
|
||||
const MINUTES_IN_DAY = 24 * 60;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helper utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get date in timezone
|
||||
* @private
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @returns {Date}
|
||||
*/
|
||||
const getDateInTimezone = (time, utcOffset) => {
|
||||
const dateString = time instanceof Date ? time.toISOString() : time;
|
||||
try {
|
||||
return utcToZonedTime(dateString, utcOffset);
|
||||
} catch (error) {
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Invalid timezone: ${utcOffset}, falling back to user timezone: ${userTimezone}`
|
||||
);
|
||||
return utcToZonedTime(dateString, userTimezone);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert time to minutes
|
||||
* @private
|
||||
* @param {number} hours
|
||||
* @param {number} minutes
|
||||
* @returns {number}
|
||||
*/
|
||||
const toMinutes = (hours = 0, minutes = 0) => hours * MINUTES_IN_HOUR + minutes;
|
||||
|
||||
/**
|
||||
* Get today's config
|
||||
* @private
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
const getTodayConfig = (time, utcOffset, workingHours) => {
|
||||
const date = getDateInTimezone(time, utcOffset);
|
||||
const dayOfWeek = date.getDay();
|
||||
return workingHours.find(slot => slot.dayOfWeek === dayOfWeek) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if current time is within working range, handling midnight crossing
|
||||
* @private
|
||||
* @param {number} currentMinutes
|
||||
* @param {number} openMinutes
|
||||
* @param {number} closeMinutes
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isTimeWithinRange = (currentMinutes, openMinutes, closeMinutes) => {
|
||||
const crossesMidnight = closeMinutes <= openMinutes;
|
||||
|
||||
return crossesMidnight
|
||||
? currentMinutes >= openMinutes || currentMinutes < closeMinutes
|
||||
: currentMinutes >= openMinutes && currentMinutes < closeMinutes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a map keyed by `dayOfWeek` for all slots that are NOT closed all day.
|
||||
* @private
|
||||
*
|
||||
* @param {Array<Object>} workingHours - Full array of working-hour slot configs.
|
||||
* @returns {Map<number, Object>} Map where the key is the numeric day (0-6) and the value is the slot config.
|
||||
*/
|
||||
const getOpenDaysMap = workingHours =>
|
||||
new Map(
|
||||
(workingHours || [])
|
||||
.filter(slot => !slot.closedAllDay)
|
||||
.map(slot => [slot.dayOfWeek, slot])
|
||||
);
|
||||
|
||||
/**
|
||||
* Determine if today's slot is still upcoming.
|
||||
* @private
|
||||
* Returns an object with details if the slot is yet to open, otherwise `null`.
|
||||
*
|
||||
* @param {number} currentDay - `Date#getDay()` value (0-6) for current time.
|
||||
* @param {number} currentMinutes - Minutes since midnight for current time.
|
||||
* @param {Map<number, Object>} openDays - Map produced by `getOpenDaysMap`.
|
||||
* @returns {Object|null} Slot details (config, minutesUntilOpen, etc.) or `null`.
|
||||
*/
|
||||
const checkTodayAvailability = (currentDay, currentMinutes, openDays) => {
|
||||
const todayConfig = openDays.get(currentDay);
|
||||
if (!todayConfig || todayConfig.openAllDay) return null;
|
||||
|
||||
const todayOpenMinutes = toMinutes(
|
||||
todayConfig.openHour ?? 0,
|
||||
todayConfig.openMinutes ?? 0
|
||||
);
|
||||
|
||||
// Haven't opened yet today
|
||||
if (currentMinutes < todayOpenMinutes) {
|
||||
return {
|
||||
config: todayConfig,
|
||||
minutesUntilOpen: todayOpenMinutes - currentMinutes,
|
||||
daysUntilOpen: 0,
|
||||
dayOfWeek: currentDay,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search the upcoming days (including tomorrow) for the next open slot.
|
||||
* @private
|
||||
*
|
||||
* @param {number} currentDay - Day index (0-6) representing today.
|
||||
* @param {number} currentMinutes - Minutes since midnight for current time.
|
||||
* @param {Map<number, Object>} openDays - Map of open day configs.
|
||||
* @returns {Object|null} Details of the next slot or `null` if none found.
|
||||
*/
|
||||
const findNextSlot = (currentDay, currentMinutes, openDays) =>
|
||||
Array.from({ length: DAYS_IN_WEEK }, (_, i) => i + 1)
|
||||
.map(daysAhead => {
|
||||
const targetDay = (currentDay + daysAhead) % DAYS_IN_WEEK;
|
||||
const config = openDays.get(targetDay);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
// Calculate minutes until this slot opens
|
||||
const slotOpenMinutes = config.openAllDay
|
||||
? 0
|
||||
: toMinutes(config.openHour ?? 0, config.openMinutes ?? 0);
|
||||
const minutesUntilOpen =
|
||||
MINUTES_IN_DAY -
|
||||
currentMinutes + // remaining mins today
|
||||
(daysAhead - 1) * MINUTES_IN_DAY + // full days between
|
||||
slotOpenMinutes; // opening on target day
|
||||
|
||||
return {
|
||||
config,
|
||||
minutesUntilOpen,
|
||||
daysUntilOpen: daysAhead,
|
||||
dayOfWeek: targetDay,
|
||||
};
|
||||
})
|
||||
.find(Boolean) || null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exported functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if open all day
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isOpenAllDay = (time, utcOffset, workingHours = []) => {
|
||||
const todayConfig = getTodayConfig(time, utcOffset, workingHours);
|
||||
return todayConfig?.openAllDay === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if closed all day
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isClosedAllDay = (time, utcOffset, workingHours = []) => {
|
||||
const todayConfig = getTodayConfig(time, utcOffset, workingHours);
|
||||
return todayConfig?.closedAllDay === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if in working hours
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isInWorkingHours = (time, utcOffset, workingHours = []) => {
|
||||
if (!workingHours.length) return false;
|
||||
|
||||
const todayConfig = getTodayConfig(time, utcOffset, workingHours);
|
||||
if (!todayConfig) return false;
|
||||
|
||||
// Handle all-day states
|
||||
if (todayConfig.openAllDay) return true;
|
||||
if (todayConfig.closedAllDay) return false;
|
||||
|
||||
// Check time-based availability
|
||||
const date = getDateInTimezone(time, utcOffset);
|
||||
const currentMinutes = toMinutes(date.getHours(), date.getMinutes());
|
||||
|
||||
const openMinutes = toMinutes(
|
||||
todayConfig.openHour ?? 0,
|
||||
todayConfig.openMinutes ?? 0
|
||||
);
|
||||
const closeMinutes = toMinutes(
|
||||
todayConfig.closeHour ?? 0,
|
||||
todayConfig.closeMinutes ?? 0
|
||||
);
|
||||
|
||||
return isTimeWithinRange(currentMinutes, openMinutes, closeMinutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find next available slot with detailed information
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export const findNextAvailableSlotDetails = (
|
||||
time,
|
||||
utcOffset,
|
||||
workingHours = []
|
||||
) => {
|
||||
const date = getDateInTimezone(time, utcOffset);
|
||||
const currentDay = date.getDay();
|
||||
const currentMinutes = toMinutes(date.getHours(), date.getMinutes());
|
||||
|
||||
const openDays = getOpenDaysMap(workingHours);
|
||||
|
||||
// No open days at all
|
||||
if (openDays.size === 0) return null;
|
||||
|
||||
// Check today first
|
||||
const todaySlot = checkTodayAvailability(
|
||||
currentDay,
|
||||
currentMinutes,
|
||||
openDays
|
||||
);
|
||||
if (todaySlot) return todaySlot;
|
||||
|
||||
// Find next slot
|
||||
return findNextSlot(currentDay, currentMinutes, openDays);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find minutes until next available slot
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export const findNextAvailableSlotDiff = (
|
||||
time,
|
||||
utcOffset,
|
||||
workingHours = []
|
||||
) => {
|
||||
if (isInWorkingHours(time, utcOffset, workingHours)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nextSlot = findNextAvailableSlotDetails(time, utcOffset, workingHours);
|
||||
return nextSlot ? nextSlot.minutesUntilOpen : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if online
|
||||
* @param {boolean} workingHoursEnabled
|
||||
* @param {Date|string} time
|
||||
* @param {string} utcOffset
|
||||
* @param {Array} workingHours
|
||||
* @param {boolean} hasOnlineAgents
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isOnline = (
|
||||
workingHoursEnabled,
|
||||
time,
|
||||
utcOffset,
|
||||
workingHours,
|
||||
hasOnlineAgents
|
||||
) => {
|
||||
if (!workingHoursEnabled) {
|
||||
return hasOnlineAgents;
|
||||
}
|
||||
|
||||
const inWorkingHours = isInWorkingHours(time, utcOffset, workingHours);
|
||||
return inWorkingHours && hasOnlineAgents;
|
||||
};
|
||||
580
app/javascript/widget/helpers/specs/availabilityHelpers.spec.js
Normal file
580
app/javascript/widget/helpers/specs/availabilityHelpers.spec.js
Normal file
@@ -0,0 +1,580 @@
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
import {
|
||||
isOpenAllDay,
|
||||
isClosedAllDay,
|
||||
isInWorkingHours,
|
||||
findNextAvailableSlotDetails,
|
||||
findNextAvailableSlotDiff,
|
||||
isOnline,
|
||||
} from '../availabilityHelpers';
|
||||
|
||||
// Mock date-fns-tz
|
||||
vi.mock('date-fns-tz', () => ({
|
||||
utcToZonedTime: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('availabilityHelpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('isOpenAllDay', () => {
|
||||
it('should return true when slot is marked as open_all_day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z'); // Monday
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openAllDay: true }];
|
||||
|
||||
expect(isOpenAllDay(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when slot is not open_all_day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, closeHour: 17, openAllDay: false },
|
||||
];
|
||||
|
||||
expect(isOpenAllDay(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no config exists for the day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 2, openHour: 9, closeHour: 17 }, // Tuesday config
|
||||
];
|
||||
|
||||
expect(isOpenAllDay(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClosedAllDay', () => {
|
||||
it('should return true when slot is marked as closed_all_day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, closedAllDay: true }];
|
||||
|
||||
expect(isClosedAllDay(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when slot is not closed_all_day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, closeHour: 17, closedAllDay: false },
|
||||
];
|
||||
|
||||
expect(isClosedAllDay(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no config exists for the day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 2, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isClosedAllDay(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInWorkingHours', () => {
|
||||
it('should return false when no working hours are configured', () => {
|
||||
expect(isInWorkingHours(new Date(), 'UTC', [])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when open_all_day is true', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openAllDay: true }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when closed_all_day is true', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, closedAllDay: true }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when current time is within working hours', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{
|
||||
dayOfWeek: 1,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
},
|
||||
];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current time is before opening', () => {
|
||||
const mockDate = new Date('2024-01-15T08:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(8);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when current time is after closing', () => {
|
||||
const mockDate = new Date('2024-01-15T18:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(18);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle minutes in time comparison', () => {
|
||||
const mockDate = new Date('2024-01-15T09:30:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(9);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(30);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{
|
||||
dayOfWeek: 1,
|
||||
openHour: 9,
|
||||
openMinutes: 15,
|
||||
closeHour: 17,
|
||||
closeMinutes: 30,
|
||||
},
|
||||
];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no config for current day', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 2, openHour: 9, closeHour: 17 }, // Only Tuesday
|
||||
];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNextAvailableSlotDetails', () => {
|
||||
it('should return null when no open days exist', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 0, closedAllDay: true },
|
||||
{ dayOfWeek: 1, closedAllDay: true },
|
||||
{ dayOfWeek: 2, closedAllDay: true },
|
||||
{ dayOfWeek: 3, closedAllDay: true },
|
||||
{ dayOfWeek: 4, closedAllDay: true },
|
||||
{ dayOfWeek: 5, closedAllDay: true },
|
||||
{ dayOfWeek: 6, closedAllDay: true },
|
||||
];
|
||||
|
||||
expect(
|
||||
findNextAvailableSlotDetails(new Date(), 'UTC', workingHours)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('should return today slot when not opened yet', () => {
|
||||
const mockDate = new Date('2024-01-15T08:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(8);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, openMinutes: 30, closeHour: 17 },
|
||||
];
|
||||
|
||||
const result = findNextAvailableSlotDetails(
|
||||
new Date(),
|
||||
'UTC',
|
||||
workingHours
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
config: workingHours[0],
|
||||
minutesUntilOpen: 90, // 1.5 hours = 90 minutes
|
||||
daysUntilOpen: 0,
|
||||
dayOfWeek: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return tomorrow slot when today is past closing', () => {
|
||||
const mockDate = new Date('2024-01-15T18:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(18);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, closeHour: 17 },
|
||||
{ dayOfWeek: 2, openHour: 9, closeHour: 17 },
|
||||
];
|
||||
|
||||
const result = findNextAvailableSlotDetails(
|
||||
new Date(),
|
||||
'UTC',
|
||||
workingHours
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
config: workingHours[1],
|
||||
minutesUntilOpen: 900, // 15 hours = 900 minutes
|
||||
daysUntilOpen: 1,
|
||||
dayOfWeek: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip closed days and find next open day', () => {
|
||||
const mockDate = new Date('2024-01-15T18:00:00.000Z'); // Monday evening
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(18);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, closeHour: 17 },
|
||||
{ dayOfWeek: 2, closedAllDay: true },
|
||||
{ dayOfWeek: 3, closedAllDay: true },
|
||||
{ dayOfWeek: 4, openHour: 10, closeHour: 16 },
|
||||
];
|
||||
|
||||
const result = findNextAvailableSlotDetails(
|
||||
new Date(),
|
||||
'UTC',
|
||||
workingHours
|
||||
);
|
||||
|
||||
// Monday 18:00 to Thursday 10:00
|
||||
// Rest of Monday: 6 hours (18:00 to 24:00) = 360 minutes
|
||||
// Tuesday: 24 hours = 1440 minutes
|
||||
// Wednesday: 24 hours = 1440 minutes
|
||||
// Thursday morning: 10 hours = 600 minutes
|
||||
// Total: 360 + 1440 + 1440 + 600 = 3840 minutes
|
||||
expect(result).toEqual({
|
||||
config: workingHours[3],
|
||||
minutesUntilOpen: 3840, // 64 hours = 3840 minutes
|
||||
daysUntilOpen: 3,
|
||||
dayOfWeek: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle open_all_day slots', () => {
|
||||
const mockDate = new Date('2024-01-15T18:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(18);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, closeHour: 17 },
|
||||
{ dayOfWeek: 2, openAllDay: true },
|
||||
];
|
||||
|
||||
const result = findNextAvailableSlotDetails(
|
||||
new Date(),
|
||||
'UTC',
|
||||
workingHours
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
config: workingHours[1],
|
||||
minutesUntilOpen: 360, // 6 hours to midnight = 360 minutes
|
||||
daysUntilOpen: 1,
|
||||
dayOfWeek: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap around week correctly', () => {
|
||||
const mockDate = new Date('2024-01-20T18:00:00.000Z'); // Saturday evening
|
||||
mockDate.getDay = vi.fn().mockReturnValue(6);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(18);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openHour: 9, closeHour: 17 }, // Monday
|
||||
];
|
||||
|
||||
const result = findNextAvailableSlotDetails(
|
||||
new Date(),
|
||||
'UTC',
|
||||
workingHours
|
||||
);
|
||||
|
||||
// Saturday 18:00 to Monday 9:00
|
||||
// Rest of Saturday: 6 hours = 360 minutes
|
||||
// Sunday: 24 hours = 1440 minutes
|
||||
// Monday morning: 9 hours = 540 minutes
|
||||
// Total: 360 + 1440 + 540 = 2340 minutes
|
||||
expect(result).toEqual({
|
||||
config: workingHours[0],
|
||||
minutesUntilOpen: 2340, // 39 hours = 2340 minutes
|
||||
daysUntilOpen: 2,
|
||||
dayOfWeek: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle today open_all_day correctly', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 1, openAllDay: true },
|
||||
{ dayOfWeek: 2, openHour: 9, closeHour: 17 },
|
||||
];
|
||||
|
||||
// Should skip today since it's open_all_day and look for next slot
|
||||
const result = findNextAvailableSlotDetails(
|
||||
new Date(),
|
||||
'UTC',
|
||||
workingHours
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
config: workingHours[1],
|
||||
minutesUntilOpen: 1380, // Rest of today + 9 hours tomorrow = 1380 minutes
|
||||
daysUntilOpen: 1,
|
||||
dayOfWeek: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNextAvailableSlotDiff', () => {
|
||||
it('should return 0 when currently in working hours', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(findNextAvailableSlotDiff(new Date(), 'UTC', workingHours)).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should return minutes until next slot when not in working hours', () => {
|
||||
const mockDate = new Date('2024-01-15T08:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(8);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(findNextAvailableSlotDiff(new Date(), 'UTC', workingHours)).toBe(
|
||||
60
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when no next slot available', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [
|
||||
{ dayOfWeek: 0, closedAllDay: true },
|
||||
{ dayOfWeek: 1, closedAllDay: true },
|
||||
{ dayOfWeek: 2, closedAllDay: true },
|
||||
{ dayOfWeek: 3, closedAllDay: true },
|
||||
{ dayOfWeek: 4, closedAllDay: true },
|
||||
{ dayOfWeek: 5, closedAllDay: true },
|
||||
{ dayOfWeek: 6, closedAllDay: true },
|
||||
];
|
||||
|
||||
expect(findNextAvailableSlotDiff(new Date(), 'UTC', workingHours)).toBe(
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOnline', () => {
|
||||
it('should return agent status when working hours disabled', () => {
|
||||
expect(isOnline(false, new Date(), 'UTC', [], true)).toBe(true);
|
||||
expect(isOnline(false, new Date(), 'UTC', [], false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should check both working hours and agents when enabled', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
// In working hours + agents available = online
|
||||
expect(isOnline(true, new Date(), 'UTC', workingHours, true)).toBe(true);
|
||||
|
||||
// In working hours but no agents = offline
|
||||
expect(isOnline(true, new Date(), 'UTC', workingHours, false)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when outside working hours even with agents', () => {
|
||||
const mockDate = new Date('2024-01-15T08:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(8);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isOnline(true, new Date(), 'UTC', workingHours, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle open_all_day with agents', () => {
|
||||
const mockDate = new Date('2024-01-15T02:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(2);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openAllDay: true }];
|
||||
|
||||
expect(isOnline(true, new Date(), 'UTC', workingHours, true)).toBe(true);
|
||||
expect(isOnline(true, new Date(), 'UTC', workingHours, false)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle string date input', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(
|
||||
isOnline(true, '2024-01-15T10:00:00.000Z', 'UTC', workingHours, true)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Timezone handling', () => {
|
||||
it('should correctly handle different timezones', () => {
|
||||
const mockDate = new Date('2024-01-15T15:30:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(15);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(30);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'Asia/Kolkata', workingHours)).toBe(
|
||||
true
|
||||
);
|
||||
expect(vi.mocked(utcToZonedTime)).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Asia/Kolkata'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle UTC offset format', () => {
|
||||
const mockDate = new Date('2024-01-15T10:00:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(10);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), '+05:30', workingHours)).toBe(true);
|
||||
expect(vi.mocked(utcToZonedTime)).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'+05:30'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle working hours at exact boundaries', () => {
|
||||
// Test at exact opening time
|
||||
const mockDate1 = new Date('2024-01-15T09:00:00.000Z');
|
||||
mockDate1.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate1.getHours = vi.fn().mockReturnValue(9);
|
||||
mockDate1.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate1);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
|
||||
// Test at exact closing time
|
||||
const mockDate2 = new Date('2024-01-15T17:00:00.000Z');
|
||||
mockDate2.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate2.getHours = vi.fn().mockReturnValue(17);
|
||||
mockDate2.getMinutes = vi.fn().mockReturnValue(0);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate2);
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle one minute before closing', () => {
|
||||
const mockDate = new Date('2024-01-15T16:59:00.000Z');
|
||||
mockDate.getDay = vi.fn().mockReturnValue(1);
|
||||
mockDate.getHours = vi.fn().mockReturnValue(16);
|
||||
mockDate.getMinutes = vi.fn().mockReturnValue(59);
|
||||
vi.mocked(utcToZonedTime).mockReturnValue(mockDate);
|
||||
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
expect(isInWorkingHours(new Date(), 'UTC', workingHours)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,13 +19,19 @@
|
||||
},
|
||||
"TEAM_AVAILABILITY": {
|
||||
"ONLINE": "We are online",
|
||||
"OFFLINE": "We are away at the moment"
|
||||
"OFFLINE": "We are away at the moment",
|
||||
"BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible"
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"IN_A_FEW_MINUTES": "Typically replies in a few minutes",
|
||||
"IN_A_FEW_HOURS": "Typically replies in a few hours",
|
||||
"IN_A_DAY": "Typically replies in a day",
|
||||
"BACK_IN": "We will be back online"
|
||||
"BACK_IN_HOURS": "We will be back online in {n} hour | We will be back online in {n} hours",
|
||||
"BACK_IN_MINUTES": "We will be back online in {time} minutes",
|
||||
"BACK_AT_TIME": "We will be back online at {time}",
|
||||
"BACK_ON_DAY": "We will be back online on {day}",
|
||||
"BACK_TOMORROW": "We will be back online tomorrow",
|
||||
"BACK_IN_SOME_TIME": "We will be back online in some time"
|
||||
},
|
||||
"DAY_NAMES": {
|
||||
"SUNDAY": "Sunday",
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
import { isTimeAfter } from 'shared/helpers/DateHelper';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
channelConfig() {
|
||||
return window.chatwootWebChannel;
|
||||
},
|
||||
replyTime() {
|
||||
return window.chatwootWebChannel.replyTime;
|
||||
},
|
||||
replyTimeStatus() {
|
||||
switch (this.replyTime) {
|
||||
case 'in_a_few_minutes':
|
||||
return this.$t('REPLY_TIME.IN_A_FEW_MINUTES');
|
||||
case 'in_a_few_hours':
|
||||
return this.$t('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
case 'in_a_day':
|
||||
return this.$t('REPLY_TIME.IN_A_DAY');
|
||||
default:
|
||||
return this.$t('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
}
|
||||
},
|
||||
replyWaitMessage() {
|
||||
const { workingHoursEnabled } = this.channelConfig;
|
||||
if (workingHoursEnabled) {
|
||||
return this.isOnline
|
||||
? this.replyTimeStatus
|
||||
: `${this.$t('REPLY_TIME.BACK_IN')} ${this.timeLeftToBackInOnline}`;
|
||||
}
|
||||
return this.isOnline
|
||||
? this.replyTimeStatus
|
||||
: this.$t('TEAM_AVAILABILITY.OFFLINE');
|
||||
},
|
||||
outOfOfficeMessage() {
|
||||
return this.channelConfig.outOfOfficeMessage;
|
||||
},
|
||||
isInBetweenTheWorkingHours() {
|
||||
const {
|
||||
openHour,
|
||||
openMinute,
|
||||
closeHour,
|
||||
closeMinute,
|
||||
closedAllDay,
|
||||
openAllDay,
|
||||
} = this.currentDayAvailability;
|
||||
|
||||
if (openAllDay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (closedAllDay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { utcOffset } = this.channelConfig;
|
||||
const today = this.getDateWithOffset(utcOffset);
|
||||
const currentHours = today.getHours();
|
||||
const currentMinutes = today.getMinutes();
|
||||
const isAfterStartTime = isTimeAfter(
|
||||
currentHours,
|
||||
currentMinutes,
|
||||
openHour,
|
||||
openMinute
|
||||
);
|
||||
const isBeforeEndTime = isTimeAfter(
|
||||
closeHour,
|
||||
closeMinute,
|
||||
currentHours,
|
||||
currentMinutes
|
||||
);
|
||||
return isAfterStartTime && isBeforeEndTime;
|
||||
},
|
||||
currentDayAvailability() {
|
||||
const { utcOffset } = this.channelConfig;
|
||||
const dayOfTheWeek = this.getDateWithOffset(utcOffset).getDay();
|
||||
const [workingHourConfig = {}] = this.channelConfig.workingHours.filter(
|
||||
workingHour => workingHour.day_of_week === dayOfTheWeek
|
||||
);
|
||||
return {
|
||||
closedAllDay: workingHourConfig.closed_all_day,
|
||||
openHour: workingHourConfig.open_hour,
|
||||
openMinute: workingHourConfig.open_minutes,
|
||||
closeHour: workingHourConfig.close_hour,
|
||||
closeMinute: workingHourConfig.close_minutes,
|
||||
openAllDay: workingHourConfig.open_all_day,
|
||||
};
|
||||
},
|
||||
isInBusinessHours() {
|
||||
const { workingHoursEnabled } = this.channelConfig;
|
||||
return workingHoursEnabled ? this.isInBetweenTheWorkingHours : true;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDateWithOffset(utcOffset) {
|
||||
return utcToZonedTime(new Date().toISOString(), utcOffset);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
import {
|
||||
timeSlotParse,
|
||||
defaultTimeSlot,
|
||||
} from 'dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js';
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
import { generateRelativeTime } from 'shared/helpers/DateHelper';
|
||||
|
||||
const MINUTE_ROUNDING_FACTOR = 5;
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dayNames: [
|
||||
this.$t('DAY_NAMES.SUNDAY'),
|
||||
this.$t('DAY_NAMES.MONDAY'),
|
||||
this.$t('DAY_NAMES.TUESDAY'),
|
||||
this.$t('DAY_NAMES.WEDNESDAY'),
|
||||
this.$t('DAY_NAMES.THURSDAY'),
|
||||
this.$t('DAY_NAMES.FRIDAY'),
|
||||
this.$t('DAY_NAMES.SATURDAY'),
|
||||
],
|
||||
timeSlots: [...defaultTimeSlot],
|
||||
timeSlot: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
channelConfig() {
|
||||
return window.chatwootWebChannel;
|
||||
},
|
||||
workingHours() {
|
||||
return this.channelConfig.workingHours;
|
||||
},
|
||||
newDateWithTimeZone() {
|
||||
return utcToZonedTime(new Date(), this.timeZoneValue);
|
||||
},
|
||||
presentHour() {
|
||||
return this.newDateWithTimeZone.getHours();
|
||||
},
|
||||
presentMinute() {
|
||||
return this.newDateWithTimeZone.getMinutes();
|
||||
},
|
||||
currentDay() {
|
||||
const date = this.newDateWithTimeZone;
|
||||
const day = date.getDay();
|
||||
const currentDay = Object.keys(this.dayNames).find(
|
||||
key => this.dayNames[key] === this.dayNames[day]
|
||||
);
|
||||
return Number(currentDay);
|
||||
},
|
||||
timeZoneValue() {
|
||||
return this.channelConfig.timezone;
|
||||
},
|
||||
languageCode() {
|
||||
return window.chatwootWebChannel.locale;
|
||||
},
|
||||
currentDayWorkingHours() {
|
||||
return this.workingHours.find(
|
||||
slot => slot.day_of_week === this.currentDay
|
||||
);
|
||||
},
|
||||
nextDayWorkingHours() {
|
||||
let nextDay = this.getNextDay(this.currentDay);
|
||||
let nextWorkingHour = this.getNextWorkingHour(nextDay);
|
||||
|
||||
// It gets the next working hour for the next day. If there is no working hour for the next day,
|
||||
// it keeps iterating through the days of the week until it finds the next working hour.
|
||||
while (!nextWorkingHour) {
|
||||
nextDay = this.getNextDay(nextDay);
|
||||
nextWorkingHour = this.getNextWorkingHour(nextDay);
|
||||
}
|
||||
return nextWorkingHour;
|
||||
},
|
||||
currentDayTimings() {
|
||||
const {
|
||||
open_hour: openHour,
|
||||
open_minutes: openMinute,
|
||||
close_hour: closeHour,
|
||||
} = this.currentDayWorkingHours ?? {};
|
||||
return {
|
||||
openHour,
|
||||
openMinute,
|
||||
closeHour,
|
||||
};
|
||||
},
|
||||
nextDayTimings() {
|
||||
const { open_hour: openHour, open_minutes: openMinute } =
|
||||
this.nextDayWorkingHours ?? {};
|
||||
return {
|
||||
openHour,
|
||||
openMinute,
|
||||
};
|
||||
},
|
||||
dayDiff() {
|
||||
// Here this is used to get the difference between current day and next working day
|
||||
const nextDay = this.nextDayWorkingHours.day_of_week;
|
||||
const totalDays = 6;
|
||||
return nextDay > this.currentDay
|
||||
? nextDay - this.currentDay - 1
|
||||
: totalDays - this.currentDay + nextDay;
|
||||
},
|
||||
dayNameOfNextWorkingDay() {
|
||||
return this.dayNames[this.nextDayWorkingHours.day_of_week];
|
||||
},
|
||||
hoursAndMinutesBackInOnline() {
|
||||
if (this.presentHour >= this.currentDayTimings.closeHour) {
|
||||
return this.getHoursAndMinutesUntilNextDayOpen(
|
||||
this.nextDayWorkingHours.open_all_day
|
||||
? 0
|
||||
: this.nextDayTimings.openHour,
|
||||
this.nextDayTimings.openMinute,
|
||||
this.currentDayTimings.closeHour
|
||||
);
|
||||
}
|
||||
return this.getHoursAndMinutesUntilNextDayOpen(
|
||||
this.currentDayTimings.openHour,
|
||||
this.currentDayTimings.openMinute,
|
||||
this.currentDayTimings.closeHour
|
||||
);
|
||||
},
|
||||
exactTimeInAmPm() {
|
||||
return `${
|
||||
this.timeSlot.day === this.currentDay ? `at ${this.timeSlot.from}` : ''
|
||||
}`;
|
||||
},
|
||||
hoursAndMinutesLeft() {
|
||||
const { hoursLeft, minutesLeft } = this.hoursAndMinutesBackInOnline;
|
||||
|
||||
const timeLeftChars = [];
|
||||
|
||||
if (hoursLeft > 0) {
|
||||
const roundedUpHoursLeft = minutesLeft > 0 ? hoursLeft + 1 : hoursLeft;
|
||||
const hourRelative = generateRelativeTime(
|
||||
roundedUpHoursLeft,
|
||||
'hour',
|
||||
this.languageCode
|
||||
);
|
||||
timeLeftChars.push(`${hourRelative}`);
|
||||
}
|
||||
|
||||
if (minutesLeft > 0 && hoursLeft === 0) {
|
||||
const roundedUpMinLeft =
|
||||
Math.ceil(minutesLeft / MINUTE_ROUNDING_FACTOR) *
|
||||
MINUTE_ROUNDING_FACTOR;
|
||||
const minRelative = generateRelativeTime(
|
||||
roundedUpMinLeft,
|
||||
'minutes',
|
||||
this.languageCode
|
||||
);
|
||||
timeLeftChars.push(`${minRelative}`);
|
||||
}
|
||||
|
||||
return timeLeftChars.join(' ');
|
||||
},
|
||||
hoursAndMinutesToBack() {
|
||||
const { hoursLeft, minutesLeft } = this.hoursAndMinutesBackInOnline;
|
||||
if (hoursLeft >= 3) {
|
||||
return this.exactTimeInAmPm;
|
||||
}
|
||||
if (hoursLeft > 0 || minutesLeft > 0) {
|
||||
return this.hoursAndMinutesLeft;
|
||||
}
|
||||
return 'in some time';
|
||||
},
|
||||
timeLeftToBackInOnline() {
|
||||
if (
|
||||
this.hoursAndMinutesBackInOnline.hoursLeft >= 24 ||
|
||||
(this.timeSlot.day !== this.currentDay && this.dayDiff === 0)
|
||||
) {
|
||||
const hourRelative = generateRelativeTime(
|
||||
this.dayDiff + 1,
|
||||
'days',
|
||||
this.languageCode
|
||||
);
|
||||
return `${hourRelative}`;
|
||||
}
|
||||
if (
|
||||
this.dayDiff >= 1 &&
|
||||
this.presentHour >= this.currentDayTimings.closeHour
|
||||
) {
|
||||
return `on ${this.dayNameOfNextWorkingDay}`;
|
||||
}
|
||||
return this.hoursAndMinutesToBack;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setTimeSlot();
|
||||
},
|
||||
methods: {
|
||||
getNextDay(day) {
|
||||
// This code calculates the next day of the week based on the current day. If the current day is Saturday (6), then the next day will be Sunday (0).
|
||||
return (day + 1) % 7;
|
||||
},
|
||||
getNextWorkingHour(day) {
|
||||
const workingHour = this.workingHours.find(
|
||||
slot => slot.day_of_week === day
|
||||
);
|
||||
if (workingHour && !workingHour.closed_all_day) {
|
||||
return workingHour;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getHoursAndMinutesUntilNextDayOpen(
|
||||
openHour, // If the present time is after the closing time of the current day, then the openHour will be the opening hour of the next day else it will be the opening hour of the current day.
|
||||
openMinutes, // If the present time is after the closing time of the current day, then the openMinutes will be the opening minutes of the next day else it will be the opening minutes of the current day.
|
||||
closeHour // The closeHour will be the closing hour of the current day. It will be used to calculate the time remaining until the next day's opening hours.
|
||||
) {
|
||||
// This code calculates the time remaining until the next day's opening hours,
|
||||
// given the current time, the opening hours, and the closing hours of the current day.
|
||||
if (closeHour < openHour) {
|
||||
openHour += 24;
|
||||
}
|
||||
let diffMinutes =
|
||||
openHour * 60 +
|
||||
openMinutes -
|
||||
(this.presentHour * 60 + this.presentMinute);
|
||||
diffMinutes = diffMinutes < 0 ? diffMinutes + 24 * 60 : diffMinutes;
|
||||
const [hoursLeft, minutesLeft] = [
|
||||
Math.floor(diffMinutes / 60),
|
||||
diffMinutes % 60,
|
||||
];
|
||||
|
||||
// It returns the remaining time in hours and minutes as an object with keys hours and minutes.
|
||||
return { hoursLeft, minutesLeft };
|
||||
},
|
||||
setTimeSlot() {
|
||||
// It checks if the working hours feature is enabled for the store.
|
||||
|
||||
const timeSlots = this.workingHours;
|
||||
|
||||
// If the present hour is after the closing hour of the current day,
|
||||
// then the next day's working hours will be used to calculate the time remaining until the next day's opening hours,
|
||||
// else the current day's working hours will be used
|
||||
const currentSlot =
|
||||
this.presentHour >= this.currentDayTimings.closeHour
|
||||
? this.nextDayWorkingHours
|
||||
: this.currentDayWorkingHours;
|
||||
|
||||
// It parses the working hours to get the time slots in AM/PM format.
|
||||
const slots = timeSlotParse(timeSlots).length
|
||||
? timeSlotParse(timeSlots)
|
||||
: defaultTimeSlot;
|
||||
this.timeSlots = slots;
|
||||
|
||||
// It finds the time slot for the current slot.
|
||||
this.timeSlot = this.timeSlots.find(
|
||||
slot => slot.day === currentSlot.day_of_week
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
export default {
|
||||
methods: {
|
||||
async replaceRoute(name, params = {}) {
|
||||
if (this.$route.name !== name) {
|
||||
return this.$router.replace({ name, params });
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import availabilityMixin from '../availability';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
global.chatwootWebChannel = {
|
||||
workingHoursEnabled: true,
|
||||
workingHours: [
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 8,
|
||||
open_minutes: 30,
|
||||
close_hour: 17,
|
||||
close_minutes: 35,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 8,
|
||||
open_minutes: 30,
|
||||
close_hour: 17,
|
||||
close_minutes: 30,
|
||||
open_all_day: false,
|
||||
},
|
||||
],
|
||||
utcOffset: '-07:00',
|
||||
};
|
||||
|
||||
let Component;
|
||||
|
||||
describe('availabilityMixin', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
Component = defineComponent({
|
||||
mixins: [availabilityMixin],
|
||||
render() {
|
||||
return h('div');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns valid isInBetweenWorkingHours if in different timezone', () => {
|
||||
vi.useFakeTimers().setSystemTime(
|
||||
new Date('Thu Apr 14 2022 06:04:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.isInBetweenTheWorkingHours).toBe(true);
|
||||
});
|
||||
|
||||
it('returns valid isInBetweenWorkingHours if in same timezone', () => {
|
||||
global.chatwootWebChannel.utcOffset = '+05:30';
|
||||
|
||||
vi.useFakeTimers().setSystemTime(
|
||||
new Date('Thu Apr 14 2022 09:01:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.isInBetweenTheWorkingHours).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if closed all day', () => {
|
||||
global.chatwootWebChannel.utcOffset = '-07:00';
|
||||
global.chatwootWebChannel.workingHours = [
|
||||
{ day_of_week: 3, closed_all_day: true },
|
||||
];
|
||||
|
||||
vi.useFakeTimers().setSystemTime(
|
||||
new Date('Thu Apr 14 2022 09:01:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.isInBetweenTheWorkingHours).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if open all day', () => {
|
||||
global.chatwootWebChannel.utcOffset = '-07:00';
|
||||
global.chatwootWebChannel.workingHours = [
|
||||
{ day_of_week: 3, open_all_day: true },
|
||||
];
|
||||
|
||||
vi.useFakeTimers().setSystemTime(
|
||||
new Date('Thu Apr 14 2022 09:01:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.isInBetweenTheWorkingHours).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,402 +0,0 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import nextAvailabilityTimeMixin from '../nextAvailabilityTime';
|
||||
|
||||
describe('nextAvailabilityTimeMixin', () => {
|
||||
const chatwootWebChannel = {
|
||||
workingHoursEnabled: true,
|
||||
workingHours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let Component;
|
||||
|
||||
beforeEach(() => {
|
||||
Component = defineComponent({
|
||||
mixins: [nextAvailabilityTimeMixin],
|
||||
render() {
|
||||
return h('div');
|
||||
},
|
||||
});
|
||||
window.chatwootWebChannel = chatwootWebChannel;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.chatwootWebChannel;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return day names', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
expect(wrapper.vm.dayNames).toEqual([
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return channelConfig', () => {
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.channelConfig).toEqual(chatwootWebChannel);
|
||||
});
|
||||
|
||||
it('should return workingHours', () => {
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.workingHours).toEqual(chatwootWebChannel.workingHours);
|
||||
});
|
||||
|
||||
it('should return currentDayWorkingHours', () => {
|
||||
const currentDay = new Date().getDay();
|
||||
const expectedWorkingHours = chatwootWebChannel.workingHours.find(
|
||||
slot => slot.day_of_week === currentDay
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
expect(wrapper.vm.currentDayWorkingHours).toEqual(expectedWorkingHours);
|
||||
});
|
||||
|
||||
it('should return nextDayWorkingHours', () => {
|
||||
const currentDay = new Date().getDay();
|
||||
const nextDay = currentDay === 6 ? 0 : currentDay + 1;
|
||||
const expectedWorkingHours = chatwootWebChannel.workingHours.find(
|
||||
slot => slot.day_of_week === nextDay
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
expect(wrapper.vm.nextDayWorkingHours).toEqual(expectedWorkingHours);
|
||||
});
|
||||
|
||||
it('should return presentHour', () => {
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.presentHour).toBe(new Date().getHours());
|
||||
});
|
||||
|
||||
it('should return presentMinute', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
expect(wrapper.vm.presentMinute).toBe(new Date().getMinutes());
|
||||
});
|
||||
|
||||
it('should return currentDay', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
const date = new Date();
|
||||
const day = date.getDay();
|
||||
const currentDay = Object.keys(wrapper.vm.dayNames).find(
|
||||
key => wrapper.vm.dayNames[key] === wrapper.vm.dayNames[day]
|
||||
);
|
||||
expect(wrapper.vm.currentDay).toBe(Number(currentDay));
|
||||
});
|
||||
|
||||
it('should return currentDayTimings', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
const {
|
||||
open_hour: openHour,
|
||||
open_minutes: openMinute,
|
||||
close_hour: closeHour,
|
||||
} = wrapper.vm.currentDayWorkingHours;
|
||||
expect(wrapper.vm.currentDayTimings).toEqual({
|
||||
openHour,
|
||||
openMinute,
|
||||
closeHour,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nextDayTimings', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
const { open_hour: openHour, open_minutes: openMinute } =
|
||||
wrapper.vm.nextDayWorkingHours;
|
||||
|
||||
expect(wrapper.vm.nextDayTimings).toEqual({
|
||||
openHour,
|
||||
openMinute,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return dayDiff', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
const currentDay = wrapper.vm.currentDay;
|
||||
const nextDay = wrapper.vm.nextDayWorkingHours.day_of_week;
|
||||
const totalDays = 6;
|
||||
const expectedDayDiff =
|
||||
nextDay > currentDay
|
||||
? nextDay - currentDay - 1
|
||||
: totalDays - currentDay + nextDay;
|
||||
|
||||
expect(wrapper.vm.dayDiff).toEqual(expectedDayDiff);
|
||||
});
|
||||
|
||||
it('should return dayNameOfNextWorkingDay', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
const nextDay = wrapper.vm.nextDayWorkingHours.day_of_week;
|
||||
const expectedDayName = wrapper.vm.dayNames[nextDay];
|
||||
expect(wrapper.vm.dayNameOfNextWorkingDay).toEqual(expectedDayName);
|
||||
});
|
||||
|
||||
it('should return hoursAndMinutesBackInOnline', () => {
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
const currentDayCloseHour =
|
||||
chatwootWebChannel.workingHours[wrapper.vm.currentDay].close_hour;
|
||||
const nextDayOpenHour =
|
||||
chatwootWebChannel.workingHours[
|
||||
wrapper.vm.currentDay === 6 ? 0 : wrapper.vm.currentDay + 1
|
||||
].open_hour;
|
||||
const nextDayOpenMinute =
|
||||
chatwootWebChannel.workingHours[
|
||||
wrapper.vm.currentDay === 6 ? 0 : wrapper.vm.currentDay + 1
|
||||
].open_minutes;
|
||||
const expectedHoursAndMinutes =
|
||||
wrapper.vm.getHoursAndMinutesUntilNextDayOpen(
|
||||
nextDayOpenHour,
|
||||
nextDayOpenMinute,
|
||||
currentDayCloseHour
|
||||
);
|
||||
expect(wrapper.vm.hoursAndMinutesBackInOnline).toEqual(
|
||||
expectedHoursAndMinutes
|
||||
);
|
||||
});
|
||||
|
||||
it('should return getNextDay', () => {
|
||||
const wrapper = mount(Component);
|
||||
expect(wrapper.vm.getNextDay(6)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return in 30 minutes', () => {
|
||||
vi.useFakeTimers('modern').setSystemTime(
|
||||
new Date('Thu Apr 14 2022 14:04:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
|
||||
chatwootWebChannel.workingHours[4].open_hour = 18;
|
||||
chatwootWebChannel.workingHours[4].open_minutes = 0;
|
||||
chatwootWebChannel.workingHours[4].close_hour = 23;
|
||||
expect(wrapper.vm.timeLeftToBackInOnline).toBe('in 30 minutes');
|
||||
});
|
||||
|
||||
it('should return in 2 hours', () => {
|
||||
vi.useFakeTimers('modern').setSystemTime(
|
||||
new Date('Thu Apr 14 2022 22:04:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
chatwootWebChannel.workingHours[4].open_hour = 19;
|
||||
expect(wrapper.vm.timeLeftToBackInOnline).toBe('in 2 hours');
|
||||
});
|
||||
|
||||
it('should return at 09:00 AM', () => {
|
||||
vi.useFakeTimers('modern').setSystemTime(
|
||||
new Date('Thu Apr 15 2022 22:04:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
chatwootWebChannel.workingHours[4].open_hour = 10;
|
||||
expect(wrapper.vm.timeLeftToBackInOnline).toBe('at 09:00 AM');
|
||||
});
|
||||
|
||||
it('should return tomorrow', () => {
|
||||
vi.useFakeTimers('modern').setSystemTime(
|
||||
new Date('Thu Apr 1 2022 23:04:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
chatwootWebChannel.workingHours[4].open_hour = 9;
|
||||
chatwootWebChannel.workingHours[4].close_hour = 16;
|
||||
expect(wrapper.vm.timeLeftToBackInOnline).toBe('tomorrow');
|
||||
});
|
||||
|
||||
it.skip('should return on Saturday', () => {
|
||||
vi.useFakeTimers('modern').setSystemTime(
|
||||
new Date('Thu Apr 14 2022 23:04:46 GMT+0530')
|
||||
);
|
||||
const wrapper = mount(Component);
|
||||
wrapper.vm.dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
|
||||
chatwootWebChannel.workingHours[4].open_hour = 9;
|
||||
chatwootWebChannel.workingHours[4].close_hour = 16;
|
||||
chatwootWebChannel.workingHours[5].closed_all_day = true;
|
||||
expect(wrapper.vm.timeLeftToBackInOnline).toBe('on Saturday');
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import TeamAvailability from 'widget/components/TeamAvailability.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import { useRouter } from 'vue-router';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import ArticleContainer from '../components/pageComponents/Home/Article/ArticleContainer.vue';
|
||||
export default {
|
||||
@@ -10,7 +10,11 @@ export default {
|
||||
ArticleContainer,
|
||||
TeamAvailability,
|
||||
},
|
||||
mixins: [configMixin, routerMixin],
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
return { router };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
availableAgents: 'agent/availableAgents',
|
||||
@@ -21,9 +25,9 @@ export default {
|
||||
methods: {
|
||||
startConversation() {
|
||||
if (this.preChatFormEnabled && !this.conversationSize) {
|
||||
return this.replaceRoute('prechat-form');
|
||||
return this.router.replace({ name: 'prechat-form' });
|
||||
}
|
||||
return this.replaceRoute('messages');
|
||||
return this.router.replace({ name: 'messages' });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { useRouter } from 'vue-router';
|
||||
import PreChatForm from '../components/PreChat/Form.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import routerMixin from '../mixins/routerMixin';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import { ON_CONVERSATION_CREATED } from '../constants/widgetBusEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
@@ -11,7 +11,11 @@ export default {
|
||||
components: {
|
||||
PreChatForm,
|
||||
},
|
||||
mixins: [configMixin, routerMixin],
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
return { router };
|
||||
},
|
||||
mounted() {
|
||||
// Register event listener for conversation creation
|
||||
emitter.on(ON_CONVERSATION_CREATED, this.handleConversationCreated);
|
||||
@@ -24,7 +28,7 @@ export default {
|
||||
...mapActions('conversationAttributes', ['clearConversationAttributes']),
|
||||
handleConversationCreated() {
|
||||
// Redirect to messages page after conversation is created
|
||||
this.replaceRoute('messages');
|
||||
this.router.replace({ name: 'messages' });
|
||||
// Only after successful navigation, reset the isUpdatingRoute UIflag in app/javascript/widget/router.js
|
||||
// See issue: https://github.com/chatwoot/chatwoot/issues/10736
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user