diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
index 4cb645577..e63a40656 100644
--- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
@@ -274,6 +274,12 @@
"TITLE": "Set your availability",
"SUBTITLE": "Set your availability on your livechat widget",
"WEEKLY_TITLE": "Set your weekly hours",
+ "TIMEZONE_LABEL": "Select timezone",
+ "UPDATE": "Update business hours settings",
+ "TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
+ "UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
+ "UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
+ "TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
"DAY": {
"ENABLE": "Enable availability for this day",
"UNAVAILABLE": "Unavailable",
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
index 81ea650d7..83d6b57f6 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
@@ -254,6 +254,9 @@
+
+
+
@@ -266,12 +269,14 @@ import SettingsSection from '../../../../components/SettingsSection';
import inboxMixin from 'shared/mixins/inboxMixin';
import FacebookReauthorize from './facebook/Reauthorize';
import PreChatFormSettings from './PreChatForm/Settings';
+import WeeklyAvailability from './components/WeeklyAvailability';
export default {
components: {
SettingsSection,
FacebookReauthorize,
PreChatFormSettings,
+ WeeklyAvailability,
},
mixins: [alertMixin, configMixin, inboxMixin],
data() {
@@ -329,6 +334,10 @@ export default {
key: 'preChatForm',
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),
},
+ {
+ key: 'businesshours',
+ name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
+ },
{
key: 'configuration',
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue
index 6e2cbb151..9e6338605 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue
@@ -92,11 +92,13 @@ export default {
...this.timeSlot,
from: timeSlots[0],
to: timeSlots[16],
+ valid: true,
}
: {
...this.timeSlot,
from: '',
to: '',
+ valid: false,
};
this.$emit('update', newSlot);
},
@@ -106,9 +108,12 @@ export default {
return this.timeSlot.from;
},
set(value) {
+ const fromDate = parse(value, 'hh:mm a', new Date());
+ const valid = differenceInMinutes(this.toDate, fromDate) / 60 > 0;
this.$emit('update', {
...this.timeSlot,
from: value,
+ valid,
});
},
},
@@ -117,10 +122,21 @@ export default {
return this.timeSlot.to;
},
set(value) {
- this.$emit('update', {
- ...this.timeSlot,
- to: value,
- });
+ const toDate = parse(value, 'hh:mm a', new Date());
+ if (value === '12:00 AM') {
+ this.$emit('update', {
+ ...this.timeSlot,
+ to: value,
+ valid: true,
+ });
+ } else {
+ const valid = differenceInMinutes(toDate, this.fromDate) / 60 > 0;
+ this.$emit('update', {
+ ...this.timeSlot,
+ to: value,
+ valid,
+ });
+ }
},
},
fromDate() {
@@ -130,10 +146,14 @@ export default {
return parse(this.toTime, 'hh:mm a', new Date());
},
totalHours() {
- return differenceInMinutes(this.toDate, this.fromDate) / 60;
+ const totalHours = differenceInMinutes(this.toDate, this.fromDate) / 60;
+ if (this.toTime === '12:00 AM') {
+ return 24 + totalHours;
+ }
+ return totalHours;
},
hasError() {
- return this.totalHours < 0;
+ return !this.timeSlot.valid;
},
},
};
@@ -157,8 +177,8 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
- padding: var(--space-normal);
- height: var(--space-larger);
+ padding: var(--space-small) 0;
+ min-height: var(--space-larger);
box-sizing: content-box;
border-bottom: 1px solid var(--color-border-light);
}
@@ -213,7 +233,7 @@ export default {
}
.date-error {
- padding: var(--space-small) 0;
+ padding-top: var(--space-smaller);
}
.error {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WeeklyAvailability.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WeeklyAvailability.vue
new file mode 100644
index 000000000..4107c5151
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WeeklyAvailability.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js
index dba5ad7e0..2bd9a281a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js
@@ -1,3 +1,53 @@
+import parse from 'date-fns/parse';
+import getHours from 'date-fns/getHours';
+import getMinutes from 'date-fns/getMinutes';
+import timeZoneData from './timezones.json';
+
+export const defaultTimeSlot = [
+ {
+ day: 0,
+ to: '',
+ from: '',
+ valid: false,
+ },
+ {
+ day: 1,
+ to: '',
+ from: '',
+ valid: false,
+ },
+ {
+ day: 2,
+ to: '',
+ from: '',
+ valid: false,
+ },
+ {
+ day: 3,
+ to: '',
+ from: '',
+ valid: false,
+ },
+ {
+ day: 4,
+ to: '',
+ from: '',
+ valid: false,
+ },
+ {
+ day: 5,
+ to: '',
+ from: '',
+ valid: false,
+ },
+ {
+ day: 6,
+ to: '',
+ from: '',
+ valid: false,
+ },
+];
+
export const generateTimeSlots = (step = 15) => {
/*
Generates a list of time strings from 12:00 AM to next 24 hours. Each new string
@@ -18,3 +68,70 @@ export const generateTimeSlots = (step = 15) => {
}
return slots;
};
+
+export const getTime = (hour, minute) => {
+ const merdian = hour > 11 ? 'PM' : 'AM';
+ const modHour = hour > 12 ? hour % 12 : hour || 12;
+ const parsedHour = modHour < 10 ? `0${modHour}` : modHour;
+ const parsedMinute = minute < 10 ? `0${minute}` : minute;
+ return `${parsedHour}:${parsedMinute} ${merdian}`;
+};
+
+export const timeSlotParse = timeSlots => {
+ return timeSlots.map(slot => {
+ const {
+ day_of_week: day,
+ open_hour: openHour,
+ open_minutes: openMinutes,
+ close_hour: closeHour,
+ close_minutes: closeMinutes,
+ closed_all_day: closedAllDay,
+ } = slot;
+ const from = closedAllDay ? '' : getTime(openHour, openMinutes);
+ const to = closedAllDay ? '' : getTime(closeHour, closeMinutes);
+
+ return {
+ day,
+ to,
+ from,
+ valid: !closedAllDay,
+ };
+ });
+};
+
+export const timeSlotTransform = timeSlots => {
+ return timeSlots.map(slot => {
+ const closed = !(slot.to && slot.from);
+ let fromDate = '';
+ let toDate = '';
+ let openHour = '';
+ let openMinutes = '';
+ let closeHour = '';
+ let closeMinutes = '';
+
+ if (!closed) {
+ fromDate = parse(slot.from, 'hh:mm a', new Date());
+ toDate = parse(slot.to, 'hh:mm a', new Date());
+ openHour = getHours(fromDate);
+ openMinutes = getMinutes(fromDate);
+ closeHour = getHours(toDate);
+ closeMinutes = getMinutes(toDate);
+ }
+
+ return {
+ day_of_week: slot.day,
+ closed_all_day: closed,
+ open_hour: openHour,
+ open_minutes: openMinutes,
+ close_hour: closeHour,
+ close_minutes: closeMinutes,
+ };
+ });
+};
+
+export const timeZoneOptions = () => {
+ return Object.keys(timeZoneData).map(key => ({
+ label: key,
+ value: timeZoneData[key],
+ }));
+};
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js
index b5f0cbef7..62b9b28ac 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js
@@ -1,4 +1,10 @@
-import { generateTimeSlots } from '../businessHour';
+import {
+ generateTimeSlots,
+ getTime,
+ timeSlotParse,
+ timeSlotTransform,
+ timeZoneOptions,
+} from '../businessHour';
describe('#generateTimeSlots', () => {
it('returns correct number of time slots', () => {
@@ -15,3 +21,65 @@ describe('#generateTimeSlots', () => {
]);
});
});
+
+describe('#getTime', () => {
+ it('returns parses 24 hour time correctly', () => {
+ expect(getTime(15, 30)).toStrictEqual('03:30 PM');
+ });
+ it('returns parses 12 hour time correctly', () => {
+ expect(getTime(12, 30)).toStrictEqual('12:30 PM');
+ });
+});
+
+describe('#timeSlotParse', () => {
+ it('returns parses correctly', () => {
+ const slot = {
+ day_of_week: 1,
+ open_hour: 1,
+ open_minutes: 30,
+ close_hour: 4,
+ close_minutes: 30,
+ closed_all_day: false,
+ };
+
+ expect(timeSlotParse([slot])).toStrictEqual([
+ {
+ day: 1,
+ from: '01:30 AM',
+ to: '04:30 AM',
+ valid: true,
+ },
+ ]);
+ });
+});
+
+describe('#timeSlotTransform', () => {
+ it('returns transforms correctly', () => {
+ const slot = {
+ day: 1,
+ from: '01:30 AM',
+ to: '04:30 AM',
+ valid: true,
+ };
+
+ expect(timeSlotTransform([slot])).toStrictEqual([
+ {
+ day_of_week: 1,
+ open_hour: 1,
+ open_minutes: 30,
+ close_hour: 4,
+ close_minutes: 30,
+ closed_all_day: false,
+ },
+ ]);
+ });
+});
+
+describe('#timeZoneOptions', () => {
+ it('returns transforms correctly', () => {
+ expect(timeZoneOptions()[0]).toStrictEqual({
+ value: 'Etc/GMT+12',
+ label: 'International Date Line West',
+ });
+ });
+});
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json
new file mode 100644
index 000000000..8cfe6f882
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json
@@ -0,0 +1,153 @@
+{
+ "International Date Line West": "Etc/GMT+12",
+ "Midway Island": "Pacific/Midway",
+ "American Samoa": "Pacific/Pago_Pago",
+ "Hawaii": "Pacific/Honolulu",
+ "Alaska": "America/Juneau",
+ "Pacific Time (US & Canada)": "America/Los_Angeles",
+ "Tijuana": "America/Tijuana",
+ "Mountain Time (US & Canada)": "America/Denver",
+ "Arizona": "America/Phoenix",
+ "Chihuahua": "America/Chihuahua",
+ "Mazatlan": "America/Mazatlan",
+ "Central Time (US & Canada)": "America/Chicago",
+ "Saskatchewan": "America/Regina",
+ "Guadalajara": "America/Mexico_City",
+ "Mexico City": "America/Mexico_City",
+ "Monterrey": "America/Monterrey",
+ "Central America": "America/Guatemala",
+ "Eastern Time (US & Canada)": "America/New_York",
+ "Indiana (East)": "America/Indiana/Indianapolis",
+ "Bogota": "America/Bogota",
+ "Lima": "America/Lima",
+ "Quito": "America/Lima",
+ "Atlantic Time (Canada)": "America/Halifax",
+ "Caracas": "America/Caracas",
+ "La Paz": "America/La_Paz",
+ "Santiago": "America/Santiago",
+ "Newfoundland": "America/St_Johns",
+ "Brasilia": "America/Sao_Paulo",
+ "Buenos Aires": "America/Argentina/Buenos_Aires",
+ "Montevideo": "America/Montevideo",
+ "Georgetown": "America/Guyana",
+ "Puerto Rico": "America/Puerto_Rico",
+ "Greenland": "America/Godthab",
+ "Mid-Atlantic": "Atlantic/South_Georgia",
+ "Azores": "Atlantic/Azores",
+ "Cape Verde Is.": "Atlantic/Cape_Verde",
+ "Dublin": "Europe/Dublin",
+ "Edinburgh": "Europe/London",
+ "Lisbon": "Europe/Lisbon",
+ "London": "Europe/London",
+ "Casablanca": "Africa/Casablanca",
+ "Monrovia": "Africa/Monrovia",
+ "UTC": "Etc/UTC",
+ "Belgrade": "Europe/Belgrade",
+ "Bratislava": "Europe/Bratislava",
+ "Budapest": "Europe/Budapest",
+ "Ljubljana": "Europe/Ljubljana",
+ "Prague": "Europe/Prague",
+ "Sarajevo": "Europe/Sarajevo",
+ "Skopje": "Europe/Skopje",
+ "Warsaw": "Europe/Warsaw",
+ "Zagreb": "Europe/Zagreb",
+ "Brussels": "Europe/Brussels",
+ "Copenhagen": "Europe/Copenhagen",
+ "Madrid": "Europe/Madrid",
+ "Paris": "Europe/Paris",
+ "Amsterdam": "Europe/Amsterdam",
+ "Berlin": "Europe/Berlin",
+ "Bern": "Europe/Zurich",
+ "Zurich": "Europe/Zurich",
+ "Rome": "Europe/Rome",
+ "Stockholm": "Europe/Stockholm",
+ "Vienna": "Europe/Vienna",
+ "West Central Africa": "Africa/Algiers",
+ "Bucharest": "Europe/Bucharest",
+ "Cairo": "Africa/Cairo",
+ "Helsinki": "Europe/Helsinki",
+ "Kyiv": "Europe/Kiev",
+ "Riga": "Europe/Riga",
+ "Sofia": "Europe/Sofia",
+ "Tallinn": "Europe/Tallinn",
+ "Vilnius": "Europe/Vilnius",
+ "Athens": "Europe/Athens",
+ "Istanbul": "Europe/Istanbul",
+ "Minsk": "Europe/Minsk",
+ "Jerusalem": "Asia/Jerusalem",
+ "Harare": "Africa/Harare",
+ "Pretoria": "Africa/Johannesburg",
+ "Kaliningrad": "Europe/Kaliningrad",
+ "Moscow": "Europe/Moscow",
+ "St. Petersburg": "Europe/Moscow",
+ "Volgograd": "Europe/Volgograd",
+ "Samara": "Europe/Samara",
+ "Kuwait": "Asia/Kuwait",
+ "Riyadh": "Asia/Riyadh",
+ "Nairobi": "Africa/Nairobi",
+ "Baghdad": "Asia/Baghdad",
+ "Tehran": "Asia/Tehran",
+ "Abu Dhabi": "Asia/Muscat",
+ "Muscat": "Asia/Muscat",
+ "Baku": "Asia/Baku",
+ "Tbilisi": "Asia/Tbilisi",
+ "Yerevan": "Asia/Yerevan",
+ "Kabul": "Asia/Kabul",
+ "Ekaterinburg": "Asia/Yekaterinburg",
+ "Islamabad": "Asia/Karachi",
+ "Karachi": "Asia/Karachi",
+ "Tashkent": "Asia/Tashkent",
+ "Chennai": "Asia/Kolkata",
+ "Kolkata": "Asia/Kolkata",
+ "Mumbai": "Asia/Kolkata",
+ "New Delhi": "Asia/Kolkata",
+ "Kathmandu": "Asia/Kathmandu",
+ "Astana": "Asia/Dhaka",
+ "Dhaka": "Asia/Dhaka",
+ "Sri Jayawardenepura": "Asia/Colombo",
+ "Almaty": "Asia/Almaty",
+ "Novosibirsk": "Asia/Novosibirsk",
+ "Rangoon": "Asia/Rangoon",
+ "Bangkok": "Asia/Bangkok",
+ "Hanoi": "Asia/Bangkok",
+ "Jakarta": "Asia/Jakarta",
+ "Krasnoyarsk": "Asia/Krasnoyarsk",
+ "Beijing": "Asia/Shanghai",
+ "Chongqing": "Asia/Chongqing",
+ "Hong Kong": "Asia/Hong_Kong",
+ "Urumqi": "Asia/Urumqi",
+ "Kuala Lumpur": "Asia/Kuala_Lumpur",
+ "Singapore": "Asia/Singapore",
+ "Taipei": "Asia/Taipei",
+ "Perth": "Australia/Perth",
+ "Irkutsk": "Asia/Irkutsk",
+ "Ulaanbaatar": "Asia/Ulaanbaatar",
+ "Seoul": "Asia/Seoul",
+ "Osaka": "Asia/Tokyo",
+ "Sapporo": "Asia/Tokyo",
+ "Tokyo": "Asia/Tokyo",
+ "Yakutsk": "Asia/Yakutsk",
+ "Darwin": "Australia/Darwin",
+ "Adelaide": "Australia/Adelaide",
+ "Canberra": "Australia/Melbourne",
+ "Melbourne": "Australia/Melbourne",
+ "Sydney": "Australia/Sydney",
+ "Brisbane": "Australia/Brisbane",
+ "Hobart": "Australia/Hobart",
+ "Vladivostok": "Asia/Vladivostok",
+ "Guam": "Pacific/Guam",
+ "Port Moresby": "Pacific/Port_Moresby",
+ "Magadan": "Asia/Magadan",
+ "Srednekolymsk": "Asia/Srednekolymsk",
+ "Solomon Is.": "Pacific/Guadalcanal",
+ "New Caledonia": "Pacific/Noumea",
+ "Fiji": "Pacific/Fiji",
+ "Kamchatka": "Asia/Kamchatka",
+ "Marshall Is.": "Pacific/Majuro",
+ "Auckland": "Pacific/Auckland",
+ "Wellington": "Pacific/Auckland",
+ "Nuku'alofa": "Pacific/Tongatapu",
+ "Tokelau Is.": "Pacific/Fakaofo",
+ "Chatham Is.": "Pacific/Chatham",
+ "Samoa": "Pacific/Apia"
+}
diff --git a/app/models/concerns/out_of_offisable.rb b/app/models/concerns/out_of_offisable.rb
index 72d1e0467..53b466cdf 100644
--- a/app/models/concerns/out_of_offisable.rb
+++ b/app/models/concerns/out_of_offisable.rb
@@ -19,7 +19,7 @@ module OutOfOffisable
end
def weekly_schedule
- working_hours.select(*OFFISABLE_ATTRS).as_json(except: :id)
+ working_hours.order(day_of_week: :asc).select(*OFFISABLE_ATTRS).as_json(except: :id)
end
# accepts an array of hashes similiar to the format of weekly_schedule