Merge branch 'master' into OS-116-Ticket-events

# Conflicts:
#	tests/init.rb
This commit is contained in:
ivan 2016-11-20 19:02:47 -03:00
commit c8ee619000
38 changed files with 1670 additions and 98 deletions

View File

@ -10,5 +10,45 @@ export default {
data: {}
})
};
},
retrieveMyTickets() {
return {
type: 'MY_TICKETS',
payload: API.call({
path: '/staff/get-tickets',
data: {}
})
};
},
retrieveNewTickets() {
return {
type: 'NEW_TICKETS',
payload: API.call({
path: '/staff/get-new-tickets',
data: {}
})
};
},
retrieveAllTickets(page) {
return {
type: 'ALL_TICKETS',
payload: API.call({
path: '/staff/get-all-tickets',
data: {page}
})
};
},
searchTickets(query, page) {
return {
type: 'ALL_TICKETS',
payload: API.call({
path: '/staff/search-tickets',
data: {query, page}
})
};
}
};

View File

@ -1,9 +1,8 @@
import API from 'lib-app/api-call';
import AdminDataActions from 'actions/admin-data-actions';
import sessionStore from 'lib-app/session-store';
import store from 'app/store';
import ConfigActions from 'actions/config-actions';
export default {
login(loginData) {
return {
@ -13,6 +12,10 @@ export default {
data: loginData
}).then((result) => {
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff));
if(result.data.staff) {
store.dispatch(AdminDataActions.retrieveCustomResponses());
}
return result;
})

View File

@ -0,0 +1,188 @@
const _ = require('lodash');
const TicketInfo = ReactMock();
const Table = ReactMock();
const Button = ReactMock();
const Tooltip = ReactMock();
const Dropdown = ReactMock();
const i18n = stub().returnsArg(0);
const TicketList = requireUnit('app-components/ticket-list', {
'app-components/ticket-info': TicketInfo,
'core-components/table': Table,
'core-components/button': Button,
'core-components/tooltip': Tooltip,
'core-components/drop-down': Dropdown,
'lib-app/i18n': i18n
});
describe('TicketList component', function () {
let ticketList, table, dropdown;
let tickets = (function() {
let ticket = {
unread: false,
closed: false,
title: 'This is not working',
ticketNumber: 123124,
date: '20160215',
department: {
id: 1,
name: 'Sales Support'
},
priority: 'low',
author: {
id: 3,
name: 'Francisco Villegas'
}
};
let list = _.range(5).map(() => ticket);
list = list.concat(_.range(5).map(() => {
return _.extend({}, ticket, {
department: {
id: 2,
name: 'Tech Help'
}
})
}));
return list;
})();
function renderTicketList(props = {}) {
ticketList = TestUtils.renderIntoDocument(
<TicketList tickets={tickets} {...props}/>
);
table = TestUtils.scryRenderedComponentsWithType(ticketList, Table);
dropdown = TestUtils.scryRenderedComponentsWithType(ticketList, Dropdown);
}
it('should pass correct props to Table', function () {
renderTicketList();
expect(table[0].props.loading).to.equal(false);
expect(table[0].props.pageSize).to.equal(10);
expect(table[0].props.headers).to.deep.equal([
{
key: 'number',
value: i18n('NUMBER'),
className: 'ticket-list__number col-md-1'
},
{
key: 'title',
value: i18n('TITLE'),
className: 'ticket-list__title col-md-6'
},
{
key: 'department',
value: i18n('DEPARTMENT'),
className: 'ticket-list__department col-md-3'
},
{
key: 'date',
value: i18n('DATE'),
className: 'ticket-list__date col-md-2'
}
]);
});
it('should pass loading to Table', function () {
renderTicketList({loading: true});
expect(table[0].props.loading).to.equal(true);
});
it('should pass correct compare function to Table', function () {
let minCompare = table[0].props.comp;
let row1 = {
closed: false,
unread: false,
date: '20160405'
};
let row2 = {
closed: false,
unread: false,
date: '20160406'
};
expect(minCompare(row1, row2)).to.equal(1);
row1.unread = true;
expect(minCompare(row1, row2)).to.equal(-1);
row2.unread = true;
expect(minCompare(row1, row2)).to.equal(1);
row2.date = '20160401';
expect(minCompare(row1, row2)).to.equal(-1);
});
describe('when using secondary type', function () {
beforeEach(function () {
renderTicketList({
type: 'secondary',
departments: [
{id: 1, name: 'Sales Support'},
{id: 2, name: 'Tech Help'}
]
});
});
it('should pass correct props to Table', function () {
expect(table[0].props.headers).to.deep.equal([
{
key: 'number',
value: i18n('NUMBER'),
className: 'ticket-list__number col-md-1'
},
{
key: 'title',
value: i18n('TITLE'),
className: 'ticket-list__title col-md-4'
},
{
key: 'priority',
value: i18n('PRIORITY'),
className: 'ticket-list__priority col-md-1'
},
{
key: 'department',
value: i18n('DEPARTMENT'),
className: 'ticket-list__department col-md-2'
},
{
key: 'author',
value: i18n('AUTHOR'),
className: 'ticket-list__author col-md-2'
},
{
key: 'date',
value: i18n('DATE'),
className: 'ticket-list__date col-md-2'
}
]);
});
it('should pass correct props to dropdown', function () {
expect(dropdown[0].props.items).to.deep.equal([
{content: i18n('ALL_DEPARTMENTS')},
{content: 'Sales Support'},
{content: 'Tech Help'}
]);
expect(dropdown[0].props.size).to.equal('medium');
});
it('should filter tickets by department when DropDown changes', function () {
dropdown[0].props.onChange({index: 1});
_.forEach(table[0].props.rows, function (row) {
expect(row.department).to.equal('Sales Support');
});
dropdown[0].props.onChange({index: 2});
_.forEach(table[0].props.rows, function (row) {
expect(row.department).to.equal('Tech Help');
});
dropdown[0].props.onChange({index: 0});
expect(table[0].props.rows.length).to.equal(10);
});
});
});

View File

@ -17,7 +17,7 @@ class TicketEvent extends React.Component {
]),
author: React.PropTypes.object,
content: React.PropTypes.string,
date: React.PropTypes.number
date: React.PropTypes.string
};
render() {

View File

@ -1,16 +1,21 @@
import React from 'react';
import _ from 'lodash';
import i18n from 'lib-app/i18n';
import DateTransformer from 'lib-core/date-transformer';
import TicketInfo from 'app-components/ticket-info';
import Table from 'core-components/table';
import Button from 'core-components/button';
import Tooltip from 'core-components/tooltip';
import TicketInfo from 'app-components/ticket-info';
import DateTransformer from 'lib-core/date-transformer';
import DropDown from 'core-components/drop-down';
class TicketList extends React.Component {
static propTypes = {
departments: React.PropTypes.array,
loading: React.PropTypes.bool,
ticketPath: React.PropTypes.string,
showDepartmentDropdown: React.PropTypes.bool,
tickets: React.PropTypes.arrayOf(React.PropTypes.object),
type: React.PropTypes.oneOf([
'primary',
@ -19,18 +24,72 @@ class TicketList extends React.Component {
};
static defaultProps = {
showDepartmentDropdown: true,
loading: false,
tickets: [],
departments: [],
ticketPath: '/dashboard/ticket/',
type: 'primary'
};
state = {
selectedDepartment: 0
};
render() {
return (
<div className="ticket-list">
<Table headers={this.getTableHeaders()} rows={this.getTableRows()} pageSize={10} comp={this.compareFunction} />
{(this.props.type === 'secondary' && this.props.showDepartmentDropdown) ? this.renderDepartmentsDropDown() : null}
<Table {...this.getTableProps()} />
</div>
);
}
renderDepartmentsDropDown() {
return (
<div className="ticket-list__department-selector">
<DropDown {...this.getDepartmentDropdownProps()} />
</div>
);
}
getDepartmentDropdownProps() {
return {
items: this.getDepartments(),
onChange: (event) => {
this.setState({
selectedDepartment: event.index && this.props.departments[event.index - 1].id
});
},
size: 'medium'
};
}
getTableProps() {
return {
loading: this.props.loading,
headers: this.getTableHeaders(),
rows: this.getTableRows(),
pageSize: 10,
comp: this.compareFunction,
page: this.props.page,
pages: this.props.pages,
onPageChange: this.props.onPageChange
};
}
getDepartments() {
let departments = this.props.departments.map((department) => {
return {content: department.name};
});
departments.unshift({
content: i18n('ALL_DEPARTMENTS')
});
return departments;
}
getTableHeaders() {
if (this.props.type == 'primary' ) {
return [
@ -92,7 +151,13 @@ class TicketList extends React.Component {
}
getTableRows() {
return this.props.tickets.map(this.gerTicketTableObject.bind(this));
return this.getTickets().map(this.gerTicketTableObject.bind(this));
}
getTickets() {
return (this.state.selectedDepartment) ? _.filter(this.props.tickets, (ticket) => {
return ticket.department.id == this.state.selectedDepartment
}) : this.props.tickets;
}
gerTicketTableObject(ticket) {
@ -105,7 +170,7 @@ class TicketList extends React.Component {
</Tooltip>
),
title: (
<Button className="ticket-list__title-link" type="clean" route={{to: '/dashboard/ticket/' + ticket.ticketNumber}}>
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticket.ticketNumber}}>
{titleText}
</Button>
),
@ -137,8 +202,6 @@ class TicketList extends React.Component {
}
compareFunction(row1, row2) {
let ans = 0;
if (row1.closed == row2.closed) {
if (row1.unread == row2.unread) {
let s1 = row1.date;

View File

@ -2,6 +2,10 @@
.ticket-list {
&__department-selector {
margin-bottom: 25px;
}
&__number {
text-align: left;
}

View File

@ -1,11 +1,10 @@
import React from 'react';
import _ from 'lodash';
import RichTextEditor from 'react-rte-browserify';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import store from 'app/store';
import SessionStore from 'lib-app/session-store';
import SessionActions from 'actions/session-actions';
import TicketEvent from 'app-components/ticket-event';
import AreYouSure from 'app-components/are-you-sure';
@ -20,6 +19,7 @@ class TicketViewer extends React.Component {
ticket: React.PropTypes.object,
onChange: React.PropTypes.func,
editable: React.PropTypes.bool,
customResponses: React.PropTypes.array,
assignmentAllowed: React.PropTypes.bool
};
@ -32,13 +32,11 @@ class TicketViewer extends React.Component {
}
};
constructor(props) {
super(props);
this.state = {
loading: false
};
}
state = {
loading: false,
commentValue: RichTextEditor.createEmptyValue(),
commentEdited: false
};
render() {
const ticket = this.props.ticket;
@ -58,8 +56,9 @@ class TicketViewer extends React.Component {
</div>
<div className="ticket-viewer__response">
<div className="ticket-viewer__response-title row">{i18n('RESPOND')}</div>
{this.renderCustomResponses()}
<div className="ticket-viewer__response-field row">
<Form onSubmit={this.onSubmit.bind(this)} loading={this.state.loading}>
<Form {...this.getCommentFormProps()}>
<FormField name="content" validation="TEXT_AREA" required field="textarea" />
<SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton>
</Form>
@ -186,6 +185,44 @@ class TicketViewer extends React.Component {
);
}
renderCustomResponses() {
let customResponsesNode = null;
if (this.props.customResponses && this.props.editable) {
let customResponses = this.props.customResponses.map((customResponse) => {
return {
content: customResponse.name
};
});
customResponses.unshift({
content: i18n('SELECT_CUSTOM_RESPONSE')
});
customResponsesNode = (
<div className="ticket-viewer__response-custom row">
<DropDown items={customResponses} size="medium" onChange={this.onCustomResponsesChanged.bind(this)}/>
</div>
);
}
return customResponsesNode;
}
getCommentFormProps() {
return {
onSubmit: this.onSubmit.bind(this),
loading: this.state.loading,
onChange: (formState) => {this.setState({
commentValue: formState.content,
commentEdited: true
})},
values: {
'content': this.state.commentValue
}
};
}
onDepartmentDropdownChanged(event) {
AreYouSure.openModal(null, this.changeDepartment.bind(this, event.index));
}
@ -242,6 +279,21 @@ class TicketViewer extends React.Component {
}).then(this.onTicketModification.bind(this));
}
onCustomResponsesChanged({index}) {
let replaceContentWithCustomResponse = () => {
this.setState({
commentValue: RichTextEditor.createValueFromString(this.props.customResponses[index-1].content || '', 'html'),
commentEdited: false
});
};
if (this.state.commentEdited && index) {
AreYouSure.openModal(null, replaceContentWithCustomResponse);
} else {
replaceContentWithCustomResponse();
}
}
onSubmit(formState) {
this.setState({
loading: true

View File

@ -72,5 +72,11 @@
padding: 20px;
text-align: left;
}
&-custom {
background-color: $very-light-grey;
padding: 20px 0 0 20px;
text-align: left;
}
}
}

View File

@ -1,14 +1,78 @@
import React from 'react';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import AdminDataAction from 'actions/admin-data-actions';
import Header from 'core-components/header';
import TicketList from 'app-components/ticket-list';
import SearchBox from 'core-components/search-box';
class AdminPanelAllTickets extends React.Component {
static defaultProps = {
departments: [],
tickets: []
};
state = {
page: 1,
query: ''
};
componentDidMount() {
this.props.dispatch(AdminDataAction.retrieveAllTickets());
}
render() {
return (
<div>
/admin/panel/tickets/all-tickets
<div className="admin-panel-my-tickets">
<Header title={i18n('ALL_TICKETS')} description={i18n('ALL_TICKETS_DESCRIPTION')} />
<div className="admin-panel-my-tickets__search-box">
<SearchBox onSearch={this.onSearch.bind(this)} />
</div>
<TicketList {...this.getTicketListProps()}/>
</div>
);
}
getTicketListProps() {
return {
showDepartmentDropdown: false,
departments: this.props.departments,
tickets: this.props.tickets,
type: 'secondary',
loading: this.props.loading,
ticketPath: '/admin/panel/tickets/view-ticket/',
onPageChange: this.onPageChange.bind(this),
page: this.state.page,
pages: this.props.pages
};
}
onSearch(query) {
this.setState({query, page: 1});
this.props.dispatch(AdminDataAction.searchTickets(query));
}
onPageChange(event) {
this.setState({
page: event.target.value
});
if(this.state.query) {
this.props.dispatch(AdminDataAction.searchTickets(this.state.query, event.target.value));
} else {
this.props.dispatch(AdminDataAction.retrieveAllTickets(event.target.value));
}
}
}
export default AdminPanelAllTickets;
export default connect((store) => {
return {
departments: store.session.userDepartments,
tickets: store.adminData.allTickets,
pages: store.adminData.allTicketsPages,
loading: !store.adminData.allTicketsLoaded
};
})(AdminPanelAllTickets);

View File

@ -0,0 +1,6 @@
.admin-panel-my-tickets {
&__search-box {
padding: 0 50px;
margin-bottom: 30px;
}
}

View File

@ -1,14 +1,47 @@
import React from 'react';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import AdminDataAction from 'actions/admin-data-actions';
import Header from 'core-components/header';
import TicketList from 'app-components/ticket-list';
class AdminPanelMyTickets extends React.Component {
static defaultProps = {
departments: [],
tickets: []
};
componentDidMount() {
this.props.dispatch(AdminDataAction.retrieveMyTickets());
}
render() {
return (
<div>
/admin/panel/tickets/my-tickets
<div className="admin-panel-my-tickets">
<Header title={i18n('MY_TICKETS')} description={i18n('MY_TICKETS_DESCRIPTION')} />
<TicketList {...this.getProps()}/>
</div>
);
}
getProps() {
return {
departments: this.props.departments,
tickets: this.props.tickets,
type: 'secondary',
loading: this.props.loading,
ticketPath: '/admin/panel/tickets/view-ticket/'
};
}
}
export default AdminPanelMyTickets;
export default connect((store) => {
return {
departments: store.session.userDepartments,
tickets: store.adminData.myTickets,
loading: !store.adminData.myTicketsLoaded
};
})(AdminPanelMyTickets);

View File

@ -1,14 +1,47 @@
import React from 'react';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import AdminDataAction from 'actions/admin-data-actions';
import Header from 'core-components/header';
import TicketList from 'app-components/ticket-list';
class AdminPanelNewTickets extends React.Component {
static defaultProps = {
departments: [],
tickets: []
};
componentDidMount() {
this.props.dispatch(AdminDataAction.retrieveNewTickets());
}
render() {
return (
<div>
/admin/panel/tickets/new-tickets
<div className="admin-panel-my-tickets">
<Header title={i18n('NEW_TICKETS')} description={i18n('NEW_TICKETS_DESCRIPTION')} />
<TicketList {...this.getProps()}/>
</div>
);
}
getProps() {
return {
departments: this.props.departments,
tickets: this.props.tickets,
type: 'secondary',
loading: this.props.loading,
ticketPath: '/admin/panel/tickets/view-ticket/'
};
}
}
export default AdminPanelNewTickets;
export default connect((store) => {
return {
departments: store.session.userDepartments,
tickets: store.adminData.newTickets,
loading: !store.adminData.newTicketsLoaded
};
})(AdminPanelNewTickets);

View File

@ -1,5 +1,6 @@
import React from 'react';
import _ from 'lodash';
import {connect} from 'react-redux';
import API from 'lib-app/api-call';
import i18n from 'lib-app/i18n';
@ -62,19 +63,15 @@ class AdminPanelViewTicket extends React.Component {
ticket: this.state.ticket,
onChange: this.retrieveTicket.bind(this),
assignmentAllowed: true,
customResponses: this.props.customResponses,
editable: _.get(this.state.ticket, 'owner.id') === SessionStore.getUserData().id
};
}
retrieveTicket() {
this.setState({
loading: true,
ticket: {}
});
API.call({
path: '/ticket/get',
date: {
data: {
ticketNumber: this.props.params.ticketNumber
}
}).then(this.onRetrieveSuccess.bind(this)).catch(this.onRetrieveFail.bind(this))
@ -85,6 +82,15 @@ class AdminPanelViewTicket extends React.Component {
loading: false,
ticket: result.data
});
if(result.data.unreadStaff) {
API.call({
path: '/ticket/seen',
data: {
ticketNumber: this.props.params.ticketNumber
}
})
}
}
onRetrieveFail() {
@ -95,4 +101,8 @@ class AdminPanelViewTicket extends React.Component {
}
}
export default AdminPanelViewTicket;
export default connect((store) => {
return {
customResponses: store.adminData.customResponses
};
})(AdminPanelViewTicket);

View File

@ -15,6 +15,7 @@ const DropDown = require('core-components/drop-down');
const Menu = require('core-components/menu');
const Tooltip = require('core-components/tooltip');
const Table = require('core-components/table');
const InfoTooltip = require('core-components/info-tooltip');
let dropDownItems = [{content: 'English'}, {content: 'Spanish'}, {content: 'German'}, {content: 'Portuguese'}, {content: 'Japanese'}];
let secondaryMenuItems = [
@ -168,6 +169,12 @@ let DemoPage = React.createClass({
return ans;
}}/>
)
},
{
title: 'InfoTooltip',
render: (
<InfoTooltip type="warning" text="No staff member is assigned to this department." />
)
}
],
@ -197,4 +204,4 @@ let DemoPage = React.createClass({
}
});
export default DemoPage;
export default DemoPage;

View File

@ -2,6 +2,9 @@ import React from 'react';
import _ from 'lodash';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import SessionActions from 'actions/session-actions';
import TicketViewer from 'app-components/ticket-viewer';
@ -11,16 +14,35 @@ class DashboardTicketPage extends React.Component {
tickets: React.PropTypes.array
};
componentDidMount() {
let ticket = this.getTicketData();
if(ticket.unread) {
API.call({
path: '/ticket/seen',
data: {
ticketNumber: ticket.ticketNumber
}
});
}
}
render() {
let ticketView = i18n('NO_PERMISSION');
if(!_.isEmpty(this.getTicketData())) {
ticketView = <TicketViewer ticket={this.getTicketData()} onChange={this.retrieveUserData.bind(this)}/>;
}
return (
<div className="dashboard-ticket-page">
<TicketViewer ticket={this.getTicketData()} onChange={this.retrieveUserData.bind(this)}/>
{ticketView}
</div>
);
}
getTicketData() {
return _.find(this.props.tickets, {ticketNumber: this.props.params.ticketNumber});
return _.find(this.props.tickets, {ticketNumber: this.props.params.ticketNumber}) || {};
}
retrieveUserData() {
@ -32,4 +54,4 @@ export default connect((store) => {
return {
tickets: store.session.userTickets
};
})(DashboardTicketPage);
})(DashboardTicketPage);

View File

@ -0,0 +1,54 @@
import React from 'react';
import classNames from 'classnames';
import i18n from 'lib-app/i18n';
import Icon from 'core-components/icon';
import Tooltip from 'core-components/tooltip';
class InfoTooltip extends React.Component {
static propTypes = {
type: React.PropTypes.oneOf(['default', 'warning']),
text: React.PropTypes.string.isRequired
};
static defaultProps = {
type: 'default'
};
render() {
let name = (this.props.type === 'default') ? 'question-circle' : 'exclamation-triangle';
return (
<div className={this.getClass()}>
<Tooltip content={this.renderText()} openOnHover>
<span className="info-tooltip__icon">
<Icon name={name}/>
</span>
</Tooltip>
</div>
);
}
renderText() {
let message = (this.props.type === 'default') ? i18n('INFO') : i18n('WARNING');
return (
<div className="info-tooltip__text">
<div className="info-tooltip__text-title">
{message}
</div>
{this.props.text}
</div>
);
}
getClass() {
let classes = {
'info-tooltip': true,
'info-tooltip_warning': (this.props.type === 'warning')
};
return classNames(classes);
}
}
export default InfoTooltip;

View File

@ -0,0 +1,26 @@
@import "../scss/vars";
.info-tooltip {
&__text {
&-title {
color: $secondary-blue;
font-size: $font-size--md;
}
}
&__icon {
color: $secondary-blue;
}
&_warning {
.info-tooltip__icon {
color: $primary-red;
}
.info-tooltip__text {
&-title {
color: $primary-red;
}
}
}
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import Input from 'core-components/input';
import Icon from 'core-components/icon';
import keyCode from 'keycode';
class SearchBox extends React.Component {
static propTypes = {
onSearch: React.PropTypes.func
};
state = {
value: ''
};
render() {
return (
<div className="search-box">
<Input className="search-box__text" value={this.state.value} onChange={this.onChange.bind(this)} onKeyDown={this.onKeyDown.bind(this)} />
<span className="search-box__icon">
<Icon name="search" />
</span>
</div>
);
}
onChange(event) {
this.setState({
value: event.target.value
});
}
onKeyDown(event) {
if(keyCode(event) === 'enter' && this.props.onSearch) {
this.props.onSearch(this.state.value);
}
}
}
export default SearchBox;

View File

@ -0,0 +1,21 @@
@import "../scss/vars";
.search-box {
position: relative;
color: $dark-grey;
&__text {
width: 100%;
font-size: $font-size--lg;
.input__text {
padding-left: 50px;
}
}
&__icon {
position: absolute;
top: 15px;
left: 20px;
}
}

View File

@ -13,9 +13,12 @@ class Table extends React.Component {
className: React.PropTypes.string
})),
rows: React.PropTypes.arrayOf(React.PropTypes.object),
pageSize: React.PropTypes.number,
loading: React.PropTypes.bool,
type: React.PropTypes.oneOf(['default']),
page: React.PropTypes.number,
pages: React.PropTypes.number,
pageSize: React.PropTypes.number,
onPageChange: React.PropTypes.func,
comp: React.PropTypes.func
};
@ -41,7 +44,7 @@ class Table extends React.Component {
</tbody>
</table>
{(this.props.loading) ? this.renderLoading() : null}
{(this.props.pageSize && this.props.rows.length > this.props.pageSize) ? this.renderNavigation() : null}
{this.renderPagination()}
</div>
);
}
@ -59,8 +62,8 @@ class Table extends React.Component {
renderRow(row, index) {
const headersKeys = this.props.headers.map(header => header.key);
const minIndex = this.props.pageSize * (this.state.page - 1);
const maxIndex = this.props.pageSize * this.state.page;
const minIndex = this.props.pageSize * ((this.props.page) ? 0 : this.state.page - 1);
const maxIndex = this.props.pageSize * ((this.props.page) ? 1 : this.state.page);
const shouldRenderRow = !this.props.pageSize || (index >= minIndex && index < maxIndex);
return (shouldRenderRow) ? (
@ -81,12 +84,15 @@ class Table extends React.Component {
);
}
renderPagination() {
return (this.props.pages || (this.props.pageSize && this.props.rows.length > this.props.pageSize)) ? this.renderNavigation() : null
}
renderNavigation() {
const pages = Math.ceil(this.props.rows.length / this.props.pageSize) + 1;
const items = _.range(1, pages).map((index) => {return {content: index};});
const items = _.range(1, this.getPages()).map((index) => {return {content: index};});
return (
<Menu className="table__navigation" type="navigation" items={items} onItemClick={this.onNavigationItemClick.bind(this)}/>
<Menu className="table__navigation" type="navigation" items={items} selectedIndex={this.getPageNumber() - 1} onItemClick={this.onNavigationItemClick.bind(this)} />
);
}
@ -102,6 +108,10 @@ class Table extends React.Component {
this.setState({
page: index + 1
});
if(this.props.onPageChange) {
this.props.onPageChange({target: {value: index + 1}});
}
}
getRowClass(row) {
@ -114,11 +124,19 @@ class Table extends React.Component {
}
getRows() {
let v = _.clone(this.props.rows);
v.sort(this.props.comp);
return v;
let sortedRows = _.clone(this.props.rows);
sortedRows.sort(this.props.comp);
return sortedRows;
}
getPages() {
return (this.props.pages !== undefined) ? this.props.pages + 1 : Math.ceil(this.props.rows.length / this.props.pageSize) + 1;
}
getPageNumber() {
return (this.props.page !== undefined) ? this.props.page: this.state.page;
}
}
export default Table;

View File

@ -50,7 +50,9 @@
}
&__loading-wrapper {
min-height: 200px;
min-height: 380px;
position: relative;
background-color: $grey;
}
&__loading {

View File

@ -17,6 +17,7 @@
background-color: #F7F7F7;
color: black;
padding: 10px;
z-index: 1000;
}
&__pointer {

View File

@ -1,3 +1,5 @@
import _ from 'lodash';
module.exports = [
{
path: '/staff/get',
@ -8,16 +10,572 @@ module.exports = [
data: {
name: 'Emilia Clarke',
email: 'staff@opensupports.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
level: 1,
staff: true,
departments: [
{id: 1, name: 'Sales Support'},
{id: 2, name: 'Technical Issues'},
{id: 3, name: 'System and Administration'}
{id: 2, name: 'Technical Issues'}
]
}
};
}
},
{
path: '/staff/get-tickets',
time: 300,
response: function () {
return {
status: 'success',
data: [
{
ticketNumber: '445441',
title: 'Problem with installation',
content: 'I had a problem with the installation of the php server',
department: {
id: 2,
name: 'Technical Issues'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: true,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: [
{
type: 'ASSIGN',
date: '20150409',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150409',
content: 'Do you have apache installed? It generally happens if you dont have apache.',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'UN_ASSIGN',
date: '20150410',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'DEPARTMENT_CHANGED',
date: '20150411',
content: 'System support',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150412',
content: 'I have already installed apache, but the problem persists',
author: {
name: 'Haskell Curry',
steve: 'haskell@lambda.com',
staff: false
}
},
{
type: 'PRIORITY_CHANGED',
date: '20150413',
content: 'MEDIUM',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150511',
content: 'Thanks!, I solved it by myself',
author: {
name: 'Haskell Curry',
steve: 'haskell@lambda.com',
staff: false
}
},
{
type: 'CLOSE',
date: '20150513',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'RE_OPEN',
date: '20151018',
author: {
name: 'Haskell Curry',
email: 'haskell@lambda.com',
staff: false
}
}
]
},
{
ticketNumber: '878552',
title: 'Lorem ipsum door',
content: 'I had a problem with the installation of the php server',
department: {
id: 2,
name: 'Technical Issues'
},
date: '20160415',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: false,
closed: false,
priority: 'medium',
author: {
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
name: 'Steve Jobs'
},
events: [
{
type: 'ASSIGN',
date: '20150409',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150409',
content: 'Do you have apache installed? It generally happens if you dont have apache.',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'UN_ASSIGN',
date: '20150410',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'DEPARTMENT_CHANGED',
date: '20150411',
content: 'System support',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150412',
content: 'I have already installed apache, but the problem persists',
author: {
name: 'Haskell Curry',
steve: 'haskell@lambda.com',
staff: false
}
},
{
type: 'PRIORITY_CHANGED',
date: '20150413',
content: 'MEDIUM',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150511',
content: 'Thanks!, I soved it by myself',
author: {
name: 'Haskell Curry',
steve: 'haskell@lambda.com',
staff: false
}
},
{
type: 'CLOSE',
date: '20150513',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'RE_OPEN',
date: '20151018',
author: {
name: 'Haskell Curry',
email: 'haskell@lambda.com',
staff: false
}
}
]
},
{
ticketNumber: '118551',
title: 'Lorem ipsum door',
content: 'I had a problem with the installation of the php server',
department: {
id: 2,
name: 'Technical Issues'
},
date: '20150409',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: false,
closed: false,
priority: 'high',
author: {
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
name: 'Steve Jobs'
},
events: [
{
type: 'ASSIGN',
date: '20150409',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150409',
content: 'Do you have apache installed? It generally happens if you dont have apache.',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'UN_ASSIGN',
date: '20150410',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'DEPARTMENT_CHANGED',
date: '20150411',
content: 'System support',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150412',
content: 'I have already installed apache, but the problem persists',
author: {
name: 'Haskell Curry',
steve: 'haskell@lambda.com',
staff: false
}
},
{
type: 'PRIORITY_CHANGED',
date: '20150413',
content: 'MEDIUM',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'COMMENT',
date: '20150511',
content: 'Thanks!, I soved it by myself',
author: {
name: 'Haskell Curry',
steve: 'haskell@lambda.com',
staff: false
}
},
{
type: 'CLOSE',
date: '20150513',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
{
type: 'RE_OPEN',
date: '20151018',
author: {
name: 'Haskell Curry',
email: 'haskell@lambda.com',
staff: false
}
}
]
},
{
ticketNumber: '445441',
title: 'Inscription ACM ICPC',
content: 'I had a problem with the installation of the php server',
department: {
id: 1,
name: 'Sales Support'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: false,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: []
}
]
}
}
},
{
path: '/staff/get-new-tickets',
time: 300,
response: function () {
return {
status: 'success',
data: [
{
ticketNumber: '445441',
title: 'Inscription ACM ICPC',
content: 'I had a problem with the installation of the php server',
department: {
id: 1,
name: 'Sales Support'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: true,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: []
},
{
ticketNumber: '445441',
title: 'Inscription ACM ICPC',
content: 'I had a problem with the installation of the php server',
department: {
id: 1,
name: 'Sales Support'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: true,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: []
},
{
ticketNumber: '445441',
title: 'Code jam is awesome',
content: 'I had a problem with the installation of the php server',
department: {
id: 2,
name: 'Technical Issues'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: true,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: []
}
]
}
}
},
{
path: '/staff/get-all-tickets',
time: 1000,
response: function () {
return {
status: 'success',
data: {
tickets: _.range(0, 10).map(() => {
return {
ticketNumber: '445441',
title: 'Inscription ACM ICPC',
content: 'I had a problem with the installation of the php server',
department: {
id: 1,
name: 'Sales Support'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: false,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: []
};
}),
pages: 4
}
}
}
},
{
path: '/staff/search-tickets',
time: 300,
response: function () {
return {
status: 'success',
data: {
tickets: _.range(0, 10).map(() => {
return {
ticketNumber: '445441',
title: 'Inscription ACM ICPC',
content: 'I had a problem with the installation of the php server',
department: {
id: 1,
name: 'Sales Support'
},
date: '20160416',
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: false,
closed: false,
priority: 'low',
author: {
id: 12,
name: 'Haskell Curry',
email: 'haskell@lambda.com'
},
owner: {
id: 15,
name: 'Steve Jobs',
email: 'steve@jobs.com'
},
events: []
};
}),
pages: 2
}
}
}
}
];

View File

@ -39,11 +39,11 @@ module.exports = [
return {
status: 'success',
data: [
{name: 'Common issue #1', language: 'en', content: 'some content'},
{name: 'Common issue #2', language: 'en', content: 'some content'},
{name: 'Common issue #3', language: 'en', content: 'some content'},
{name: 'Häufiges Problem #1', language: 'de', content: 'einige Inhalte'},
{name: 'Häufiges Problem #2', language: 'de', content: 'einige Inhalte'}
{name: 'Common issue #1', language: 'en', content: 'some content 1'},
{name: 'Common issue #2', language: 'en', content: 'some content 2'},
{name: 'Common issue #3', language: 'en', content: 'some content 3'},
{name: 'Häufiges Problem #1', language: 'de', content: 'einige Inhalte 1'},
{name: 'Häufiges Problem #2', language: 'de', content: 'einige Inhalte 2'}
]
};
}
@ -78,6 +78,16 @@ module.exports = [
};
}
},
{
path: '/ticket/seen',
time: 200,
response: function () {
return {
status: 'success',
data: {}
};
}
},
{
path: '/ticket/get',
time: 1000,
@ -96,6 +106,7 @@ module.exports = [
file: 'http://www.opensupports.com/some_file.zip',
language: 'en',
unread: false,
unreadStaff: true,
closed: false,
priority: 'medium',
author: {
@ -112,7 +123,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -123,7 +134,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -133,7 +144,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -144,7 +155,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -165,7 +176,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -185,7 +196,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -203,4 +214,4 @@ module.exports = [
};
}
}
];
];

View File

@ -165,7 +165,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -176,7 +176,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -186,7 +186,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -197,7 +197,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -218,7 +218,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -238,7 +238,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -281,7 +281,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -292,7 +292,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -302,7 +302,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -313,7 +313,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -334,7 +334,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -354,7 +354,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -397,7 +397,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -408,7 +408,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -418,7 +418,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -429,7 +429,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -450,7 +450,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},
@ -470,7 +470,7 @@ module.exports = [
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
profilePic: 'http://www.opensupports.com/profilepic.jpg',
staff: true
}
},

View File

@ -71,6 +71,10 @@ export default {
'ASSIGN_TO_ME': 'Assign to me',
'UN_ASSIGN': 'Unassign',
'VIEW_TICKET': 'View Ticket',
'SELECT_CUSTOM_RESPONSE': 'Select a custom response...',
'WARNING': 'Warning',
'INFO': 'Information',
'ALL_DEPARTMENTS': 'All Departments',
//VIEW DESCRIPTIONS
'CREATE_TICKET_DESCRIPTION': 'This is a form for creating tickets. Fill the form and send us your issues/doubts/suggestions. Our support system will answer it as soon as possible.',
@ -80,6 +84,9 @@ export default {
'ACCOUNT_DESCRIPTION': 'All your tickets are stored in your accounts\'s profile. Keep track off all your tickets you send to our staff team.',
'SUPPORT_CENTER_DESCRIPTION': 'Welcome to our support center. You can contact us through a tickets system. Your tickets will be answered by our staff.',
'CUSTOM_RESPONSES_DESCRIPTION': 'Custom responses are automated responses for common problems',
'MY_TICKETS_DESCRIPTION': 'Here you can view the tickets you are responsible for.',
'NEW_TICKETS_DESCRIPTION': 'Here you can view all the new tickets that are not assigned by anyone.',
'ALL_TICKETS_DESCRIPTION': 'Here you can view the tickets of the departments you are assigned.',
'TICKET_VIEW_DESCRIPTION': 'This ticket has been sent by a customer. Here you can respond or assign the ticket',
//ERRORS
@ -107,4 +114,4 @@ export default {
'OLD_PASSWORD_INCORRECT': 'Old password is incorrect',
'WILL_LOSE_CHANGES': 'You haven\'t save. Your changes will be lost.',
'WILL_DELETE_CUSTOM_RESPONSE': 'The custom response will be deleted.'
};
};

View File

@ -1,28 +1,93 @@
import _ from 'lodash';
import Reducer from 'reducers/reducer';
import sessionStore from 'lib-app/session-store';
class AdminDataReducer extends Reducer {
getInitialState() {
return {
customResponses: [],
customResponsesLoaded: false
customResponsesLoaded: false,
myTickets: [],
myTicketsLoaded: false,
newTickets: [],
newTicketsLoaded: false,
allTickets: [],
allTicketsLoaded: false
};
}
getTypeHandlers() {
return {
'CUSTOM_RESPONSES_FULFILLED': this.onCustomResponses
'CUSTOM_RESPONSES_FULFILLED': this.onCustomResponses,
'SESSION_CHECKED': this.onSessionChecked,
'MY_TICKETS_FULFILLED': this.onMyTicketsRetrieved,
'MY_TICKETS_PENDING': this.onMyTicketsPending,
'NEW_TICKETS_FULFILLED': this.onNewTicketsRetrieved,
'NEW_TICKETS_PENDING': this.onNewTicketsPending,
'ALL_TICKETS_FULFILLED': this.onAllTicketsRetrieved,
'ALL_TICKETS_PENDING': this.onAllTicketsPending
};
}
onCustomResponses(state, payload) {
sessionStore.setItem('customResponses', JSON.stringify(payload.data));
return _.extend({}, state, {
customResponses: payload.data,
customResponsesLoaded: true
});
}
onSessionChecked(state) {
const customResponses = sessionStore.getItem('customResponses');
return _.extend({}, state, {
customResponses: JSON.parse(customResponses),
customResponsesLoaded: true
});
}
onMyTicketsRetrieved(state, payload) {
return _.extend({}, state, {
myTickets: payload.data,
myTicketsLoaded: true
})
}
onMyTicketsPending(state) {
return _.extend({}, state, {
myTicketsLoaded: false
})
}
onNewTicketsRetrieved(state, payload) {
return _.extend({}, state, {
newTickets: payload.data,
newTicketsLoaded: true
})
}
onNewTicketsPending(state) {
return _.extend({}, state, {
newTicketsLoaded: false
})
}
onAllTicketsRetrieved(state, payload) {
return _.extend({}, state, {
allTickets: payload.data.tickets,
allTicketsPages: payload.data.pages,
allTicketsLoaded: true
})
}
onAllTicketsPending(state) {
return _.extend({}, state, {
allTicketsLoaded: false
})
}
}
export default AdminDataReducer.getInstance();

View File

@ -4,6 +4,8 @@ require_once 'staff/assign-ticket.php';
require_once 'staff/un-assign-ticket.php';
require_once 'staff/get-tickets.php';
require_once 'staff/get-new-tickets.php';
require_once 'staff/get-all-tickets.php';
require_once 'staff/search-tickets.php';
$systemControllerGroup = new ControllerGroup();
$systemControllerGroup->setGroupPath('/staff');
@ -13,5 +15,7 @@ $systemControllerGroup->addController(new AssignStaffController);
$systemControllerGroup->addController(new UnAssignStaffController);
$systemControllerGroup->addController(new GetTicketStaffController);
$systemControllerGroup->addController(new GetNewTicketsStaffController);
$systemControllerGroup->addController(new GetAllTicketsStaffController);
$systemControllerGroup->addController(new SearchTicketStaffController);
$systemControllerGroup->finalize();

View File

@ -0,0 +1,54 @@
<?php
use Respect\Validation\Validator as DataValidator;
class GetAllTicketsStaffController extends Controller {
const PATH = '/get-all-tickets';
public function validations() {
return[
'permission' => 'staff_1',
'requestData' => [
'page' => [
'validation' => DataValidator::numeric(),
'error' => ERRORS::INVALID_PAGE
]
]
];
}
public function handler() {
Response::respondSuccess([
'tickets' => $this->getTicketList()->toArray(),
'pages' => $this->getTotalPages()
]);
}
private function getTicketList() {
$page = Controller::request('page');
$query = $this->getStaffDepartmentsQueryFilter();
$query .= 'ORDER BY id DESC LIMIT 10 OFFSET ' . (($page-1)*10);
return Ticket::find($query);
}
private function getTotalPages() {
$query = $this->getStaffDepartmentsQueryFilter();
return ceil(Ticket::count($query) / 10);
}
private function getStaffDepartmentsQueryFilter() {
$user = Controller::getLoggedUser();
$query = ' (';
foreach ($user->sharedDepartmentList as $department) {
$query .= 'department_id=' . $department->id . ' OR ';
}
$query = substr($query,0,-3);
$query .= ') ';
return $query;
}
}

View File

@ -0,0 +1,74 @@
<?php
use Respect\Validation\Validator as DataValidator;
class SearchTicketStaffController extends Controller {
const PATH = '/search-tickets';
public function validations() {
return[
'permission' => 'staff_1',
'requestData' => [
'query' => [
'validation' => DataValidator::alpha(),
'error' => ERRORS::INVALID_QUERY
],
'page' => [
'validation' => DataValidator::numeric(),
'error' => ERRORS::INVALID_PAGE
]
]
];
}
public function handler() {
Response::respondSuccess([
'tickets' => $this->getTicketList()->toArray(),
'pages' => $this->getTotalPages()
]);
}
private function getTicketList() {
$query = $this->getSearchQuery();
return Ticket::find($query, [
Controller::request('query') . '%',
'%' . Controller::request('query') . '%',
Controller::request('query') . '%'
]);
}
private function getSearchQuery() {
$page = Controller::request('page');
$query = " (title LIKE ? OR title LIKE ?) AND ";
$query .= $this->getStaffDepartmentsQueryFilter();
$query .= "ORDER BY CASE WHEN (title LIKE ?) THEN 1 ELSE 2 END ASC LIMIT 10 OFFSET " . (($page-1)*10);
return $query;
}
private function getTotalPages() {
$query = " (title LIKE ? OR title LIKE ?) AND ";
$query .= $this->getStaffDepartmentsQueryFilter();
$ticketQuantity = Ticket::count($query, [
Controller::request('query') . '%',
'%' . Controller::request('query') . '%'
]);
return ceil($ticketQuantity / 10);
}
private function getStaffDepartmentsQueryFilter() {
$user = Controller::getLoggedUser();
$query = ' (';
foreach ($user->sharedDepartmentList as $department) {
$query .= 'department_id=' . $department->id . ' OR ';
}
$query = substr($query, 0, -3);
$query .= ') ';
return $query;
}
}

View File

@ -88,7 +88,7 @@ class InitSettingsController extends Controller {
'name' => 'Emilia Clarke',
'email' => 'staff@opensupports.com',
'password' => Hashing::hashPassword('staff'),
'profilePic' => 'http://i65.tinypic.com/9bep95.jpg',
'profilePic' => 'http://www.opensupports.com/profilepic.jpg',
'level' => 3,
'sharedDepartmentList' => Department::getAll(),
'sharedTicketList' => []

View File

@ -16,7 +16,7 @@ class CreateController extends Controller {
'permission' => 'user',
'requestData' => [
'title' => [
'validation' => DataValidator::length(3, 30),
'validation' => DataValidator::length(10, 200),
'error' => ERRORS::INVALID_TITLE
],
'content' => [

View File

@ -19,4 +19,6 @@ class ERRORS {
const INVALID_LANGUAGE = 'INVALID_LANGUAGE';
const TICKET_ALREADY_ASSIGNED = 'TICKET_ALREADY_ASSIGNED';
const INVALID_PRIORITY = 'INVALID_PRIORITY';
const INVALID_PAGE = 'INVALID_PAGE';
const INVALID_QUERY = 'INVALID_QUERY';
}

View File

@ -16,8 +16,9 @@ abstract class DataStore {
return ($bean) ? new static($bean) : new NullDataStore();
}
public static function count() {
return RedBean::count(static::TABLE);
public static function count($addSQL = '', $bindings = array()) {
return RedBean::count(static::TABLE, $addSQL, $bindings);
}
public static function getAll() {
@ -30,10 +31,10 @@ abstract class DataStore {
return $dataStoreList;
}
public static function find($query) {
$beanList = RedBean::find(static::TABLE,$query);
public static function find($query = '', $matches = []) {
$beanList = RedBean::find(static::TABLE, $query, $matches);
return DataStoreList::getList(ucfirst(static::TABLE),$beanList);
return DataStoreList::getList(ucfirst(static::TABLE), $beanList);
}
private static function validateProp($propToValidate) {

View File

@ -45,7 +45,7 @@ class Ticket extends DataStore {
}
public function generateUniqueTicketNumber() {
$ticketQuantity = Ticket::count('ticket');
$ticketQuantity = Ticket::count();
$minValue = 100000;
$maxValue = 999999;
@ -53,7 +53,7 @@ class Ticket extends DataStore {
$ticketNumber = Hashing::getRandomTicketNumber($minValue, $maxValue);
} else {
$firstTicketNumber = Ticket::getTicket(1)->ticketNumber;
$gap = 176611;
$gap = 176611; //TODO: USE RANDOM PRIME INSTEAD
$ticketNumber = ($firstTicketNumber - $minValue + $ticketQuantity * $gap) % ($maxValue - $minValue + 1) + $minValue;
}

View File

@ -31,6 +31,7 @@ require './staff/un-assign-ticket.rb'
require './staff/get-tickets.rb'
require './ticket/change-priority.rb'
require './staff/get-new-tickets.rb'
require './staff/get-all-tickets.rb'
require './ticket/events.rb'

View File

@ -0,0 +1,98 @@
describe 'Retrieve all tickets' do
describe '/staff/get-all-tickets' do
Scripts.login('login@os4.com', 'loginpass')
def createTicket(title)
request('/ticket/create',{
title: title,
content: 'The north remembers',
departmentId: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
end
it 'should return last tickets tickets' do
createTicket('Integer sit amet tellus cursus')
createTicket('consequat tortor sed')
createTicket('Fusce lacinia felis quis molestie pellentesque')
createTicket('Aliquam fringilla dapibus lacus')
createTicket('Aenean enim orci')
createTicket('luctus in sagittis non')
createTicket('consectetur at velit')
createTicket('Etiam et maximus quam')
createTicket('Donec facilisis pelleipsumntesque feugiat')
createTicket('Cras gravida bibendum vehicula')
createTicket('Fusce venenatis iaculis commodo')
createTicket('quis vulputate lectus feugiat eu')
createTicket('ipsum Aenean maximus quis leo et eleifend')
createTicket('In vel ex semper nisl sollicitudin')
createTicket('volutpat vel nec enim')
createTicket('Ut semper viverra nulla')
createTicket('Duis consequat nec metus a vestibulum')
createTicket('Vestibulum porta justo id sem bibendum lacinia')
createTicket('Phasellus erat ipsum')
createTicket('imperdiet vel auctor sed')
createTicket('placerat id velit')
createTicket('Quisque egestas ipsum')
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
response = request('/staff/get-all-tickets', {
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(response['status']).should.equal('success')
(response['data']['pages']).should.equal(4)
(response['data']['tickets'].size).should.equal(10)
(response['data']['tickets'][0]['title']).should.equal('Quisque egestas ipsum')
(response['data']['tickets'][1]['title']).should.equal('placerat id velit')
(response['data']['tickets'][2]['title']).should.equal('imperdiet vel auctor sed')
(response['data']['tickets'][3]['title']).should.equal('Phasellus erat ipsum')
(response['data']['tickets'][4]['title']).should.equal('Vestibulum porta justo id sem bibendum lacinia')
(response['data']['tickets'][5]['title']).should.equal('Duis consequat nec metus a vestibulum')
(response['data']['tickets'][6]['title']).should.equal('Ut semper viverra nulla')
(response['data']['tickets'][7]['title']).should.equal('volutpat vel nec enim')
(response['data']['tickets'][8]['title']).should.equal('In vel ex semper nisl sollicitudin')
(response['data']['tickets'][9]['title']).should.equal('ipsum Aenean maximus quis leo et eleifend')
end
it 'should work with pagination' do
response = request('/staff/get-all-tickets', {
page: 2,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(response['status']).should.equal('success')
(response['data']['pages']).should.equal(4)
(response['data']['tickets'].size).should.equal(10)
(response['data']['tickets'][0]['title']).should.equal('quis vulputate lectus feugiat eu')
(response['data']['tickets'][1]['title']).should.equal('Fusce venenatis iaculis commodo')
(response['data']['tickets'][2]['title']).should.equal('Cras gravida bibendum vehicula')
(response['data']['tickets'][3]['title']).should.equal('Donec facilisis pelleipsumntesque feugiat')
(response['data']['tickets'][4]['title']).should.equal('Etiam et maximus quam')
(response['data']['tickets'][5]['title']).should.equal('consectetur at velit')
(response['data']['tickets'][6]['title']).should.equal('luctus in sagittis non')
(response['data']['tickets'][7]['title']).should.equal('Aenean enim orci')
(response['data']['tickets'][8]['title']).should.equal('Aliquam fringilla dapibus lacus')
(response['data']['tickets'][9]['title']).should.equal('Fusce lacinia felis quis molestie pellentesque')
end
end
describe '/staff/search-tickets' do
response = request('/staff/search-tickets', {
query: 'ipsum',
page: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(response['status']).should.equal('success')
(response['data']['pages']).should.equal(4)
(response['data']['tickets'].size).should.equal(10)
(response['data']['tickets'][0]['title']).should.equal('ipsum Aenean maximus quis leo et eleifend')
end
end

View File

@ -29,8 +29,11 @@ describe '/ticket/create' do
end
it 'should fail if title is very long' do
long_text = ''
300.times {long_text << 'a'}
result = request('/ticket/create',{
title: 'I WISH I WAS THE MONSTER YOU THINK I AM. -Tyrion',
title: long_text,
departmentId: 1,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token