diff --git a/Gemfile b/Gemfile index a3b3c0ae8..615267a77 100644 --- a/Gemfile +++ b/Gemfile @@ -89,7 +89,7 @@ gem 'wisper', '2.0.0' ##--- gems for channels ---## gem 'facebook-messenger' gem 'line-bot-api' -gem 'twilio-ruby', '~> 5.66' +gem 'twilio-ruby' # twitty will handle subscription of twitter account events # gem 'twitty', git: 'https://github.com/chatwoot/twitty' gem 'twitty', '~> 0.1.5' @@ -212,6 +212,8 @@ group :development do gem 'stackprof' # Should install the associated chrome extension to view query logs gem 'meta_request', '>= 0.8.3' + + gem 'tidewave' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index be258f524..8fa38a31e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -230,6 +230,35 @@ GEM addressable (~> 2.8) drb (2.2.3) dry-cli (1.1.0) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) ecma-re-validator (0.4.0) regexp_parser (~> 2.2) elastic-apm (4.6.2) @@ -252,8 +281,10 @@ GEM railties (>= 5.0.0) faker (3.2.0) i18n (>= 1.8.11, < 2) - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) + faraday (2.13.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) faraday-mashify (0.1.1) @@ -261,13 +292,20 @@ GEM hashie faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.1.0) - net-http + faraday-net_http (3.4.0) + net-http (>= 0.5.0) faraday-net_http_persistent (2.1.0) faraday (~> 2.5) net-http-persistent (~> 4.0) faraday-retry (2.2.1) faraday (~> 2.0) + fast-mcp (1.5.0) + addressable (~> 2.8) + base64 + dry-schema (~> 1.14) + json (~> 2.0) + mime-types (~> 3.4) + rack (~> 3.1) fcm (1.0.8) faraday (>= 1.0.0, < 3.0) googleauth (~> 1) @@ -421,7 +459,7 @@ GEM judoscale-sidekiq (1.8.2) judoscale-ruby (= 1.8.2) sidekiq (>= 5.0) - jwt (2.8.1) + jwt (2.10.1) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -503,7 +541,7 @@ GEM mutex_m (0.3.0) neighbor (0.2.3) activerecord (>= 5.2) - net-http (0.4.1) + net-http (0.6.0) uri net-http-persistent (4.0.2) connection_pool (~> 2.2) @@ -591,7 +629,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.15) + rack (3.2.0) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -600,19 +638,20 @@ GEM rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) - rack-protection (3.2.0) + rack-protection (4.1.1) base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack - rack-session (1.0.2) - rack (< 3) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rackup (1.0.1) - rack (< 3) - webrick + rackup (2.2.1) + rack (>= 3) rails (7.1.5.2) actioncable (= 7.1.5.2) actionmailbox (= 7.1.5.2) @@ -843,13 +882,17 @@ GEM telephone_number (1.4.20) test-prof (1.2.1) thor (1.4.0) + tidewave (0.2.0) + fast-mcp (~> 1.5.0) + rack (>= 2.0) + rails (>= 7.1.0) tilt (2.3.0) time_diff (0.3.0) activesupport i18n timeout (0.4.3) trailblazer-option (0.1.2) - twilio-ruby (5.77.0) + twilio-ruby (7.6.0) faraday (>= 0.9, < 3.0) jwt (>= 1.5, < 3.0) nokogiri (>= 1.6, < 2.0) @@ -896,7 +939,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.9.1) websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) @@ -1040,8 +1082,9 @@ DEPENDENCIES stripe telephone_number test-prof + tidewave time_diff - twilio-ruby (~> 5.66) + twilio-ruby twitty (~> 0.1.5) tzinfo-data uglifier diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 4d9d11cf5..6cb9d3dd0 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -3.4.2 +3.4.3 diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 78b4b9e2f..4750e3b4a 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -70,11 +70,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def sync_templates - unless @inbox.channel.is_a?(Channel::Whatsapp) - return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } - end + return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel? - Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel) + trigger_template_sync render status: :ok, json: { message: 'Template sync initiated successfully' } rescue StandardError => e render status: :internal_server_error, json: { error: e.message } @@ -185,6 +183,18 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController [] end end + + def whatsapp_channel? + @inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?) + end + + def trigger_template_sync + if @inbox.whatsapp? + Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel) + elsif @inbox.twilio? && @inbox.channel.whatsapp? + Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel) + end + end end Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController') diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue deleted file mode 100644 index ce1520f79..000000000 --- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - {{ - t( - 'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.TEMPLATE_NAME', - { templateName: templateName } - ) - }} - - - - - {{ - t( - 'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.VARIABLES' - ) - }} - - - - - {{ key }} - - - - - - - - - - diff --git a/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue b/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue index 63b5f8eef..07c6167f7 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue @@ -41,9 +41,7 @@ const originalEmailText = computed(() => { }); const originalEmailHtml = computed( - () => - contentAttributes?.value?.email?.htmlContent?.full ?? - originalEmailText.value + () => contentAttributes?.value?.email?.htmlContent?.full || '' ); const messageContent = computed(() => { diff --git a/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue index aa28bcdd3..cf62a6d5d 100644 --- a/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue +++ b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue @@ -1,4 +1,13 @@ @@ -142,7 +210,7 @@ export default { @@ -155,12 +223,58 @@ export default { /> - + + + {{ $t('INBOX_MGMT.FINISH.WHATSAPP_QR_INSTRUCTION') }} + + + + + + + + {{ $t('INBOX_MGMT.FINISH.MESSENGER_QR_INSTRUCTION') }} + + + + + + + + {{ $t('INBOX_MGMT.FINISH.TELEGRAM_QR_INSTRUCTION') }} + + + + + + + + {{ $t('INBOX_MGMT.HELP_CENTER.LABEL') }} diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue index e19ad23bb..7559f7c3e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue @@ -22,7 +22,6 @@ const state = reactive({ authToken: '', apiKeySid: '', apiKeySecret: '', - twimlAppSid: '', }); const uiFlags = useMapGetter('inboxes/getUIFlags'); @@ -33,7 +32,6 @@ const validationRules = { authToken: { required }, apiKeySid: { required }, apiKeySecret: { required }, - twimlAppSid: { required }, }; const v$ = useVuelidate(validationRules, state); @@ -55,9 +53,6 @@ const formErrors = computed(() => ({ apiKeySecret: v$.value.apiKeySecret?.$error ? t('INBOX_MGMT.ADD.VOICE.TWILIO.API_KEY_SECRET.REQUIRED') : '', - twimlAppSid: v$.value.twimlAppSid?.$error - ? t('INBOX_MGMT.ADD.VOICE.TWILIO.TWIML_APP_SID.REQUIRED') - : '', })); function getProviderConfig() { @@ -67,7 +62,6 @@ function getProviderConfig() { api_key_sid: state.apiKeySid, api_key_secret: state.apiKeySecret, }; - if (state.twimlAppSid) config.outgoing_application_sid = state.twimlAppSid; return config; } @@ -160,17 +154,6 @@ async function createChannel() { @blur="v$.apiKeySecret?.$touch" /> - - + + + + + + + + + { + 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 }) => { diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 25f90912d..3ab1ba1f5 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -50,35 +50,11 @@ const updateCampaignReadStatus = baseDomain => { }); }; -const sanitizeURL = url => { - if (url === '') return ''; - - try { - // any invalid url will not be accepted - // example - JaVaScRiP%0at:alert(document.domain)" - // this has an obfuscated javascript protocol - const parsedURL = new URL(url); - - // filter out dangerous protocols like `javascript`, `data`, `vbscript` - if (!['https', 'http'].includes(parsedURL.protocol)) { - throw new Error('Invalid Protocol'); - } - } catch (e) { - // eslint-disable-next-line no-console - console.error('Invalid URL', e); - } - - return 'about:blank'; // blank page URL -}; - export const IFrameHelper = { getUrl({ baseUrl, websiteToken }) { - baseUrl = sanitizeURL(baseUrl); return `${baseUrl}/widget?website_token=${websiteToken}`; }, createFrame: ({ baseUrl, websiteToken }) => { - baseUrl = sanitizeURL(baseUrl); - if (IFrameHelper.getAppFrame()) { return; } @@ -126,12 +102,10 @@ export const IFrameHelper = { window.onmessage = e => { if ( typeof e.data !== 'string' || - e.data.indexOf('chatwoot-widget:') !== 0 || - e.origin !== window.location.origin + e.data.indexOf('chatwoot-widget:') !== 0 ) { return; } - const message = JSON.parse(e.data.replace('chatwoot-widget:', '')); if (typeof IFrameHelper.events[message.event] === 'function') { IFrameHelper.events[message.event](message); @@ -166,9 +140,7 @@ export const IFrameHelper = { }, setupAudioListeners: () => { - let { baseUrl = '' } = window.$chatwoot; - baseUrl = sanitizeURL(baseUrl); - + const { baseUrl = '' } = window.$chatwoot; getAlertAudio(baseUrl, { type: 'widget', alertTone: 'ding' }).then(() => initOnEvents.forEach(event => { document.removeEventListener( @@ -262,7 +234,6 @@ export const IFrameHelper = { }, popoutChatWindow: ({ baseUrl, websiteToken, locale }) => { - baseUrl = sanitizeURL(baseUrl); const cwCookie = Cookies.get('cw_conversation'); window.$chatwoot.toggle('close'); popoutChatWindow(baseUrl, websiteToken, locale, cwCookie); diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index 7b7b4f331..d87db470d 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -36,7 +36,6 @@ export const CONVERSATION_PRIORITY_ORDER = { // Size in mega bytes export const MAXIMUM_FILE_UPLOAD_SIZE = 40; -export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; export const ALLOWED_FILE_TYPES = 'image/*,' + @@ -50,18 +49,6 @@ export const ALLOWED_FILE_TYPES = 'application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document,'; -export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP = - 'image/png, image/jpeg,' + - 'audio/mpeg, audio/opus, audio/ogg, audio/amr,' + - 'video/mp4,' + - 'application/pdf,'; -// https://developers.line.biz/en/reference/messaging-api/#image-message, https://developers.line.biz/en/reference/messaging-api/#video-message -export const ALLOWED_FILE_TYPES_FOR_LINE = 'image/png, image/jpeg,video/mp4'; - -// https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/messaging-api#requirements -export const ALLOWED_FILE_TYPES_FOR_INSTAGRAM = - 'image/png, image/jpeg, video/mp4, video/mov, video/webm'; - export const CSAT_RATINGS = [ { key: 'disappointed', diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index bf8ec492b..c1f48f6f4 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -57,15 +57,15 @@ export default { isALineChannel() { return this.channelType === INBOX_TYPES.LINE; }, + isAVoiceChannel() { + return this.channelType === INBOX_TYPES.VOICE; + }, isAnEmailChannel() { return this.channelType === INBOX_TYPES.EMAIL; }, isATelegramChannel() { return this.channelType === INBOX_TYPES.TELEGRAM; }, - isAVoiceChannel() { - return this.channelType === INBOX_TYPES.VOICE; - }, isATwilioSMSChannel() { const { medium: medium = '' } = this.inbox; return this.isATwilioChannel && medium === 'sms'; diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 48409cef2..379b2bc53 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -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(); diff --git a/app/javascript/widget/components/Availability/AvailabilityContainer.vue b/app/javascript/widget/components/Availability/AvailabilityContainer.vue new file mode 100644 index 000000000..367564751 --- /dev/null +++ b/app/javascript/widget/components/Availability/AvailabilityContainer.vue @@ -0,0 +1,87 @@ + + + + + + + {{ headerText }} + + + + + + + + diff --git a/app/javascript/widget/components/Availability/AvailabilityText.story.vue b/app/javascript/widget/components/Availability/AvailabilityText.story.vue new file mode 100644 index 000000000..aafcb3556 --- /dev/null +++ b/app/javascript/widget/components/Availability/AvailabilityText.story.vue @@ -0,0 +1,217 @@ + + + + + + + + + diff --git a/app/javascript/widget/components/Availability/AvailabilityText.vue b/app/javascript/widget/components/Availability/AvailabilityText.vue new file mode 100644 index 000000000..649224d20 --- /dev/null +++ b/app/javascript/widget/components/Availability/AvailabilityText.vue @@ -0,0 +1,178 @@ + + + + + + + {{ replyTimeMessage }} + + + + + {{ + isOnline + ? replyTimeMessage + : t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE') + }} + + + + + + {{ t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE') }} + + + + + {{ t('REPLY_TIME.BACK_IN_SOME_TIME') }} + + + + + {{ t('REPLY_TIME.BACK_TOMORROW') }} + + + + + {{ + t('REPLY_TIME.BACK_ON_DAY', { + day: dayNames[nextSlot.config.dayOfWeek], + }) + }} + + + + + {{ + t('REPLY_TIME.BACK_IN_MINUTES', { + time: `${roundedMinutesUntilOpen}`, + }) + }} + + + + + {{ t('REPLY_TIME.BACK_IN_HOURS', adjustedHoursUntilOpen) }} + + + + + {{ + t('REPLY_TIME.BACK_AT_TIME', { + time: formattedOpeningTime, + }) + }} + + + diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index 2bc6ba7ec..c85727a2b 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -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, diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue index 7c497b70f..578fb984e 100644 --- a/app/javascript/widget/components/ChatHeader.vue +++ b/app/javascript/widget/components/ChatHeader.vue @@ -1,55 +1,26 @@ - @@ -79,9 +50,12 @@ export default { ${isOnline ? 'bg-n-teal-10' : 'hidden'}`" /> - - {{ replyWaitMessage }} - + diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 6fe8e45a1..84f0036fd 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -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, diff --git a/app/javascript/widget/components/TeamAvailability.vue b/app/javascript/widget/components/TeamAvailability.vue index 866854c09..cc08d34c2 100644 --- a/app/javascript/widget/components/TeamAvailability.vue +++ b/app/javascript/widget/components/TeamAvailability.vue @@ -1,74 +1,27 @@ - @@ -76,17 +29,8 @@ export default { - - - - {{ headerMessage }} - - - {{ replyWaitMessage }} - - - - + + 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'); + }); + }); +}); diff --git a/app/javascript/widget/composables/useAvailability.js b/app/javascript/widget/composables/useAvailability.js new file mode 100644 index 000000000..621584c01 --- /dev/null +++ b/app/javascript/widget/composables/useAvailability.js @@ -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, + }; +} diff --git a/app/javascript/widget/helpers/availabilityHelpers.js b/app/javascript/widget/helpers/availabilityHelpers.js new file mode 100644 index 000000000..256ebec55 --- /dev/null +++ b/app/javascript/widget/helpers/availabilityHelpers.js @@ -0,0 +1,340 @@ +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} workingHours - Full array of working-hour slot configs. + * @returns {Map} 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} 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} 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; + +/** + * Convert slot details from inbox timezone to user timezone + * @private + * @param {Date} time - Current time + * @param {string} inboxTz - Inbox timezone + * @param {Object} slotDetails - Original slot details + * @returns {Object} Slot details with user timezone adjustments + */ +const convertSlotToUserTimezone = (time, inboxTz, slotDetails) => { + if (!slotDetails) return null; + + const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // If timezones match, no conversion needed + if (inboxTz === userTz) return slotDetails; + + // Calculate when the slot opens (absolute time) + const now = time instanceof Date ? time : new Date(time); + const openingTime = new Date( + now.getTime() + slotDetails.minutesUntilOpen * 60000 + ); + + // Convert to user timezone + const openingInUserTz = getDateInTimezone(openingTime, userTz); + const nowInUserTz = getDateInTimezone(now, userTz); + + // Calculate days difference in user timezone + const openingDate = new Date(openingInUserTz); + openingDate.setHours(0, 0, 0, 0); + const todayDate = new Date(nowInUserTz); + todayDate.setHours(0, 0, 0, 0); + const daysUntilOpen = Math.round((openingDate - todayDate) / 86400000); + + // Return with user timezone adjustments + return { + ...slotDetails, + config: { + ...slotDetails.config, + openHour: openingInUserTz.getHours(), + openMinutes: openingInUserTz.getMinutes(), + dayOfWeek: openingInUserTz.getDay(), + }, + daysUntilOpen, + dayOfWeek: openingInUserTz.getDay(), + }; +}; + +// --------------------------------------------------------------------------- +// 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 + * Returns times adjusted to user's timezone for display + * @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 + ); + + // Find the slot (today or next) + const slotDetails = + todaySlot || findNextSlot(currentDay, currentMinutes, openDays); + + // Convert to user timezone for display + return convertSlotToUserTimezone(time, utcOffset, slotDetails); +}; + +/** + * 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; +}; diff --git a/app/javascript/widget/helpers/specs/availabilityHelpers.spec.js b/app/javascript/widget/helpers/specs/availabilityHelpers.spec.js new file mode 100644 index 000000000..867e30dcc --- /dev/null +++ b/app/javascript/widget/helpers/specs/availabilityHelpers.spec.js @@ -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); + }); + }); +}); diff --git a/app/javascript/widget/i18n/locale/am.json b/app/javascript/widget/i18n/locale/am.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/am.json +++ b/app/javascript/widget/i18n/locale/am.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/ar.json b/app/javascript/widget/i18n/locale/ar.json index 21b1c072e..110fadcd1 100644 --- a/app/javascript/widget/i18n/locale/ar.json +++ b/app/javascript/widget/i18n/locale/ar.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "متواجدون لخدمتك", - "OFFLINE": "نحن بعيدون في الوقت الحالي" + "OFFLINE": "نحن بعيدون في الوقت الحالي", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "عادة نقوم بالرد خلال بضع دقائق", "IN_A_FEW_HOURS": "عادة نقوم بالرد خلال بضع ساعات", "IN_A_DAY": "عادة نقوم بالرد خلال يوم واحد", - "BACK_IN": "سوف نعود إلى الإنترنت" + "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", diff --git a/app/javascript/widget/i18n/locale/az.json b/app/javascript/widget/i18n/locale/az.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/az.json +++ b/app/javascript/widget/i18n/locale/az.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/bg.json b/app/javascript/widget/i18n/locale/bg.json index 1fd4bc35d..c26a30565 100644 --- a/app/javascript/widget/i18n/locale/bg.json +++ b/app/javascript/widget/i18n/locale/bg.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "На линия сме", - "OFFLINE": "В момента не сме на линия" + "OFFLINE": "В момента не сме на линия", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Обикновено отговаряме до няколко минути", "IN_A_FEW_HOURS": "Обикновено отговаряме до няколко часа", "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", diff --git a/app/javascript/widget/i18n/locale/ca.json b/app/javascript/widget/i18n/locale/ca.json index 6ae69fcb6..ce3b99da7 100644 --- a/app/javascript/widget/i18n/locale/ca.json +++ b/app/javascript/widget/i18n/locale/ca.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Estem en línia", - "OFFLINE": "Estem fora en aquest moment" + "OFFLINE": "Estem fora en aquest moment", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Normalment responem en pocs minuts", "IN_A_FEW_HOURS": "Normalment responem en poques hores", "IN_A_DAY": "Normalment respon en un dia", - "BACK_IN": "Tornarem a estar en línia" + "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": "Diumenge", diff --git a/app/javascript/widget/i18n/locale/cs.json b/app/javascript/widget/i18n/locale/cs.json index adefb1eb4..4e7234d59 100644 --- a/app/javascript/widget/i18n/locale/cs.json +++ b/app/javascript/widget/i18n/locale/cs.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Jsme online", - "OFFLINE": "V současné době jsme pryč" + "OFFLINE": "V současné době jsme pryč", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Většinou odpovíme během pár minut", "IN_A_FEW_HOURS": "Většinou odpovíme během pár hodin", "IN_A_DAY": "Obvykle odpoví za den", - "BACK_IN": "Budem online zase" + "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": "Neděle", diff --git a/app/javascript/widget/i18n/locale/da.json b/app/javascript/widget/i18n/locale/da.json index 31303d4d5..ef7adfc51 100644 --- a/app/javascript/widget/i18n/locale/da.json +++ b/app/javascript/widget/i18n/locale/da.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Vi er online", - "OFFLINE": "Vi er ikke tilgængelige i øjeblikket" + "OFFLINE": "Vi er ikke tilgængelige i øjeblikket", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Svarer typisk på et par minutter", "IN_A_FEW_HOURS": "Svarer typisk på et par timer", "IN_A_DAY": "Svarer typisk på en dag", - "BACK_IN": "Vi er tilbage 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": "søndag", diff --git a/app/javascript/widget/i18n/locale/de.json b/app/javascript/widget/i18n/locale/de.json index b7490c368..fd6de1f2e 100644 --- a/app/javascript/widget/i18n/locale/de.json +++ b/app/javascript/widget/i18n/locale/de.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Wir sind online", - "OFFLINE": "Wir sind momentan abwesend" + "OFFLINE": "Wir sind momentan abwesend", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Wir antworten üblicherweise innerhalb weniger Minuten", "IN_A_FEW_HOURS": "Wir antworten üblicherweise innerhalb weniger Stunden", "IN_A_DAY": "Wir antworten üblicherweise innerhalb eines Tages", - "BACK_IN": "Wir werden wieder online sein" + "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": "Sonntag", diff --git a/app/javascript/widget/i18n/locale/el.json b/app/javascript/widget/i18n/locale/el.json index 1cb35a76b..64bb71e74 100644 --- a/app/javascript/widget/i18n/locale/el.json +++ b/app/javascript/widget/i18n/locale/el.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Είμαστε online", - "OFFLINE": "Προς το παρόν, είμαστε εκτός" + "OFFLINE": "Προς το παρόν, είμαστε εκτός", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Τυπικά έχετε απάντηση σε μερικά λεπτά", "IN_A_FEW_HOURS": "Τυπικά έχετε απάντηση σε μερικές σε μερικές ώρες", "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", diff --git a/app/javascript/widget/i18n/locale/en.json b/app/javascript/widget/i18n/locale/en.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/en.json +++ b/app/javascript/widget/i18n/locale/en.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/es.json b/app/javascript/widget/i18n/locale/es.json index 9c807ed84..7227ab056 100644 --- a/app/javascript/widget/i18n/locale/es.json +++ b/app/javascript/widget/i18n/locale/es.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Estamos en línea", - "OFFLINE": "Estamos ausentes en este momento" + "OFFLINE": "Estamos ausentes en este momento", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Normalmente responde en unos minutos", "IN_A_FEW_HOURS": "Normalmente responde en unas pocas horas", "IN_A_DAY": "Normalmente responde en un día", - "BACK_IN": "Volveremos a estar en línea" + "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": "Domingo", diff --git a/app/javascript/widget/i18n/locale/fa.json b/app/javascript/widget/i18n/locale/fa.json index 982df19ac..9fdbd0a38 100644 --- a/app/javascript/widget/i18n/locale/fa.json +++ b/app/javascript/widget/i18n/locale/fa.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "ما آنلاین هستیم", - "OFFLINE": "در حال حاضر دردسترس نیستیم" + "OFFLINE": "در حال حاضر دردسترس نیستیم", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ می دهند", "IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند", "IN_A_DAY": "به طور معمول در یک روز پاسخ می دهند", - "BACK_IN": "دوباره آنلاین خواهیم شد" + "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": "یکشنبه", diff --git a/app/javascript/widget/i18n/locale/fi.json b/app/javascript/widget/i18n/locale/fi.json index a5d742303..b11170020 100644 --- a/app/javascript/widget/i18n/locale/fi.json +++ b/app/javascript/widget/i18n/locale/fi.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Olemme online-tilassa", - "OFFLINE": "Olemme tällä hetkellä poissa" + "OFFLINE": "Olemme tällä hetkellä poissa", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Vastaa tyypillisesti muutamassa minuutissa", "IN_A_FEW_HOURS": "Tyypillisesti vastaa muutaman tunnin kuluessa", "IN_A_DAY": "Vastaa tyypillisesti päivässä", - "BACK_IN": "Tulemme olemaan takaisin linjoilla" + "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": "Sunnuntai", diff --git a/app/javascript/widget/i18n/locale/fr.json b/app/javascript/widget/i18n/locale/fr.json index ff25bbd45..578d0217f 100644 --- a/app/javascript/widget/i18n/locale/fr.json +++ b/app/javascript/widget/i18n/locale/fr.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Nous sommes en ligne", - "OFFLINE": "Nous sommes absents pour le moment" + "OFFLINE": "Nous sommes absents pour le moment", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Répond généralement en quelques minutes", "IN_A_FEW_HOURS": "Répond généralement en quelques heures", "IN_A_DAY": "Répond généralement dans la journée", - "BACK_IN": "Nous serons de retour en ligne" + "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": "Dimanche", diff --git a/app/javascript/widget/i18n/locale/he.json b/app/javascript/widget/i18n/locale/he.json index 1eaa103db..5d10f0a8b 100644 --- a/app/javascript/widget/i18n/locale/he.json +++ b/app/javascript/widget/i18n/locale/he.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "אנחנו אונליין", - "OFFLINE": "אנחנו לא זמינים כרגע" + "OFFLINE": "אנחנו לא זמינים כרגע", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "זמן מענה טיפוסי תוך כמה דקות", "IN_A_FEW_HOURS": "מענה ממוצע תוך כמה שעות", "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": "יום ראשון", diff --git a/app/javascript/widget/i18n/locale/hi.json b/app/javascript/widget/i18n/locale/hi.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/hi.json +++ b/app/javascript/widget/i18n/locale/hi.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/hr.json b/app/javascript/widget/i18n/locale/hr.json index 37cf7b53e..1ae76f16f 100644 --- a/app/javascript/widget/i18n/locale/hr.json +++ b/app/javascript/widget/i18n/locale/hr.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Online smo", - "OFFLINE": "Trenutačno nismo online" + "OFFLINE": "Trenutačno nismo online", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Obično odgovara za nekoliko minuta", "IN_A_FEW_HOURS": "Obično odgovara za nekoliko sati", "IN_A_DAY": "Obično odgovara u roku jednog dana", - "BACK_IN": "Bit ćemo ponovno 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": "Nedjelja", diff --git a/app/javascript/widget/i18n/locale/hu.json b/app/javascript/widget/i18n/locale/hu.json index 81c4b6886..5ea7c095a 100644 --- a/app/javascript/widget/i18n/locale/hu.json +++ b/app/javascript/widget/i18n/locale/hu.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Online vagyunk", - "OFFLINE": "Jelenleg nem vagyunk elérhetőek" + "OFFLINE": "Jelenleg nem vagyunk elérhetőek", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Néhány percen belül válaszol", "IN_A_FEW_HOURS": "Néhány órán belül válaszol", "IN_A_DAY": "Általánban egy napon belül válaszol", - "BACK_IN": "Legközelebb online leszünk ekkor:" + "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": "vasárnap", diff --git a/app/javascript/widget/i18n/locale/hy.json b/app/javascript/widget/i18n/locale/hy.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/hy.json +++ b/app/javascript/widget/i18n/locale/hy.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/id.json b/app/javascript/widget/i18n/locale/id.json index e7fbec8ad..59f86956f 100644 --- a/app/javascript/widget/i18n/locale/id.json +++ b/app/javascript/widget/i18n/locale/id.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Kami sedang online", - "OFFLINE": "Kami sedang tidak tersedia saat ini" + "OFFLINE": "Kami sedang tidak tersedia saat ini", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Biasanya membalas dalam beberapa menit", "IN_A_FEW_HOURS": "Biasanya membalas dalam beberapa jam", "IN_A_DAY": "Biasanya membalas dalam sehari", - "BACK_IN": "Kami akan kembali 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": "Minggu", diff --git a/app/javascript/widget/i18n/locale/is.json b/app/javascript/widget/i18n/locale/is.json index b794493f4..0ab53118a 100644 --- a/app/javascript/widget/i18n/locale/is.json +++ b/app/javascript/widget/i18n/locale/is.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Við erum tengd", - "OFFLINE": "Það er enginn við í augnablikinu" + "OFFLINE": "Það er enginn við í augnablikinu", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Svarar iðulega innan nokkura mínútna", "IN_A_FEW_HOURS": "Svarar iðulega innan nokkura klukkustunda", "IN_A_DAY": "Svarar iðulega samdægurs", - "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", diff --git a/app/javascript/widget/i18n/locale/it.json b/app/javascript/widget/i18n/locale/it.json index 7e0385220..13cf4a433 100644 --- a/app/javascript/widget/i18n/locale/it.json +++ b/app/javascript/widget/i18n/locale/it.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Siamo online", - "OFFLINE": "Siamo offline in questo momento" + "OFFLINE": "Siamo offline in questo momento", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "In genere risponde in pochi minuti", "IN_A_FEW_HOURS": "In genere risponde in poche ore", "IN_A_DAY": "In genere risponde in un giorno", - "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", @@ -117,7 +123,7 @@ } }, "PORTAL": { - "POPULAR_ARTICLES": "Popular Articles", + "POPULAR_ARTICLES": "Articoli Popolari", "VIEW_ALL_ARTICLES": "View all articles", "IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again." }, diff --git a/app/javascript/widget/i18n/locale/ja.json b/app/javascript/widget/i18n/locale/ja.json index 17c4beae0..969b5483a 100644 --- a/app/javascript/widget/i18n/locale/ja.json +++ b/app/javascript/widget/i18n/locale/ja.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "オンライン", - "OFFLINE": "留守中" + "OFFLINE": "留守中", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "通常数分以内にご返信します。", "IN_A_FEW_HOURS": "通常数時間以内にご返信します。", "IN_A_DAY": "通常数日以内にご返信します。", - "BACK_IN": "オンラインに戻ります" + "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": "日", diff --git a/app/javascript/widget/i18n/locale/ka.json b/app/javascript/widget/i18n/locale/ka.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/ka.json +++ b/app/javascript/widget/i18n/locale/ka.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/ko.json b/app/javascript/widget/i18n/locale/ko.json index ef8d15a6e..d084e243b 100644 --- a/app/javascript/widget/i18n/locale/ko.json +++ b/app/javascript/widget/i18n/locale/ko.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "온라인", - "OFFLINE": "부재중" + "OFFLINE": "부재중", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "보통 몇 분 안에 응답", "IN_A_FEW_HOURS": "보통 몇 시간 내에 응답", "IN_A_DAY": "보통 하루 안에 응답", - "BACK_IN": "곧 돌아오겠습니다." + "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": "일요일", diff --git a/app/javascript/widget/i18n/locale/lt.json b/app/javascript/widget/i18n/locale/lt.json index fb61b586e..6b8e031bf 100644 --- a/app/javascript/widget/i18n/locale/lt.json +++ b/app/javascript/widget/i18n/locale/lt.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Mes prisijungę", - "OFFLINE": "Šiuo metu esame atsijungę" + "OFFLINE": "Šiuo metu esame atsijungę", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Paprastai atsako per kelias minutes", "IN_A_FEW_HOURS": "Paprastai atsako per kelias valandas", "IN_A_DAY": "Paprastai atsako per dieną", - "BACK_IN": "Mes grįšime" + "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": "Sekmadienis", diff --git a/app/javascript/widget/i18n/locale/lv.json b/app/javascript/widget/i18n/locale/lv.json index 5262e87f7..702fb041f 100644 --- a/app/javascript/widget/i18n/locale/lv.json +++ b/app/javascript/widget/i18n/locale/lv.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Mēs esam tiešsaistē", - "OFFLINE": "Šobrīd mēs neesam uz vietas" + "OFFLINE": "Šobrīd mēs neesam uz vietas", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Parasti atbild pēc dažām minūtēm", "IN_A_FEW_HOURS": "Parasti atbild pēc dažām stundām", "IN_A_DAY": "Parasti atbild vienas dienas laikā", - "BACK_IN": "Mēs atgriezīsimies tiešsaistē" + "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": "Svētdiena", diff --git a/app/javascript/widget/i18n/locale/ml.json b/app/javascript/widget/i18n/locale/ml.json index c4761e99f..96a650cb4 100644 --- a/app/javascript/widget/i18n/locale/ml.json +++ b/app/javascript/widget/i18n/locale/ml.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "ഞങ്ങൾ ഓൺലൈനിലാണ്", - "OFFLINE": "ഞങ്ങൾ ഇപ്പോൾ അകലെയാണ്" + "OFFLINE": "ഞങ്ങൾ ഇപ്പോൾ അകലെയാണ്", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "സാധാരണയായി കുറച്ച് മിനിറ്റിനുള്ളിൽ മറുപടി നൽകുന്നു", "IN_A_FEW_HOURS": "സാധാരണയായി കുറച്ച് മണിക്കൂറിനുള്ളിൽ മറുപടി നൽകുന്നു", "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", diff --git a/app/javascript/widget/i18n/locale/ms.json b/app/javascript/widget/i18n/locale/ms.json index 4dcbc5f57..d813e4eb1 100644 --- a/app/javascript/widget/i18n/locale/ms.json +++ b/app/javascript/widget/i18n/locale/ms.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/ne.json b/app/javascript/widget/i18n/locale/ne.json index 5201951e5..20120afd9 100644 --- a/app/javascript/widget/i18n/locale/ne.json +++ b/app/javascript/widget/i18n/locale/ne.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "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": "धेरै जसो केहि मिनेटमा जवाफ हुन्छ", "IN_A_FEW_HOURS": "धेरै जसो केहि घण्टामा जवाफ हुन्छ", "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", diff --git a/app/javascript/widget/i18n/locale/nl.json b/app/javascript/widget/i18n/locale/nl.json index a33be5f90..0dd99b863 100644 --- a/app/javascript/widget/i18n/locale/nl.json +++ b/app/javascript/widget/i18n/locale/nl.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "We zijn online", - "OFFLINE": "We zijn momenteel afwezig" + "OFFLINE": "We zijn momenteel afwezig", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Reageert meestal binnen een paar minuten", "IN_A_FEW_HOURS": "Reageert meestal binnen een paar uur", "IN_A_DAY": "Reageert meestal binnen een dag", - "BACK_IN": "We zullen weer online zijn" + "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": "zondag", diff --git a/app/javascript/widget/i18n/locale/no.json b/app/javascript/widget/i18n/locale/no.json index fa2c5a2b2..9034df45b 100644 --- a/app/javascript/widget/i18n/locale/no.json +++ b/app/javascript/widget/i18n/locale/no.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Vi er pålogget", - "OFFLINE": "Vi er for øyeblikket borte" + "OFFLINE": "Vi er for øyeblikket borte", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Svarer vanligvis innen et par timer", "IN_A_FEW_HOURS": "Svarer vanligvis innen et par timer", "IN_A_DAY": "Svarer vanligvis innen en dag", - "BACK_IN": "Vi vil være tilbake 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": "Søndag", diff --git a/app/javascript/widget/i18n/locale/pl.json b/app/javascript/widget/i18n/locale/pl.json index c143e3b00..bea067b26 100644 --- a/app/javascript/widget/i18n/locale/pl.json +++ b/app/javascript/widget/i18n/locale/pl.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Jesteśmy dostępni", - "OFFLINE": "W tej chwili jesteśmy niedostępni" + "OFFLINE": "W tej chwili jesteśmy niedostępni", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Zwykle odpowiadamy w ciągu paru minut", "IN_A_FEW_HOURS": "Zwykle odpowiadamy w ciągu paru godzin", "IN_A_DAY": "Zwykle odpowiadamy w przeciągu jednego dnia", - "BACK_IN": "Będziemy dostępni ponownie" + "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": "Niedziela", diff --git a/app/javascript/widget/i18n/locale/pt.json b/app/javascript/widget/i18n/locale/pt.json index c1fffb681..300ffb59f 100644 --- a/app/javascript/widget/i18n/locale/pt.json +++ b/app/javascript/widget/i18n/locale/pt.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Estamos online", - "OFFLINE": "Estamos ausentes" + "OFFLINE": "Estamos ausentes", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Normalmente respondemos em poucos minutos", "IN_A_FEW_HOURS": "Normalmente respondemos em poucas horas", "IN_A_DAY": "Normalmente respondemos num dia", - "BACK_IN": "Voltaremos 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": "Domingo", diff --git a/app/javascript/widget/i18n/locale/pt_BR.json b/app/javascript/widget/i18n/locale/pt_BR.json index ea1316b8b..7a2678962 100644 --- a/app/javascript/widget/i18n/locale/pt_BR.json +++ b/app/javascript/widget/i18n/locale/pt_BR.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Estamos conectados", - "OFFLINE": "Estamos ausentes no momento" + "OFFLINE": "Estamos ausentes no momento", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Normalmente responde em alguns minutos", "IN_A_FEW_HOURS": "Normalmente responde em algumas horas", "IN_A_DAY": "Normalmente responde em um dia", - "BACK_IN": "Estaremos de volta on-line" + "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": "Domingo", diff --git a/app/javascript/widget/i18n/locale/ro.json b/app/javascript/widget/i18n/locale/ro.json index e030c50b2..cd77700b8 100644 --- a/app/javascript/widget/i18n/locale/ro.json +++ b/app/javascript/widget/i18n/locale/ro.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Suntem online", - "OFFLINE": "Suntem plecați în acest moment" + "OFFLINE": "Suntem plecați în acest moment", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "De obicei, răspunsurile în câteva minute", "IN_A_FEW_HOURS": "De obicei, răspunsurile în câteva ore", "IN_A_DAY": "De obicei, răspunsurile într-o zi", - "BACK_IN": "Vom reveni 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": "Duminică", diff --git a/app/javascript/widget/i18n/locale/ru.json b/app/javascript/widget/i18n/locale/ru.json index 734a3a035..37cb9ba04 100644 --- a/app/javascript/widget/i18n/locale/ru.json +++ b/app/javascript/widget/i18n/locale/ru.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Мы в сети", - "OFFLINE": "В данный момент мы отсутствуем" + "OFFLINE": "В данный момент мы отсутствуем", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Обычно отвечаем в течение нескольких минут", "IN_A_FEW_HOURS": "Обычно отвечаем в течение нескольких часов", "IN_A_DAY": "Обычно отвечаем в течение дня", - "BACK_IN": "Мы будем онлайн" + "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": "Воскресенье", diff --git a/app/javascript/widget/i18n/locale/sh.json b/app/javascript/widget/i18n/locale/sh.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/sh.json +++ b/app/javascript/widget/i18n/locale/sh.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/sk.json b/app/javascript/widget/i18n/locale/sk.json index 60dc03162..b7ac2a763 100644 --- a/app/javascript/widget/i18n/locale/sk.json +++ b/app/javascript/widget/i18n/locale/sk.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Sme online", - "OFFLINE": "Momentálne nie sme k dispozícii" + "OFFLINE": "Momentálne nie sme k dispozícii", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Zvyčajne odpovedá do niekoľkých minút", "IN_A_FEW_HOURS": "Zvyčajne odpovedá do niekoľkých hodín", "IN_A_DAY": "Zvyčajne odpovedá do dňa", - "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": "Nedeľa", diff --git a/app/javascript/widget/i18n/locale/sl.json b/app/javascript/widget/i18n/locale/sl.json index 8665e1e1b..0c95f1268 100644 --- a/app/javascript/widget/i18n/locale/sl.json +++ b/app/javascript/widget/i18n/locale/sl.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Smo na voljo", - "OFFLINE": "Trenutno nismo na voljo" + "OFFLINE": "Trenutno nismo na voljo", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Običajno odgovorimo v nekaj minutah", "IN_A_FEW_HOURS": "Običajno odgovorimo v nekaj urah", "IN_A_DAY": "Običajno odgovorimo v enem dnevu", - "BACK_IN": "Kmalu se vrnemo" + "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": "Nedelja", diff --git a/app/javascript/widget/i18n/locale/sq.json b/app/javascript/widget/i18n/locale/sq.json index 34fe645e9..3efbe543e 100644 --- a/app/javascript/widget/i18n/locale/sq.json +++ b/app/javascript/widget/i18n/locale/sq.json @@ -14,18 +14,24 @@ }, "THUMBNAIL": { "AUTHOR": { - "NOT_AVAILABLE": "Not available" + "NOT_AVAILABLE": "Nuk është i disponueshëm" } }, "TEAM_AVAILABILITY": { "ONLINE": "Ne jemi online", - "OFFLINE": "Nuk jemi online për momentin" + "OFFLINE": "Nuk jemi online për momentin", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Zakonisht përgjigjet brenda pak minutash", "IN_A_FEW_HOURS": "Zakonisht përgjigjemi brenda disa orësh", "IN_A_DAY": "Zakonisht përgjigjemi brenda një dite", - "BACK_IN": "Ne do të kthehemi serish 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": "E diele", diff --git a/app/javascript/widget/i18n/locale/sr.json b/app/javascript/widget/i18n/locale/sr.json index 039982ee6..c0eae2c59 100644 --- a/app/javascript/widget/i18n/locale/sr.json +++ b/app/javascript/widget/i18n/locale/sr.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Dostupni smo", - "OFFLINE": "Trenutno nismo dostupni" + "OFFLINE": "Trenutno nismo dostupni", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Obično odgovaramo u roku od nekoliko minuta", "IN_A_FEW_HOURS": "Obično odgovaramo u roku od nekoliko sati", "IN_A_DAY": "Obično odgovaramo u roku od jednog dana", - "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", diff --git a/app/javascript/widget/i18n/locale/sv.json b/app/javascript/widget/i18n/locale/sv.json index d65e39358..4d6e1723c 100644 --- a/app/javascript/widget/i18n/locale/sv.json +++ b/app/javascript/widget/i18n/locale/sv.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Vi är online", - "OFFLINE": "Vi är borta för tillfället" + "OFFLINE": "Vi är borta för tillfället", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Svarar vanligtvis inom några minuter", "IN_A_FEW_HOURS": "Svarar vanligtvis inom några timmar", "IN_A_DAY": "Svar vanligtvis inom en dag", - "BACK_IN": "Vi kommer tillbaka 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": "Söndag", diff --git a/app/javascript/widget/i18n/locale/ta.json b/app/javascript/widget/i18n/locale/ta.json index 27e069ea7..91ccddf96 100644 --- a/app/javascript/widget/i18n/locale/ta.json +++ b/app/javascript/widget/i18n/locale/ta.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/th.json b/app/javascript/widget/i18n/locale/th.json index a1e39c2f5..fcfed1fde 100644 --- a/app/javascript/widget/i18n/locale/th.json +++ b/app/javascript/widget/i18n/locale/th.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "เรากำลังออนไลน์", - "OFFLINE": "เราไม่อยู่" + "OFFLINE": "เราไม่อยู่", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "ปกติจะตอบกลับภายในไม่กี่นาที", "IN_A_FEW_HOURS": "ปกติจะตอบกลับในไม่กี่ชั่วโมง", "IN_A_DAY": "ปกติจะตอบกลับภายใน 1 วัน", - "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", diff --git a/app/javascript/widget/i18n/locale/tl.json b/app/javascript/widget/i18n/locale/tl.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/tl.json +++ b/app/javascript/widget/i18n/locale/tl.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/tr.json b/app/javascript/widget/i18n/locale/tr.json index b346504fe..d61b1767f 100644 --- a/app/javascript/widget/i18n/locale/tr.json +++ b/app/javascript/widget/i18n/locale/tr.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Çevrimiçi", - "OFFLINE": "Şu an operatörlerimiz müsait değil" + "OFFLINE": "Şu an operatörlerimiz müsait değil", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Genellikle birkaç dakika içinde yanıt verir", "IN_A_FEW_HOURS": "Genellikle birkaç saat içinde yanıt verir", "IN_A_DAY": "Genellikle bir gün içinde yanıtlar", - "BACK_IN": "Tekrar çevrim içi olacağız" + "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": "Pazar", diff --git a/app/javascript/widget/i18n/locale/uk.json b/app/javascript/widget/i18n/locale/uk.json index aed413394..6850623a4 100644 --- a/app/javascript/widget/i18n/locale/uk.json +++ b/app/javascript/widget/i18n/locale/uk.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Ми онлайн", - "OFFLINE": "Нас наразі немає" + "OFFLINE": "Нас наразі немає", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Зазвичай, відповідаємо за декілька хвилин", "IN_A_FEW_HOURS": "Зазвичай, відповідаємо за декілька годин", "IN_A_DAY": "Зазвичай, відповідаємо протягом доби", - "BACK_IN": "Ми повернемось в мережу" + "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": "Неділя", diff --git a/app/javascript/widget/i18n/locale/ur.json b/app/javascript/widget/i18n/locale/ur.json index 3869ab5d6..7b03483a4 100644 --- a/app/javascript/widget/i18n/locale/ur.json +++ b/app/javascript/widget/i18n/locale/ur.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/ur_IN.json b/app/javascript/widget/i18n/locale/ur_IN.json index 4f244c566..c3d6ddfc1 100644 --- a/app/javascript/widget/i18n/locale/ur_IN.json +++ b/app/javascript/widget/i18n/locale/ur_IN.json @@ -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", diff --git a/app/javascript/widget/i18n/locale/vi.json b/app/javascript/widget/i18n/locale/vi.json index d12dbc8db..312883388 100644 --- a/app/javascript/widget/i18n/locale/vi.json +++ b/app/javascript/widget/i18n/locale/vi.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Chúng tôi đang trực tuyến", - "OFFLINE": "Hiện tại chúng tôi đang bận chút" + "OFFLINE": "Hiện tại chúng tôi đang bận chút", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Thường trả lời sau vài phút", "IN_A_FEW_HOURS": "Thường trả lời sau vài giờ", "IN_A_DAY": "Thường trả lời trong một ngày", - "BACK_IN": "Chúng tôi sẽ quay lại" + "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": "Chủ Nhật", diff --git a/app/javascript/widget/i18n/locale/zh_CN.json b/app/javascript/widget/i18n/locale/zh_CN.json index dd834af87..3eb08c8f0 100644 --- a/app/javascript/widget/i18n/locale/zh_CN.json +++ b/app/javascript/widget/i18n/locale/zh_CN.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "在线", - "OFFLINE": "当前已离线" + "OFFLINE": "当前已离线", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "通常在几分钟内回复您", "IN_A_FEW_HOURS": "通常在几小时内回复您", "IN_A_DAY": "通常在一天之内回复您", - "BACK_IN": "我们将重新上线" + "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": "周日", diff --git a/app/javascript/widget/i18n/locale/zh_TW.json b/app/javascript/widget/i18n/locale/zh_TW.json index 3f46d258f..70a5a7c47 100644 --- a/app/javascript/widget/i18n/locale/zh_TW.json +++ b/app/javascript/widget/i18n/locale/zh_TW.json @@ -19,13 +19,19 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "我們在線上", - "OFFLINE": "我們目前不在線上" + "OFFLINE": "我們目前不在線上", + "BACK_AS_SOON_AS_POSSIBLE": "We will be back as soon as possible" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "通常在幾分鐘內回覆", "IN_A_FEW_HOURS": "通常在幾小時內回覆", "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": "星期日", diff --git a/app/javascript/widget/mixins/availability.js b/app/javascript/widget/mixins/availability.js deleted file mode 100644 index 746a5e095..000000000 --- a/app/javascript/widget/mixins/availability.js +++ /dev/null @@ -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); - }, - }, -}; diff --git a/app/javascript/widget/mixins/nextAvailabilityTime.js b/app/javascript/widget/mixins/nextAvailabilityTime.js deleted file mode 100644 index 0fc963083..000000000 --- a/app/javascript/widget/mixins/nextAvailabilityTime.js +++ /dev/null @@ -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 - ); - }, - }, -}; diff --git a/app/javascript/widget/mixins/routerMixin.js b/app/javascript/widget/mixins/routerMixin.js deleted file mode 100644 index b3b37f6fd..000000000 --- a/app/javascript/widget/mixins/routerMixin.js +++ /dev/null @@ -1,10 +0,0 @@ -export default { - methods: { - async replaceRoute(name, params = {}) { - if (this.$route.name !== name) { - return this.$router.replace({ name, params }); - } - return undefined; - }, - }, -}; diff --git a/app/javascript/widget/mixins/specs/availabilityMixin.spec.js b/app/javascript/widget/mixins/specs/availabilityMixin.spec.js deleted file mode 100644 index 5a4e051d1..000000000 --- a/app/javascript/widget/mixins/specs/availabilityMixin.spec.js +++ /dev/null @@ -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); - }); -}); diff --git a/app/javascript/widget/mixins/specs/nextAvailabilityTime.spec.js b/app/javascript/widget/mixins/specs/nextAvailabilityTime.spec.js deleted file mode 100644 index b9717cf43..000000000 --- a/app/javascript/widget/mixins/specs/nextAvailabilityTime.spec.js +++ /dev/null @@ -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'); - }); -}); diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue index 123153c65..332783895 100755 --- a/app/javascript/widget/views/Home.vue +++ b/app/javascript/widget/views/Home.vue @@ -1,7 +1,7 @@
+ {{ $t('INBOX_MGMT.FINISH.WHATSAPP_QR_INSTRUCTION') }} +
+ {{ $t('INBOX_MGMT.FINISH.MESSENGER_QR_INSTRUCTION') }} +
+ {{ $t('INBOX_MGMT.FINISH.TELEGRAM_QR_INSTRUCTION') }} +