Move src to dashboard (#152)

This commit is contained in:
Pranav Raj S
2019-10-16 14:36:17 +05:30
committed by GitHub
parent 012a2743f2
commit 2783fb6006
187 changed files with 29 additions and 29 deletions

View File

@@ -0,0 +1,5 @@
<template>
<div class="row auth-wrap login align-center">
<router-view></router-view>
</div>
</template>

View File

@@ -0,0 +1,32 @@
<template>
<loading-state :message="$t('CONFIRM_EMAIL')"></loading-state>
</template>
<script>
/* eslint-disable */
import LoadingState from '../../components/widgets/LoadingState';
import Auth from '../../api/auth';
export default {
props: {
confirmationToken: String,
redirectUrl: String,
config: String,
},
components: {
LoadingState,
},
mounted() {
this.confirmToken();
},
methods: {
confirmToken() {
Auth.verifyPasswordToken({
confirmationToken: this.confirmationToken
}).then(res => {
window.location = res.data.redirect_url;
}).catch(res => {
window.location = res.data.redirect_url;
});
}
}
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<form class="login-box medium-4 column align-self-middle" v-on:submit.prevent="login()">
<div class="column log-in-form">
<h4>{{$t('SET_NEW_PASSWORD.TITLE')}}</h4>
<label :class="{ 'error': $v.credentials.password.$error }">
{{$t('LOGIN.PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" v-model.trim="credentials.password" @input="$v.credentials.password.$touch">
<span class="message" v-if="$v.credentials.password.$error">
{{$t('SET_NEW_PASSWORD.PASSWORD.ERROR')}}
</span>
</label>
<label :class="{ 'error': $v.credentials.confirmPassword.$error }">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')" v-model.trim="credentials.confirmPassword" @input="$v.credentials.confirmPassword.$touch">
<span class="message" v-if="$v.credentials.confirmPassword.$error">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')}}
</span>
</label>
<woot-submit-button
:disabled="$v.credentials.password.$invalid || $v.credentials.confirmPassword.$invalid || newPasswordAPI.showLoading"
:button-text="$t('SET_NEW_PASSWORD.SUBMIT')"
:loading="newPasswordAPI.showLoading"
button-class="expanded"
>
</woot-submit-button>
<!-- <input type="submit" class="button " v-on:click.prevent="login()" v-bind:value="" > -->
</div>
</form>
</template>
<script>
/* global bus */
import { required, minLength } from 'vuelidate/lib/validators';
import Auth from '../../api/auth';
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
export default {
components: {
WootSubmitButton,
},
props: {
resetPasswordToken: String,
redirectUrl: String,
config: String,
},
data() {
return {
// We need to initialize the component with any
// properties that will be used in it
credentials: {
confirmPassword: '',
password: '',
},
newPasswordAPI: {
message: '',
showLoading: false,
},
error: '',
};
},
mounted() {
// If url opened without token
// redirect to login
if (!this.resetPasswordToken) {
window.location = '/';
}
},
validations: {
credentials: {
password: {
required,
minLength: minLength(6),
},
confirmPassword: {
required,
minLength: minLength(6),
isEqPassword(value) {
if (value !== this.credentials.password) {
return false;
}
return true;
},
},
},
},
methods: {
showAlert(message) {
// Reset loading, current selected agent
this.newPasswordAPI.showLoading = false;
bus.$emit('newToastMessage', message);
},
login() {
this.newPasswordAPI.showLoading = true;
const credentials = {
confirmPassword: this.credentials.confirmPassword,
password: this.credentials.password,
resetPasswordToken: this.resetPasswordToken,
};
Auth.setNewPassword(credentials)
.then((res) => {
if (res.status === 200) {
window.location = res.data.redirect_url;
}
})
.catch(() => {
this.showAlert(this.$t('SET_NEW_PASSWORD.API.ERROR_MESSAGE'));
});
},
},
};
</script>

View File

@@ -0,0 +1,83 @@
<template>
<form
class="login-box medium-4 column align-self-middle"
@submit.prevent="submit()"
>
<h4>{{ $t('RESET_PASSWORD.TITLE') }}</h4>
<div class="column log-in-form">
<label :class="{ error: $v.credentials.email.$error }">
{{ $t('RESET_PASSWORD.EMAIL.LABEL') }}
<input
v-model.trim="credentials.email"
type="text"
:placeholder="$t('RESET_PASSWORD.EMAIL.PLACEHOLDER')"
@input="$v.credentials.email.$touch"
/>
<span v-if="$v.credentials.email.$error" class="message">
{{ $t('RESET_PASSWORD.EMAIL.ERROR') }}
</span>
</label>
<woot-submit-button
:disabled="$v.credentials.email.$invalid || resetPassword.showLoading"
:button-text="$t('RESET_PASSWORD.SUBMIT')"
:loading="resetPassword.showLoading"
button-class="expanded"
/>
</div>
</form>
</template>
<script>
/* global bus */
import { required, minLength, email } from 'vuelidate/lib/validators';
import Auth from '../../api/auth';
import { frontendURL } from '../../helper/URLHelper';
export default {
data() {
return {
// We need to initialize the component with any
// properties that will be used in it
credentials: {
email: '',
},
resetPassword: {
message: '',
showLoading: false,
},
error: '',
};
},
validations: {
credentials: {
email: {
required,
email,
minLength: minLength(4),
},
},
},
methods: {
showAlert(message) {
// Reset loading, current selected agent
this.resetPassword.showLoading = false;
bus.$emit('newToastMessage', message);
},
submit() {
this.resetPassword.showLoading = true;
Auth.resetPassword(this.credentials)
.then(res => {
let successMessage = this.$t('RESET_PASSWORD.API.SUCCESS_MESSAGE');
if (res.data && res.data.message) {
successMessage = res.data.message;
}
this.showAlert(successMessage);
window.location = frontendURL('login');
})
.catch(() => {
this.showAlert(this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'));
});
},
},
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<div class="medium-10 column signup">
<div class="text-center medium-12 signup--hero">
<img
src="~dashboard/assets/images/woot-logo.svg"
alt="Woot-logo"
class="hero--logo"
/>
<h2 class="hero--title">
{{ $t('REGISTER.TRY_WOOT') }}
</h2>
<p class="hero--sub">
{{ $t('REGISTER.TRY_WOOT_SUB') }}
</p>
</div>
<div class="row align-center">
<div class="medium-5 column">
<ul class="signup--features">
<li><i class="ion-beer beer"></i>Unlimited Facebook Pages</li>
<li><i class="ion-stats-bars report"></i>Robust Reporting</li>
<li><i class="ion-chatbox-working canned"></i>Canned Responses</li>
<li><i class="ion-loop uptime"></i>Auto Assignment</li>
<li><i class="ion-locked secure"></i>Enterprise level security</li>
</ul>
</div>
<div class="medium-5 column">
<form class="signup--box login-box " @submit.prevent="submit()">
<div class="column log-in-form">
<label :class="{ error: $v.credentials.name.$error }">
{{ $t('REGISTER.ACCOUNT_NAME.LABEL') }}
<input
v-model.trim="credentials.name"
type="text"
:placeholder="$t('REGISTER.ACCOUNT_NAME.PLACEHOLDER')"
@input="$v.credentials.name.$touch"
/>
<span v-if="$v.credentials.name.$error" class="message">
{{ $t('REGISTER.ACCOUNT_NAME.ERROR') }}
</span>
</label>
<label :class="{ error: $v.credentials.email.$error }">
{{ $t('REGISTER.EMAIL.LABEL') }}
<input
v-model.trim="credentials.email"
type="email"
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
@input="$v.credentials.email.$touch"
/>
<span v-if="$v.credentials.email.$error" class="message">
{{ $t('REGISTER.EMAIL.ERROR') }}
</span>
</label>
<woot-submit-button
:disabled="
$v.credentials.name.$invalid ||
$v.credentials.email.$invalid ||
register.showLoading
"
:button-text="$t('REGISTER.SUBMIT')"
:loading="register.showLoading"
button-class="large expanded"
>
</woot-submit-button>
<p class="accept--terms" v-html="$t('REGISTER.TERMS_ACCEPT')"></p>
</div>
</form>
<div class="column text-center sigin--footer">
<span>Already have an account?</span>
<router-link to="auth/login">
{{ $t('LOGIN.TITLE') }}
</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
/* global bus */
import { required, minLength, email } from 'vuelidate/lib/validators';
import Auth from '../../api/auth';
import { frontendURL } from '../../helper/URLHelper';
export default {
data() {
return {
// We need to initialize the component with any
// properties that will be used in it
credentials: {
name: '',
email: '',
},
register: {
message: '',
showLoading: false,
},
error: '',
};
},
validations: {
credentials: {
name: {
required,
minLength: minLength(4),
},
email: {
required,
email,
},
},
},
methods: {
showAlert(message) {
// Reset loading, current selected agent
this.register.showLoading = false;
bus.$emit('newToastMessage', message);
},
submit() {
this.register.showLoading = true;
Auth.register(this.credentials)
.then(res => {
if (res.status === 200) {
window.location = frontendURL('dashboard');
}
})
.catch(error => {
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
if (error.response && error.response.data.message) {
errorMessage = error.response.data.message;
}
this.showAlert(errorMessage);
});
},
},
};
</script>

View File

@@ -0,0 +1,48 @@
import Auth from './Auth';
import Confirmation from './Confirmation';
import Signup from './Signup';
import PasswordEdit from './PasswordEdit';
import ResetPassword from './ResetPassword';
import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('auth'),
name: 'auth',
component: Auth,
children: [
{
path: 'confirmation',
name: 'auth_confirmation',
component: Confirmation,
props: route => ({
config: route.query.config,
confirmationToken: route.query.confirmation_token,
redirectUrl: route.query.route_url,
}),
},
{
path: 'password/edit',
name: 'auth_password_edit',
component: PasswordEdit,
props: route => ({
config: route.query.config,
resetPasswordToken: route.query.reset_password_token,
redirectUrl: route.query.route_url,
}),
},
{
path: 'signup',
name: 'auth_signup',
component: Signup,
},
{
path: 'reset/password',
name: 'auth_reset_password',
component: ResetPassword,
},
],
},
],
};

View File

@@ -0,0 +1,27 @@
<template>
<div class="row app-wrapper">
<sidebar :route="currentRoute"></sidebar>
<router-view></router-view>
</div>
</template>
<script>
/* eslint no-console: 0 */
import Sidebar from '../../components/layout/Sidebar';
export default {
props: {
mainViewComponent: String,
sidebarMenu: String,
page: String,
},
components: {
Sidebar,
},
computed: {
currentRoute() {
return ' ';
},
},
};
</script>

View File

@@ -0,0 +1,67 @@
<template>
<section class="app-content columns">
<chat-list :conversationInbox="inboxId" :pageTitle="$t('CHAT_LIST.TAB_HEADING')" ></chat-list>
<conversation-box :inbox-id="inboxId"></conversation-box>
</section>
</template>
<script>
/* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex';
import ChatList from '../../../components/ChatList';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox';
export default {
components: {
ChatList,
ConversationBox,
},
data() {
return {
pageTitle: this.$state,
};
},
computed: {
...mapGetters({
menuItems: 'getMenuItems',
chatList: 'getAllConversations',
}),
},
props: ['inboxId', 'conversationId'],
mounted() {
this.$watch('$store.state.route', () => {
switch (this.$store.state.route.name) {
case 'inbox_conversation':
this.setActiveChat();
break;
case 'inbox_dashboard':
if (this.inboxId) {
this.$store.dispatch('setActiveInbox', this.inboxId);
}
break;
default:
this.$store.dispatch('setActiveInbox', null);
break;
}
});
this.$watch('chatList.length', () => {
this.setActiveChat();
});
},
methods: {
setActiveChat() {
const conversationId = parseInt(this.conversationId, 10);
const [chat] = this.chatList.filter(c => c.id === conversationId);
if (!chat) return;
this.$store.dispatch('setActiveChat', chat).then(() => {
bus.$emit('scrollToMessage');
});
},
},
};
</script>

View File

@@ -0,0 +1,35 @@
/* eslint arrow-body-style: 0 */
import ConversationView from './ConversationView';
import { frontendURL } from '../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('dashboard'),
name: 'home',
roles: ['administrator', 'agent'],
component: ConversationView,
props: () => {
return { inboxId: 0 };
},
},
{
path: frontendURL('inbox/:inbox_id'),
name: 'inbox_dashboard',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return { inboxId: route.params.inbox_id };
},
},
{
path: frontendURL('conversations/:conversation_id'),
name: 'inbox_conversation',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return { conversationId: route.params.conversation_id };
},
},
],
};

View File

@@ -0,0 +1,14 @@
import AppContainer from './Dashboard';
import settings from './settings/settings.routes';
import conversation from './conversation/conversation.routes';
import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL(''),
component: AppContainer,
children: [...conversation.routes, ...settings.routes],
},
],
};

View File

@@ -0,0 +1,50 @@
<template>
<div class="settings-header">
<h1 class="page-title">
<back-button v-if="!showButton"></back-button>
<i :class="icon"></i>
<span>{{ headerTitle }}</span>
</h1>
<router-link
:to="buttonRoute"
class="button icon success"
v-if="showNewButton && showButton && currentRole"
>
<i class="icon ion-android-add-circle"></i>
{{buttonText}}
</router-link>
</div>
</template>
<script>
import BackButton from '../../../components/widgets/BackButton';
import Auth from '../../../api/auth';
export default {
props: {
headerTitle: String,
buttonRoute: String,
buttonText: String,
icon: String,
showButton: Boolean,
showNewButton: Boolean,
hideButtonRoutes: {
type: Array,
default() {
return ['agent_list', 'settings_inbox_list'];
},
},
},
computed: {
iconClass() {
return `icon ${this.props.icon}`;
},
currentRole() {
const { role } = Auth.getCurrentUser();
return role === 'administrator';
},
},
components: {
BackButton,
},
};
</script>

View File

@@ -0,0 +1,18 @@
<template>
<div class="column">
<h2 class="page-sub-title">
{{headerTitle}}
</h2>
<p class="small-12 column" v-html="headerContent">
</p>
</div>
</template>
<script>
export default {
props: {
headerTitle: String,
headerContent: String,
},
};
</script>

View File

@@ -0,0 +1,54 @@
<template>
<section class="app-content columns">
<div class="view-box columns bg-light">
<settings-header
button-route="new"
:icon="icon"
:header-title="$t(headerTitle)"
:button-text="$t(headerButtonText)"
:show-button="showButton()"
:show-new-button="showNewButton()"
/>
<!-- <transition name="slide-fade"> -->
<keep-alive>
<router-view></router-view>
</keep-alive>
<!-- </transition> -->
</div>
</section>
</template>
<script>
/* eslint no-console: 0 */
import SettingsHeader from './SettingsHeader';
export default {
data() {
return {
};
},
props: {
headerTitle: String,
headerButtonText: String,
icon: String,
newButtonRoutes: Array,
},
components: {
SettingsHeader,
},
computed: {
currentPage() {
return this.$store.state.route.name;
},
},
methods: {
showButton() {
/* eslint-disable no-unneeded-ternary */
return this.newButtonRoutes ? this.newButtonRoutes.indexOf(this.currentPage) > -1 : true;
},
showNewButton() {
return this.newButtonRoutes ? true : false;
},
},
};
</script>

View File

@@ -0,0 +1,146 @@
<template>
<woot-modal :show.sync="show" :on-close="onClose">
<div class="column content-box">
<woot-modal-header
:header-image="headerImage"
:header-title="$t('AGENT_MGMT.ADD.TITLE')"
:header-content="$t('AGENT_MGMT.ADD.DESC')"
/>
<form class="row" v-on:submit.prevent="addAgent()">
<div class="medium-12 columns">
<label :class="{ 'error': $v.agentName.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.NAME.LABEL') }}
<input type="text" v-model.trim="agentName" @input="$v.agentName.$touch" :placeholder="$t('AGENT_MGMT.ADD.FORM.NAME.PLACEHOLDER')">
</label>
</div>
<div class="medium-12 columns">
<label :class="{ 'error': $v.agentType.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.LABEL') }}
<multiselect
v-model="agentType"
:options="agentTypeList"
:searchable="false"
label="label"
:placeholder="$t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.PLACEHOLDER')"
@select="setPageName"
:allow-empty="true"
:close-on-select="true"
/>
<span class="message" v-if="$v.agentType.$error">
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.ERROR') }}
</span>
</label>
</div>
<div class="medium-12 columns">
<label :class="{ 'error': $v.agentEmail.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.EMAIL.LABEL') }}
<input type="text" v-model.trim="agentEmail" @input="$v.agentEmail.$touch" :placeholder="$t('AGENT_MGMT.ADD.FORM.EMAIL.PLACEHOLDER')">
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-submit-button
:disabled="$v.agentEmail.$invalid || $v.agentName.$invalid || addAgentsApi.showLoading"
:button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')"
:loading="addAgentsApi.showLoading"
/>
<a @click="onClose">Cancel</a>
</div>
</div>
</form>
</div>
</woot-modal>
</template>
<script>
/* global bus */
/* eslint no-console: 0 */
import { required, minLength, email } from 'vuelidate/lib/validators';
import PageHeader from '../SettingsSubPageHeader';
const agentImg = require('assets/images/agent.svg');
export default {
props: [
'onClose',
],
components: {
PageHeader,
},
data() {
return {
agentName: '',
agentEmail: '',
agentType: this.$t('AGENT_MGMT.AGENT_TYPES')[1],
vertical: 'bottom',
horizontal: 'center',
addAgentsApi: {
showAlert: false,
showLoading: false,
message: '',
},
agentTypeList: this.$t('AGENT_MGMT.AGENT_TYPES'),
show: true,
};
},
computed: {
headerImage() {
return agentImg;
},
},
validations: {
agentName: {
required,
minLength: minLength(4),
},
agentEmail: {
required,
email,
},
agentType: {
required,
},
},
methods: {
setPageName({ name }) {
this.$v.agentType.$touch();
this.agentType = name;
},
showAlert() {
bus.$emit('newToastMessage', this.addAgentsApi.message);
},
resetForm() {
this.agentName = this.agentEmail = '';
this.$v.agentName.$reset();
this.$v.agentEmail.$reset();
},
addAgent() {
// Show loading on button
this.addAgentsApi.showLoading = true;
// Make API Calls
this.$store.dispatch('addAgent', {
name: this.agentName,
email: this.agentEmail,
role: this.agentType.name.toLowerCase(),
})
.then(() => {
// Reset Form, Show success message
this.addAgentsApi.showLoading = false;
this.addAgentsApi.message = this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE');
this.showAlert();
this.resetForm();
this.onClose();
})
.catch(() => {
this.addAgentsApi.showLoading = false;
this.addAgentsApi.message = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
this.showAlert();
});
},
},
};
</script>

View File

@@ -0,0 +1,40 @@
<template>
<modal
:show.sync="show"
:on-close="onClose"
>
<woot-modal-header
:header-title="title"
:header-content="message"
/>
<div class="modal-footer delete-item">
<button class="button" @click="onClose">
{{ rejectText }}
</button>
<button class="alert button" @click="onConfirm">
{{ confirmText }}
</button>
</div>
</modal>
</template>
<script>
import PageHeader from '../SettingsSubPageHeader';
import Modal from '../../../../components/Modal';
export default {
components: {
Modal,
PageHeader,
},
props: {
show: Boolean,
onClose: Function,
onConfirm: Function,
title: String,
message: String,
confirmText: String,
rejectText: String,
},
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<div class="column content-box">
<woot-modal-header
:header-title="pageTitle"
/>
<form class="row medium-8" v-on:submit.prevent="editAgent()">
<div class="medium-12 columns">
<label :class="{ 'error': $v.agentName.$error }">
{{ $t('AGENT_MGMT.EDIT.FORM.NAME.LABEL') }}
<input type="text" v-model.trim="agentName" @input="$v.agentName.$touch" :placeholder="$t('AGENT_MGMT.EDIT.FORM.NAME.PLACEHOLDER')">
</label>
</div>
<div class="medium-12 columns">
<label :class="{ 'error': $v.agentType.$error }">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.LABEL') }}
<multiselect
v-model.trim="agentType"
:options="agentTypeList"
label="label"
:placeholder="$t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.PLACEHOLDER')"
:searchable="false"
@select="setPageName"
/>
<span class="message" v-if="$v.agentType.$error">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.ERROR') }}
</span>
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-submit-button
:disabled="$v.agentType.$invalid || $v.agentName.$invalid || editAgentsApi.showLoading"
:button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
:loading="editAgentsApi.showLoading"
/>
<a @click="onClose">Cancel</a>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
/* global bus */
/* eslint no-console: 0 */
import { required, minLength } from 'vuelidate/lib/validators';
import PageHeader from '../SettingsSubPageHeader';
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
import Modal from '../../../../components/Modal';
export default {
components: {
PageHeader,
WootSubmitButton,
Modal,
},
props: {
id: Number,
name: String,
type: String,
onClose: Function,
},
data() {
return {
editAgentsApi: {
showAlert: false,
showLoading: false,
message: '',
},
agentTypeList: this.$t('AGENT_MGMT.AGENT_TYPES'),
agentName: this.name,
agentType: {
name: this.type,
label: this.type,
},
show: true,
};
},
validations: {
agentName: {
required,
minLength: minLength(4),
},
agentType: {
required,
},
},
computed: {
pageTitle() {
return `${this.$t('AGENT_MGMT.EDIT.TITLE')} - ${this.name}`;
},
},
methods: {
setPageName({ name }) {
this.$v.agentType.$touch();
this.agentType = name;
},
showAlert() {
bus.$emit('newToastMessage', this.editAgentsApi.message);
},
resetForm() {
this.agentName = this.agentType = '';
this.$v.agentName.$reset();
this.$v.agentType.$reset();
},
editAgent() {
// Show loading on button
this.editAgentsApi.showLoading = true;
// Make API Calls
this.$store.dispatch('editAgent', {
id: this.id,
name: this.agentName,
role: this.agentType.name.toLowerCase(),
})
.then(() => {
// Reset Form, Show success message
this.editAgentsApi.showLoading = false;
this.editAgentsApi.message = this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE');
this.showAlert();
this.resetForm();
setTimeout(() => {
this.onClose();
}, 10);
})
.catch(() => {
this.editAgentsApi.showLoading = false;
this.editAgentsApi.message = this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE');
this.showAlert();
});
},
},
};
</script>

View File

@@ -0,0 +1,224 @@
<template>
<div class="column content-box">
<button
class="button nice icon success btn-fixed-right-top"
@click="openAddPopup()"
>
<i class="icon ion-android-add-circle"></i>
{{ $t('AGENT_MGMT.HEADER_BTN_TXT') }}
</button>
<!-- Canned Response API Status -->
<!-- List Agents -->
<div class="row">
<div class="small-8 columns">
<woot-loading-state
v-if="fetchStatus"
:message="$t('AGENT_MGMT.LOADING')"
/>
<p v-if="!fetchStatus && !agentList.length">
{{ $t('AGENT_MGMT.LIST.404') }}
</p>
<table v-if="!fetchStatus && agentList.length" class="woot-table">
<tbody>
<tr v-for="(agent, index) in agentList" :key="agent.email">
<!-- Gravtar Image -->
<td>
<img class="woot-thumbnail" :src="gravatarUrl(agent.email)" />
</td>
<!-- Agent Name + Email -->
<td>
<span class="agent-name">{{ agent.name }}</span>
<span>{{ agent.email }}</span>
</td>
<!-- Agent Role + Verification Status -->
<td>
<span class="agent-name">{{ agent.role }}</span>
<span v-if="agent.confirmed">
{{ $t('AGENT_MGMT.LIST.VERIFIED') }}
</span>
<span v-if="!agent.confirmed">
{{ $t('AGENT_MGMT.LIST.VERIFICATION_PENDING') }}
</span>
</td>
<!-- Actions -->
<td>
<div v-if="showActions(agent)" class="button-wrapper">
<div @click="openEditPopup(agent)">
<woot-submit-button
:button-text="$t('AGENT_MGMT.EDIT.BUTTON_TEXT')"
icon-class="ion-edit"
button-class="link hollow grey-btn"
/>
</div>
<div @click="openDeletePopup(agent, index)">
<woot-submit-button
:button-text="$t('AGENT_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[agent.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn"
/>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="small-4 columns">
<span v-html="$t('AGENT_MGMT.SIDEBAR_TXT')"></span>
</div>
</div>
<!-- Add Agent -->
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-agent :on-close="hideAddPopup" />
</woot-modal>
<!-- Edit Agent -->
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-agent
v-if="showEditPopup"
:id="currentAgent.id"
:name="currentAgent.name"
:type="currentAgent.role"
:email="currentAgent.email"
:on-close="hideEditPopup"
/>
</woot-modal>
<!-- Delete Agent -->
<delete-agent
:show.sync="showDeletePopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('AGENT_MGMT.DELETE.CONFIRM.TITLE')"
:message="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
<!-- Loader Status -->
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import md5 from 'md5';
import AddAgent from './AddAgent';
import EditAgent from './EditAgent';
import DeleteAgent from './DeleteAgent';
export default {
components: {
AddAgent,
EditAgent,
DeleteAgent,
},
data() {
return {
loading: {},
showAddPopup: false,
showDeletePopup: false,
showEditPopup: false,
agentAPI: {
message: '',
},
currentAgent: {},
};
},
computed: {
...mapGetters({
agentList: 'getAgents',
fetchStatus: 'getAgentFetchStatus',
}),
deleteConfirmText() {
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.YES')} ${
this.currentAgent.name
}`;
},
deleteRejectText() {
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.NO')} ${
this.currentAgent.name
}`;
},
deleteMessage() {
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.currentAgent.name
} ?`;
},
},
mounted() {
this.$store.dispatch('fetchAgents');
},
methods: {
showActions(agent) {
if (agent.role === 'administrator') {
const adminList = this.agentList.filter(
item => item.role === 'administrator'
);
return adminList.length !== 1;
}
return true;
},
// List Functions
// Gravatar URL
gravatarUrl(email) {
const hash = md5(email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?d=monsterid`;
},
// Edit Function
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
// Edit Function
openEditPopup(agent) {
this.showEditPopup = true;
this.currentAgent = agent;
},
hideEditPopup() {
this.showEditPopup = false;
},
// Delete Function
openDeletePopup(agent) {
this.showDeletePopup = true;
this.currentAgent = agent;
},
closeDeletePopup() {
this.showDeletePopup = false;
},
confirmDeletion() {
this.loading[this.currentAgent.id] = true;
this.closeDeletePopup();
this.deleteAgent(this.currentAgent.id);
},
deleteAgent(id) {
this.$store
.dispatch('deleteAgent', {
id,
})
.then(() => {
this.showAlert(this.$t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
})
.catch(() => {
this.showAlert(this.$t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
});
},
// Show SnackBar
showAlert(message) {
// Reset loading, current selected agent
this.loading[this.currentAgent.id] = false;
this.currentAgent = {};
// Show message
this.agentAPI.message = message;
bus.$emit('newToastMessage', message);
},
},
};
</script>

View File

@@ -0,0 +1,30 @@
import SettingsContent from '../Wrapper';
import AgentHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('settings/agents'),
component: SettingsContent,
props: {
headerTitle: 'AGENT_MGMT.HEADER',
icon: 'ion-person-stalker',
showNewButton: false,
},
children: [
{
path: '',
name: 'agents_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'agent_list',
component: AgentHome,
roles: ['administrator'],
},
],
},
],
};

View File

@@ -0,0 +1,22 @@
<template>
<div class="column content-box account-locked">
<div class="lock-message">
<!-- No inboxes attached -->
<div>
<img src="~dashboard/assets/images/lock.svg" alt="Lock" />
<span v-html="$t('BILLING.ACCOUNT_LOCKED')">
</span>
</div>
</div>
</div>
</template>
<script>
/* eslint no-console: 0 */
/* global bus */
export default {
props: ['state'],
};
</script>

View File

@@ -0,0 +1,120 @@
<template>
<div class="column content-box billing">
<woot-loading-state v-if="fetchStatus" :message="$t('BILLING.LOADING')" />
<div class="row" v-if="billingDetails">
<div class="small-12 columns billing__stats">
<div class="account-row">
<span class="title">{{ $t('BILLING.ACCOUNT_STATE') }}</span>
<span class="value">{{ billingDetails.state }} </span>
</div>
<div class="account-row">
<span class="title">{{ $t('BILLING.AGENT_COUNT') }}</span>
<span class="value">{{ billingDetails.agents_count }} </span>
</div>
<div class="account-row">
<span class="title">{{ $t('BILLING.PER_AGENT_COST') }}</span>
<span class="value">${{ billingDetails.per_agent_cost }} </span>
</div>
<div class="account-row">
<span class="title">{{ $t('BILLING.TOTAL_COST') }}</span>
<span class="value">${{ billingDetails.total_cost }} </span>
</div>
</div>
<div class="small-12 columns billing__form">
<iframe :src="billingDetails.iframe_url" v-if="iframeUrl && !isShowEmptyState"></iframe>
<div v-if="isShowEmptyState">
<empty-state :title="emptyStateTitle" :message="emptyStateMessage">
<div class="medium-12 columns text-center">
<button class="button success nice" @click="billingButtonClick()">{{buttonText}}</button>
</div>
</empty-state>
</div>
</div>
</div>
</div>
</template>
<script>
/* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex';
import EmptyState from '../../../../components/widgets/EmptyState';
export default {
props: ['state'],
data() {
return {
is_adding_source: false,
};
},
components: {
EmptyState,
},
computed: {
...mapGetters({
billingDetails: 'getBillingDetails',
fetchStatus: 'billingFetchStatus',
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
}),
redirectMessage() {
if (!this.state) {
return '';
}
if (this.state === 'succeeded') {
return this.$t('BILLING.STATUS.SUCCESS');
}
return this.$t('BILLING.STATUS.ERROR');
},
iframeUrl() {
return typeof this.billingDetails.iframe_url === 'string';
},
isShowEmptyState() {
if (this.billingDetails !== null) {
if (this.is_adding_source) {
return false;
}
}
return true;
},
buttonText() {
if (this.billingDetails !== null) {
return this.billingDetails.payment_source_added ? this.$t('BILLING.BUTTON.EDIT') : this.$t('BILLING.BUTTON.ADD');
}
return this.$t('BILLING.BUTTON.ADD');
},
emptyStateTitle() {
if (this.daysLeft <= 0 || this.subscriptionData.state === 'cancelled') {
return this.$t('BILLING.TRIAL.TITLE');
}
return '';
},
emptyStateMessage() {
if (this.daysLeft <= 0 || this.subscriptionData.state === 'cancelled') {
return this.$t('BILLING.TRIAL.MESSAGE');
}
return '';
},
},
mounted() {
if (this.state) {
bus.$emit('newToastMessage', this.redirectMessage);
}
this.$store.dispatch('fetchSubscription');
},
methods: {
billingButtonClick() {
this.is_adding_source = true;
},
},
};
</script>

View File

@@ -0,0 +1,32 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import AccountLocked from './AccountLocked';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('settings/billing'),
component: SettingsContent,
props: {
headerTitle: 'BILLING.HEADER',
icon: 'ion-card',
},
children: [
{
path: '',
name: 'billing',
component: Index,
roles: ['administrator'],
props: route => ({ state: route.query.state }),
},
],
},
{
path: '/deactivated',
name: 'billing_deactivated',
component: AccountLocked,
roles: ['agent'],
},
],
};

View File

@@ -0,0 +1,128 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<div class="column content-box">
<woot-modal-header
:header-image="headerImage"
:header-title="$t('CANNED_MGMT.ADD.TITLE')"
:header-content="$t('CANNED_MGMT.ADD.DESC')"
/>
<form class="row" v-on:submit.prevent="addAgent()">
<div class="medium-12 columns">
<label :class="{ 'error': $v.shortCode.$error }">
{{ $t('CANNED_MGMT.ADD.FORM.SHORT_CODE.LABEL') }}
<input type="text" v-model.trim="shortCode" @input="$v.shortCode.$touch" :placeholder="$t('CANNED_MGMT.ADD.FORM.SHORT_CODE.PLACEHOLDER')">
</label>
</div>
<div class="medium-12 columns">
<label :class="{ 'error': $v.content.$error }">
{{ $t('CANNED_MGMT.ADD.FORM.CONTENT.LABEL') }}
<input type="text" v-model.trim="content" @input="$v.content.$touch" :placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')">
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-submit-button
:disabled="$v.content.$invalid || $v.shortCode.$invalid || addCanned.showLoading"
:button-text="$t('CANNED_MGMT.ADD.FORM.SUBMIT')"
:loading="addCanned.showLoading"
/>
<a @click="onClose">Cancel</a>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
/* global bus */
/* eslint no-console: 0 */
import { required, minLength } from 'vuelidate/lib/validators';
import PageHeader from '../SettingsSubPageHeader';
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
import Modal from '../../../../components/Modal';
const cannedImg = require('assets/images/canned.svg');
export default {
props: ['onClose'],
components: {
PageHeader,
WootSubmitButton,
Modal,
},
data() {
return {
shortCode: '',
content: '',
agentType: '',
vertical: 'bottom',
horizontal: 'center',
addCanned: {
showAlert: false,
showLoading: false,
message: '',
},
agentTypeList: this.$t('CANNED_MGMT.AGENT_TYPES'),
show: true,
};
},
computed: {
headerImage() {
return cannedImg;
},
},
validations: {
shortCode: {
required,
minLength: minLength(2),
},
content: {
required,
},
agentType: {
required,
},
},
methods: {
setPageName({ name }) {
this.$v.agentType.$touch();
this.agentType = name;
},
showAlert() {
bus.$emit('newToastMessage', this.addCanned.message);
},
resetForm() {
this.shortCode = this.content = '';
this.$v.shortCode.$reset();
this.$v.content.$reset();
},
addAgent() {
// Show loading on button
this.addCanned.showLoading = true;
// Make API Calls
this.$store.dispatch('addCannedResponse', {
short_code: this.shortCode,
content: this.content,
})
.then(() => {
// Reset Form, Show success message
this.addCanned.showLoading = false;
this.addCanned.message = this.$t('CANNED_MGMT.ADD.API.SUCCESS_MESSAGE');
this.showAlert();
this.resetForm();
this.onClose();
})
.catch(() => {
this.addCanned.showLoading = false;
this.addCanned.message = this.$t('CANNED_MGMT.ADD.API.ERROR_MESSAGE');
this.showAlert();
});
},
},
};
</script>

View File

@@ -0,0 +1,41 @@
<template>
<modal
:show.sync="show"
:on-close="onClose"
>
<woot-modal-header
:header-title="title"
:header-content="message"
/>
<div class="modal-footer delete-item">
<button class="button" @click="onClose">
{{ rejectText }}
</button>
<button class="alert button" @click="onConfirm">
{{ confirmText }}
</button>
</div>
</modal>
</template>
<script>
import PageHeader from '../SettingsSubPageHeader';
import Modal from '../../../../components/Modal';
export default {
components: {
Modal,
PageHeader,
},
props: {
show: Boolean,
onClose: Function,
onConfirm: Function,
title: String,
message: String,
confirmText: String,
rejectText: String,
},
};
</script>

View File

@@ -0,0 +1,123 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<div class="column content-box">
<woot-modal-header
:header-title="pageTitle"
/>
<form class="row medium-8" v-on:submit.prevent="editCannedResponse()">
<div class="medium-12 columns">
<label :class="{ 'error': $v.shortCode.$error }">
{{ $t('CANNED_MGMT.EDIT.FORM.SHORT_CODE.LABEL') }}
<input type="text" v-model.trim="shortCode" @input="$v.shortCode.$touch" :placeholder="$t('CANNED_MGMT.EDIT.FORM.SHORT_CODE.PLACEHOLDER')">
</label>
</div>
<div class="medium-12 columns">
<label :class="{ 'error': $v.content.$error }">
{{ $t('CANNED_MGMT.EDIT.FORM.CONTENT.LABEL') }}
<input type="text" v-model.trim="content" @input="$v.content.$touch" :placeholder="$t('CANNED_MGMT.EDIT.FORM.CONTENT.PLACEHOLDER')">
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-submit-button
:disabled="$v.content.$invalid || $v.shortCode.$invalid || editCanned.showLoading"
:button-text="$t('CANNED_MGMT.EDIT.FORM.SUBMIT')"
:loading="editCanned.showLoading"
/>
<a @click="onClose">Cancel</a>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
/* global bus */
/* eslint no-console: 0 */
import { required, minLength } from 'vuelidate/lib/validators';
import PageHeader from '../SettingsSubPageHeader';
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
import Modal from '../../../../components/Modal';
export default {
components: {
PageHeader,
WootSubmitButton,
Modal,
},
props: {
id: Number,
edcontent: String,
edshortCode: String,
onClose: Function,
},
data() {
return {
editCanned: {
showAlert: false,
showLoading: false,
message: '',
},
shortCode: this.edshortCode,
content: this.edcontent,
show: true,
};
},
validations: {
shortCode: {
required,
minLength: minLength(2),
},
content: {
required,
},
},
computed: {
pageTitle() {
return `${this.$t('CANNED_MGMT.EDIT.TITLE')} - ${this.edshortCode}`;
},
},
methods: {
setPageName({ name }) {
this.$v.content.$touch();
this.content = name;
},
showAlert() {
bus.$emit('newToastMessage', this.editCanned.message);
},
resetForm() {
this.shortCode = this.content = '';
this.$v.shortCode.$reset();
this.$v.content.$reset();
},
editCannedResponse() {
// Show loading on button
this.editCanned.showLoading = true;
// Make API Calls
this.$store.dispatch('editCannedResponse', {
id: this.id,
name: this.shortCode,
content: this.content,
})
.then(() => {
// Reset Form, Show success message
this.editCanned.showLoading = false;
this.editCanned.message = this.$t('CANNED_MGMT.EDIT.API.SUCCESS_MESSAGE');
this.showAlert();
this.resetForm();
setTimeout(() => {
this.onClose();
}, 10);
})
.catch(() => {
this.editCanned.showLoading = false;
this.editCanned.message = this.$t('CANNED_MGMT.EDIT.API.ERROR_MESSAGE');
this.showAlert();
});
},
},
};
</script>

View File

@@ -0,0 +1,207 @@
<template>
<div class="column content-box">
<button
class="button nice icon success btn-fixed-right-top"
@click="openAddPopup()"
>
<i class="icon ion-android-add-circle"></i>
{{ $t('CANNED_MGMT.HEADER_BTN_TXT') }}
</button>
<!-- List Canned Response -->
<div class="row">
<div class="small-8 columns">
<p
v-if="!fetchStatus && !cannedResponseList.length"
class="no-items-error-message"
>
{{ $t('CANNED_MGMT.LIST.404') }}
</p>
<woot-loading-state
v-if="fetchStatus"
:message="$t('CANNED_MGMT.LOADING')"
/>
<table
v-if="!fetchStatus && cannedResponseList.length"
class="woot-table"
>
<thead>
<!-- Header -->
<th
v-for="thHeader in $t('CANNED_MGMT.LIST.TABLE_HEADER')"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<tr
v-for="(cannedItem, index) in cannedResponseList"
:key="cannedItem.short_code"
>
<!-- Short Code -->
<td>{{ cannedItem.short_code }}</td>
<!-- Content -->
<td>{{ cannedItem.content }}</td>
<!-- Action Buttons -->
<td class="button-wrapper">
<div @click="openEditPopup(cannedItem)">
<woot-submit-button
:button-text="$t('CANNED_MGMT.EDIT.BUTTON_TEXT')"
icon-class="ion-edit"
button-class="link hollow grey-btn"
/>
</div>
<div @click="openDeletePopup(cannedItem, index)">
<woot-submit-button
:button-text="$t('CANNED_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[cannedItem.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="small-4 columns">
<span v-html="$t('CANNED_MGMT.SIDEBAR_TXT')"></span>
</div>
</div>
<!-- Add Agent -->
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-canned :on-close="hideAddPopup" />
</woot-modal>
<!-- Edit Canned Response -->
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-canned
v-if="showEditPopup"
:id="selectedResponse.id"
:edshort-code="selectedResponse.short_code"
:edcontent="selectedResponse.content"
:on-close="hideEditPopup"
/>
</woot-modal>
<!-- Delete Canned Response -->
<delete-canned
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('CANNED_MGMT.DELETE.CONFIRM.TITLE')"
:message="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import AddCanned from './AddCanned';
import EditCanned from './EditCanned';
import DeleteCanned from './DeleteCanned';
export default {
components: {
AddCanned,
EditCanned,
DeleteCanned,
},
data() {
return {
loading: {},
showAddPopup: false,
showEditPopup: false,
showDeleteConfirmationPopup: false,
selectedResponse: {},
cannedResponseAPI: {
message: '',
},
};
},
computed: {
...mapGetters({
cannedResponseList: 'getCannedResponses',
fetchStatus: 'getCannedFetchStatus',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedResponse.short_code
}`;
},
deleteRejectText() {
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedResponse.short_code
}`;
},
deleteMessage() {
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedResponse.short_code
} ?`;
},
},
mounted() {
// Fetch API Call
this.$store.dispatch('fetchCannedResponse');
},
methods: {
showAlert(message) {
// Reset loading, current selected agent
this.loading[this.selectedResponse.id] = false;
this.selectedResponse = {};
// Show message
this.cannedResponseAPI.message = message;
bus.$emit('newToastMessage', message);
},
// Edit Function
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
// Edit Modal Functions
openEditPopup(response) {
this.showEditPopup = true;
this.selectedResponse = response;
},
hideEditPopup() {
this.showEditPopup = false;
},
// Delete Modal Functions
openDeletePopup(response) {
this.showDeleteConfirmationPopup = true;
this.selectedResponse = response;
},
closeDeletePopup() {
this.showDeleteConfirmationPopup = false;
},
// Set loading and call Delete API
confirmDeletion() {
this.loading[this.selectedResponse.id] = true;
this.closeDeletePopup();
this.deleteCannedResponse(this.selectedResponse.id);
},
deleteCannedResponse(id) {
this.$store
.dispatch('deleteCannedResponse', {
id,
})
.then(() => {
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.SUCCESS_MESSAGE'));
})
.catch(() => {
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.ERROR_MESSAGE'));
});
},
},
};
</script>

View File

@@ -0,0 +1,30 @@
import SettingsContent from '../Wrapper';
import CannedHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('settings/canned-response'),
component: SettingsContent,
props: {
headerTitle: 'CANNED_MGMT.HEADER',
icon: 'ion-chatbox-working',
showNewButton: false,
},
children: [
{
path: '',
name: 'canned_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'canned_list',
roles: ['administrator', 'agent'],
component: CannedHome,
},
],
},
],
};

View File

@@ -0,0 +1,88 @@
<template>
<div class="wizard-body columns content-box small-9">
<loading-state :message="emptyStateMessage" v-if="showLoader"></loading-state>
<form class="row" v-on:submit.prevent="addAgents()" v-if="!showLoader">
<div class="medium-12 columns">
<page-header
:header-title="$t('INBOX_MGMT.ADD.AGENTS.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.AGENTS.DESC')"
/>
</div>
<div class="medium-7 columns">
<div class="medium-12 columns">
<label :class="{ 'error': $v.selectedAgents.$error }">Agents
<multiselect v-model="selectedAgents" :options="agentList" track-by="id" label="name" :multiple="true" :close-on-select="false" :clear-on-select="false" :hide-selected="true" placeholder="Pick some" @select="$v.selectedAgents.$touch"></multiselect>
<span class="message" v-if="$v.selectedAgents.$error">Add atleast one agent to your new Inbox</span>
</label>
</div>
<div class="medium-12 columns text-right">
<input type="submit" value="Create Inbox" class="button">
</div>
</div>
</form>
</div>
</template>
<script>
/* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex';
import ChannelItem from '../../../../components/widgets/ChannelItem';
import ChannelApi from '../../../../api/channels';
import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader';
import LoadingState from '../../../../components/widgets/LoadingState';
export default {
components: {
ChannelItem,
PageHeader,
LoadingState,
},
validations: {
selectedAgents: {
isEmpty() {
return !!this.selectedAgents.length;
},
},
},
data() {
return {
emptyStateMessage: this.$t('INBOX_MGMT.AGENTS.ADD_AGENTS'),
showLoader: false,
selectedAgents: [],
};
},
computed: {
...mapGetters({
agentList: 'getAgents',
}),
},
mounted() {
this.$store.dispatch('fetchAgents');
},
methods: {
addAgents() {
this.isCreating = true;
const inboxId = this.$route.params.inbox_id;
ChannelApi.addAgentsToChannel(inboxId, this.selectedAgents.map(x => x.id))
.then(() => {
this.isCreating = false;
router.replace({ name: 'settings_inbox_finish', params: { page: 'new', inbox_id: this.$route.params.inbox_id } });
}).catch((error) => {
bus.$emit('newToastMessage', error.message);
this.isCreating = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="wizard-body small-9 columns">
<page-header
:header-title="$t('INBOX_MGMT.ADD.AUTH.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.AUTH.DESC')"
/>
<div class="row channels">
<channel-item
v-for="channel in channelList"
:key="channel"
:channel="channel"
/>
</div>
</div>
</template>
<script>
/* global bus */
import ChannelItem from '../../../../components/widgets/ChannelItem';
import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader';
export default {
components: {
ChannelItem,
PageHeader,
},
data() {
return {
channelList: ['facebook', 'twitter', 'telegram', 'line'],
};
},
created() {
bus.$on('channelItemClick', channel => {
this.initChannelAuth(channel);
});
},
methods: {
initChannelAuth(channel) {
if (channel === 'facebook') {
router.push({
name: 'settings_inboxes_page_channel',
params: { page: 'new', sub_page: 'facebook' },
});
}
},
},
};
</script>

View File

@@ -0,0 +1,41 @@
<template>
<modal
:show.sync="show"
:on-close="onClose"
>
<woot-modal-header
:header-title="title"
:header-content="message"
/>
<div class="modal-footer delete-item">
<button class="button" @click="onClose">
{{ rejectText }}
</button>
<button class="alert button" @click="onConfirm">
{{ confirmText }}
</button>
</div>
</modal>
</template>
<script>
import PageHeader from '../SettingsSubPageHeader';
import Modal from '../../../../components/Modal';
export default {
components: {
Modal,
PageHeader,
},
props: {
show: Boolean,
onClose: Function,
onConfirm: Function,
title: String,
message: String,
confirmText: String,
rejectText: String,
},
};
</script>

View File

@@ -0,0 +1,212 @@
<template>
<div class="wizard-body columns content-box small-9">
<div class="login-init full-height" v-if="!hasLoginStarted">
<a href="#" @click="startLogin()"><img src="~dashboard/assets/images/channels/facebook_login.png" alt="Facebook-logo"/></a>
<p>{{ $t('INBOX_MGMT.ADD.FB.HELP') }}</p>
</div>
<div v-else>
<loading-state :message="emptyStateMessage" v-if="showLoader"></loading-state>
<form class="row" v-on:submit.prevent="createChannel()" v-if="!showLoader">
<div class="medium-12 columns">
<page-header
:header-title="$t('INBOX_MGMT.ADD.DETAILS.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.DETAILS.DESC')"
/>
</div>
<div class="medium-7 columns">
<div class="medium-12 columns">
<div class="input-wrap" :class="{ 'error': $v.selectedPage.$error }">Choose Page
<multiselect
v-model.trim="selectedPage"
:close-on-select="true"
:allow-empty="true"
:options="getSelectablePages"
track-by="id"
label="name"
placeholder="Pick a value"
selected-label=''
@select="setPageName"
/>
<span class="message" v-if="$v.selectedPage.$error">Select a page from the list</span>
</div>
</div>
<div class="medium-12 columns">
<label :class="{ 'error': $v.pageName.$error }">Inbox Name
<input type="text" v-model.trim="pageName" @input="$v.pageName.$touch" placeholder="Pick A Name Your Inbox">
<span class="message" v-if="$v.pageName.$error">Add a name for your inbox</span>
</label>
</div>
<div class="medium-12 columns text-right">
<input type="submit" value="Create Inbox" class="button">
</div>
</div>
</form>
</div>
</div>
</template>
<script>
/* eslint no-console: 0 */
/* eslint-env browser */
/* global FB */
/* global bus */
/* global $v, __FB_ID__ */
import { required } from 'vuelidate/lib/validators';
import ChannelApi from '../../../../api/channels';
import LoadingState from '../../../../components/widgets/LoadingState';
import PageHeader from '../SettingsSubPageHeader';
import router from '../../../index';
export default {
components: {
LoadingState,
PageHeader,
},
data() {
return {
isCreating: false,
omniauth_token: '',
user_access_token: '',
channel: 'facebook',
selectedPage: { name: null, id: null },
pageName: '',
pageList: [],
emptyStateMessage: this.$t('INBOX_MGMT.DETAILS.LOADING_FB'),
hasLoginStarted: false,
};
},
validations: {
pageName: {
required,
},
selectedPage: {
isEmpty() {
return this.selectedPage !== null && !!this.selectedPage.name;
},
},
},
created() {
this.initFB();
this.loadFBsdk();
},
computed: {
showLoader() {
return !this.user_access_token || this.isCreating;
},
getSelectablePages() {
return this.pageList.filter(item => (!item.exists));
},
},
mounted() {
this.initFB();
},
methods: {
startLogin() {
this.hasLoginStarted = true;
this.tryFBlogin();
},
setPageName({ name }) {
this.$v.selectedPage.$touch();
this.pageName = name;
},
initChannelAuth(channel) {
if (channel === 'facebook') {
this.loadFBsdk();
}
},
initFB() {
if (window.fbSDKLoaded === undefined) {
window.fbAsyncInit = () => {
FB.init({
appId: __FB_ID__,
xfbml: true,
version: 'v2.8',
status: true,
});
window.fbSDKLoaded = true;
FB.AppEvents.logPageView();
// this.tryFBlogin();
};
} else {
// this.tryFBlogin();
}
},
loadFBsdk() {
((d, s, id) => {
let js;
const fjs = js = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s);
js.id = id;
js.src = '//connect.facebook.net/en_US/sdk.js';
fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');
},
tryFBlogin() {
FB.login((response) => {
if (response.status === 'connected') {
this.fetchPages(response.authResponse.accessToken);
} else if (response.status === 'not_authorized') {
// The person is logged into Facebook, but not your app.
this.emptyStateMessage = this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH');
} else {
// The person is not logged into Facebook, so we're not sure if
// they are logged into this app or not.
this.emptyStateMessage = this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH');
}
}, { scope: 'manage_pages,read_page_mailboxes,pages_messaging,pages_messaging_phone_number' });
},
fetchPages(_token) {
ChannelApi.fetchFacebookPages(_token).then((response) => {
this.pageList = response.data.data.page_details;
this.user_access_token = response.data.data.user_access_token;
}).catch();
},
channelParams() {
return {
user_access_token: this.user_access_token,
page_access_token: this.selectedPage.access_token,
page_name: this.selectedPage.name,
page_id: this.selectedPage.id,
inbox_name: this.pageName,
};
},
createChannel() {
this.$v.$touch();
if (!this.$v.$error) {
this.emptyStateMessage = this.$t('INBOX_MGMT.DETAILS.CREATING_CHANNEL');
this.isCreating = true;
this.$store.dispatch('addInboxItem', {
channel: this.channel,
params: this.channelParams(),
}).then((response) => {
console.log(response);
router.replace({ name: 'settings_inboxes_add_agents', params: { page: 'new', inbox_id: response.data.id } });
}).catch((error) => {
console.log(error);
this.isCreating = false;
});
}
},
},
};
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div class="wizard-body columns content-box small-9">
<empty-state :title="$t('INBOX_MGMT.FINISH.TITLE')" :message="$t('INBOX_MGMT.FINISH.MESSAGE')" :buttonText="$t('INBOX_MGMT.FINISH.BUTTON_TEXT')">
<div class="medium-12 columns text-center">
<router-link class="button success nice" :to="{ name: 'inbox_dashboard', params: { inboxId: this.$route.params.inbox_id }}">{{$t('INBOX_MGMT.FINISH.BUTTON_TEXT')}}</router-link>
</div>
</empty-state>
</div>
</template>
<script>
import EmptyState from '../../../../components/widgets/EmptyState';
export default {
components: {
EmptyState,
},
};
</script>

View File

@@ -0,0 +1,18 @@
<template>
<div class="row content-box full-height">
<woot-wizard class="small-3 columns" :items="wizardItems"></woot-wizard>
<router-view></router-view>
</div>
</template>
<script>
/* eslint no-console: 0 */
/* eslint-env browser */
export default {
data() {
return {
wizardItems: this.$t('INBOX_MGMT.CREATE_FLOW'),
};
},
};
</script>

View File

@@ -0,0 +1,171 @@
<template>
<div class="column content-box">
<!-- List Canned Response -->
<div class="row">
<div class="small-8 columns">
<p v-if="!inboxesList.length" class="no-items-error-message">
{{ $t('INBOX_MGMT.LIST.404') }}
<router-link
v-if="isAdmin()"
:to="frontendURL('settings/inboxes/new')"
>
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
</router-link>
</p>
<table v-if="inboxesList.length" class="woot-table">
<tbody>
<tr v-for="item in inboxesList" :key="item.label">
<td>
<img
class="woot-thumbnail"
:src="item.avatarUrl"
alt="No Page Image"
/>
</td>
<!-- Short Code -->
<td>
<span class="agent-name">{{ item.label }}</span>
<span>Facebook</span>
</td>
<!-- Action Buttons -->
<td>
<div class="button-wrapper">
<div v-if="isAdmin()" @click="openSettings(item)">
<woot-submit-button
:button-text="$t('INBOX_MGMT.SETTINGS')"
icon-class="ion-gear-b"
button-class="link hollow grey-btn"
/>
</div>
<!-- <div>
<woot-submit-button
:button-text="$t('INBOX_MGMT.REAUTH')"
icon-class="ion-edit"
button-class="link hollow grey-btn"
/>
</div> -->
<div v-if="isAdmin()" @click="openDelete(item)">
<woot-submit-button
:button-text="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[item.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn"
/>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="small-4 columns">
<span v-html="$t('INBOX_MGMT.SIDEBAR_TXT')"></span>
</div>
</div>
<settings
v-if="showSettings"
:show.sync="showSettings"
:on-close="closeSettings"
:inbox="selectedInbox"
/>
<delete-inbox
:show.sync="showDeletePopup"
:on-close="closeDelete"
:on-confirm="confirmDeletion"
:title="$t('INBOX_MGMT.DELETE.CONFIRM.TITLE')"
:message="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import Settings from './Settings';
import DeleteInbox from './DeleteInbox';
import adminMixin from '../../../../mixins/isAdmin';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
components: {
Settings,
DeleteInbox,
},
mixins: [adminMixin],
data() {
return {
loading: {},
showSettings: false,
showDeletePopup: false,
selectedInbox: {},
};
},
computed: {
...mapGetters({
inboxesList: 'getInboxesList',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedInbox.label
}`;
},
deleteRejectText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedInbox.label
}`;
},
deleteMessage() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedInbox.label
} ?`;
},
},
methods: {
openSettings(inbox) {
this.showSettings = true;
this.selectedInbox = inbox;
},
closeSettings() {
this.showSettings = false;
this.selectedInbox = {};
},
deleteInbox({ channel_id }) {
this.$store
.dispatch('deleteInbox', channel_id)
.then(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
)
)
.catch(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
)
);
},
confirmDeletion() {
this.deleteInbox(this.selectedInbox);
this.closeDelete();
},
openDelete(inbox) {
this.showDeletePopup = true;
this.selectedInbox = inbox;
},
closeDelete() {
this.showDeletePopup = false;
this.selectedInbox = {};
},
frontendURL,
},
};
</script>

View File

@@ -0,0 +1,142 @@
<template>
<woot-modal class-name="settings-modal" :show.sync="show" :on-close="onClose">
<div class="settings">
<woot-modal-header
:header-image="inbox.avatarUrl"
:header-title="inbox.label"
/>
<div class="code-wrapper">
<p class="title">{{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING') }}</p>
<p class="sub-head">{{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD') }}</p>
<p class="code">
<code>
{{ messengerScript }}
</code>
</p>
</div>
<div class="agent-wrapper">
<p class="title">{{ $t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS') }}</p>
<p class="sub-head">{{ $t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT') }}</p>
<multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
placeholder="Pick some"
@select="$v.selectedAgents.$touch"
/>
<div @click="updateAgents()">
<woot-submit-button
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:loading="isUpdating"
/>
</div>
</div>
</div>
</div>
</woot-modal>
</template>
<script>
/* global bus, __FB_ID__ */
/* eslint no-console: 0 */
/* eslint-disable no-useless-escape */
import { mapGetters } from 'vuex';
import PageHeader from '../SettingsSubPageHeader';
export default {
props: [
'onClose',
'inbox',
'show',
],
components: {
PageHeader,
},
data() {
return {
selectedAgents: [],
isUpdating: false,
messengerScript: `<script>
window.fbAsyncInit = function() {
FB.init({
appId: "${__FB_ID__}",
xfbml: true,
version: "v2.6"
});
};
(function(d, s, id){
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) { return; }
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
<\/script>
<div class="fb-messengermessageus"
messenger_app_id="${__FB_ID__}"
page_id="${this.inbox.pageId}"
color="blue"
size="standard" >
</div>`,
};
},
computed: {
...mapGetters({
agentList: 'getAgents',
}),
},
mounted() {
this.$store.dispatch('fetchAgents')
.then(() => {
this.fetchAttachedAgents();
});
},
methods: {
fetchAttachedAgents() {
this.$store.dispatch('listInboxAgents', {
inboxId: this.inbox.channel_id,
})
.then((response) => {
const { payload } = response.data;
payload.forEach((el) => {
const [item] = this.agentList.filter(agent => agent.id === el.user_id);
if (item) this.selectedAgents.push(item);
});
})
.catch((error) => {
console.log(error);
});
},
updateAgents() {
const agentList = this.selectedAgents.map(el => el.id);
this.isUpdating = true;
this.$store.dispatch('updateInboxAgents', {
inboxId: this.inbox.channel_id,
agentList,
}).then(() => {
this.isUpdating = false;
bus.$emit('newToastMessage', this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
}).catch(() => {
this.isUpdating = false;
bus.$emit('newToastMessage', this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
});
},
},
validations: {
selectedAgents: {
isEmpty() {
return !!this.selectedAgents.length;
},
},
},
};
</script>

View File

@@ -0,0 +1,20 @@
import CONSTANTS from '../../../../constants';
import FacebookView from './Facebook';
export default {
create() {
return {
name: 'new-channel-view',
render(h) {
if (this.channel_name === CONSTANTS.CHANNELS.FACEBOOK) {
return h(FacebookView);
}
return null;
},
props: {
channel_name: String,
},
};
},
};

View File

@@ -0,0 +1,70 @@
/* eslint arrow-body-style: 0 */
import SettingsContent from '../Wrapper';
import InboxHome from './Index';
import InboxChannel from './InboxChannels';
import ChannelList from './ChannelList';
import channelFactory from './channel-factory';
import AddAgents from './AddAgents';
import FinishSetup from './FinishSetup';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('settings/inboxes'),
component: SettingsContent,
props: {
headerTitle: 'INBOX_MGMT.HEADER',
headerButtonText: 'SETTINGS.INBOXES.NEW_INBOX',
icon: 'ion-archive',
newButtonRoutes: ['settings_inbox_list'],
},
children: [
{
path: '',
name: 'settings_inbox',
redirect: 'list',
},
{
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
roles: ['administrator', 'agent'],
},
{
path: 'new',
component: InboxChannel,
children: [
{
path: '',
name: 'settings_inbox_new',
component: ChannelList,
roles: ['administrator'],
},
{
path: ':inbox_id/finish',
name: 'settings_inbox_finish',
component: FinishSetup,
roles: ['administrator'],
},
{
path: ':sub_page',
name: 'settings_inboxes_page_channel',
component: channelFactory.create(),
roles: ['administrator'],
props: route => {
return { channel_name: route.params.sub_page };
},
},
{
path: ':inbox_id/agents',
name: 'settings_inboxes_add_agents',
roles: ['administrator'],
component: AddAgents,
},
],
},
],
},
],
};

View File

@@ -0,0 +1,128 @@
<template>
<div class="column content-box">
<div class="small-3 pull-right">
<multiselect
v-model="currentDateRangeSelection"
track-by="name"
label="name"
placeholder="Select one"
:options="dateRange"
:searchable="false"
:allow-empty="true"
@select="changeDateSelection"
/>
</div>
<div class="row">
<woot-report-stats-card
v-for="(metric, index) in metrics"
:key="metric.NAME"
:desc="metric.DESC"
:heading="metric.NAME"
:index="index"
:on-click="changeSelection"
:point="accountSummary[metric.KEY]"
:selected="index === currentSelection"
/>
</div>
<div class="report-bar">
<woot-loading-state
v-if="accountReport.isFetching"
:message="$t('REPORT.LOADING_CHART')"
/>
<div v-else class="chart-container">
<woot-bar v-if="accountReport.data.length" :collection="collection" />
<span v-else class="empty-state">
{{ $t('REPORT.NO_ENOUGH_DATA') }}
</span>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import moment from 'moment';
export default {
data() {
return {
currentSelection: 0,
currentDateRangeSelection: this.$t('REPORT.DATE_RANGE')[0],
dateRange: this.$t('REPORT.DATE_RANGE'),
};
},
computed: {
...mapGetters({
accountSummary: 'getAccountSummary',
accountReport: 'getAccountReports',
}),
to() {
const m = moment.utc();
m.set({ hour: 23, minute: 59, second: 59, millisecond: 999 });
return m.unix();
},
from() {
const diff = this.currentDateRangeSelection.id ? 29 : 6;
const m = moment.utc().subtract(diff, 'days');
m.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
return m.unix();
},
collection() {
if (this.accountReport.isFetching) {
return {};
}
if (!this.accountReport.data.length) return {};
const labels = this.accountReport.data.map(element =>
moment.unix(element.timestamp).format('DD/MMM')
);
const data = this.accountReport.data.map(element => element.value);
return {
labels,
datasets: [
{
label: this.metrics[this.currentSelection].NAME,
backgroundColor: '#1f93ff',
data,
},
],
};
},
metrics() {
return this.$t('REPORT.METRICS');
},
},
mounted() {
this.fetchAllData();
},
methods: {
fetchAllData() {
const { from, to } = this;
this.$store.dispatch('fetchAccountSummary', {
from,
to,
});
this.$store.dispatch('fetchAccountReport', {
metric: this.metrics[this.currentSelection].KEY,
from,
to,
});
},
changeDateSelection(selectedRange) {
this.currentDateRangeSelection = selectedRange;
this.fetchAllData();
},
changeSelection(index) {
this.currentSelection = index;
this.fetchChartData();
},
fetchChartData() {
const { from, to } = this;
this.$store.dispatch('fetchAccountReport', {
metric: this.metrics[this.currentSelection].KEY,
from,
to,
});
},
},
};
</script>

View File

@@ -0,0 +1,25 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('reports'),
component: SettingsContent,
props: {
headerTitle: 'REPORT.HEADER',
headerButtonText: 'REPORT.HEADER_BTN_TXT',
icon: 'ion-arrow-graph-up-right',
},
children: [
{
path: '',
name: 'settings_account_reports',
roles: ['administrator'],
component: Index,
},
],
},
],
};

View File

@@ -0,0 +1,28 @@
import agent from './agents/agent.routes';
import inbox from './inbox/inbox.routes';
import canned from './canned/canned.routes';
import reports from './reports/reports.routes';
import billing from './billing/billing.routes';
import Auth from '../../../api/auth';
import { frontendURL } from '../../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('settings'),
name: 'settings_home',
roles: ['administrator', 'agent'],
redirect: () => {
if (Auth.isAdmin()) {
return frontendURL('settings/agents');
}
return frontendURL('settings/canned-response');
},
},
...inbox.routes,
...agent.routes,
...canned.routes,
...reports.routes,
...billing.routes,
],
};

View File

@@ -0,0 +1,115 @@
/* eslint no-console: 0 */
import VueRouter from 'vue-router';
import auth from '../api/auth';
import login from './login/login.routes';
import dashboard from './dashboard/dashboard.routes';
import authRoute from './auth/auth.routes';
import { frontendURL } from '../helper/URLHelper';
const routes = [
...login.routes,
...dashboard.routes,
...authRoute.routes,
{
path: '/',
redirect: frontendURL('dashboard'),
},
];
window.roleWiseRoutes = {
agent: [],
administrator: [],
};
// generateRoleWiseRoute - updates window object with agent/admin route
const generateRoleWiseRoute = route => {
route.forEach(element => {
if (element.children) {
generateRoleWiseRoute(element.children);
}
if (element.roles) {
element.roles.forEach(roleEl => {
window.roleWiseRoutes[roleEl].push(element.name);
});
}
});
};
// Create a object of routes
// accessible by each role.
// returns an object with roles as keys and routeArr as values
generateRoleWiseRoute(routes);
export const router = new VueRouter({
mode: 'history',
routes, // short for routes: routes
});
const unProtectedRoutes = ['login', 'auth_signup', 'auth_reset_password'];
const authIgnoreRoutes = [
'auth_confirmation',
'pushBack',
'auth_password_edit',
];
function routeIsAccessibleFor(route, role) {
return window.roleWiseRoutes[role].includes(route);
}
const routeValidators = [
{
protected: false,
loggedIn: true,
handler: () => 'dashboard',
},
{
protected: true,
loggedIn: false,
handler: () => 'login',
},
{
protected: true,
loggedIn: true,
handler: to => {
const user = auth.getCurrentUser();
const isAccessible = routeIsAccessibleFor(to, user.role);
return isAccessible ? null : 'dashboard';
},
},
{
protected: false,
loggedIn: false,
handler: () => null,
},
];
export const validateAuthenticateRoutePermission = (to, from, next) => {
const isLoggedIn = auth.isLoggedIn();
const isProtectedRoute = !unProtectedRoutes.includes(to.name);
const strategy = routeValidators.find(
validator =>
validator.protected === isProtectedRoute &&
validator.loggedIn === isLoggedIn
);
const nextRoute = strategy.handler(to.name);
return nextRoute ? next(frontendURL(nextRoute)) : next();
};
const validateRouteAccess = (to, from, next) => {
if (authIgnoreRoutes.includes(to.name)) {
return next();
}
return validateAuthenticateRoutePermission(to, from, next);
};
// protecting routes
router.beforeEach((to, from, next) => {
if (!to.name) {
return next(frontendURL('dashboard'));
}
return validateRouteAccess(to, from, next);
});
export default router;

View File

@@ -0,0 +1,112 @@
import 'expect-more-jest';
import { validateAuthenticateRoutePermission } from './index';
import auth from '../api/auth';
jest.mock('./dashboard/dashboard.routes', () => ({
routes: [],
}));
jest.mock('./auth/auth.routes', () => ({
routes: [],
}));
jest.mock('./login/login.routes', () => ({
routes: [],
}));
jest.mock('../constants', () => {
return {
APP_BASE_URL: '/',
PUSHER: false,
get apiUrl() {
return `${this.APP_BASE_URL}/`;
},
GRAVATAR_URL: 'https://www.gravatar.com/avatar',
CHANNELS: {
FACEBOOK: 'facebook',
},
ASSIGNEE_TYPE_SLUG: {
MINE: 0,
UNASSIGNED: 1,
OPEN: 1,
},
};
});
window.roleWiseRoutes = {};
describe(`behavior`, () => {
describe(`when route is not protected`, () => {
it(`should go to the dashboard when user is logged in`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(true);
spyOn(auth, 'getCurrentUser').and.returnValue({
role: 'user',
});
const to = {
name: 'login',
};
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
expect(next).toHaveBeenCalledWith('/app/dashboard');
});
});
describe(`when route is protected`, () => {
describe(`when user not logged in`, () => {
it(`should redirect to login`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(false);
spyOn(auth, 'getCurrentUser').and.returnValue(null);
const to = {
name: 'some-protected-route',
};
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
expect(next).toHaveBeenCalledWith('/app/login');
});
});
describe(`when user is logged in`, () => {
describe(`when route is not accessible to current user`, () => {
it(`should redirect to dashboard`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(true);
spyOn(auth, 'getCurrentUser').and.returnValue({
role: 'user',
});
window.roleWiseRoutes.user = ['dashboard'];
const to = {
name: 'admin',
};
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
expect(next).toHaveBeenCalledWith('/app/dashboard');
});
});
describe(`when route is accessible to current user`, () => {
it(`should go there`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(true);
spyOn(auth, 'getCurrentUser').and.returnValue({
role: 'user',
});
window.roleWiseRoutes.user = ['dashboard', 'admin'];
const to = {
name: 'admin',
};
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
expect(next).toHaveBeenCalledWith();
});
});
});
});
});

View File

@@ -0,0 +1,113 @@
<template>
<div class="medium-12 column login">
<div class="text-center medium-12 login__hero align-self-top">
<img src="~dashboard/assets/images/woot-logo.svg" alt="Woot-logo" class="hero__logo" />
<h2 class="hero__title">{{$t('LOGIN.TITLE')}}</h2>
</div>
<div class="row align-center">
<div class="small-12 medium-4 column">
<form class="login-box column align-self-top" v-on:submit.prevent="login()">
<div class="column log-in-form">
<!-- <h4 class="text-center">{{$t('LOGIN.TITLE')}}</h4> -->
<label :class="{ 'error': $v.credentials.email.$error }">
{{$t('LOGIN.EMAIL.LABEL')}}
<input type="text" v-bind:placeholder="$t('LOGIN.EMAIL.PLACEHOLDER')" v-model.trim="credentials.email" @input="$v.credentials.email.$touch">
</label>
<label :class="{ 'error': $v.credentials.password.$error }">
{{$t('LOGIN.PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('LOGIN.PASSWORD.PLACEHOLDER')" v-model.trim="credentials.password" @input="$v.credentials.password.$touch">
</label>
<woot-submit-button
:disabled="$v.credentials.email.$invalid || $v.credentials.password.$invalid || loginApi.showLoading"
:button-text="$t('LOGIN.SUBMIT')"
:loading="loginApi.showLoading"
button-class="large expanded"
>
</woot-submit-button>
<!-- <input type="submit" class="button " v-on:click.prevent="login()" v-bind:value="" > -->
</div>
</form>
<div class="column text-center sigin__footer">
<p>
<router-link to="auth/reset/password">
{{$t('LOGIN.FORGOT_PASSWORD')}}
</router-link>
</p>
<p>
<router-link to="auth/signup">
{{ $t('LOGIN.CREATE_NEW_ACCOUNT') }}
</router-link>
</p>
</div>
</div>
</div>
</div>
</template>
<script>
/* global bus */
import { required, email } from 'vuelidate/lib/validators';
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
// import router from '../../routes';
export default {
components: {
WootSubmitButton,
},
data() {
return {
// We need to initialize the component with any
// properties that will be used in it
credentials: {
email: '',
password: '',
},
loginApi: {
message: '',
showLoading: false,
},
error: '',
};
},
validations: {
credentials: {
password: {
required,
},
email: {
required,
email,
},
},
},
methods: {
showAlert(message) {
// Reset loading, current selected agent
this.loginApi.showLoading = false;
this.loginApi.message = message;
bus.$emit('newToastMessage', this.loginApi.message);
},
login() {
this.loginApi.showLoading = true;
const credentials = {
email: this.credentials.email,
password: this.credentials.password,
};
this.$store
.dispatch('login', credentials)
.then(() => {
this.showAlert(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
})
.catch((response) => {
if (response && response.status === 401) {
this.showAlert(this.$t('LOGIN.API.UNAUTH'));
return;
}
this.showAlert(this.$t('LOGIN.API.ERROR_MESSAGE'));
});
},
},
};
</script>

View File

@@ -0,0 +1,12 @@
import Login from './Login';
import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
path: frontendURL('login'),
name: 'login',
component: Login,
},
],
};