Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions app/livechat/client/views/app/business-hours/BusinessHours.ts
Original file line number Diff line number Diff line change
@@ -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());
7 changes: 0 additions & 7 deletions app/livechat/client/views/app/business-hours/IBusinessHour.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour';

export interface IBusinessHourBehavior {
getView(): string;
showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean;
showBackButton(): boolean;
showTimezoneTemplate(): boolean;
}
12 changes: 8 additions & 4 deletions app/livechat/client/views/app/business-hours/Single.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
});
});
4 changes: 2 additions & 2 deletions app/livechat/imports/server/rest/businessHours.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
4 changes: 2 additions & 2 deletions app/livechat/server/api/lib/businessHours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
99 changes: 55 additions & 44 deletions app/livechat/server/business-hour/AbstractBusinessHour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading