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",
|
||||
"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",
|
||||
|
|
|
@ -254,6 +254,9 @@
|
|||
<div v-if="selectedTabKey === 'preChatForm'">
|
||||
<pre-chat-form-settings :inbox="inbox" />
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'businesshours'">
|
||||
<weekly-availability :inbox="inbox" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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'),
|
||||
|
|
|
@ -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) {
|
||||
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 {
|
||||
|
|
|
@ -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) => {
|
||||
/*
|
||||
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],
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue