Feat: Show working hours on widget (#1823)

Feat: Display out of office message based on business hours

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Nithin David Thomas 2021-03-13 11:44:28 +05:30 committed by GitHub
parent 1d2e1a2823
commit 6c87001a0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 21 deletions

View file

@ -2,6 +2,7 @@ import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format'; import format from 'date-fns/format';
import isToday from 'date-fns/isToday'; import isToday from 'date-fns/isToday';
import isYesterday from 'date-fns/isYesterday'; import isYesterday from 'date-fns/isYesterday';
import parseISO from 'date-fns/parseISO';
export const formatUnixDate = (date, dateFormat = 'MMM dd, yyyy') => { export const formatUnixDate = (date, dateFormat = 'MMM dd, yyyy') => {
const unixDate = fromUnixTime(date); const unixDate = fromUnixTime(date);
@ -14,3 +15,13 @@ export const formatDate = ({ date, todayText, yesterdayText }) => {
if (isYesterday(dateValue)) return yesterdayText; if (isYesterday(dateValue)) return yesterdayText;
return date; return date;
}; };
export const buildDateFromTime = (hr, min, utcOffset, date = new Date()) => {
const today = format(date, 'yyyy-MM-dd');
const timeString = `${today}T${hr}:${min}:00${utcOffset}`;
return parseISO(timeString);
};
export const formatDigitToString = val => {
return val > 9 ? `${val}` : `0${val}`;
};

View file

@ -1,4 +1,9 @@
import { formatDate, formatUnixDate } from '../DateHelper'; import {
formatDate,
formatUnixDate,
buildDateFromTime,
formatDigitToString,
} from '../DateHelper';
describe('#DateHelper', () => { describe('#DateHelper', () => {
it('should format unix date correctly without dateFormat', () => { it('should format unix date correctly without dateFormat', () => {
@ -39,4 +44,27 @@ describe('#DateHelper', () => {
}) })
).toEqual('Yesterday'); ).toEqual('Yesterday');
}); });
describe('#buildDate', () => {
it('returns correctly parsed date', () => {
const date = new Date();
date.setFullYear(2021);
date.setMonth(2);
date.setDate(5);
const result = buildDateFromTime(12, 15, '.465Z', date);
expect(result + '').toEqual(
'Fri Mar 05 2021 12:15:00 GMT+0000 (Coordinated Universal Time)'
);
});
});
describe('#formatDigitToString', () => {
it('returns date compatabile string from number is less than 9', () => {
expect(formatDigitToString(8)).toEqual('08');
});
it('returns date compatabile string from number is greater than 9', () => {
expect(formatDigitToString(11)).toEqual('11');
});
});
}); });

View file

@ -31,14 +31,13 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import HeaderActions from './HeaderActions'; import HeaderActions from './HeaderActions';
import configMixin from 'widget/mixins/configMixin'; import configMixin from 'widget/mixins/configMixin';
import teamAvailabilityMixin from 'widget/mixins/teamAvailabilityMixin';
export default { export default {
name: 'ChatHeader', name: 'ChatHeader',
components: { components: {
HeaderActions, HeaderActions,
}, },
mixins: [configMixin, teamAvailabilityMixin], mixins: [configMixin],
props: { props: {
avatarUrl: { avatarUrl: {
type: String, type: String,

View file

@ -1,15 +1,27 @@
<template> <template>
<div class="px-4"> <div class="px-4">
<div class="flex items-center justify-between mb-4"> <div
v-if="channelConfig.workingHoursEnabled"
class="flex items-center justify-between mb-4"
>
<div class="text-black-700"> <div class="text-black-700">
<div class="text-base leading-5 font-medium mb-1"> <div class="text-base leading-5 font-medium mb-1">
{{ teamAvailabilityStatus }} {{
isInBetweenTheWorkingHours
? $t('TEAM_AVAILABILITY.ONLINE')
: $t('TEAM_AVAILABILITY.OFFLINE')
}}
</div> </div>
<div class="text-xs leading-4 mt-1"> <div class="text-xs leading-4 mt-1">
{{ replyTimeStatus }} {{
isInBetweenTheWorkingHours ? replyTimeStatus : outOfOfficeMessage
}}
</div> </div>
</div> </div>
<available-agents :agents="availableAgents" /> <available-agents
v-if="isInBetweenTheWorkingHours"
:agents="availableAgents"
/>
</div> </div>
<woot-button <woot-button
class="font-medium" class="font-medium"
@ -27,9 +39,13 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import AvailableAgents from 'widget/components/AvailableAgents.vue'; import AvailableAgents from 'widget/components/AvailableAgents.vue';
import { getContrastingTextColor } from 'shared/helpers/ColorHelper'; import { getContrastingTextColor } from 'shared/helpers/ColorHelper';
import {
buildDateFromTime,
formatDigitToString,
} from 'shared/helpers/DateHelper';
import WootButton from 'shared/components/Button'; import WootButton from 'shared/components/Button';
import configMixin from 'widget/mixins/configMixin'; import configMixin from 'widget/mixins/configMixin';
import teamAvailabilityMixin from 'widget/mixins/teamAvailabilityMixin'; import compareAsc from 'date-fns/compareAsc';
export default { export default {
name: 'TeamAvailability', name: 'TeamAvailability',
@ -37,7 +53,7 @@ export default {
AvailableAgents, AvailableAgents,
WootButton, WootButton,
}, },
mixins: [configMixin, teamAvailabilityMixin], mixins: [configMixin],
props: { props: {
availableAgents: { availableAgents: {
type: Array, type: Array,
@ -49,6 +65,53 @@ export default {
textColor() { textColor() {
return getContrastingTextColor(this.widgetColor); return getContrastingTextColor(this.widgetColor);
}, },
isInBetweenTheWorkingHours() {
const {
closedAllDay,
openHour,
openMinute,
closeHour,
closeMinute,
} = this.currentDayAvailability;
if (closedAllDay) {
return false;
}
const { utcOffset } = this.channelConfig;
const startTime = buildDateFromTime(
formatDigitToString(openHour),
formatDigitToString(openMinute),
utcOffset
);
const endTime = buildDateFromTime(
formatDigitToString(closeHour),
formatDigitToString(closeMinute),
utcOffset
);
if (
compareAsc(new Date(), startTime) === 1 &&
compareAsc(endTime, new Date()) === 1
) {
return true;
}
return false;
},
currentDayAvailability() {
const dayOfTheWeek = new Date().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,
};
},
}, },
methods: { methods: {
startConversation() { startConversation() {

View file

@ -46,5 +46,8 @@ export default {
return this.$t('REPLY_TIME.IN_A_FEW_HOURS'); return this.$t('REPLY_TIME.IN_A_FEW_HOURS');
} }
}, },
outOfOfficeMessage() {
return this.channelConfig.outOfOfficeMessage;
},
}, },
}; };

View file

@ -1,10 +0,0 @@
export default {
computed: {
teamAvailabilityStatus() {
if (this.availableAgents.length) {
return this.$t('TEAM_AVAILABILITY.ONLINE');
}
return this.$t('TEAM_AVAILABILITY.OFFLINE');
},
},
};

View file

@ -21,7 +21,11 @@
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>, enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
replyTime: '<%= @web_widget.reply_time %>', replyTime: '<%= @web_widget.reply_time %>',
preChatFormEnabled: <%= @web_widget.pre_chat_form_enabled %>, preChatFormEnabled: <%= @web_widget.pre_chat_form_enabled %>,
preChatFormOptions: <%= @web_widget.pre_chat_form_options.to_json.html_safe %> preChatFormOptions: <%= @web_widget.pre_chat_form_options.to_json.html_safe %>,
workingHoursEnabled: <%= @web_widget.inbox.working_hours_enabled %>,
workingHours: <%= @web_widget.inbox.working_hours.to_json.html_safe %>,
outOfOfficeMessage: <%= @web_widget.inbox.out_of_office_message.to_json.html_safe %>,
utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].formatted_offset %>'
} }
window.chatwootWidgetDefaults = { window.chatwootWidgetDefaults = {
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>, useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,

View file

@ -18,7 +18,7 @@ RSpec.describe WorkingHour do
context 'when on sunday 1pm' do context 'when on sunday 1pm' do
before do before do
Time.zone = 'UTC' Time.zone = 'UTC'
create(:working_hour, day_of_week: 7, closed_all_day: true) create(:working_hour, day_of_week: 0, closed_all_day: true)
travel_to '01.11.2020 13:00'.to_datetime travel_to '01.11.2020 13:00'.to_datetime
end end