Feat: business hours settings page (#1812)
Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
dbf515ab5a
commit
1d2e1a2823
8 changed files with 577 additions and 11 deletions
|
@ -274,6 +274,12 @@
|
||||||
"TITLE": "Set your availability",
|
"TITLE": "Set your availability",
|
||||||
"SUBTITLE": "Set your availability on your livechat widget",
|
"SUBTITLE": "Set your availability on your livechat widget",
|
||||||
"WEEKLY_TITLE": "Set your weekly hours",
|
"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": {
|
"DAY": {
|
||||||
"ENABLE": "Enable availability for this day",
|
"ENABLE": "Enable availability for this day",
|
||||||
"UNAVAILABLE": "Unavailable",
|
"UNAVAILABLE": "Unavailable",
|
||||||
|
|
|
@ -254,6 +254,9 @@
|
||||||
<div v-if="selectedTabKey === 'preChatForm'">
|
<div v-if="selectedTabKey === 'preChatForm'">
|
||||||
<pre-chat-form-settings :inbox="inbox" />
|
<pre-chat-form-settings :inbox="inbox" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedTabKey === 'businesshours'">
|
||||||
|
<weekly-availability :inbox="inbox" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -266,12 +269,14 @@ import SettingsSection from '../../../../components/SettingsSection';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import FacebookReauthorize from './facebook/Reauthorize';
|
import FacebookReauthorize from './facebook/Reauthorize';
|
||||||
import PreChatFormSettings from './PreChatForm/Settings';
|
import PreChatFormSettings from './PreChatForm/Settings';
|
||||||
|
import WeeklyAvailability from './components/WeeklyAvailability';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
FacebookReauthorize,
|
FacebookReauthorize,
|
||||||
PreChatFormSettings,
|
PreChatFormSettings,
|
||||||
|
WeeklyAvailability,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, configMixin, inboxMixin],
|
mixins: [alertMixin, configMixin, inboxMixin],
|
||||||
data() {
|
data() {
|
||||||
|
@ -329,6 +334,10 @@ export default {
|
||||||
key: 'preChatForm',
|
key: 'preChatForm',
|
||||||
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),
|
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'businesshours',
|
||||||
|
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'configuration',
|
key: 'configuration',
|
||||||
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
|
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
|
||||||
|
|
|
@ -92,11 +92,13 @@ export default {
|
||||||
...this.timeSlot,
|
...this.timeSlot,
|
||||||
from: timeSlots[0],
|
from: timeSlots[0],
|
||||||
to: timeSlots[16],
|
to: timeSlots[16],
|
||||||
|
valid: true,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
...this.timeSlot,
|
...this.timeSlot,
|
||||||
from: '',
|
from: '',
|
||||||
to: '',
|
to: '',
|
||||||
|
valid: false,
|
||||||
};
|
};
|
||||||
this.$emit('update', newSlot);
|
this.$emit('update', newSlot);
|
||||||
},
|
},
|
||||||
|
@ -106,9 +108,12 @@ export default {
|
||||||
return this.timeSlot.from;
|
return this.timeSlot.from;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
|
const fromDate = parse(value, 'hh:mm a', new Date());
|
||||||
|
const valid = differenceInMinutes(this.toDate, fromDate) / 60 > 0;
|
||||||
this.$emit('update', {
|
this.$emit('update', {
|
||||||
...this.timeSlot,
|
...this.timeSlot,
|
||||||
from: value,
|
from: value,
|
||||||
|
valid,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -117,10 +122,21 @@ export default {
|
||||||
return this.timeSlot.to;
|
return this.timeSlot.to;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$emit('update', {
|
const toDate = parse(value, 'hh:mm a', new Date());
|
||||||
...this.timeSlot,
|
if (value === '12:00 AM') {
|
||||||
to: value,
|
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() {
|
fromDate() {
|
||||||
|
@ -130,10 +146,14 @@ export default {
|
||||||
return parse(this.toTime, 'hh:mm a', new Date());
|
return parse(this.toTime, 'hh:mm a', new Date());
|
||||||
},
|
},
|
||||||
totalHours() {
|
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() {
|
hasError() {
|
||||||
return this.totalHours < 0;
|
return !this.timeSlot.valid;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -157,8 +177,8 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: var(--space-normal);
|
padding: var(--space-small) 0;
|
||||||
height: var(--space-larger);
|
min-height: var(--space-larger);
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
|
@ -213,7 +233,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-error {
|
.date-error {
|
||||||
padding: var(--space-small) 0;
|
padding-top: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
<template>
|
||||||
|
<div class="settings--content">
|
||||||
|
<settings-section
|
||||||
|
:title="$t('INBOX_MGMT.BUSINESS_HOURS.TITLE')"
|
||||||
|
:sub-title="$t('INBOX_MGMT.BUSINESS_HOURS.SUBTITLE')"
|
||||||
|
>
|
||||||
|
<form @submit.prevent="updateInbox">
|
||||||
|
<label for="toggle-business-hours" class="toggle-input-wrap">
|
||||||
|
<input
|
||||||
|
v-model="isBusinessHoursEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
name="toggle-business-hours"
|
||||||
|
/>
|
||||||
|
{{ $t('INBOX_MGMT.BUSINESS_HOURS.TOGGLE_AVAILABILITY') }}
|
||||||
|
</label>
|
||||||
|
<p>{{ $t('INBOX_MGMT.BUSINESS_HOURS.TOGGLE_HELP') }}</p>
|
||||||
|
<div v-if="isBusinessHoursEnabled" class="business-hours-wrap">
|
||||||
|
<label class="unavailable-input-wrap">
|
||||||
|
{{ $t('INBOX_MGMT.BUSINESS_HOURS.UNAVAILABLE_MESSAGE_LABEL') }}
|
||||||
|
<textarea v-model="unavailableMessage" type="text" />
|
||||||
|
</label>
|
||||||
|
<div class="timezone-input-wrap">
|
||||||
|
<label>
|
||||||
|
{{ $t('INBOX_MGMT.BUSINESS_HOURS.TIMEZONE_LABEL') }}
|
||||||
|
</label>
|
||||||
|
<multiselect
|
||||||
|
v-model="timeZone"
|
||||||
|
:options="timeZones"
|
||||||
|
deselect-label=""
|
||||||
|
select-label=""
|
||||||
|
selected-label=""
|
||||||
|
track-by="value"
|
||||||
|
label="label"
|
||||||
|
:close-on-select="true"
|
||||||
|
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
||||||
|
:allow-empty="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{ $t('INBOX_MGMT.BUSINESS_HOURS.WEEKLY_TITLE') }}
|
||||||
|
</label>
|
||||||
|
<business-day
|
||||||
|
v-for="timeSlot in timeSlots"
|
||||||
|
:key="timeSlot.day"
|
||||||
|
:day-name="dayNames[timeSlot.day]"
|
||||||
|
:time-slot="timeSlot"
|
||||||
|
@update="data => onSlotUpdate(timeSlot.day, data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-submit-button
|
||||||
|
:button-text="$t('INBOX_MGMT.BUSINESS_HOURS.UPDATE')"
|
||||||
|
:loading="uiFlags.isUpdatingInbox"
|
||||||
|
:disabled="hasError"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</settings-section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import SettingsSection from 'dashboard/components/SettingsSection';
|
||||||
|
import BusinessDay from './BusinessDay';
|
||||||
|
import {
|
||||||
|
timeSlotParse,
|
||||||
|
timeSlotTransform,
|
||||||
|
defaultTimeSlot,
|
||||||
|
timeZoneOptions,
|
||||||
|
} from '../helpers/businessHour';
|
||||||
|
|
||||||
|
const DEFAULT_TIMEZONE = {
|
||||||
|
label: 'America/Los_Angeles',
|
||||||
|
key: 'America/Los_Angeles',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SettingsSection,
|
||||||
|
BusinessDay,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
inbox: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isBusinessHoursEnabled: false,
|
||||||
|
unavailableMessage: this.$t(
|
||||||
|
'INBOX_MGMT.BUSINESS_HOURS.UNAVAILABLE_MESSAGE_DEFAULT'
|
||||||
|
),
|
||||||
|
timeZone: DEFAULT_TIMEZONE,
|
||||||
|
dayNames: {
|
||||||
|
0: 'Sunday',
|
||||||
|
1: 'Monday',
|
||||||
|
2: 'Tuesday',
|
||||||
|
3: 'Wednesday',
|
||||||
|
4: 'Thursday',
|
||||||
|
5: 'Friday',
|
||||||
|
6: 'Saturday',
|
||||||
|
},
|
||||||
|
timeSlots: [...defaultTimeSlot],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||||
|
hasError() {
|
||||||
|
if (!this.isBusinessHoursEnabled) return false;
|
||||||
|
return this.timeSlots.filter(slot => slot.from && !slot.valid).length > 0;
|
||||||
|
},
|
||||||
|
timeZones() {
|
||||||
|
return [...timeZoneOptions()];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
inbox() {
|
||||||
|
this.setDefaults();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setDefaults();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setDefaults() {
|
||||||
|
const {
|
||||||
|
working_hours_enabled: isEnabled = false,
|
||||||
|
out_of_office_message: unavailableMessage,
|
||||||
|
working_hours: timeSlots = [],
|
||||||
|
timezone: timeZone,
|
||||||
|
} = this.inbox;
|
||||||
|
const slots = timeSlotParse(timeSlots).length
|
||||||
|
? timeSlotParse(timeSlots)
|
||||||
|
: defaultTimeSlot;
|
||||||
|
this.isBusinessHoursEnabled = isEnabled;
|
||||||
|
this.unavailableMessage =
|
||||||
|
unavailableMessage ||
|
||||||
|
this.$t('INBOX_MGMT.BUSINESS_HOURS.UNAVAILABLE_MESSAGE_DEFAULT');
|
||||||
|
this.timeSlots = slots;
|
||||||
|
this.timeZone =
|
||||||
|
this.timeZones.find(item => timeZone === item.value) ||
|
||||||
|
DEFAULT_TIMEZONE;
|
||||||
|
},
|
||||||
|
onSlotUpdate(slotIndex, slotData) {
|
||||||
|
this.timeSlots = this.timeSlots.map(item =>
|
||||||
|
item.day === slotIndex ? slotData : item
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async updateInbox() {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: this.inbox.id,
|
||||||
|
formData: false,
|
||||||
|
working_hours_enabled: this.isBusinessHoursEnabled,
|
||||||
|
out_of_office_message: this.unavailableMessage,
|
||||||
|
working_hours: timeSlotTransform(this.timeSlots),
|
||||||
|
timezone: this.timeZone.value,
|
||||||
|
channel: {},
|
||||||
|
};
|
||||||
|
await this.$store.dispatch('inboxes/updateInbox', payload);
|
||||||
|
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.timezone-input-wrap {
|
||||||
|
max-width: 60rem;
|
||||||
|
|
||||||
|
&::v-deep .multiselect {
|
||||||
|
margin-top: var(--space-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable-input-wrap {
|
||||||
|
max-width: 60rem;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: var(--space-jumbo);
|
||||||
|
margin-top: var(--space-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-hours-wrap {
|
||||||
|
margin-bottom: var(--space-medium);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -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) => {
|
export const generateTimeSlots = (step = 15) => {
|
||||||
/*
|
/*
|
||||||
Generates a list of time strings from 12:00 AM to next 24 hours. Each new string
|
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;
|
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],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { generateTimeSlots } from '../businessHour';
|
import {
|
||||||
|
generateTimeSlots,
|
||||||
|
getTime,
|
||||||
|
timeSlotParse,
|
||||||
|
timeSlotTransform,
|
||||||
|
timeZoneOptions,
|
||||||
|
} from '../businessHour';
|
||||||
|
|
||||||
describe('#generateTimeSlots', () => {
|
describe('#generateTimeSlots', () => {
|
||||||
it('returns correct number of time slots', () => {
|
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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ module OutOfOffisable
|
||||||
end
|
end
|
||||||
|
|
||||||
def weekly_schedule
|
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
|
end
|
||||||
|
|
||||||
# accepts an array of hashes similiar to the format of weekly_schedule
|
# accepts an array of hashes similiar to the format of weekly_schedule
|
||||||
|
|
Loading…
Reference in a new issue