Skip to content

Commit 6c2f4ff

Browse files
Marcos Spessatto Defendirenatobecker
andauthored
[IMPROVE] Refactor Omnichannel Office Hours feature (#17824)
* Change omnichannel office hours data structure * Rename omnichannel office hours templates * Renaming things from office hour to business hour * Partially remove LivechatOfficeHour model * remove obsolete setting * Remove settings from the template * Add support to cron jobs on omnichannel business hours * Add dynamic template to omnichannel business hours * Open and close business hours * Remove references to LivechatOfficeHours * Open and close business hour automatically on startup and setting changes * Improve open and close business hours process * Removing separated interfaces * Apply suggestions from review * Changing pt-br term * General improvements on business hour layout * Apply suggestions from review * Remove online verification * Change data structure * Ignore if collection does not exist Co-authored-by: Renato Becker <[email protected]>
1 parent 9ee519b commit 6c2f4ff

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1028
-434
lines changed

app/authorization/server/startup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Meteor.startup(function() {
110110
{ _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] },
111111
{ _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] },
112112
{ _id: 'view-livechat-facebook', roles: ['livechat-manager', 'admin'] },
113-
{ _id: 'view-livechat-officeHours', roles: ['livechat-manager', 'admin'] },
113+
{ _id: 'view-livechat-business-hours', roles: ['livechat-manager', 'admin'] },
114114
{ _id: 'view-livechat-room-closed-same-department', roles: ['livechat-manager', 'admin'] },
115115
{ _id: 'view-livechat-room-closed-by-another-agent', roles: ['livechat-manager', 'admin'] },
116116
{ _id: 'view-livechat-room-customfields', roles: ['livechat-manager', 'livechat-agent', 'admin'] },

app/livechat/client/collections/livechatOfficeHour.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

app/livechat/client/route.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ AccountBox.addRoute({
124124
}, livechatManagerRoutes, load);
125125

126126
AccountBox.addRoute({
127-
name: 'livechat-officeHours',
128-
path: '/officeHours',
127+
name: 'livechat-business-hours',
128+
path: '/businessHours',
129129
sideNav: 'livechatFlex',
130-
i18nPageTitle: 'Office_Hours',
131-
pageTemplate: 'livechatOfficeHours',
130+
i18nPageTitle: 'Business_Hours',
131+
pageTemplate: 'livechatMainBusinessHours',
132132
}, livechatManagerRoutes, load);
133133

134134
AccountBox.addRoute({

app/livechat/client/views/admin.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import './app/livechatCustomFieldForm';
1111
import './app/livechatDepartmentForm';
1212
import './app/livechatDepartments';
1313
import './app/livechatInstallation';
14-
import './app/livechatOfficeHours';
1514
import './app/livechatTriggers';
1615
import './app/livechatTriggersForm';
1716
import './app/livechatManagers';
1817
import './app/integrations/livechatIntegrationWebhook';
1918
import './app/integrations/livechatIntegrationFacebook';
2019
import './app/triggers/livechatTriggerAction';
2120
import './app/triggers/livechatTriggerCondition';
21+
import './app/business-hours/livechatBusinessHoursForm';
22+
import './app/business-hours/livechatMainBusinessHours';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Meteor } from 'meteor/meteor';
2+
3+
import { IBusinessHour } from './IBusinessHour';
4+
import { SingleBusinessHour } from './Single';
5+
import { callbacks } from '../../../../../callbacks/client';
6+
7+
class BusinessHoursManager {
8+
private businessHour: IBusinessHour;
9+
10+
onStartBusinessHourManager(businessHour: IBusinessHour): void {
11+
this.registerBusinessHour(businessHour);
12+
}
13+
14+
registerBusinessHour(businessHour: IBusinessHour): void {
15+
this.businessHour = businessHour;
16+
}
17+
18+
getTemplate(): string {
19+
return this.businessHour.getView();
20+
}
21+
}
22+
23+
export const businessHourManager = new BusinessHoursManager();
24+
25+
Meteor.startup(() => {
26+
const { BusinessHourClass } = callbacks.run('on-business-hour-start', { BusinessHourClass: SingleBusinessHour });
27+
businessHourManager.onStartBusinessHourManager(new BusinessHourClass() as IBusinessHour);
28+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface IBusinessHour {
2+
getView(): string;
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IBusinessHour } from './IBusinessHour';
2+
3+
export class SingleBusinessHour implements IBusinessHour {
4+
getView(): string {
5+
return 'livechatBusinessHoursForm';
6+
}
7+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<template name="livechatBusinessHoursForm">
2+
{{#requiresPermission 'view-livechat-business-hours'}}
3+
<form class="rocket-form" id="businessHoursForm">
4+
5+
<!-- days open -->
6+
<fieldset>
7+
<legend>{{_ "Open_days_of_the_week"}}</legend>
8+
{{#each day in days}}
9+
{{#if open day}}
10+
<label class="dayOpenCheck"><input type="checkbox" name={{openName day}} checked>{{name day}}
11+
</label>
12+
{{else}}
13+
<label class="dayOpenCheck"><input type="checkbox" name={{openName day}}>{{name day}}</label>
14+
{{/if}}
15+
{{/each}}
16+
</fieldset>
17+
18+
<!-- times -->
19+
<fieldset>
20+
<legend>{{_ "Hours"}}</legend>
21+
{{#each day in days}}
22+
<div class="input-line">
23+
<h1><strong>{{name day}}</strong></h1>
24+
<table style="width:100%;">
25+
<tr>
26+
<td>{{_ "Open"}}:</td>
27+
<td>{{_ "Close"}}:</td>
28+
</tr>
29+
<tr>
30+
<td>
31+
<div style="margin-right:30px">
32+
<input type="time" class="preview-settings rc-input__element" name={{startName
33+
day}} id={{startName day}} value={{start day}} style="width=100px;">
34+
</div>
35+
</td>
36+
<td>
37+
<div style="margin-right:30px">
38+
<input type="time" class="preview-settings rc-input__element" name={{finishName
39+
day}} id={{finishName day}} value={{finish day}} style="width=100px;">
40+
</div>
41+
</td>
42+
</tr>
43+
</table>
44+
</div>
45+
{{/each}}
46+
</fieldset>
47+
48+
49+
<div class="rc-button__group submit">
50+
<button class="rc-button rc-button--primary"><i class="icon-floppy"></i>{{_ "Save"}}</button>
51+
</div>
52+
</form>
53+
{{/requiresPermission}}
54+
</template>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { Meteor } from 'meteor/meteor';
2+
import { ReactiveVar } from 'meteor/reactive-var';
3+
import { Template } from 'meteor/templating';
4+
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
5+
import toastr from 'toastr';
6+
import moment from 'moment';
7+
8+
import { t, handleError, APIClient } from '../../../../../utils/client';
9+
import './livechatBusinessHoursForm.html';
10+
11+
Template.livechatBusinessHoursForm.helpers({
12+
days() {
13+
return Template.instance().businessHour.get().workHours;
14+
},
15+
startName(day) {
16+
return `${ day.day }_start`;
17+
},
18+
finishName(day) {
19+
return `${ day.day }_finish`;
20+
},
21+
openName(day) {
22+
return `${ day.day }_open`;
23+
},
24+
start(day) {
25+
return Template.instance().dayVars[day.day].start.get();
26+
},
27+
finish(day) {
28+
return Template.instance().dayVars[day.day].finish.get();
29+
},
30+
name(day) {
31+
return TAPi18n.__(day.day);
32+
},
33+
open(day) {
34+
return Template.instance().dayVars[day.day].open.get();
35+
},
36+
});
37+
38+
const splitDayAndPeriod = (value) => value.split('_');
39+
40+
Template.livechatBusinessHoursForm.events({
41+
'change .preview-settings, keydown .preview-settings'(e, instance) {
42+
const [day, period] = splitDayAndPeriod(e.currentTarget.name);
43+
44+
const newTime = moment(e.currentTarget.value, 'HH:mm');
45+
46+
// check if start and stop do not cross
47+
if (period === 'start') {
48+
if (newTime.isSameOrBefore(moment(instance.dayVars[day].finish.get(), 'HH:mm'))) {
49+
instance.dayVars[day].start.set(e.currentTarget.value);
50+
} else {
51+
e.currentTarget.value = instance.dayVars[day].start.get();
52+
}
53+
} else if (period === 'finish') {
54+
if (newTime.isSameOrAfter(moment(instance.dayVars[day].start.get(), 'HH:mm'))) {
55+
instance.dayVars[day].finish.set(e.currentTarget.value);
56+
} else {
57+
e.currentTarget.value = instance.dayVars[day].finish.get();
58+
}
59+
}
60+
},
61+
'change .dayOpenCheck input'(e, instance) {
62+
const [day, period] = splitDayAndPeriod(e.currentTarget.name);
63+
instance.dayVars[day][period].set(e.target.checked);
64+
},
65+
'change .preview-settings, keyup .preview-settings'(e, instance) {
66+
let { value } = e.currentTarget;
67+
if (e.currentTarget.type === 'radio') {
68+
value = value === 'true';
69+
instance[e.currentTarget.name].set(value);
70+
}
71+
},
72+
'submit .rocket-form'(e, instance) {
73+
e.preventDefault();
74+
75+
// convert all times to utc then update them in db
76+
const days = [];
77+
for (const d in instance.dayVars) {
78+
if (instance.dayVars.hasOwnProperty(d)) {
79+
const day = instance.dayVars[d];
80+
const start = moment(day.start.get(), 'HH:mm').format('HH:mm');
81+
const finish = moment(day.finish.get(), 'HH:mm').format('HH:mm');
82+
days.push({
83+
day: d,
84+
start,
85+
finish,
86+
open: day.open.get(),
87+
});
88+
}
89+
}
90+
Meteor.call('livechat:saveBusinessHour', {
91+
...instance.businessHour.get(),
92+
workHours: days,
93+
}, function(err /* ,result*/) {
94+
if (err) {
95+
return handleError(err);
96+
}
97+
toastr.success(t('Business_hours_updated'));
98+
});
99+
},
100+
});
101+
102+
const createDefaultBusinessHour = () => {
103+
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
104+
const closedDays = ['Saturday', 'Sunday'];
105+
return {
106+
workHours: days.map((day) => ({
107+
day,
108+
start: '00:00',
109+
finish: '00:00',
110+
open: !closedDays.includes(day),
111+
})),
112+
};
113+
};
114+
115+
116+
Template.livechatBusinessHoursForm.onCreated(async function() {
117+
this.dayVars = createDefaultBusinessHour().workHours.reduce((acc, day) => {
118+
acc[day.day] = {
119+
start: new ReactiveVar(day.start),
120+
finish: new ReactiveVar(day.finish),
121+
open: new ReactiveVar(day.open),
122+
};
123+
return acc;
124+
}, {});
125+
this.businessHour = new ReactiveVar({});
126+
127+
const { businessHour } = await APIClient.v1.get('livechat/business-hour');
128+
this.businessHour.set({
129+
...createDefaultBusinessHour(),
130+
});
131+
if (businessHour) {
132+
this.businessHour.set(businessHour);
133+
businessHour.workHours.forEach((d) => {
134+
if (businessHour.timezone.name) {
135+
this.dayVars[d.day].start.set(moment.utc(d.start.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm'));
136+
this.dayVars[d.day].finish.set(moment.utc(d.finish.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm'));
137+
} else {
138+
this.dayVars[d.day].start.set(moment.utc(d.start.utc.time, 'HH:mm').local().format('HH:mm'));
139+
this.dayVars[d.day].finish.set(moment.utc(d.finish.utc.time, 'HH:mm').local().format('HH:mm'));
140+
}
141+
this.dayVars[d.day].open.set(d.open);
142+
});
143+
}
144+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template name="livechatMainBusinessHours">
2+
{{#requiresPermission 'view-livechat-business-hours'}}
3+
<div class="livechat-businessHours-div">
4+
{{> Template.dynamic template=getTemplate}}
5+
</div>
6+
{{/requiresPermission}}
7+
</template>

0 commit comments

Comments
 (0)