Regression: Improve Omnichannel Business Hours (#18050)

Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com>
pull/18070/head^2
Marcos Spessatto Defendi 6 years ago committed by GitHub
parent 6f3fc56899
commit cc42193fdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 34
      app/livechat/client/views/app/business-hours/BusinessHours.ts
  2. 7
      app/livechat/client/views/app/business-hours/IBusinessHour.ts
  3. 8
      app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts
  4. 12
      app/livechat/client/views/app/business-hours/Single.ts
  5. 3
      app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.html
  6. 38
      app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.js
  7. 4
      app/livechat/imports/server/rest/businessHours.js
  8. 4
      app/livechat/server/api/lib/businessHours.ts
  9. 99
      app/livechat/server/business-hour/AbstractBusinessHour.ts
  10. 145
      app/livechat/server/business-hour/BusinessHourManager.ts
  11. 33
      app/livechat/server/business-hour/Default.ts
  12. 45
      app/livechat/server/business-hour/Helper.ts
  13. 61
      app/livechat/server/business-hour/Single.ts
  14. 11
      app/livechat/server/business-hour/index.ts
  15. 26
      app/livechat/server/hooks/processRoomAbandonment.js
  16. 12
      app/livechat/server/lib/Helper.js
  17. 3
      app/livechat/server/lib/Livechat.js
  18. 8
      app/livechat/server/startup.js
  19. 14
      app/models/server/models/LivechatBusinessHours.ts
  20. 1
      app/models/server/models/LivechatDepartment.js
  21. 1
      app/models/server/models/Users.js
  22. 85
      app/models/server/raw/LivechatBusinessHours.ts
  23. 20
      app/models/server/raw/LivechatDepartment.js
  24. 13
      app/models/server/raw/Users.js
  25. 13
      definition/ILivechatBusinessHour.ts
  26. 7
      ee/app/livechat-enterprise/client/SingleBusinessHour.ts
  27. 2
      ee/app/livechat-enterprise/client/route.js
  28. 37
      ee/app/livechat-enterprise/client/startup.ts
  29. 12
      ee/app/livechat-enterprise/client/views/app/customTemplates/businessHoursCustomFieldsForm.html
  30. 14
      ee/app/livechat-enterprise/client/views/app/customTemplates/businessHoursCustomFieldsForm.js
  31. 34
      ee/app/livechat-enterprise/client/views/app/customTemplates/businessHoursFormField.js
  32. 13
      ee/app/livechat-enterprise/client/views/app/customTemplates/businessHoursTimezoneFormField.html
  33. 3
      ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js
  34. 1
      ee/app/livechat-enterprise/client/views/app/registerCustomTemplates.js
  35. 16
      ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts
  36. 8
      ee/app/livechat-enterprise/client/views/business-hours/livechatBusinessHours.js
  37. 4
      ee/app/livechat-enterprise/server/api/business-hours.ts
  38. 84
      ee/app/livechat-enterprise/server/business-hour/Custom.ts
  39. 66
      ee/app/livechat-enterprise/server/business-hour/Helper.ts
  40. 268
      ee/app/livechat-enterprise/server/business-hour/Multiple.ts
  41. 1
      ee/app/livechat-enterprise/server/business-hour/index.ts
  42. 13
      ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js
  43. 1
      ee/app/livechat-enterprise/server/hooks/index.js
  44. 11
      ee/app/livechat-enterprise/server/hooks/onBusinessHourStart.ts
  45. 27
      ee/app/livechat-enterprise/server/hooks/onChangeAgentDepartment.ts
  46. 1
      ee/app/livechat-enterprise/server/index.js
  47. 5
      ee/app/livechat-enterprise/server/methods/removeBusinessHour.ts
  48. 17
      ee/app/livechat-enterprise/server/startup.js
  49. 4
      ee/app/models/server/raw/LivechatDepartmentAgents.ts
  50. 257
      package-lock.json
  51. 1
      package.json
  52. 18
      server/main.d.ts
  53. 1
      server/startup/migrations/index.js
  54. 28
      server/startup/migrations/v195.js
  55. 35
      server/startup/migrations/v197.js
  56. 2
      typings.d.ts

@ -1,33 +1,37 @@
import { IBusinessHour } from './IBusinessHour';
import { SingleBusinessHour } from './Single';
import { IBusinessHourBehavior } from './IBusinessHourBehavior';
import { SingleBusinessHourBehavior } from './Single';
import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour';
class BusinessHoursManager {
private businessHour: IBusinessHour;
private behavior: IBusinessHourBehavior;
constructor(businessHour: IBusinessHour) {
this.setBusinessHourManager(businessHour);
constructor(businessHour: IBusinessHourBehavior) {
this.setBusinessHourBehavior(businessHour);
}
setBusinessHourManager(businessHour: IBusinessHour): void {
this.registerBusinessHourMethod(businessHour);
setBusinessHourBehavior(businessHour: IBusinessHourBehavior): void {
this.registerBusinessHourBehavior(businessHour);
}
registerBusinessHourMethod(businessHour: IBusinessHour): void {
this.businessHour = businessHour;
registerBusinessHourBehavior(behavior: IBusinessHourBehavior): void {
this.behavior = behavior;
}
getTemplate(): string {
return this.businessHour.getView();
return this.behavior.getView();
}
shouldShowCustomTemplate(businessHourData: ILivechatBusinessHour): boolean {
return this.businessHour.shouldShowCustomTemplate(businessHourData);
showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean {
return this.behavior.showCustomTemplate(businessHourData);
}
shouldShowBackButton(): boolean {
return this.businessHour.shouldShowBackButton();
showBackButton(): boolean {
return this.behavior.showBackButton();
}
showTimezoneTemplate(): boolean {
return this.behavior.showTimezoneTemplate();
}
}
export const businessHourManager = new BusinessHoursManager(new SingleBusinessHour() as IBusinessHour);
export const businessHourManager = new BusinessHoursManager(new SingleBusinessHourBehavior());

@ -1,7 +0,0 @@
import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour';
export interface IBusinessHour {
getView(): string;
shouldShowCustomTemplate(businessHourData: ILivechatBusinessHour): boolean;
shouldShowBackButton(): boolean;
}

@ -0,0 +1,8 @@
import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour';
export interface IBusinessHourBehavior {
getView(): string;
showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean;
showBackButton(): boolean;
showTimezoneTemplate(): boolean;
}

@ -1,15 +1,19 @@
import { IBusinessHour } from './IBusinessHour';
import { IBusinessHourBehavior } from './IBusinessHourBehavior';
export class SingleBusinessHour implements IBusinessHour {
export class SingleBusinessHourBehavior implements IBusinessHourBehavior {
getView(): string {
return 'livechatBusinessHoursForm';
}
shouldShowCustomTemplate(): boolean {
showCustomTemplate(): boolean {
return false;
}
shouldShowBackButton(): boolean {
showBackButton(): boolean {
return false;
}
showTimezoneTemplate(): boolean {
return false;
}
}

@ -1,6 +1,9 @@
<template name="livechatBusinessHoursForm">
{{#requiresPermission 'view-livechat-business-hours'}}
<form class="rocket-form" id="businessHoursForm">
{{#if timezoneTemplate}}
{{> Template.dynamic template=timezoneTemplate data=data }}
{{/if}}
{{#if customFieldsTemplate}}
{{> Template.dynamic template=customFieldsTemplate data=data }}

@ -37,13 +37,19 @@ Template.livechatBusinessHoursForm.helpers({
return Template.instance().dayVars[day.day].open.get();
},
customFieldsTemplate() {
if (!businessHourManager.shouldShowCustomTemplate(Template.instance().businessHour.get())) {
if (!businessHourManager.showCustomTemplate(Template.instance().businessHour.get())) {
return;
}
return getCustomFormTemplate('livechatBusinessHoursForm');
},
timezoneTemplate() {
if (!businessHourManager.showTimezoneTemplate()) {
return;
}
return getCustomFormTemplate('livechatBusinessHoursTimezoneForm');
},
showBackButton() {
return businessHourManager.shouldShowBackButton();
return businessHourManager.showBackButton();
},
data() {
return Template.instance().businessHour;
@ -157,24 +163,24 @@ Template.livechatBusinessHoursForm.onCreated(async function() {
...createDefaultBusinessHour(),
});
this.autorun(async () => {
if (FlowRouter.current().route.name.includes('new')) {
return;
}
const id = FlowRouter.getParam('_id');
const type = FlowRouter.getParam('type');
let url = 'livechat/business-hour';
if (id) {
url += `?_id=${ id }`;
if (id && type) {
url += `?_id=${ id }&type=${ type }`;
}
const { businessHour } = await APIClient.v1.get(url);
if (businessHour) {
this.businessHour.set(businessHour);
businessHour.workHours.forEach((d) => {
if (businessHour.timezone.name) {
this.dayVars[d.day].start.set(moment.utc(d.start.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm'));
this.dayVars[d.day].finish.set(moment.utc(d.finish.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm'));
} else {
this.dayVars[d.day].start.set(moment.utc(d.start.utc.time, 'HH:mm').local().format('HH:mm'));
this.dayVars[d.day].finish.set(moment.utc(d.finish.utc.time, 'HH:mm').local().format('HH:mm'));
}
this.dayVars[d.day].open.set(d.open);
});
if (!businessHour) {
return;
}
this.businessHour.set(businessHour);
businessHour.workHours.forEach((d) => {
this.dayVars[d.day].start.set(moment.utc(d.start.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm'));
this.dayVars[d.day].finish.set(moment.utc(d.finish.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm'));
this.dayVars[d.day].open.set(d.open);
});
});
});

@ -3,8 +3,8 @@ import { findLivechatBusinessHour } from '../../../server/api/lib/businessHours'
API.v1.addRoute('livechat/business-hour', { authRequired: true }, {
get() {
const { _id } = this.queryParams;
const { businessHour } = Promise.await(findLivechatBusinessHour(this.userId, _id));
const { _id, type } = this.queryParams;
const { businessHour } = Promise.await(findLivechatBusinessHour(this.userId, _id, type));
return API.v1.success({
businessHour,
});

@ -2,12 +2,12 @@ import { hasPermissionAsync } from '../../../../authorization/server/functions/h
import { businessHourManager } from '../../business-hour';
import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusinessHour';
export async function findLivechatBusinessHour(userId: string, id?: string): Promise<Record<string, ILivechatBusinessHour>> {
export async function findLivechatBusinessHour(userId: string, id?: string, type?: string): Promise<Record<string, ILivechatBusinessHour>> {
if (!await hasPermissionAsync(userId, 'view-livechat-business-hours')) {
throw new Error('error-not-authorized');
}
return {
businessHour: await businessHourManager.getBusinessHour(id),
businessHour: await businessHourManager.getBusinessHour(id, type) as ILivechatBusinessHour,
};
}

@ -2,93 +2,104 @@ import moment from 'moment';
import { ILivechatBusinessHour } from '../../../../definition/ILivechatBusinessHour';
import {
IWorkHoursForCreateCronJobs, LivechatBusinessHoursRaw,
IWorkHoursCronJobsWrapper, LivechatBusinessHoursRaw,
} from '../../../models/server/raw/LivechatBusinessHours';
import { UsersRaw } from '../../../models/server/raw/Users';
import { LivechatBusinessHours, Users } from '../../../models/server/raw';
import { ILivechatDepartment } from '../../../../definition/ILivechatDepartment';
export interface IBusinessHour {
saveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<void>;
export interface IBusinessHourBehavior {
findHoursToCreateJobs(): Promise<IWorkHoursCronJobsWrapper[]>;
openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void>;
closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void>;
onDisableBusinessHours(): Promise<void>;
onAddAgentToDepartment(options?: Record<string, any>): Promise<any>;
onRemoveAgentFromDepartment(options?: Record<string, any>): Promise<any>;
onRemoveDepartment(department?: ILivechatDepartment): Promise<any>;
onStartBusinessHours(): Promise<void>;
afterSaveBusinessHours(businessHourData: ILivechatBusinessHour): Promise<void>;
allowAgentChangeServiceStatus(agentId: string): Promise<boolean>;
}
export interface IBusinessHourType {
name: string;
getBusinessHour(id: string): Promise<ILivechatBusinessHour | undefined>;
findHoursToCreateJobs(): Promise<IWorkHoursForCreateCronJobs[]>;
openBusinessHoursByDayHour(day: string, hour: string): Promise<void>;
closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void>;
removeBusinessHoursFromUsers(): Promise<void>;
saveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<ILivechatBusinessHour>;
removeBusinessHourById(id: string): Promise<void>;
removeBusinessHourFromUsers(departmentId: string, businessHourId: string): Promise<void>;
openBusinessHoursIfNeeded(): Promise<void>;
removeBusinessHourFromUsersByIds(userIds: string[], businessHourId: string): Promise<void>;
addBusinessHourToUsersByIds(userIds: string[], businessHourId: string): Promise<void>;
setDefaultToUsersIfNeeded(userIds: string[]): Promise<void>;
}
export abstract class AbstractBusinessHour {
export abstract class AbstractBusinessHourBehavior {
protected BusinessHourRepository: LivechatBusinessHoursRaw = LivechatBusinessHours;
protected UsersRepository: UsersRaw = Users;
async findHoursToCreateJobs(): Promise<IWorkHoursForCreateCronJobs[]> {
async findHoursToCreateJobs(): Promise<IWorkHoursCronJobsWrapper[]> {
return this.BusinessHourRepository.findHoursToScheduleJobs();
}
async onDisableBusinessHours(): Promise<void> {
await this.UsersRepository.removeBusinessHoursFromAllUsers();
}
async allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
return this.UsersRepository.isAgentWithinBusinessHours(agentId);
}
}
async removeBusinessHoursFromUsers(): Promise<void> {
await this.UsersRepository.removeBusinessHoursFromUsers();
await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
}
export abstract class AbstractBusinessHourType {
protected BusinessHourRepository: LivechatBusinessHoursRaw = LivechatBusinessHours;
protected UsersRepository: UsersRaw = Users;
protected async getBusinessHoursThatMustBeOpened(currentTime: any, activeBusinessHours: ILivechatBusinessHour[]): Promise<Record<string, any>[]> {
return activeBusinessHours
.filter((businessHour) => businessHour.workHours
.filter((hour) => hour.open)
.some((hour) => {
const localTimeStart = moment(`${ hour.start.cron.dayOfWeek }:${ hour.start.cron.time }`, 'dddd:HH:mm');
const localTimeFinish = moment(`${ hour.finish.cron.dayOfWeek }:${ hour.finish.cron.time }`, 'dddd:HH:mm');
return currentTime.isSameOrAfter(localTimeStart) && currentTime.isSameOrBefore(localTimeFinish);
}))
.map((businessHour) => ({
_id: businessHour._id,
type: businessHour.type,
}));
protected async baseSaveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<string> {
businessHourData.active = Boolean(businessHourData.active);
businessHourData = this.convertWorkHours(businessHourData);
if (businessHourData._id) {
await this.BusinessHourRepository.updateOne(businessHourData._id, businessHourData);
return businessHourData._id;
}
const { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);
return insertedId;
}
protected convertWorkHoursWithServerTimezone(businessHourData: ILivechatBusinessHour): ILivechatBusinessHour {
private convertWorkHours(businessHourData: ILivechatBusinessHour): ILivechatBusinessHour {
businessHourData.workHours.forEach((hour: any) => {
const startUtc = moment.tz(`${ hour.day }:${ hour.start }`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
const finishUtc = moment.tz(`${ hour.day }:${ hour.finish }`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
hour.start = {
time: hour.start,
utc: {
dayOfWeek: this.formatDayOfTheWeekFromUTC(`${ hour.day }:${ hour.start }`, 'dddd'),
time: this.formatDayOfTheWeekFromUTC(`${ hour.day }:${ hour.start }`, 'HH:mm'),
dayOfWeek: startUtc.clone().format('dddd'),
time: startUtc.clone().format('HH:mm'),
},
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezone(`${ hour.day }:${ hour.start }`, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezone(`${ hour.day }:${ hour.start }`, ' HH:mm'),
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'HH:mm'),
},
};
hour.finish = {
time: hour.finish,
utc: {
dayOfWeek: this.formatDayOfTheWeekFromUTC(`${ hour.day }:${ hour.finish }`, 'dddd'),
time: this.formatDayOfTheWeekFromUTC(`${ hour.day }:${ hour.finish }`, 'HH:mm'),
dayOfWeek: finishUtc.clone().format('dddd'),
time: finishUtc.clone().format('HH:mm'),
},
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezone(`${ hour.day }:${ hour.finish }`, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezone(`${ hour.day }:${ hour.finish }`, 'HH:mm'),
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(finishUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(finishUtc, 'HH:mm'),
},
};
});
return businessHourData;
}
protected formatDayOfTheWeekFromServerTimezone(hour: string, format: string): string {
return moment(hour, 'dddd:HH:mm').format(format);
protected getUTCFromTimezone(timezone?: string): string {
if (!timezone) {
return String(moment().utcOffset() / 60);
}
return moment.tz(timezone).format('Z');
}
protected formatDayOfTheWeekFromUTC(hour: string, format: string): string {
return moment(hour, 'dddd:HH:mm').utc().format(format);
private formatDayOfTheWeekFromServerTimezoneAndUtcHour(utc: any, format: string): string {
return moment(utc.format('dddd:HH:mm'), 'dddd:HH:mm').add(moment().utcOffset() / 60, 'hours').format(format);
}
}

@ -1,10 +1,10 @@
import moment from 'moment';
import { ILivechatBusinessHour } from '../../../../definition/ILivechatBusinessHour';
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
import { ICronJobs } from '../../../utils/server/lib/cron/Cronjobs';
import { IBusinessHour } from './AbstractBusinessHour';
import { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour';
import { settings } from '../../../settings/server';
import { ILivechatDepartment } from '../../../../definition/ILivechatDepartment';
import { callbacks } from '../../../callbacks/server';
const cronJobDayDict: Record<string, number> = {
Sunday: 0,
@ -17,133 +17,124 @@ const cronJobDayDict: Record<string, number> = {
};
export class BusinessHourManager {
private businessHour: IBusinessHour;
private types: Map<string, IBusinessHourType> = new Map();
private behavior: IBusinessHourBehavior;
private cronJobs: ICronJobs;
private cronJobsCache: string[] = [];
constructor() {
constructor(cronJobs: ICronJobs) {
this.cronJobs = cronJobs;
this.openWorkHoursCallback = this.openWorkHoursCallback.bind(this);
this.closeWorkHoursCallback = this.closeWorkHoursCallback.bind(this);
}
onStartBusinessHourManager(businessHour: IBusinessHour, cronJobs: ICronJobs): void {
this.cronJobs = cronJobs;
this.registerBusinessHourMethod(businessHour);
}
registerBusinessHourMethod(businessHour: IBusinessHour): void {
this.businessHour = businessHour;
}
async dispatchOnStartTasks(): Promise<void> {
async startManager(): Promise<void> {
await this.createCronJobsForWorkHours();
await this.openBusinessHoursIfNeeded();
}
async dispatchOnCloseTasks(): Promise<void> {
await this.removeBusinessHoursFromAgents();
await this.removeCronJobs();
this.setupCallbacks();
this.behavior.onStartBusinessHours();
}
async saveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<void> {
await this.businessHour.saveBusinessHour(businessHourData);
if (!settings.get('Livechat_enable_business_hours')) {
return;
}
await this.dispatchOnStartTasks();
}
async getBusinessHour(id?: string): Promise<ILivechatBusinessHour | undefined> {
return this.businessHour.getBusinessHour(id as string);
async stopManager(): Promise<void> {
this.removeCronJobs();
this.clearCronJobsCache();
this.removeCallbacks();
await this.behavior.onDisableBusinessHours();
}
async allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
if (!settings.get('Livechat_enable_business_hours')) {
return true;
}
return this.businessHour.allowAgentChangeServiceStatus(agentId);
return this.behavior.allowAgentChangeServiceStatus(agentId);
}
async removeBusinessHourIdFromUsers(department: ILivechatDepartment): Promise<void> {
return this.businessHour.removeBusinessHourFromUsers(department._id, department.businessHourId as string);
registerBusinessHourType(businessHourType: IBusinessHourType): void {
this.types.set(businessHourType.name, businessHourType);
}
async removeBusinessHourById(id: string): Promise<void> {
await this.businessHour.removeBusinessHourById(id);
if (!settings.get('Livechat_enable_business_hours')) {
return;
}
await this.createCronJobsForWorkHours();
await this.openBusinessHoursIfNeeded();
registerBusinessHourBehavior(behavior: IBusinessHourBehavior): void {
this.behavior = behavior;
}
async removeBusinessHourFromUsersByIds(userIds: string[], businessHourId: string): Promise<void> {
if (!settings.get('Livechat_enable_business_hours')) {
async getBusinessHour(id?: string, type?: string): Promise<ILivechatBusinessHour | undefined> {
const businessHourType = this.getBusinessHourType(type as string || LivechatBusinessHourTypes.DEFAULT);
if (!businessHourType) {
return;
}
await this.businessHour.removeBusinessHourFromUsersByIds(userIds, businessHourId);
return businessHourType.getBusinessHour(id as string);
}
async setDefaultToUsersIfNeeded(userIds: string[]): Promise<void> {
async saveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<void> {
const type = this.getBusinessHourType(businessHourData.type as string || LivechatBusinessHourTypes.DEFAULT) as IBusinessHourType;
const saved = await type.saveBusinessHour(businessHourData);
if (!settings.get('Livechat_enable_business_hours')) {
return;
}
await this.businessHour.setDefaultToUsersIfNeeded(userIds);
await this.behavior.afterSaveBusinessHours(saved);
await this.createCronJobsForWorkHours();
}
async addBusinessHourToUsersByIds(userIds: string[], businessHourId: string): Promise<void> {
async removeBusinessHourByIdAndType(id: string, type: string): Promise<void> {
const businessHourType = this.getBusinessHourType(type) as IBusinessHourType;
await businessHourType.removeBusinessHourById(id);
if (!settings.get('Livechat_enable_business_hours')) {
return;
}
await this.createCronJobsForWorkHours();
}
await this.businessHour.addBusinessHourToUsersByIds(userIds, businessHourId);
private setupCallbacks(): void {
callbacks.add('livechat.removeAgentDepartment', this.behavior.onRemoveAgentFromDepartment.bind(this), callbacks.priority.HIGH, 'business-hour-livechat-on-remove-agent-department');
callbacks.add('livechat.afterRemoveDepartment', this.behavior.onRemoveDepartment.bind(this), callbacks.priority.HIGH, 'business-hour-livechat-after-remove-department');
callbacks.add('livechat.saveAgentDepartment', this.behavior.onAddAgentToDepartment.bind(this), callbacks.priority.HIGH, 'business-hour-livechat-on-save-agent-department');
}
private removeCronJobs(): void {
this.cronJobsCache.forEach((jobName) => this.cronJobs.remove(jobName));
private removeCallbacks(): void {
callbacks.remove('livechat.removeAgentDepartment', 'business-hour-livechat-on-remove-agent-department');
callbacks.remove('livechat.afterRemoveDepartment', 'business-hour-livechat-after-remove-department');
callbacks.remove('livechat.saveAgentDepartment', 'business-hour-livechat-on-save-agent-department');
}
private async createCronJobsForWorkHours(): Promise<void> {
this.removeCronJobs();
this.clearCronJobsCache();
const workHours = await this.businessHour.findHoursToCreateJobs();
workHours.forEach((workHour) => {
const { start, finish, day } = workHour;
start.forEach((hour) => {
const jobName = `${ workHour.day }/${ hour }/open`;
const time = moment(hour, 'HH:mm');
const scheduleAt = `${ time.minutes() } ${ time.hours() } * * ${ cronJobDayDict[day] }`;
this.addToCache(jobName);
this.cronJobs.add(jobName, scheduleAt, this.openWorkHoursCallback);
});
finish.forEach((hour) => {
const jobName = `${ workHour.day }/${ hour }/open`;
const time = moment(hour, 'HH:mm');
const scheduleAt = `${ time.minutes() } ${ time.hours() } * * ${ cronJobDayDict[day] }`;
this.addToCache(jobName);
this.cronJobs.add(jobName, scheduleAt, this.closeWorkHoursCallback);
});
});
}
const [workHours] = await this.behavior.findHoursToCreateJobs();
if (!workHours) {
return;
}
private async removeBusinessHoursFromAgents(): Promise<void> {
return this.businessHour.removeBusinessHoursFromUsers();
const { start, finish } = workHours;
start.forEach(({ day, times }) => this.scheduleCronJob(times, day, 'open', this.openWorkHoursCallback));
finish.forEach(({ day, times }) => this.scheduleCronJob(times, day, 'close', this.closeWorkHoursCallback));
}
private async openBusinessHoursIfNeeded(): Promise<void> {
return this.businessHour.openBusinessHoursIfNeeded();
private scheduleCronJob(items: string[], day: string, type: string, job: Function): void {
items.forEach((hour) => {
const jobName = `${ day }/${ hour }/${ type }`;
const time = moment(hour, 'HH:mm');
const scheduleAt = `${ time.minutes() } ${ time.hours() } * * ${ cronJobDayDict[day] }`;
this.addToCache(jobName);
this.cronJobs.add(jobName, scheduleAt, job);
});
}
private async openWorkHoursCallback(day: string, hour: string): Promise<void> {
return this.businessHour.openBusinessHoursByDayHour(day, hour);
return this.behavior.openBusinessHoursByDayAndHour(day, hour);
}
private async closeWorkHoursCallback(day: string, hour: string): Promise<void> {
return this.businessHour.closeBusinessHoursByDayAndHour(day, hour);
return this.behavior.closeBusinessHoursByDayAndHour(day, hour);
}
private getBusinessHourType(type: string): IBusinessHourType | undefined {
return this.types.get(type);
}
private removeCronJobs(): void {
this.cronJobsCache.forEach((jobName) => this.cronJobs.remove(jobName));
}
private addToCache(jobName: string): void {

@ -0,0 +1,33 @@
import moment from 'moment';
import { AbstractBusinessHourType, IBusinessHourType } from './AbstractBusinessHour';
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
interface IExtraProperties extends ILivechatBusinessHour {
timezoneName?: string;
}
export class DefaultBusinessHour extends AbstractBusinessHourType implements IBusinessHourType {
name = LivechatBusinessHourTypes.DEFAULT;
getBusinessHour(): Promise<ILivechatBusinessHour | undefined> {
return this.BusinessHourRepository.findOneDefaultBusinessHour();
}
async saveBusinessHour(businessHourData: IExtraProperties): Promise<ILivechatBusinessHour> {
if (!businessHourData._id) {
return businessHourData;
}
businessHourData.timezone = {
name: businessHourData.timezoneName || moment.tz.guess(),
utc: this.getUTCFromTimezone(businessHourData.timezoneName),
};
delete businessHourData.timezoneName;
await this.baseSaveBusinessHour(businessHourData);
return businessHourData;
}
removeBusinessHourById(): Promise<void> {
return Promise.resolve();
}
}

@ -0,0 +1,45 @@
import moment from 'moment';
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
import { LivechatBusinessHours, Users } from '../../../models/server/raw';
import { createDefaultBusinessHourRow } from '../../../models/server/models/LivechatBusinessHours';
export const filterBusinessHoursThatMustBeOpened = async (businessHours: ILivechatBusinessHour[]): Promise<Record<string, any>[]> => {
const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');
return businessHours
.filter((businessHour) => businessHour.active && businessHour.workHours
.filter((hour) => hour.open)
.some((hour) => {
const localTimeStart = moment(`${ hour.start.cron.dayOfWeek }:${ hour.start.cron.time }`, 'dddd:HH:mm');
const localTimeFinish = moment(`${ hour.finish.cron.dayOfWeek }:${ hour.finish.cron.time }`, 'dddd:HH:mm');
return currentTime.isSameOrAfter(localTimeStart) && currentTime.isSameOrBefore(localTimeFinish);
}))
.map((businessHour) => ({
_id: businessHour._id,
type: businessHour.type,
}));
};
export const openBusinessHourDefault = async (): Promise<void> => {
await Users.removeBusinessHoursFromAllUsers();
const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const activeBusinessHours = await LivechatBusinessHours.findDefaultActiveAndOpenBusinessHoursByDay(day, {
fields: {
workHours: 1,
timezone: 1,
type: 1,
active: 1,
},
});
const businessHoursToOpenIds = (await filterBusinessHoursThatMustBeOpened(activeBusinessHours)).map((businessHour) => businessHour._id);
await Users.openAgentsBusinessHoursByBusinessHourId(businessHoursToOpenIds);
await Users.updateLivechatStatusBasedOnBusinessHours();
};
export const createDefaultBusinessHourIfNotExists = async (): Promise<void> => {
if (await LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count() === 0) {
await LivechatBusinessHours.insertOne(createDefaultBusinessHourRow());
}
};

@ -1,69 +1,40 @@
import moment from 'moment';
import { LivechatBusinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
import { AbstractBusinessHourBehavior, IBusinessHourBehavior } from './AbstractBusinessHour';
import { openBusinessHourDefault } from './Helper';
import { ILivechatBusinessHour, LivechatBussinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
import { AbstractBusinessHour, IBusinessHour } from './AbstractBusinessHour';
export class SingleBusinessHour extends AbstractBusinessHour implements IBusinessHour {
async saveBusinessHour(businessHourData: any): Promise<void> {
if (!businessHourData._id) {
return;
}
businessHourData = this.convertWorkHoursWithServerTimezone(businessHourData);
businessHourData.timezone = {
name: '',
utc: String(moment().utcOffset() / 60),
};
await this.BusinessHourRepository.updateOne(businessHourData._id, businessHourData);
}
getBusinessHour(): Promise<ILivechatBusinessHour> {
return this.BusinessHourRepository.findOneDefaultBusinessHour();
}
async openBusinessHoursByDayHour(day: string, hour: string): Promise<void> {
const businessHoursIds = (await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, LivechatBussinessHourTypes.SINGLE, { fields: { _id: 1 } })).map((businessHour) => businessHour._id);
this.UsersRepository.openAgentsBusinessHours(businessHoursIds);
export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior {
async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
const businessHoursIds = (await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, LivechatBusinessHourTypes.DEFAULT, { fields: { _id: 1 } })).map((businessHour) => businessHour._id);
this.UsersRepository.openAgentsBusinessHoursByBusinessHourId(businessHoursIds);
}
async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
const businessHoursIds = (await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, LivechatBussinessHourTypes.SINGLE, { fields: { _id: 1 } })).map((businessHour) => businessHour._id);
await this.UsersRepository.closeAgentsBusinessHours(businessHoursIds);
const businessHoursIds = (await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, LivechatBusinessHourTypes.DEFAULT, { fields: { _id: 1 } })).map((businessHour) => businessHour._id);
await this.UsersRepository.closeAgentsBusinessHoursByBusinessHourIds(businessHoursIds);
this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
}
async openBusinessHoursIfNeeded(): Promise<void> {
await this.removeBusinessHoursFromUsers();
const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const activeBusinessHours = await this.BusinessHourRepository.findDefaultActiveAndOpenBusinessHoursByDay(day, {
fields: {
workHours: 1,
timezone: 1,
type: 1,
},
});
const businessHoursToOpenIds = (await this.getBusinessHoursThatMustBeOpened(currentTime, activeBusinessHours)).map((businessHour) => businessHour._id);
await this.UsersRepository.openAgentsBusinessHours(businessHoursToOpenIds);
await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
async onStartBusinessHours(): Promise<void> {
return openBusinessHourDefault();
}
removeBusinessHourFromUsers(): Promise<void> {
return Promise.resolve();
afterSaveBusinessHours(): Promise<void> {
return openBusinessHourDefault();
}
removeBusinessHourById(): Promise<void> {
return Promise.resolve();
}
removeBusinessHourFromUsersByIds(): Promise<void> {
onAddAgentToDepartment(): Promise<any> {
return Promise.resolve();
}
addBusinessHourToUsersByIds(): Promise<void> {
onRemoveAgentFromDepartment(): Promise<void> {
return Promise.resolve();
}
setDefaultToUsersIfNeeded(): Promise<void> {
onRemoveDepartment(): Promise<void> {
return Promise.resolve();
}
}

@ -1,14 +1,15 @@
import { Meteor } from 'meteor/meteor';
import { BusinessHourManager } from './BusinessHourManager';
import { SingleBusinessHour } from './Single';
import { SingleBusinessHourBehavior } from './Single';
import { cronJobs } from '../../../utils/server/lib/cron/Cronjobs';
import { callbacks } from '../../../callbacks/server';
import { IBusinessHour } from './AbstractBusinessHour';
import { DefaultBusinessHour } from './Default';
export const businessHourManager = new BusinessHourManager();
export const businessHourManager = new BusinessHourManager(cronJobs);
Meteor.startup(() => {
const { BusinessHourClass } = callbacks.run('on-business-hour-start', { BusinessHourClass: SingleBusinessHour });
businessHourManager.onStartBusinessHourManager(new BusinessHourClass() as IBusinessHour, cronJobs);
const { BusinessHourBehaviorClass } = callbacks.run('on-business-hour-start', { BusinessHourBehaviorClass: SingleBusinessHourBehavior });
businessHourManager.registerBusinessHourBehavior(new BusinessHourBehaviorClass());
businessHourManager.registerBusinessHourType(new DefaultBusinessHour());
});

@ -2,36 +2,42 @@ import moment from 'moment';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { LivechatRooms, Messages } from '../../../models';
import { LivechatRooms, Messages } from '../../../models/server';
import { businessHourManager } from '../business-hour';
import { LivechatBusinessHours, LivechatDepartment } from '../../../models/server/raw';
const getSecondsWhenOfficeHoursIsDisabled = (room, agentLastMessage) => moment(new Date(room.closedAt)).diff(moment(new Date(agentLastMessage.ts)), 'seconds');
const getOfficeHoursDictionary = async () => (await businessHourManager.getBusinessHour()).workHours.reduce((acc, day) => {
const parseDays = (acc, day) => {
acc[day.day] = {
start: day.start,
finish: day.finish,
start: { day: day.start.utc.dayOfWeek, time: day.start.utc.time },
finish: { day: day.finish.utc.dayOfWeek, time: day.finish.utc.time },
open: day.open,
};
return acc;
}, {});
};
const getSecondsSinceLastAgentResponse = async (room, agentLastMessage) => {
if (!settings.get('Livechat_enable_business_hours')) {
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
}
let officeDays;
const department = room.departmentId && await LivechatDepartment.findOneById(room.departmentId);
if (department && department.businessHourId) {
const businessHour = await LivechatBusinessHours.findOneById(department.businessHourId);
officeDays = (await businessHourManager.getBusinessHour(businessHour._id, businessHour.type)).workHours.reduce(parseDays, {});
} else {
officeDays = (await businessHourManager.getBusinessHour()).workHours.reduce(parseDays, {});
}
let totalSeconds = 0;
const officeDays = await getOfficeHoursDictionary();
const endOfConversation = moment(new Date(room.closedAt));
const startOfInactivity = moment(new Date(agentLastMessage.ts));
const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days');
const inactivityDay = moment(new Date(agentLastMessage.ts));
for (let index = 0; index <= daysOfInactivity; index++) {
const today = inactivityDay.clone().format('dddd');
const officeDay = officeDays[today];
const startTodaysOfficeHour = moment(officeDay.start, 'HH:mm').add(index, 'days');
const endTodaysOfficeHour = moment(officeDay.finish, 'HH:mm').add(index, 'days');
const startTodaysOfficeHour = moment(`${ officeDay.start.day }:${ officeDay.start.time }`, 'dddd:HH:mm').add(index, 'days');
const endTodaysOfficeHour = moment(`${ officeDay.finish.day }:${ officeDay.finish.time }`, 'dddd:HH:mm').add(index, 'days');
if (officeDays[today].open) {
const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D');
const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D');

@ -381,7 +381,6 @@ export const updateDepartmentAgents = (departmentId, agents = []) => {
callbacks.run('livechat.removeAgentDepartment', { departmentId, agentsId: agentsRemoved });
}
const agentsAdded = [];
agents.forEach((agent) => {
LivechatDepartmentAgents.saveAgent({
agentId: agent.agentId,
@ -390,10 +389,15 @@ export const updateDepartmentAgents = (departmentId, agents = []) => {
count: agent.count ? parseInt(agent.count) : 0,
order: agent.order ? parseInt(agent.order) : 0,
});
agentsAdded.push(agent.agentId);
});
const diff = agents
.map((agent) => agent.agentId)
.filter((agentId) => !savedAgents.includes(agentId));
if (agentsAdded.length > 0) {
callbacks.run('livechat.saveAgentDepartment', { departmentId, agentsId: agentsAdded });
if (diff.length > 0) {
callbacks.run('livechat.saveAgentDepartment', {
departmentId,
agentsId: diff,
});
}
};

@ -901,10 +901,11 @@ export const Livechat = {
}
const ret = LivechatDepartment.removeById(_id);
const agentsIds = LivechatDepartmentAgents.findByDepartmentId(_id).fetch().map((agent) => agent.agentId);
LivechatDepartmentAgents.removeByDepartmentId(_id);
if (ret) {
Meteor.defer(() => {
callbacks.run('livechat.afterRemoveDepartment', department);
callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds });
});
}
return ret;

@ -11,6 +11,7 @@ import { RoutingManager } from './lib/RoutingManager';
import { createLivechatQueueView } from './lib/Helper';
import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor';
import { businessHourManager } from './business-hour';
import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper';
function allowAccessClosedRoomOfSameDepartment(room, user) {
if (!room || !user || room.t !== 'l' || !room.departmentId || room.open) {
@ -23,7 +24,7 @@ function allowAccessClosedRoomOfSameDepartment(room, user) {
return hasPermission(user._id, 'view-livechat-room-closed-same-department');
}
Meteor.startup(() => {
Meteor.startup(async () => {
roomTypes.setRoomFind('l', (_id) => LivechatRooms.findOneById(_id));
addRoomAccessValidator(function(room, user) {
@ -97,11 +98,12 @@ Meteor.startup(() => {
monitor.start();
});
await createDefaultBusinessHourIfNotExists();
settings.get('Livechat_enable_business_hours', async (key, value) => {
if (value) {
return businessHourManager.dispatchOnStartTasks();
return businessHourManager.startManager();
}
await businessHourManager.dispatchOnCloseTasks();
return businessHourManager.stopManager();
});
});

@ -1,17 +1,17 @@
import moment from 'moment';
import moment from 'moment-timezone';
import { ObjectId } from 'mongodb';
import { Base } from './_Base';
import { ILivechatBusinessHour, LivechatBussinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
const createDefaultBusinessHour = (): ILivechatBusinessHour => {
export const createDefaultBusinessHourRow = (): ILivechatBusinessHour => {
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const closedDays = ['Saturday', 'Sunday'];
return {
_id: new ObjectId().toHexString(),
name: '',
active: true,
type: LivechatBussinessHourTypes.SINGLE,
type: LivechatBusinessHourTypes.DEFAULT,
ts: new Date(),
workHours: days.map((day, index) => ({
day,
@ -41,7 +41,7 @@ const createDefaultBusinessHour = (): ILivechatBusinessHour => {
open: !closedDays.includes(day),
})),
timezone: {
name: '',
name: moment.tz.guess(),
utc: String(moment().utcOffset() / 60),
},
};
@ -52,10 +52,6 @@ export class LivechatBusinessHours extends Base {
super('livechat_business_hours');
this.tryEnsureIndex({ name: 1 }, { unique: true });
if (this.find({ type: LivechatBussinessHourTypes.SINGLE }).count() === 0) {
this.insert(createDefaultBusinessHour());
}
}
}

@ -10,6 +10,7 @@ export class LivechatDepartment extends Base {
super(modelOrName || 'livechat_department');
this.tryEnsureIndex({ name: 1 });
this.tryEnsureIndex({ businessHourId: 1 }, { sparse: true });
this.tryEnsureIndex({
numAgents: 1,
enabled: 1,

@ -43,6 +43,7 @@ export class Users extends Base {
this.tryEnsureIndex({ federation: 1 }, { sparse: true });
this.tryEnsureIndex({ isRemote: 1 }, { sparse: true });
this.tryEnsureIndex({ 'services.saml.inResponseTo': 1 });
this.tryEnsureIndex({ openBusinessHours: 1 }, { sparse: true });
}
getLoginTokensByUserId(userId) {

@ -4,20 +4,24 @@ import { BaseRaw } from './BaseRaw';
import {
IBusinessHourWorkHour,
ILivechatBusinessHour,
LivechatBussinessHourTypes,
LivechatBusinessHourTypes,
} from '../../../../definition/ILivechatBusinessHour';
export interface IWorkHoursForCreateCronJobs {
export interface IWorkHoursCronJobsItem {
day: string;
start: string[];
finish: string[];
times: string[];
}
export interface IWorkHoursCronJobsWrapper {
start: IWorkHoursCronJobsItem[];
finish: IWorkHoursCronJobsItem[];
}
export class LivechatBusinessHoursRaw extends BaseRaw {
public readonly col!: Collection<ILivechatBusinessHour>;
findOneDefaultBusinessHour(): Promise<ILivechatBusinessHour> {
return this.findOne({ type: LivechatBussinessHourTypes.SINGLE });
findOneDefaultBusinessHour(options?: any): Promise<ILivechatBusinessHour> {
return this.findOne({ type: LivechatBusinessHourTypes.DEFAULT }, options);
}
findActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]> {
@ -34,7 +38,7 @@ export class LivechatBusinessHoursRaw extends BaseRaw {
findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]> {
return this.find({
type: LivechatBussinessHourTypes.SINGLE,
type: LivechatBusinessHourTypes.DEFAULT,
active: true,
workHours: {
$elemMatch: {
@ -70,7 +74,7 @@ export class LivechatBusinessHoursRaw extends BaseRaw {
// TODO: Remove this function after remove the deprecated method livechat:saveOfficeHours
async updateDayOfGlobalBusinessHour(day: Omit<IBusinessHourWorkHour, 'code'>): Promise<any> {
return this.col.updateOne({
type: LivechatBussinessHourTypes.SINGLE,
type: LivechatBusinessHourTypes.DEFAULT,
'workHours.day': day.day,
}, {
$set: {
@ -81,35 +85,54 @@ export class LivechatBusinessHoursRaw extends BaseRaw {
});
}
findHoursToScheduleJobs(): Promise<IWorkHoursForCreateCronJobs[]> {
findHoursToScheduleJobs(): Promise<IWorkHoursCronJobsWrapper[]> {
return this.col.aggregate([
{ $match: { active: true } },
{
$project: { _id: 0, workHours: 1 },
},
{
$unwind: { path: '$workHours' },
},
{ $match: { 'workHours.open': true } },
{
$group: {
_id: { day: '$workHours.start.cron.dayOfWeek' },
start: { $addToSet: '$workHours.start.cron.time' },
finish: { $addToSet: '$workHours.finish.cron.time' },
},
},
{
$project: {
_id: 0,
day: '$_id.day',
start: 1,
finish: 1,
$facet: {
start: [
{ $match: { active: true } },
{ $project: { _id: 0, workHours: 1 } },
{ $unwind: { path: '$workHours' } },
{ $match: { 'workHours.open': true } },
{
$group: {
_id: { day: '$workHours.start.cron.dayOfWeek' },
times: { $addToSet: '$workHours.start.cron.time' },
},
},
{
$project: {
_id: 0,
day: '$_id.day',
times: 1,
},
},
],
finish: [
{ $match: { active: true } },
{ $project: { _id: 0, workHours: 1 } },
{ $unwind: { path: '$workHours' } },
{ $match: { 'workHours.open': true } },
{
$group: {
_id: { day: '$workHours.finish.cron.dayOfWeek' },
times: { $addToSet: '$workHours.finish.cron.time' },
},
},
{
$project: {
_id: 0,
day: '$_id.day',
times: 1,
},
},
],
},
},
]).toArray() as any;
}
async findActiveBusinessHoursToOpen(day: string, start: string, type?: LivechatBussinessHourTypes, options?: any): Promise<ILivechatBusinessHour[]> {
async findActiveBusinessHoursToOpen(day: string, start: string, type?: LivechatBusinessHourTypes, options?: any): Promise<ILivechatBusinessHour[]> {
const query: Record<string, any> = {
active: true,
workHours: {
@ -126,7 +149,7 @@ export class LivechatBusinessHoursRaw extends BaseRaw {
return this.col.find(query, options).toArray();
}
async findActiveBusinessHoursToClose(day: string, finish: string, type?: LivechatBussinessHourTypes, options?: any): Promise<ILivechatBusinessHour[]> {
async findActiveBusinessHoursToClose(day: string, finish: string, type?: LivechatBusinessHourTypes, options?: any): Promise<ILivechatBusinessHour[]> {
const query: Record<string, any> = {
active: true,
workHours: {

@ -31,6 +31,11 @@ export class LivechatDepartmentRaw extends BaseRaw {
return this.find(query, options);
}
findEnabledByBusinessHourId(businessHourId, options) {
const query = { businessHourId, enabled: true };
return this.find(query, options);
}
addBusinessHourToDepartamentsByIds(ids = [], businessHourId) {
const query = {
_id: { $in: ids },
@ -45,6 +50,21 @@ export class LivechatDepartmentRaw extends BaseRaw {
return this.col.update(query, update, { multi: true });
}
removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids = [], businessHourId) {
const query = {
_id: { $in: ids },
businessHourId,
};
const update = {
$unset: {
businessHourId: 1,
},
};
return this.col.update(query, update, { multi: true });
}
removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId) {
const query = {
businessHourId,

@ -300,7 +300,7 @@ export class UsersRaw extends BaseRaw {
return this.update(query, update, { multi: true });
}
openAgentsBusinessHours(businessHourIds) {
openAgentsBusinessHoursByBusinessHourId(businessHourIds) {
const query = {
roles: 'livechat-agent',
};
@ -317,9 +317,10 @@ export class UsersRaw extends BaseRaw {
return this.update(query, update, { multi: true });
}
openBusinessHourByAgentIds(agentIds = [], businessHourId) {
addBusinessHourByAgentIds(agentIds = [], businessHourId) {
const query = {
_id: { $in: agentIds },
roles: 'livechat-agent',
};
const update = {
@ -334,9 +335,10 @@ export class UsersRaw extends BaseRaw {
return this.update(query, update, { multi: true });
}
closeBusinessHourByAgentIds(agentIds = [], businessHourId) {
removeBusinessHourByAgentIds(agentIds = [], businessHourId) {
const query = {
_id: { $in: agentIds },
roles: 'livechat-agent',
};
const update = {
@ -379,7 +381,7 @@ export class UsersRaw extends BaseRaw {
return this.update(query, update, { multi: true });
}
closeAgentsBusinessHours(businessHourIds) {
closeAgentsBusinessHoursByBusinessHourIds(businessHourIds) {
const query = {
roles: 'livechat-agent',
};
@ -419,8 +421,9 @@ export class UsersRaw extends BaseRaw {
}).count() > 0;
}
removeBusinessHoursFromUsers() {
removeBusinessHoursFromAllUsers() {
const query = {
roles: 'livechat-agent',
openBusinessHours: {
$exists: true,
},

@ -1,8 +1,13 @@
import { ILivechatDepartment } from './ILivechatDepartment';
export enum LivechatBussinessHourTypes {
SINGLE = 'single',
MULTIPLE = 'multiple',
export enum LivechatBusinessHourTypes {
DEFAULT = 'default',
CUSTOM = 'custom',
}
export enum LivechatBusinessHourBehaviors {
SINGLE = 'Single',
MULTIPLE = 'Multiple',
}
interface IBusinessHourTime {
@ -27,7 +32,7 @@ export interface ILivechatBusinessHour {
_id: string;
name: string;
active: boolean;
type: LivechatBussinessHourTypes;
type: LivechatBusinessHourTypes;
timezone: IBusinessHourTimezone;
ts: Date;
workHours: IBusinessHourWorkHour[];

@ -0,0 +1,7 @@
import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single';
export class EESingleBusinessHourBehaviour extends SingleBusinessHourBehavior {
showTimezoneTemplate(): boolean {
return true;
}
}

@ -83,7 +83,7 @@ AccountBox.addRoute({
AccountBox.addRoute({
name: 'livechat-business-hour-edit',
path: '/business-hours/:_id/edit',
path: '/business-hours/:_id/:type/edit',
sideNav: 'livechatFlex',
i18nPageTitle: 'Edit_Business_Hour',
pageTemplate: 'livechatBusinessHoursForm',

@ -1,35 +1,34 @@
import { Meteor } from 'meteor/meteor';
import { MultipleBusinessHours } from './views/business-hours/Multiple';
import { SingleBusinessHour } from '../../../../app/livechat/client/views/app/business-hours/Single';
import { MultipleBusinessHoursBehavior } from './views/business-hours/Multiple';
import { settings } from '../../../../app/settings/client';
import { businessHourManager } from '../../../../app/livechat/client/views/app/business-hours/BusinessHours';
import { IBusinessHour } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHour';
import { IBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior';
import {
addCustomFormTemplate,
removeCustomTemplate,
} from '../../../../app/livechat/client/views/app/customTemplates/register';
import { LivechatBussinessHourTypes } from '../../../../definition/ILivechatBusinessHour';
import {
LivechatBusinessHourBehaviors,
} from '../../../../definition/ILivechatBusinessHour';
import { EESingleBusinessHourBehaviour } from './SingleBusinessHour';
import { hasLicense } from '../../license/client';
const businessHours: Record<string, IBusinessHour> = {
Multiple: new MultipleBusinessHours(),
Single: new SingleBusinessHour(),
const businessHours: Record<string, IBusinessHourBehavior> = {
Multiple: new MultipleBusinessHoursBehavior(),
Single: new EESingleBusinessHourBehaviour(),
};
Meteor.startup(function() {
settings.onload('Livechat_business_hour_type', (_, value) => {
settings.onload('Livechat_business_hour_type', async (_, value) => {
removeCustomTemplate('livechatBusinessHoursForm');
switch (String(value).toLowerCase()) {
case LivechatBussinessHourTypes.SINGLE:
businessHourManager.setBusinessHourManager(new SingleBusinessHour() as IBusinessHour);
break;
case LivechatBussinessHourTypes.MULTIPLE:
businessHourManager.setBusinessHourManager(new MultipleBusinessHours() as IBusinessHour);
addCustomFormTemplate('livechatBusinessHoursForm', 'businessHoursCustomFieldsForm');
break;
removeCustomTemplate('livechatBusinessHoursTimezoneForm');
addCustomFormTemplate('livechatBusinessHoursTimezoneForm', 'businessHoursTimezoneFormField');
if (LivechatBusinessHourBehaviors.MULTIPLE) {
addCustomFormTemplate('livechatBusinessHoursForm', 'businessHoursCustomFieldsForm');
}
if (await hasLicense('livechat-enterprise')) {
businessHourManager.registerBusinessHourBehavior(businessHours[value as string]);
}
businessHourManager.registerBusinessHourMethod(businessHours[value as string]);
});
});

@ -17,17 +17,6 @@
</label>
</div>
</div>
<div class="input-line">
<label>{{_ "Timezone"}}</label>
<div>
<select name="timezoneName" class="rc-input__element additional-field customFormField">
{{#each timezone in timezones}}
<option class="rc-select__option" value="{{timezone.key}}" selected="{{selectedOption timezone.key}}">{{_
timezone.i18nLabel}}</option>
{{/each}}
</select>
</div>
</div>
<div class="input-line">
{{> livechatAutocompleteUser
onClickTag=onClickTagDepartment
@ -54,5 +43,6 @@
<div>
<small class="secondary-font-color">{{{_ "List_of_departments_to_apply_this_business_hour"}}}</small>
</div>
<input type="hidden" class="customFormField" name="type" value="custom" />
</div>
</template>

@ -1,16 +1,9 @@
import moment from 'moment';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import './businessHoursCustomFieldsForm.html';
Template.businessHoursCustomFieldsForm.helpers({
timezones() {
return moment.tz.names().map((name) => ({
key: name,
i18nLabel: name,
}));
},
data() {
return Template.instance().businessHour.get();
},
@ -27,13 +20,6 @@ Template.businessHoursCustomFieldsForm.helpers({
active() {
return Template.instance().active.get();
},
selectedOption(val) {
const { timezone } = Template.instance().businessHour.get();
if (!timezone) {
return;
}
return timezone.name === val;
},
departmentModifier() {
return (filter, text = '') => {
const f = filter.get();

@ -0,0 +1,34 @@
import moment from 'moment';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import './businessHoursTimezoneFormField.html';
Template.businessHoursTimezoneFormField.helpers({
timezones() {
return moment.tz.names().map((name) => ({
key: name,
i18nLabel: name,
}));
},
selectedOption(val) {
const { timezone } = Template.instance().businessHour.get();
if (!timezone) {
return;
}
return timezone.name === val;
},
data() {
return Template.instance().businessHour.get();
},
});
Template.businessHoursTimezoneFormField.onCreated(function() {
this.businessHour = new ReactiveVar({});
this.autorun(() => {
// To make this template reactive we expect a ReactiveVar through the data property,
// because the parent form may not be rerender, only the dynamic template data
this.businessHour.set(this.data.get());
});
});

@ -0,0 +1,13 @@
<template name="businessHoursTimezoneFormField">
<div class="input-line">
<label>{{_ "Timezone"}}</label>
<div>
<select name="timezoneName" class="rc-input__element additional-field customFormField">
{{#each timezone in timezones}}
<option class="rc-select__option" value="{{timezone.key}}" selected="{{selectedOption timezone.key}}">{{_
timezone.i18nLabel}}</option>
{{/each}}
</select>
</div>
</div>
</template>

@ -3,6 +3,7 @@ import { Template } from 'meteor/templating';
import { APIClient, mountArrayQueryParameters } from '../../../../../../../app/utils/client';
import './livechatDepartmentCustomFieldsForm.html';
import { LivechatBusinessHourTypes } from '../../../../../../../definition/ILivechatBusinessHour';
Template.livechatDepartmentCustomFieldsForm.helpers({
department() {
@ -61,7 +62,7 @@ Template.livechatDepartmentCustomFieldsForm.onCreated(function() {
})));
}
if (department.businessHourId) {
const { businessHour } = await APIClient.v1.get(`livechat/business-hour?_id=${ department.businessHourId }`);
const { businessHour } = await APIClient.v1.get(`livechat/business-hour?_id=${ department.businessHourId }&type=${ LivechatBusinessHourTypes.CUSTOM }`);
this.businessHour.set(businessHour);
}
this.department.set(department);

@ -6,6 +6,7 @@ import './customTemplates/livechatAgentInfoCustomFieldsForm';
import './customTemplates/visitorEditCustomFieldsForm';
import './customTemplates/visitorInfoCustomForm';
import './customTemplates/businessHoursCustomFieldsForm';
import './customTemplates/businessHoursFormField';
addCustomFormTemplate('livechatAgentEditForm', 'livechatAgentEditCustomFieldsForm');
addCustomFormTemplate('livechatAgentInfoForm', 'livechatAgentInfoCustomFieldsForm');

@ -1,16 +1,20 @@
import { IBusinessHour } from '../../../../../../app/livechat/client/views/app/business-hours/IBusinessHour';
import { ILivechatBusinessHour, LivechatBussinessHourTypes } from '../../../../../../definition/ILivechatBusinessHour';
import { IBusinessHourBehavior } from '../../../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior';
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../../../definition/ILivechatBusinessHour';
export class MultipleBusinessHours implements IBusinessHour {
export class MultipleBusinessHoursBehavior implements IBusinessHourBehavior {
getView(): string {
return 'livechatBusinessHours';
}
shouldShowCustomTemplate(businessHourData: ILivechatBusinessHour): boolean {
return !businessHourData._id || businessHourData.type !== LivechatBussinessHourTypes.SINGLE;
showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean {
return !businessHourData._id || businessHourData.type !== LivechatBusinessHourTypes.DEFAULT;
}
shouldShowBackButton(): boolean {
showTimezoneTemplate(): boolean {
return true;
}
showBackButton(): boolean {
return true;
}
}

@ -9,7 +9,7 @@ import { hasLicense } from '../../../../license/client';
import './livechatBusinessHours.html';
import { modal } from '../../../../../../app/ui-utils/client';
import { APIClient, handleError, t } from '../../../../../../app/utils';
import { LivechatBussinessHourTypes } from '../../../../../../definition/ILivechatBusinessHour';
import { LivechatBusinessHourTypes } from '../../../../../../definition/ILivechatBusinessHour';
const licenseEnabled = new ReactiveVar(false);
@ -32,7 +32,7 @@ Template.livechatBusinessHours.helpers({
return instance.ready && instance.ready.get();
},
isDefault() {
return this.type === LivechatBussinessHourTypes.SINGLE;
return this.type === LivechatBusinessHourTypes.DEFAULT;
},
openDays() {
return this
@ -72,7 +72,7 @@ Template.livechatBusinessHours.events({
closeOnConfirm: false,
html: false,
}, () => {
Meteor.call('livechat:removeBusinessHour', this._id, (error/* , result*/) => {
Meteor.call('livechat:removeBusinessHour', this._id, this.type, (error/* , result*/) => {
if (error) {
return handleError(error);
}
@ -90,7 +90,7 @@ Template.livechatBusinessHours.events({
'click .business-hour-info'(e/* , instance*/) {
e.preventDefault();
FlowRouter.go('livechat-business-hour-edit', { _id: this._id });
FlowRouter.go('livechat-business-hour-edit', { _id: this._id, type: this.type });
},
'keydown #business-hour-filter'(e) {

@ -1,12 +1,16 @@
import { Promise } from 'meteor/promise';
import { API } from '../../../../../app/api/server';
import { findBusinessHours } from '../business-hour/lib/business-hour';
// @ts-ignore
API.v1.addRoute('livechat/business-hours.list', { authRequired: true }, {
get() {
const { offset, count } = this.getPaginationItems();
const { sort } = this.parseJsonQuery();
const { name } = this.queryParams;
// @ts-ignore
return API.v1.success(Promise.await(findBusinessHours(
this.userId,
{

@ -0,0 +1,84 @@
import {
AbstractBusinessHourType,
IBusinessHourType,
} from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../../definition/ILivechatBusinessHour';
import { LivechatDepartmentRaw } from '../../../../../app/models/server/raw/LivechatDepartment';
import { LivechatDepartment } from '../../../../../app/models/server/raw';
import { businessHourManager } from '../../../../../app/livechat/server/business-hour';
import LivechatDepartmentAgents, { LivechatDepartmentAgentsRaw } from '../../../models/server/raw/LivechatDepartmentAgents';
export interface IBusinessHoursExtraProperties extends ILivechatBusinessHour {
timezoneName: string;
departmentsToApplyBusinessHour: string;
}
class CustomBusinessHour extends AbstractBusinessHourType implements IBusinessHourType {
name = LivechatBusinessHourTypes.CUSTOM;
private DepartmentsRepository: LivechatDepartmentRaw = LivechatDepartment;
private DepartmentsAgentsRepository: LivechatDepartmentAgentsRaw = LivechatDepartmentAgents;
async getBusinessHour(id: string): Promise<ILivechatBusinessHour | undefined> {
if (!id) {
return;
}
const businessHour: ILivechatBusinessHour = await this.BusinessHourRepository.findOneById(id);
businessHour.departments = await this.DepartmentsRepository.findByBusinessHourId(businessHour._id, { fields: { name: 1 } }).toArray();
return businessHour;
}
async saveBusinessHour(businessHourData: IBusinessHoursExtraProperties): Promise<ILivechatBusinessHour> {
businessHourData.timezone = {
name: businessHourData.timezoneName,
utc: this.getUTCFromTimezone(businessHourData.timezoneName),
};
const departments = businessHourData.departmentsToApplyBusinessHour?.split(',').filter(Boolean);
const businessHourToReturn = { ...businessHourData };
delete businessHourData.timezoneName;
delete businessHourData.departmentsToApplyBusinessHour;
delete businessHourData.departments;
const businessHourId = await this.baseSaveBusinessHour(businessHourData);
const currentDepartments = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id);
const toRemove = [...currentDepartments.filter((dept: string) => !departments.includes(dept))];
const toAdd = [...departments.filter((dept: string) => !currentDepartments.includes(dept))];
await this.removeBusinessHourFromDepartmentsIfNeeded(businessHourId, toRemove);
await this.addBusinessHourToDepartmentsIfNeeded(businessHourId, toAdd);
businessHourToReturn._id = businessHourId;
return businessHourToReturn;
}
async removeBusinessHourById(businessHourId: string): Promise<void> {
const businessHour = await this.BusinessHourRepository.findOneById(businessHourId, {});
if (!businessHour) {
return;
}
await this.BusinessHourRepository.removeById(businessHourId);
await this.removeBusinessHourFromAgents(businessHourId);
await this.DepartmentsRepository.removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId);
return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
}
private async removeBusinessHourFromAgents(businessHourId: string): Promise<void> {
const departmentIds = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id);
const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds(departmentIds, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId);
return this.UsersRepository.removeBusinessHourByAgentIds(agentIds, businessHourId);
}
private async removeBusinessHourFromDepartmentsIfNeeded(businessHourId: string, departmentsToRemove: string[]): Promise<void> {
if (!departmentsToRemove.length) {
return;
}
await this.DepartmentsRepository.removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(departmentsToRemove, businessHourId);
}
private async addBusinessHourToDepartmentsIfNeeded(businessHourId: string, departmentsToAdd: string[]): Promise<void> {
if (!departmentsToAdd.length) {
return;
}
await this.DepartmentsRepository.addBusinessHourToDepartamentsByIds(departmentsToAdd, businessHourId);
}
}
businessHourManager.registerBusinessHourType(new CustomBusinessHour());

@ -0,0 +1,66 @@
import { Meteor } from 'meteor/meteor';
import moment from 'moment-timezone';
import {
LivechatBusinessHours,
LivechatDepartment,
LivechatDepartmentAgents,
Users,
} from '../../../../../app/models/server/raw';
import { LivechatBusinessHourTypes } from '../../../../../definition/ILivechatBusinessHour';
const getAllAgentIdsWithoutDepartment = async (): Promise<string[]> => {
const agentIdsWithDepartment = (await LivechatDepartmentAgents.find({}, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId);
const agentIdsWithoutDepartment = (await Users.findUsersInRolesWithQuery('livechat-agent', {
_id: { $nin: agentIdsWithDepartment },
}, { fields: { _id: 1 } }).toArray()).map((user: any) => user._id);
return agentIdsWithoutDepartment;
};
const getAgentIdsToHandle = async (businessHour: Record<string, any>): Promise<string[]> => {
if (businessHour.type === LivechatBusinessHourTypes.DEFAULT) {
return getAllAgentIdsWithoutDepartment();
}
const departmentIds = (await LivechatDepartment.findEnabledByBusinessHourId(businessHour._id, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id);
return (await LivechatDepartmentAgents.findByDepartmentIds(departmentIds, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId);
};
export const openBusinessHour = async (businessHour: Record<string, any>): Promise<void> => {
const agentIds: string[] = await getAgentIdsToHandle(businessHour);
await Users.addBusinessHourByAgentIds(agentIds, businessHour._id);
return Users.updateLivechatStatusBasedOnBusinessHours();
};
export const closeBusinessHour = async (businessHour: Record<string, any>): Promise<void> => {
const agentIds: string[] = await getAgentIdsToHandle(businessHour);
await Users.removeBusinessHourByAgentIds(agentIds, businessHour._id);
return Users.updateLivechatStatusBasedOnBusinessHours();
};
export const removeBusinessHourByAgentIds = async (agentIds: string[], businessHourId: string): Promise<void> => {
if (!agentIds.length) {
return;
}
await Users.removeBusinessHourByAgentIds(agentIds, businessHourId);
return Users.updateLivechatStatusBasedOnBusinessHours();
};
export const resetDefaultBusinessHourIfNeeded = async (): Promise<void> => {
Meteor.call('license:isEnterprise', async (err: any, isEnterprise: any) => {
if (err) {
throw err;
}
if (isEnterprise) {
return;
}
const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour({ fields: { _id: 1 } });
LivechatBusinessHours.update({ _id: defaultBusinessHour._id }, {
$set: {
timezone: {
name: moment.tz.guess(),
utc: String(moment().utcOffset() / 60),
},
},
});
});
};

@ -1,46 +1,54 @@
import moment from 'moment';
import {
AbstractBusinessHour,
IBusinessHour,
AbstractBusinessHourBehavior,
IBusinessHourBehavior,
} from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { ILivechatBusinessHour, LivechatBussinessHourTypes } from '../../../../../definition/ILivechatBusinessHour';
import { LivechatDepartment } from '../../../../../app/models/server/raw';
import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusinessHour';
import { LivechatDepartment } from '../../../../../app/models/server';
import { LivechatDepartment as Raw } from '../../../../../app/models/server/raw';
import { LivechatDepartmentRaw } from '../../../../../app/models/server/raw/LivechatDepartment';
import LivechatDepartmentAgents, { LivechatDepartmentAgentsRaw } from '../../../models/server/raw/LivechatDepartmentAgents';
import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper';
import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper';
interface IBusinessHoursExtraProperties extends ILivechatBusinessHour {
timezoneName: string;
departmentsToApplyBusinessHour: string;
}
export class MultipleBusinessHours extends AbstractBusinessHour implements IBusinessHour {
private DepartmentsRepository: LivechatDepartmentRaw = LivechatDepartment;
export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior {
private DepartmentsRepository: LivechatDepartmentRaw = Raw;
private DepartmentsAgentsRepository: LivechatDepartmentAgentsRaw = LivechatDepartmentAgents;
async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, undefined, {
constructor() {
super();
this.onAddAgentToDepartment = this.onAddAgentToDepartment.bind(this);
this.onRemoveAgentFromDepartment = this.onRemoveAgentFromDepartment.bind(this);
this.onRemoveDepartment = this.onRemoveDepartment.bind(this);
}
async onStartBusinessHours(): Promise<void> {
await this.UsersRepository.removeBusinessHoursFromAllUsers();
await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
fields: {
_id: 1,
workHours: 1,
timezone: 1,
type: 1,
active: 1,
},
});
for (const businessHour of businessHours) {
this.closeBusinessHour(businessHour);
}
}
async getBusinessHour(id: string): Promise<ILivechatBusinessHour | undefined> {
if (!id) {
return;
const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
for (const businessHour of businessHoursToOpen) {
this.openBusinessHour(businessHour);
}
const businessHour: ILivechatBusinessHour = await this.BusinessHourRepository.findOneById(id);
businessHour.departments = await this.DepartmentsRepository.findByBusinessHourId(businessHour._id, { fields: { name: 1 } }).toArray();
return businessHour;
}
async openBusinessHoursByDayHour(day: string, hour: string): Promise<void> {
async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, undefined, {
fields: {
_id: 1,
@ -52,177 +60,111 @@ export class MultipleBusinessHours extends AbstractBusinessHour implements IBusi
}
}
async saveBusinessHour(businessHourData: IBusinessHoursExtraProperties): Promise<void> {
businessHourData.timezone = {
name: businessHourData.timezoneName,
utc: this.getUTCFromTimezone(businessHourData.timezoneName),
};
if (businessHourData.timezone.name) {
businessHourData = this.convertWorkHoursWithSpecificTimezone(businessHourData);
} else {
businessHourData = this.convertWorkHoursWithServerTimezone(businessHourData) as IBusinessHoursExtraProperties;
}
businessHourData.active = Boolean(businessHourData.active);
businessHourData.type = businessHourData.type || LivechatBussinessHourTypes.MULTIPLE;
const departments = businessHourData.departmentsToApplyBusinessHour?.split(',');
delete businessHourData.timezoneName;
delete businessHourData.departmentsToApplyBusinessHour;
delete businessHourData.departments;
if (businessHourData._id) {
await this.BusinessHourRepository.updateOne(businessHourData._id, businessHourData);
return this.updateDepartmentBusinessHour(businessHourData._id, departments);
}
const { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);
return this.updateDepartmentBusinessHour(insertedId, departments);
}
async removeBusinessHourById(id: string): Promise<void> {
const businessHour = await this.BusinessHourRepository.findOneById(id);
if (!businessHour || businessHour.type !== LivechatBussinessHourTypes.MULTIPLE) {
return;
}
this.BusinessHourRepository.removeById(id);
this.DepartmentsRepository.removeBusinessHourFromDepartmentsByBusinessHourId(id);
}
async openBusinessHoursIfNeeded(): Promise<void> {
await this.removeBusinessHoursFromUsers();
const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, undefined, {
fields: {
workHours: 1,
timezone: 1,
_id: 1,
type: 1,
},
});
const businessHoursToOpenIds = await this.getBusinessHoursThatMustBeOpened(currentTime, activeBusinessHours);
for (const businessHour of businessHoursToOpenIds) {
this.openBusinessHour(businessHour);
for (const businessHour of businessHours) {
this.closeBusinessHour(businessHour);
}
}
async removeBusinessHourFromUsers(departmentId: string, businessHourId: string): Promise<void> {
const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds([departmentId], { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId);
await this.UsersRepository.closeBusinessHourByAgentIds(agentIds, businessHourId);
return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
}
async removeBusinessHourFromUsersByIds(userIds: string[], businessHourId: string): Promise<void> {
if (!userIds?.length) {
return;
async afterSaveBusinessHours(businessHourData: IBusinessHoursExtraProperties): Promise<void> {
const departments = businessHourData.departmentsToApplyBusinessHour?.split(',').filter(Boolean);
const currentDepartments = businessHourData.departments?.map((dept: any) => dept._id);
const toRemove = [...(currentDepartments || []).filter((dept: Record<string, any>) => !departments.includes(dept._id))];
await this.removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(businessHourData._id, toRemove);
const businessHour = await this.BusinessHourRepository.findOneById(businessHourData._id);
const businessHourIdToOpen = (await filterBusinessHoursThatMustBeOpened([businessHour])).map((businessHour) => businessHour._id);
if (!businessHourIdToOpen.length) {
return closeBusinessHour(businessHour);
}
await this.UsersRepository.closeBusinessHourByAgentIds(userIds, businessHourId);
return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours(userIds);
return openBusinessHour(businessHour);
}
async addBusinessHourToUsersByIds(userIds: string[], businessHourId: string): Promise<void> {
if (!userIds?.length) {
return;
async onAddAgentToDepartment(options: Record<string, any> = {}): Promise<any> {
const { departmentId, agentsId } = options;
const department = await this.DepartmentsRepository.findOneById(departmentId, { fields: { businessHourId: 1 } });
if (!department || !agentsId.length) {
return options;
}
await this.UsersRepository.openBusinessHourByAgentIds(userIds, businessHourId);
return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours(userIds);
}
async setDefaultToUsersIfNeeded(userIds: string[]): Promise<void> {
if (!userIds?.length) {
return;
const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
await removeBusinessHourByAgentIds(agentsId, defaultBusinessHour._id);
if (!department.businessHourId) {
return options;
}
const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const [businessHour] = await this.BusinessHourRepository.findDefaultActiveAndOpenBusinessHoursByDay(day);
const businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
if (!businessHour) {
return;
return options;
}
for (const userId of userIds) {
if (!(await this.DepartmentsAgentsRepository.findDepartmentsWithBusinessHourByAgentId(userId)).length) { // eslint-disable-line no-await-in-loop
await this.UsersRepository.openBusinessHourByAgentIds([userId], businessHour._id); // eslint-disable-line no-await-in-loop
}
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour]);
if (!businessHourToOpen.length) {
return options;
}
await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
await this.UsersRepository.addBusinessHourByAgentIds(agentsId, businessHour._id);
return options;
}
private async updateDepartmentBusinessHour(businessHourId: string, departments: string[]): Promise<void> {
if (businessHourId) {
await this.DepartmentsRepository.removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId);
}
if (!departments?.length) {
return;
async onRemoveAgentFromDepartment(options: Record<string, any> = {}): Promise<any> {
const { departmentId, agentsId } = options;
const department = await this.DepartmentsRepository.findOneById(departmentId, { fields: { businessHourId: 1 } });
if (!department || !agentsId.length) {
return options;
}
return this.DepartmentsRepository.addBusinessHourToDepartamentsByIds(departments, businessHourId);
return this.handleRemoveAgentsFromDepartments(department, agentsId, options);
}
private convertWorkHoursWithSpecificTimezone(businessHourData: IBusinessHoursExtraProperties): IBusinessHoursExtraProperties {
businessHourData.workHours.forEach((hour: any) => {
const startUtc = moment.tz(`${ hour.day }:${ hour.start }`, 'dddd:HH:mm', businessHourData.timezoneName).utc();
const finishUtc = moment.tz(`${ hour.day }:${ hour.finish }`, 'dddd:HH:mm', businessHourData.timezoneName).utc();
hour.start = {
time: hour.start,
utc: {
dayOfWeek: startUtc.clone().format('dddd'),
time: startUtc.clone().format('HH:mm'),
},
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'HH:mm'),
},
};
hour.finish = {
time: hour.finish,
utc: {
dayOfWeek: finishUtc.clone().format('dddd'),
time: finishUtc.clone().format('HH:mm'),
},
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(finishUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(finishUtc, 'HH:mm'),
},
};
});
return businessHourData;
}
private formatDayOfTheWeekFromServerTimezoneAndUtcHour(utc: any, format: string): string {
return moment(utc.format('dddd:HH:mm'), 'dddd:HH:mm').add(moment().utcOffset() / 60, 'hours').format(format);
}
private async openBusinessHour(businessHour: Record<string, any>): Promise<void> {
if (businessHour.type === LivechatBussinessHourTypes.MULTIPLE) {
const agentIds = await this.getAgentIdsFromBusinessHour(businessHour);
return this.UsersRepository.openBusinessHourByAgentIds(agentIds, businessHour._id);
async onRemoveDepartment(options: Record<string, any> = {}): Promise<any> {
const { department, agentsIds } = options;
if (!department || !agentsIds?.length) {
return options;
}
const agentIdsWithDepartment = await this.getAgentIdsWithDepartment();
return this.UsersRepository.openBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment, businessHour._id);
const deletedDepartment = LivechatDepartment.trashFindOneById(department._id);
return this.handleRemoveAgentsFromDepartments(deletedDepartment, agentsIds, options);
}
private async getAgentIdsFromBusinessHour(businessHour: Record<string, any>): Promise<string[]> {
const departmentIds = (await this.DepartmentsRepository.findByBusinessHourId(businessHour._id, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id);
const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds(departmentIds, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId);
return agentIds;
private async handleRemoveAgentsFromDepartments(department: Record<string, any>, agentsIds: string[], options: any): Promise<any> {
const agentIdsWithoutDepartment = [];
const agentIdsToRemoveCurrentBusinessHour = [];
for (const agentId of agentsIds) {
if (await this.DepartmentsAgentsRepository.findByAgentId(agentId).count() === 0) { // eslint-disable-line no-await-in-loop
agentIdsWithoutDepartment.push(agentId);
}
if (!(await this.DepartmentsAgentsRepository.findAgentsByAgentIdAndBusinessHourId(agentId, department.businessHourId)).length) { // eslint-disable-line no-await-in-loop
agentIdsToRemoveCurrentBusinessHour.push(agentId);
}
}
if (department.businessHourId) {
await removeBusinessHourByAgentIds(agentIdsToRemoveCurrentBusinessHour, department.businessHourId);
}
if (!agentIdsWithoutDepartment.length) {
return options;
}
const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]);
if (!businessHourToOpen.length) {
return options;
}
await this.UsersRepository.addBusinessHourByAgentIds(agentIdsWithoutDepartment, defaultBusinessHour._id);
return options;
}
private async getAgentIdsWithDepartment(): Promise<string[]> {
const agentIdsWithDepartment = (await this.DepartmentsAgentsRepository.find({}, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId);
return agentIdsWithDepartment;
private async openBusinessHour(businessHour: Record<string, any>): Promise<void> {
return openBusinessHour(businessHour);
}
private async closeBusinessHour(businessHour: Record<string, any>): Promise<void> {
if (businessHour.type === LivechatBussinessHourTypes.MULTIPLE) {
const agentIds = await this.getAgentIdsFromBusinessHour(businessHour);
await this.UsersRepository.closeBusinessHourByAgentIds(agentIds, businessHour._id);
return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
private async removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(businessHourId: string, departmentsToRemove: string[]): Promise<void> {
if (!departmentsToRemove.length) {
return;
}
const agentIdsWithDepartment = await this.getAgentIdsWithDepartment();
await this.UsersRepository.closeBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment, businessHour._id);
return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds(departmentsToRemove).toArray()).map((dept: any) => dept.agentId);
await removeBusinessHourByAgentIds(agentIds, businessHourId);
}
private getUTCFromTimezone(timezone: string): string {
if (!timezone) {
return String(moment().utcOffset() / 60);
}
return moment.tz(timezone).format('Z');
private async closeBusinessHour(businessHour: Record<string, any>): Promise<void> {
closeBusinessHour(businessHour);
}
}

@ -1,16 +1,11 @@
import { callbacks } from '../../../../../app/callbacks';
import { LivechatDepartment } from '../../../../../app/models/server';
import { businessHourManager } from '../../../../../app/livechat/server/business-hour';
callbacks.add('livechat.afterRemoveDepartment', (department) => {
callbacks.add('livechat.afterRemoveDepartment', (options = {}) => {
const { department } = options;
if (!department) {
return department;
return options;
}
LivechatDepartment.removeDepartmentFromForwardListById(department._id);
const deletedDepartment = LivechatDepartment.trashFindOneById(department._id);
if (!deletedDepartment.businessHourId) {
return department;
}
Promise.await(businessHourManager.removeBusinessHourIdFromUsers(deletedDepartment));
return department;
return options;
}, callbacks.priority.HIGH, 'livechat-after-remove-department');

@ -16,4 +16,3 @@ import './onSetUserStatusLivechat';
import './onCloseLivechat';
import './onSaveVisitorInfo';
import './onBusinessHourStart';
import './onChangeAgentDepartment';

@ -1,14 +1,15 @@
import { callbacks } from '../../../../../app/callbacks/server';
import { MultipleBusinessHours } from '../business-hour/Multiple';
import { MultipleBusinessHoursBehavior } from '../business-hour/Multiple';
import { settings } from '../../../../../app/settings/server';
import { LivechatBusinessHourBehaviors } from '../../../../../definition/ILivechatBusinessHour';
callbacks.add('on-business-hour-start', (options: any = {}) => {
const { BusinessHourClass } = options;
if (!BusinessHourClass) {
const { BusinessHourBehaviorClass } = options;
if (!BusinessHourBehaviorClass) {
return options;
}
if (settings.get('Livechat_business_hour_type') === 'Single') {
if (settings.get('Livechat_business_hour_type') === LivechatBusinessHourBehaviors.SINGLE) {
return options;
}
return { BusinessHourClass: MultipleBusinessHours };
return { BusinessHourBehaviorClass: MultipleBusinessHoursBehavior };
}, callbacks.priority.HIGH, 'livechat-on-business-hour-start');

@ -1,27 +0,0 @@
import { callbacks } from '../../../../../app/callbacks/server';
import { businessHourManager } from '../../../../../app/livechat/server/business-hour';
import { LivechatDepartment } from '../../../../../app/models/server';
callbacks.add('livechat.removeAgentDepartment', async (options: any = {}) => {
const { departmentId, agentsId } = options;
const department = LivechatDepartment.findOneById(departmentId, { fields: { businessHourId: 1 } });
if (!department || !department.businessHourId) {
return options;
}
await businessHourManager.removeBusinessHourFromUsersByIds(agentsId, department.businessHourId);
await businessHourManager.setDefaultToUsersIfNeeded(agentsId);
return options;
}, callbacks.priority.HIGH, 'livechat-on-remove-agent-department');
callbacks.add('livechat.saveAgentDepartment', async (options: any = {}) => {
const { departmentId, agentsId } = options;
const department = LivechatDepartment.findOneById(departmentId, { fields: { businessHourId: 1 } });
if (!department || !department.businessHourId) {
return options;
}
await businessHourManager.addBusinessHourToUsersByIds(agentsId, department.businessHourId);
return options;
}, callbacks.priority.HIGH, 'livechat-on-save-agent-department');

@ -29,6 +29,7 @@ import './hooks/onCloseLivechat';
import './hooks/onSaveVisitorInfo';
import './lib/routing/LoadBalancing';
import { onLicense } from '../../license/server';
import './business-hour';
onLicense('livechat-enterprise', () => {
require('./api');

@ -1,14 +1,15 @@
import { Meteor } from 'meteor/meteor';
import { Promise } from 'meteor/promise';
import { hasPermission } from '../../../../../app/authorization/server';
import { businessHourManager } from '../../../../../app/livechat/server/business-hour';
Meteor.methods({
'livechat:removeBusinessHour'(id: string) {
'livechat:removeBusinessHour'(id: string, type: string) {
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-business-hours')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeBusinessHour' });
}
return Promise.await(businessHourManager.removeBusinessHourById(id));
return Promise.await(businessHourManager.removeBusinessHourByIdAndType(id, type));
},
});

@ -4,16 +4,18 @@ import { settings } from '../../../../app/settings';
import { checkWaitingQueue, updatePredictedVisitorAbandonment } from './lib/Helper';
import { VisitorInactivityMonitor } from './lib/VisitorInactivityMonitor';
import './lib/query.helper';
import { MultipleBusinessHours } from './business-hour/Multiple';
import { SingleBusinessHour } from '../../../../app/livechat/server/business-hour/Single';
import { MultipleBusinessHoursBehavior } from './business-hour/Multiple';
import { SingleBusinessHourBehavior } from '../../../../app/livechat/server/business-hour/Single';
import { businessHourManager } from '../../../../app/livechat/server/business-hour';
import { resetDefaultBusinessHourIfNeeded } from './business-hour/Helper';
const visitorActivityMonitor = new VisitorInactivityMonitor();
const businessHours = {
Multiple: new MultipleBusinessHours(),
Single: new SingleBusinessHour(),
Multiple: new MultipleBusinessHoursBehavior(),
Single: new SingleBusinessHourBehavior(),
};
Meteor.startup(function() {
Meteor.startup(async function() {
settings.onload('Livechat_maximum_chats_per_agent', function(/* key, value */) {
checkWaitingQueue();
});
@ -28,7 +30,8 @@ Meteor.startup(function() {
updatePredictedVisitorAbandonment();
});
settings.onload('Livechat_business_hour_type', (_, value) => {
businessHourManager.registerBusinessHourMethod(businessHours[value]);
businessHourManager.dispatchOnStartTasks();
businessHourManager.registerBusinessHourBehavior(businessHours[value]);
businessHourManager.startManager();
});
await resetDefaultBusinessHourIfNeeded();
});

@ -2,7 +2,7 @@ import { LivechatDepartmentAgentsRaw as Raw } from '../../../../../app/models/se
import { LivechatDepartmentAgents } from '../../../../../app/models/server';
export class LivechatDepartmentAgentsRaw extends Raw {
findDepartmentsWithBusinessHourByAgentId(agentId: string): Promise<Record<string, any>> {
findAgentsByAgentIdAndBusinessHourId(agentId: string, businessHourId: string): Promise<Record<string, any>> {
const match = {
$match: { agentId },
};
@ -20,7 +20,7 @@ export class LivechatDepartmentAgentsRaw extends Raw {
preserveNullAndEmptyArrays: true,
},
};
const withBusinessHourId = { $match: { 'departments.businessHourId': { $exists: true } } };
const withBusinessHourId = { $match: { 'departments.businessHourId': businessHourId } };
const project = { $project: { departments: 0 } };
return this.col.aggregate([match, lookup, unwind, withBusinessHourId, project]).toArray();
}

257
package-lock.json generated

@ -6021,6 +6021,15 @@
"@types/node": "*"
}
},
"@types/moment-timezone": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.13.tgz",
"integrity": "sha512-SWk1qM8DRssS5YR9L4eEX7WUhK/wc96aIr4nMa6p0kTk9YhGGOJjECVhIdPEj13fvJw72Xun69gScXSZ/UmcPg==",
"dev": true,
"requires": {
"moment": ">=2.14.0"
}
},
"@types/mongodb": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.8.tgz",
@ -15499,28 +15508,28 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"resolved": false,
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"resolved": false,
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"resolved": false,
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"dev": true,
"optional": true,
@ -15531,14 +15540,14 @@
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"optional": true,
@ -15556,28 +15565,28 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true,
"optional": true
@ -15594,21 +15603,21 @@
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"resolved": false,
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"resolved": false,
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"dev": true,
"optional": true
@ -15625,14 +15634,14 @@
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"resolved": false,
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true,
"optional": true,
@ -15664,14 +15673,14 @@
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"resolved": false,
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"resolved": false,
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"optional": true,
@ -15691,7 +15700,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"optional": true,
@ -15709,14 +15718,14 @@
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"optional": true,
@ -15726,14 +15735,14 @@
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"optional": true,
@ -15743,7 +15752,7 @@
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true,
"optional": true
@ -15771,7 +15780,7 @@
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
@ -15826,7 +15835,7 @@
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
"resolved": false,
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"dev": true,
"optional": true,
@ -15855,7 +15864,7 @@
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"resolved": false,
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"optional": true,
@ -15868,21 +15877,21 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"resolved": false,
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"optional": true,
@ -15892,21 +15901,21 @@
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"resolved": false,
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"optional": true,
@ -15917,7 +15926,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"optional": true
@ -15931,7 +15940,7 @@
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"resolved": false,
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"optional": true,
@ -15944,7 +15953,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": false,
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true,
"optional": true
@ -15953,7 +15962,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"optional": true,
@ -15979,21 +15988,21 @@
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"resolved": false,
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"resolved": false,
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true,
"optional": true
@ -16007,21 +16016,21 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"optional": true,
@ -16033,7 +16042,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"optional": true,
@ -16043,7 +16052,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"optional": true,
@ -16053,7 +16062,7 @@
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"resolved": false,
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true,
"optional": true
@ -16076,14 +16085,14 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"resolved": false,
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"dev": true,
"optional": true,
@ -16093,7 +16102,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true,
"optional": true
@ -21070,7 +21079,7 @@
"dependencies": {
"asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"resolved": false,
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"requires": {
"bn.js": "^4.0.0",
@ -21080,7 +21089,7 @@
},
"assert": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
"resolved": false,
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"requires": {
"util": "0.10.3"
@ -21088,7 +21097,7 @@
"dependencies": {
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"resolved": false,
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"requires": {
"inherits": "2.0.1"
@ -21098,22 +21107,22 @@
},
"base64-js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
"resolved": false,
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"resolved": false,
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": false,
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"requires": {
"buffer-xor": "^1.0.3",
@ -21126,7 +21135,7 @@
},
"browserify-cipher": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
"resolved": false,
"integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
"requires": {
"browserify-aes": "^1.0.4",
@ -21136,7 +21145,7 @@
},
"browserify-des": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
"resolved": false,
"integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
"requires": {
"cipher-base": "^1.0.1",
@ -21147,7 +21156,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": false,
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"requires": {
"bn.js": "^4.1.0",
@ -21156,7 +21165,7 @@
},
"browserify-sign": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
"resolved": false,
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
"requires": {
"bn.js": "^4.1.1",
@ -21170,7 +21179,7 @@
},
"browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"resolved": false,
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"requires": {
"pako": "~1.0.5"
@ -21178,7 +21187,7 @@
},
"buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
"resolved": false,
"integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
"requires": {
"base64-js": "^1.0.2",
@ -21187,17 +21196,17 @@
},
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"resolved": false,
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
},
"builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
"resolved": false,
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"resolved": false,
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"requires": {
"inherits": "^2.0.1",
@ -21206,7 +21215,7 @@
},
"console-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
"requires": {
"date-now": "^0.1.4"
@ -21214,17 +21223,17 @@
},
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"create-ecdh": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
"resolved": false,
"integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
"requires": {
"bn.js": "^4.1.0",
@ -21233,7 +21242,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": false,
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"requires": {
"cipher-base": "^1.0.1",
@ -21245,7 +21254,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": false,
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"requires": {
"cipher-base": "^1.0.3",
@ -21258,7 +21267,7 @@
},
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
"resolved": false,
"integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
"requires": {
"browserify-cipher": "^1.0.0",
@ -21276,12 +21285,12 @@
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
"resolved": false,
"integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs="
},
"des.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
"requires": {
"inherits": "^2.0.1",
@ -21290,7 +21299,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": false,
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"requires": {
"bn.js": "^4.1.0",
@ -21300,12 +21309,12 @@
},
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
"resolved": false,
"integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA=="
},
"elliptic": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
"resolved": false,
"integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
"requires": {
"bn.js": "^4.4.0",
@ -21319,12 +21328,12 @@
},
"events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
"resolved": false,
"integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA=="
},
"evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"resolved": false,
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"requires": {
"md5.js": "^1.3.4",
@ -21333,7 +21342,7 @@
},
"hash-base": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
"resolved": false,
"integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
"requires": {
"inherits": "^2.0.1",
@ -21342,7 +21351,7 @@
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"resolved": false,
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
@ -21351,14 +21360,14 @@
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"requires": {
"hash.js": "^1.0.3",
@ -21368,27 +21377,27 @@
},
"https-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"resolved": false,
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"resolved": false,
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"resolved": false,
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"requires": {
"hash-base": "^3.0.0",
@ -21398,7 +21407,7 @@
},
"miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
"resolved": false,
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
"requires": {
"bn.js": "^4.0.0",
@ -21407,27 +21416,27 @@
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"resolved": false,
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
"resolved": false,
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
},
"pako": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
"resolved": false,
"integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw=="
},
"parse-asn1": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
"resolved": false,
"integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
"requires": {
"asn1.js": "^4.0.0",
@ -21440,12 +21449,12 @@
},
"path-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.0.tgz",
"resolved": false,
"integrity": "sha512-Hkavx/nY4/plImrZPHRk2CL9vpOymZLgEbMNX1U0bjcBL7QN9wODxyx0yaMZURSQaUtSEvDrfAvxa9oPb0at9g=="
},
"pbkdf2": {
"version": "3.0.17",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
"resolved": false,
"integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
"requires": {
"create-hash": "^1.1.2",
@ -21457,17 +21466,17 @@
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"resolved": false,
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"resolved": false,
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
"resolved": false,
"integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
"requires": {
"bn.js": "^4.1.0",
@ -21480,22 +21489,22 @@
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"resolved": false,
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"resolved": false,
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"querystring-es3": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"resolved": false,
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"resolved": false,
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"requires": {
"safe-buffer": "^5.1.0"
@ -21503,7 +21512,7 @@
},
"randomfill": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
"resolved": false,
"integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
"requires": {
"randombytes": "^2.0.5",
@ -21512,7 +21521,7 @@
},
"readable-stream": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
"resolved": false,
"integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==",
"requires": {
"inherits": "^2.0.3",
@ -21522,14 +21531,14 @@
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"resolved": false,
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"requires": {
"hash-base": "^3.0.0",
@ -21538,17 +21547,17 @@
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"resolved": false,
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": false,
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
@ -21557,7 +21566,7 @@
},
"stream-browserify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
"resolved": false,
"integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
"requires": {
"inherits": "~2.0.1",
@ -21566,7 +21575,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@ -21580,14 +21589,14 @@
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
@ -21597,7 +21606,7 @@
},
"stream-http": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.0.0.tgz",
"resolved": false,
"integrity": "sha512-JELJfd+btL9GHtxU3+XXhg9NLYrKFnhybfvRuDghtyVkOFydz3PKNT1df07AMr88qW03WHF+FSV0PySpXignCA==",
"requires": {
"builtin-status-codes": "^3.0.0",
@ -21608,7 +21617,7 @@
},
"string_decoder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
"resolved": false,
"integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
"requires": {
"safe-buffer": "~5.1.0"
@ -21616,7 +21625,7 @@
},
"timers-browserify": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
"resolved": false,
"integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==",
"requires": {
"setimmediate": "^1.0.4"
@ -21624,12 +21633,12 @@
},
"tty-browserify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
"resolved": false,
"integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="
},
"url": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
"resolved": false,
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
"requires": {
"punycode": "1.3.2",
@ -21638,14 +21647,14 @@
"dependencies": {
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"resolved": false,
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
"resolved": false,
"integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
"requires": {
"inherits": "2.0.3"
@ -21653,24 +21662,24 @@
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"vm-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz",
"resolved": false,
"integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw=="
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"resolved": false,
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
}
}

@ -68,6 +68,7 @@
"@types/meteor": "^1.4.37",
"@types/mocha": "^7.0.2",
"@types/mock-require": "^2.0.0",
"@types/moment-timezone": "^0.5.13",
"@types/mongodb": "^3.5.8",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^2.11.0",

18
server/main.d.ts vendored

@ -57,3 +57,21 @@ declare module 'meteor/rocketchat:tap-i18n' {
function __(s: string, options: { lng: string }): string;
}
}
declare module 'meteor/promise' {
namespace Promise {
function await(): any;
}
}
declare module 'meteor/littledata:synced-cron' {
interface ICronAddParameters {
name: string;
schedule: Function;
job: Function;
}
namespace SyncedCron {
function add(params: ICronAddParameters): string;
function remove(name: string): string;
}
}

@ -193,4 +193,5 @@ import './v193';
import './v194';
import './v195';
import './v196';
import './v197';
import './xrun';

@ -1,19 +1,31 @@
import moment from 'moment';
import moment from 'moment-timezone';
import { ObjectId } from 'mongodb';
import { Mongo } from 'meteor/mongo';
import { Migrations } from '../../../app/migrations/server';
import { Permissions, Settings } from '../../../app/models/server';
import { LivechatBusinessHours } from '../../../app/models/server/raw';
import { LivechatBussinessHourTypes } from '../../../definition/ILivechatBusinessHour';
import { LivechatBusinessHourTypes } from '../../../definition/ILivechatBusinessHour';
const migrateCollection = () => {
const LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour');
const officeHours = Promise.await(LivechatOfficeHour.rawCollection().find().toArray());
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const officeHours = [];
days.forEach((day) => {
const officeHour = LivechatOfficeHour.findOne({ day });
if (officeHour) {
officeHours.push(officeHour);
}
});
if (!officeHours || officeHours.length === 0) {
return;
}
const businessHour = {
name: '',
active: true,
type: LivechatBussinessHourTypes.SINGLE,
type: LivechatBusinessHourTypes.DEFAULT,
ts: new Date(),
workHours: officeHours.map((officeHour) => ({
day: officeHour.day,
@ -29,7 +41,7 @@ const migrateCollection = () => {
},
},
finish: {
time: '20:00',
time: officeHour.finish,
utc: {
dayOfWeek: moment(`${ officeHour.day }:${ officeHour.finish }`, 'dddd:HH:mm').utc().format('dddd'),
time: moment(`${ officeHour.day }:${ officeHour.finish }`, 'dddd:HH:mm').utc().format('HH:mm'),
@ -43,15 +55,15 @@ const migrateCollection = () => {
open: officeHour.open,
})),
timezone: {
name: '',
name: moment.tz.guess(),
utc: moment().utcOffset() / 60,
},
};
if (LivechatBusinessHours.find({ type: LivechatBussinessHourTypes.SINGLE }).count() === 0) {
if (LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count() === 0) {
businessHour._id = new ObjectId().toHexString();
LivechatBusinessHours.insertOne(businessHour);
} else {
LivechatBusinessHours.update({ type: LivechatBussinessHourTypes.SINGLE }, businessHour);
LivechatBusinessHours.update({ type: LivechatBusinessHourTypes.DEFAULT }, { $set: { ...businessHour } });
}
try {
Promise.await(LivechatOfficeHour.rawCollection().drop());

@ -0,0 +1,35 @@
import moment from 'moment-timezone';
import { Migrations } from '../../../app/migrations/server';
import { LivechatBusinessHours } from '../../../app/models/server/raw';
import { LivechatBusinessHourTypes } from '../../../definition/ILivechatBusinessHour';
const updateBusinessHours = async () => {
await LivechatBusinessHours.update({ type: 'multiple' }, {
$set: {
type: LivechatBusinessHourTypes.CUSTOM,
},
}, { multi: true });
const defaultBusinessHour = await LivechatBusinessHours.findOne({ $or: [{ type: 'single' }, { type: 'default' }] });
if (!defaultBusinessHour) {
return;
}
await LivechatBusinessHours.update({ _id: defaultBusinessHour._id }, {
$set: {
type: LivechatBusinessHourTypes.DEFAULT,
timezone: {
name: moment.tz.guess(),
utc: String(moment().utcOffset() / 60),
},
},
});
};
Migrations.add({
version: 197,
up() {
Promise.await(updateBusinessHours());
},
});

2
typings.d.ts vendored

@ -1,4 +1,6 @@
declare module 'meteor/rocketchat:tap-i18n';
declare module 'meteor/littledata:synced-cron';
declare module 'meteor/promise';
declare module 'meteor/ddp-common';
declare module 'meteor/routepolicy';
declare module 'xml-encryption';

Loading…
Cancel
Save