Merge branch 'master' into OS-78-My-tickets-view

# Conflicts:
#	client/src/data/languages/en.js
#	client/src/reducers/admin-data-reducer.js
This commit is contained in:
ivan 2016-10-27 20:54:54 -03:00
commit 22cd244846
37 changed files with 737 additions and 133 deletions

View File

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

View File

@ -4,7 +4,7 @@ import classNames from 'classnames';
import i18n from 'lib-app/i18n'; import i18n from 'lib-app/i18n';
import Icon from 'core-components/icon'; import Icon from 'core-components/icon';
class TicketAction extends React.Component { class TicketEvent extends React.Component {
static propTypes = { static propTypes = {
type: React.PropTypes.oneOf([ type: React.PropTypes.oneOf([
'COMMENT', 'COMMENT',
@ -17,7 +17,7 @@ class TicketAction extends React.Component {
]), ]),
author: React.PropTypes.object, author: React.PropTypes.object,
content: React.PropTypes.string, content: React.PropTypes.string,
date: React.PropTypes.number date: React.PropTypes.string
}; };
render() { render() {
@ -31,12 +31,12 @@ class TicketAction extends React.Component {
return ( return (
<div className={this.getClass()}> <div className={this.getClass()}>
<span className="ticket-action__connector" /> <span className="ticket-event__connector" />
<div className="col-md-1"> <div className="col-md-1">
{iconNode} {iconNode}
</div> </div>
<div className="col-md-11"> <div className="col-md-11">
{this.renderActionDescription()} {this.renderEventDescription()}
</div> </div>
</div> </div>
); );
@ -44,21 +44,21 @@ class TicketAction extends React.Component {
renderStaffPic() { renderStaffPic() {
return ( return (
<div className="ticket-action__staff-pic"> <div className="ticket-event__staff-pic">
<img src={this.props.author.profilePic} className="ticket-action__staff-pic-img" /> <img src={this.props.author.profilePic} className="ticket-event__staff-pic-img" />
</div> </div>
); );
} }
renderIcon() { renderIcon() {
return ( return (
<div className="ticket-action__icon"> <div className="ticket-event__icon">
<Icon {...this.getIconProps()}/> <Icon {...this.getIconProps()}/>
</div> </div>
); );
} }
renderActionDescription() { renderEventDescription() {
const renders = { const renders = {
'COMMENT': this.renderComment.bind(this), 'COMMENT': this.renderComment.bind(this),
'ASSIGN': this.renderAssignment.bind(this), 'ASSIGN': this.renderAssignment.bind(this),
@ -74,14 +74,14 @@ class TicketAction extends React.Component {
renderComment() { renderComment() {
return ( return (
<div className="ticket-action__comment"> <div className="ticket-event__comment">
<span className="ticket-action__comment-pointer" /> <span className="ticket-event__comment-pointer" />
<div className="ticket-action__comment-author"> <div className="ticket-event__comment-author">
<span className="ticket-action__comment-author-name">{this.props.author.name}</span> <span className="ticket-event__comment-author-name">{this.props.author.name}</span>
<span className="ticket-action__comment-author-type">({i18n((this.props.author.staff) ? 'STAFF' : 'CUSTOMER')})</span> <span className="ticket-event__comment-author-type">({i18n((this.props.author.staff) ? 'STAFF' : 'CUSTOMER')})</span>
</div> </div>
<div className="ticket-action__comment-date">{this.props.date}</div> <div className="ticket-event__comment-date">{this.props.date}</div>
<div className="ticket-action__comment-content" dangerouslySetInnerHTML={{__html: this.props.content}}></div> <div className="ticket-event__comment-content" dangerouslySetInnerHTML={{__html: this.props.content}}></div>
{this.renderFileRow(this.props.file)} {this.renderFileRow(this.props.file)}
</div> </div>
); );
@ -89,62 +89,62 @@ class TicketAction extends React.Component {
renderAssignment() { renderAssignment() {
return ( return (
<div className="ticket-action__circled"> <div className="ticket-event__circled">
<span className="ticket-action__circled-author">{this.props.author.name}</span> <span className="ticket-event__circled-author">{this.props.author.name}</span>
<span className="ticket-action__circled-text"> assigned this ticket</span> <span className="ticket-event__circled-text"> assigned this ticket</span>
<span className="ticket-action__circled-date"> on {this.props.date}</span> <span className="ticket-event__circled-date"> on {this.props.date}</span>
</div> </div>
) )
} }
renderUnAssignment() { renderUnAssignment() {
return ( return (
<div className="ticket-action__circled"> <div className="ticket-event__circled">
<span className="ticket-action__circled-author">{this.props.author.name}</span> <span className="ticket-event__circled-author">{this.props.author.name}</span>
<span className="ticket-action__circled-text"> unassigned this ticket</span> <span className="ticket-event__circled-text"> unassigned this ticket</span>
<span className="ticket-action__circled-date"> on {this.props.date}</span> <span className="ticket-event__circled-date"> on {this.props.date}</span>
</div> </div>
) )
} }
renderClosed() { renderClosed() {
return ( return (
<div className="ticket-action__circled"> <div className="ticket-event__circled">
<span className="ticket-action__circled-author">{this.props.author.name}</span> <span className="ticket-event__circled-author">{this.props.author.name}</span>
<span className="ticket-action__circled-text"> closed this ticket</span> <span className="ticket-event__circled-text"> closed this ticket</span>
<span className="ticket-action__circled-date"> on {this.props.date}</span> <span className="ticket-event__circled-date"> on {this.props.date}</span>
</div> </div>
) )
} }
renderReOpened() { renderReOpened() {
return ( return (
<div className="ticket-action__circled"> <div className="ticket-event__circled">
<span className="ticket-action__circled-author">{this.props.author.name}</span> <span className="ticket-event__circled-author">{this.props.author.name}</span>
<span className="ticket-action__circled-text"> reopen this ticket</span> <span className="ticket-event__circled-text"> reopen this ticket</span>
<span className="ticket-action__circled-date"> on {this.props.date}</span> <span className="ticket-event__circled-date"> on {this.props.date}</span>
</div> </div>
); );
} }
renderDepartmentChange() { renderDepartmentChange() {
return ( return (
<div className="ticket-action__circled"> <div className="ticket-event__circled">
<span className="ticket-action__circled-author">{this.props.author.name}</span> <span className="ticket-event__circled-author">{this.props.author.name}</span>
<span className="ticket-action__circled-text"> change department to</span> <span className="ticket-event__circled-text"> change department to</span>
<span className="ticket-action__circled-indication"> {this.props.content}</span> <span className="ticket-event__circled-indication"> {this.props.content}</span>
<span className="ticket-action__circled-date"> on {this.props.date}</span> <span className="ticket-event__circled-date"> on {this.props.date}</span>
</div> </div>
); );
} }
renderPriorityChange() { renderPriorityChange() {
return ( return (
<div className="ticket-action__circled"> <div className="ticket-event__circled">
<span className="ticket-action__circled-author">{this.props.author.name}</span> <span className="ticket-event__circled-author">{this.props.author.name}</span>
<span className="ticket-action__circled-text"> change priority to</span> <span className="ticket-event__circled-text"> change priority to</span>
<span className="ticket-action__circled-indication"> {this.props.content}</span> <span className="ticket-event__circled-indication"> {this.props.content}</span>
<span className="ticket-action__circled-date"> on {this.props.date}</span> <span className="ticket-event__circled-date"> on {this.props.date}</span>
</div> </div>
); );
} }
@ -177,14 +177,14 @@ class TicketAction extends React.Component {
}; };
const classes = { const classes = {
'row': true, 'row': true,
'ticket-action': true, 'ticket-event': true,
'ticket-action_staff': this.props.author && this.props.author.staff, 'ticket-event_staff': this.props.author && this.props.author.staff,
'ticket-action_circled': circledTypes[this.props.type], 'ticket-event_circled': circledTypes[this.props.type],
'ticket-action_unassignment': this.props.type === 'UN_ASSIGN', 'ticket-event_unassignment': this.props.type === 'UN_ASSIGN',
'ticket-action_close': this.props.type === 'CLOSE', 'ticket-event_close': this.props.type === 'CLOSE',
'ticket-action_reopen': this.props.type === 'RE_OPEN', 'ticket-event_reopen': this.props.type === 'RE_OPEN',
'ticket-action_department': this.props.type === 'DEPARTMENT_CHANGED', 'ticket-event_department': this.props.type === 'DEPARTMENT_CHANGED',
'ticket-action_priority': this.props.type === 'PRIORITY_CHANGED' 'ticket-event_priority': this.props.type === 'PRIORITY_CHANGED'
}; };
return classNames(classes); return classNames(classes);
@ -225,4 +225,4 @@ class TicketAction extends React.Component {
} }
} }
export default TicketAction; export default TicketEvent;

View File

@ -1,6 +1,6 @@
@import "../scss/vars"; @import "../scss/vars";
.ticket-action { .ticket-event {
margin-top: 20px; margin-top: 20px;
text-align: left; text-align: left;
position: relative; position: relative;
@ -91,11 +91,11 @@
} }
&_staff { &_staff {
.ticket-action__icon { .ticket-event__icon {
background-color: $primary-blue; background-color: $primary-blue;
} }
.ticket-action__comment-author-type { .ticket-event__comment-author-type {
color: $primary-blue; color: $primary-blue;
} }
} }
@ -104,11 +104,11 @@
margin-top: 35px; margin-top: 35px;
margin-bottom: 30px; margin-bottom: 30px;
.ticket-action__connector { .ticket-event__connector {
top: 28px; top: 28px;
} }
.ticket-action__icon { .ticket-event__icon {
background-color: white; background-color: white;
color: $primary-black; color: $primary-black;
border: 3px solid $light-grey; border: 3px solid $light-grey;
@ -121,48 +121,48 @@
margin-top: -8px; margin-top: -8px;
} }
.ticket-action__circled { .ticket-event__circled {
color: $primary-black; color: $primary-black;
font-size: $font-size--sm; font-size: $font-size--sm;
margin-top: 1px; margin-top: 1px;
} }
.ticket-action__circled-author { .ticket-event__circled-author {
color: $secondary-blue; color: $secondary-blue;
} }
.ticket-action__circled-indication { .ticket-event__circled-indication {
color: $primary-green; color: $primary-green;
} }
} }
&_unassignment { &_unassignment {
.ticket-action__icon { .ticket-event__icon {
padding-left: 6px; padding-left: 6px;
} }
} }
&_close { &_close {
.ticket-action__icon { .ticket-event__icon {
padding-left: 9px; padding-left: 9px;
} }
} }
&_reopen { &_reopen {
.ticket-action__icon { .ticket-event__icon {
padding-left: 9px; padding-left: 9px;
padding-top: 5px; padding-top: 5px;
} }
} }
&_department { &_department {
.ticket-action__icon { .ticket-event__icon {
padding-left: 6px; padding-left: 6px;
} }
} }
&_priority { &_priority {
.ticket-action__icon { .ticket-event__icon {
padding-left: 11px; padding-left: 11px;
padding-top: 5px; padding-top: 5px;
} }

View File

@ -1,22 +1,30 @@
import React from 'react'; import React from 'react';
import _ from 'lodash'; import _ from 'lodash';
import RichTextEditor from 'react-rte-browserify';
import i18n from 'lib-app/i18n'; import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call'; 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 TicketAction from 'app-components/ticket-action'; import TicketEvent from 'app-components/ticket-event';
import AreYouSure from 'app-components/are-you-sure';
import Form from 'core-components/form'; import Form from 'core-components/form';
import FormField from 'core-components/form-field'; import FormField from 'core-components/form-field';
import SubmitButton from 'core-components/submit-button'; import SubmitButton from 'core-components/submit-button';
import DropDown from 'core-components/drop-down';
import Button from 'core-components/button';
class TicketViewer extends React.Component { class TicketViewer extends React.Component {
static propTypes = { static propTypes = {
ticket: React.PropTypes.object ticket: React.PropTypes.object,
onChange: React.PropTypes.func,
editable: React.PropTypes.bool,
customResponses: React.PropTypes.array,
assignmentAllowed: React.PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
editable: false,
ticket: { ticket: {
author: {}, author: {},
department: {}, department: {},
@ -24,14 +32,11 @@ class TicketViewer extends React.Component {
} }
}; };
constructor(props) { state = {
super(props); loading: false,
commentValue: RichTextEditor.createEmptyValue(),
this.state = { commentEdited: false
loading: false };
};
}
render() { render() {
const ticket = this.props.ticket; const ticket = this.props.ticket;
@ -42,6 +47,92 @@ class TicketViewer extends React.Component {
<span className="ticket-viewer__number">#{ticket.ticketNumber}</span> <span className="ticket-viewer__number">#{ticket.ticketNumber}</span>
<span className="ticket-viewer__title">{ticket.title}</span> <span className="ticket-viewer__title">{ticket.title}</span>
</div> </div>
{this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()}
<div className="ticket-viewer__content">
<TicketEvent type="COMMENT" author={ticket.author} content={ticket.content} date={ticket.date} file={ticket.file}/>
</div>
<div className="ticket-viewer__comments">
{ticket.events && ticket.events.map(this.renderTicketEvent.bind(this))}
</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 {...this.getCommentFormProps()}>
<FormField name="content" validation="TEXT_AREA" required field="textarea" />
<SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton>
</Form>
</div>
</div>
</div>
);
}
renderEditableHeaders() {
const ticket = this.props.ticket;
const departments = SessionStore.getDepartments();
const priorities = {
'low': 0,
'medium': 1,
'high': 2
};
const priorityList = [
{content: i18n('LOW')},
{content: i18n('MEDIUM')},
{content: i18n('HIGH')}
];
return (
<div className="ticket-viewer__headers">
<div className="ticket-viewer__info-row-header row">
<div className="col-md-4">{i18n('DEPARTMENT')}</div>
<div className=" col-md-4">{i18n('AUTHOR')}</div>
<div className="col-md-4">{i18n('DATE')}</div>
</div>
<div className="ticket-viewer__info-row-values row">
<div className="col-md-4">
<DropDown className="ticket-viewer__editable-dropdown"
items={departments.map((department) => {return {content: department.name}})}
selectedIndex={_.findIndex(departments, {id: this.props.ticket.department.id})}
onChange={this.onDepartmentDropdownChanged.bind(this)} />
</div>
<div className="col-md-4">{ticket.author.name}</div>
<div className="col-md-4">{ticket.date}</div>
</div>
<div className="ticket-viewer__info-row-header row">
<div className="col-md-4">{i18n('PRIORITY')}</div>
<div className="col-md-4">{i18n('OWNED')}</div>
<div className="col-md-4">{i18n('STATUS')}</div>
</div>
<div className="ticket-viewer__info-row-values row">
<div className="col-md-4">
<DropDown className="ticket-viewer__editable-dropdown" items={priorityList} selectedIndex={priorities[ticket.priority]} onChange={this.onPriorityDropdownChanged.bind(this)} />
</div>
<div className="col-md-4">
<Button type={(ticket.owner) ? 'primary' : 'secondary'} size="extra-small" onClick={this.onAssignClick.bind(this)}>
{i18n(ticket.owner ? 'UN_ASSIGN' : 'ASSIGN_TO_ME')}
</Button>
</div>
<div className="col-md-4">
<Button type={(ticket.closed) ? 'secondary' : 'primary'} size="extra-small" onClick={this.onCloseClick.bind(this)}>
{i18n(ticket.closed ? 'RE_OPEN' : 'CLOSE')}
</Button>
</div>
</div>
</div>
);
}
renderHeaders() {
const ticket = this.props.ticket;
const priorities = {
'low': 'LOW',
'medium': 'MEDIUM',
'high': 'HIGH'
};
return (
<div className="ticket-viewer__headers">
<div className="ticket-viewer__info-row-header row"> <div className="ticket-viewer__info-row-header row">
<div className="ticket-viewer__department col-md-4">{i18n('DEPARTMENT')}</div> <div className="ticket-viewer__department col-md-4">{i18n('DEPARTMENT')}</div>
<div className="ticket-viewer__author col-md-4">{i18n('AUTHOR')}</div> <div className="ticket-viewer__author col-md-4">{i18n('AUTHOR')}</div>
@ -52,31 +143,157 @@ class TicketViewer extends React.Component {
<div className="ticket-viewer__author col-md-4">{ticket.author.name}</div> <div className="ticket-viewer__author col-md-4">{ticket.author.name}</div>
<div className="ticket-viewer__date col-md-4">{ticket.date}</div> <div className="ticket-viewer__date col-md-4">{ticket.date}</div>
</div> </div>
<div className="ticket-viewer__content"> <div className="ticket-viewer__info-row-header row">
<TicketAction type="COMMENT" author={ticket.author} content={ticket.content} date={ticket.date} file={ticket.file}/> <div className="ticket-viewer__department col-md-4">{i18n('PRIORITY')}</div>
<div className="ticket-viewer__author col-md-4">{i18n('OWNER')}</div>
<div className="ticket-viewer__date col-md-4">{i18n('STATUS')}</div>
</div> </div>
<div className="ticket-viewer__comments"> <div className="ticket-viewer__info-row-values row">
{ticket.actions && ticket.actions.map(this.renderAction.bind(this))} <div className="col-md-4">
</div> {i18n(priorities[this.props.ticket.priority || 'low'])}
<div className="ticket-viewer__response"> </div>
<div className="ticket-viewer__response-title row">{i18n('RESPOND')}</div> <div className="col-md-4">
<div className="ticket-viewer__response-field row"> {(ticket.owner) ? ticket.owner.name : i18n('NONE')}
<Form onSubmit={this.onSubmit.bind(this)} loading={this.state.loading}> </div>
<FormField name="content" validation="TEXT_AREA" required field="textarea" /> <div className="col-md-4">
<SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton> {this.renderOwnerNode()}
</Form>
</div> </div>
</div> </div>
</div> </div>
); );
} }
renderAction(action, index) { renderOwnerNode() {
let ownerNode = null;
if (this.props.assignmentAllowed && _.isEmpty(this.props.ticket.owner)) {
ownerNode = (
<Button type="secondary" size="extra-small" onClick={this.onAssignClick.bind(this)}>
{i18n('ASSIGN_TO_ME')}
</Button>
);
} else {
ownerNode = i18n((this.props.closed) ? 'CLOSED' : 'OPENED');
}
return ownerNode;
}
renderTicketEvent(options, index) {
return ( return (
<TicketAction {...action} key={index} /> <TicketEvent {...options} key={index} />
); );
} }
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));
}
onPriorityDropdownChanged(event) {
AreYouSure.openModal(null, this.changePriority.bind(this, event.index));
}
onAssignClick() {
API.call({
path: (this.props.ticket.owner) ? '/staff/un-assign-ticket' : '/staff/assign-ticket',
data: {
ticketNumber: this.props.ticket.ticketNumber
}
}).then(this.onTicketModification.bind(this));
}
onCloseClick() {
AreYouSure.openModal(null, this.toggleClose.bind(this));
}
toggleClose() {
API.call({
path: (this.props.ticket.closed) ? '/ticket/re-open' : '/ticket/close',
data: {
ticketNumber: this.props.ticket.ticketNumber
}
}).then(this.onTicketModification.bind(this));
}
changeDepartment(index) {
API.call({
path: '/ticket/change-department',
data: {
ticketNumber: this.props.ticket.ticketNumber,
departmentId: SessionStore.getDepartments()[index].id
}
}).then(this.onTicketModification.bind(this));
}
changePriority(index) {
const priorities = [
'low',
'medium',
'high'
];
API.call({
path: '/ticket/change-priority',
data: {
ticketNumber: this.props.ticket.ticketNumber,
priority: priorities[index]
}
}).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) { onSubmit(formState) {
this.setState({ this.setState({
loading: true loading: true
@ -94,8 +311,8 @@ class TicketViewer extends React.Component {
this.setState({ this.setState({
loading: false loading: false
}); });
store.dispatch(SessionActions.getUserData()); this.onTicketModification();
} }
onCommentFail() { onCommentFail() {
@ -103,6 +320,12 @@ class TicketViewer extends React.Component {
loading: false loading: false
}); });
} }
onTicketModification() {
if (this.props.onChange) {
this.props.onChange();
}
}
} }
export default TicketViewer; export default TicketViewer;

View File

@ -28,18 +28,15 @@
&__info-row-values { &__info-row-values {
background-color: $light-grey; background-color: $light-grey;
color: $secondary-blue; color: $secondary-blue;
padding-bottom: 10px;
} }
&__date { &__editable-dropdown {
margin: auto;
}
&__author {
}
&__department {
.drop-down__current-item {
background-color: $very-light-grey;
}
} }
&__content { &__content {
@ -75,5 +72,11 @@
padding: 20px; padding: 20px;
text-align: left; text-align: left;
} }
&-custom {
background-color: $very-light-grey;
padding: 20px 0 0 20px;
text-align: left;
}
} }
} }

View File

@ -84,7 +84,7 @@ export default (
<Route path="new-tickets" component={AdminPanelNewTickets} /> <Route path="new-tickets" component={AdminPanelNewTickets} />
<Route path="all-tickets" component={AdminPanelAllTickets} /> <Route path="all-tickets" component={AdminPanelAllTickets} />
<Route path="custom-responses" component={AdminPanelCustomResponses} /> <Route path="custom-responses" component={AdminPanelCustomResponses} />
<Route path="view-ticket" component={AdminPanelViewTicket} /> <Route path="view-ticket/:ticketNumber" component={AdminPanelViewTicket} />
</Route> </Route>
<Route path="users"> <Route path="users">

View File

@ -1,14 +1,99 @@
import React from 'react'; 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';
import SessionStore from 'lib-app/session-store';
import TicketViewer from 'app-components/ticket-viewer';
import Loading from 'core-components/loading';
import Header from 'core-components/header';
class AdminPanelViewTicket extends React.Component { class AdminPanelViewTicket extends React.Component {
state = {
loading: true,
ticket: {}
};
componentDidMount() {
this.retrieveTicket();
}
render() { render() {
return ( return (
<div> <div className="admin-panel-view-ticket">
/admin/panel/tickets/view-ticket <Header title={i18n('VIEW_TICKET')} description={i18n('TICKET_VIEW_DESCRIPTION')} />
{(this.state.loading) ? this.renderLoading() : this.renderView()}
</div> </div>
); );
} }
renderLoading() {
return (
<div className="admin-panel-view-ticket__loading">
<Loading size="large" />
</div>
)
};
renderView() {
return (_.isEmpty(this.state.ticket)) ? this.renderNoPermissionError() : this.renderTicketView();
}
renderNoPermissionError() {
return (
<div className="admin-panel-view-ticket__error">
{i18n('NO_PERMISSION')}
</div>
);
}
renderTicketView() {
return (
<div className="admin-panel-view-ticket__ticket-view">
<TicketViewer {...this.getTicketViewProps()} />
</div>
);
}
getTicketViewProps() {
return {
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() {
API.call({
path: '/ticket/get',
date: {
ticketNumber: this.props.params.ticketNumber
}
}).then(this.onRetrieveSuccess.bind(this)).catch(this.onRetrieveFail.bind(this))
}
onRetrieveSuccess(result) {
this.setState({
loading: false,
ticket: result.data
});
}
onRetrieveFail() {
this.setState({
loading: false,
ticket: {}
});
}
} }
export default AdminPanelViewTicket; export default connect((store) => {
return {
customResponses: store.adminData.customResponses
};
})(AdminPanelViewTicket);

View File

@ -0,0 +1,18 @@
@import '../../../../scss/vars';
.admin-panel-view-ticket {
margin: 10px;
&__loading {
background-color: $grey;
height: 400px;
}
&__error {
}
&__ticket-view {
margin: 20px 30px;
}
}

View File

@ -42,8 +42,8 @@ class CreateTicketForm extends React.Component {
{(!this.props.userLogged) ? this.renderEmailAndName() : null} {(!this.props.userLogged) ? this.renderEmailAndName() : null}
<div className="row"> <div className="row">
<FormField className="col-md-7" label="Title" name="title" validation="TITLE" required field="input" fieldProps={{size: 'large'}}/> <FormField className="col-md-7" label="Title" name="title" validation="TITLE" required field="input" fieldProps={{size: 'large'}}/>
<FormField className="col-md-5" label="Department" name="departmentId" field="select" fieldProps={{ <FormField className="col-md-5" label="Department" name="departmentIndex" field="select" fieldProps={{
items: SessionStore.getDepartments().map((department) => {return {content: department}}), items: SessionStore.getDepartments().map((department) => {return {content: department.name}}),
size: 'medium' size: 'medium'
}} /> }} />
</div> </div>
@ -92,7 +92,7 @@ class CreateTicketForm extends React.Component {
API.call({ API.call({
path: '/ticket/create', path: '/ticket/create',
data: _.extend({}, formState, { data: _.extend({}, formState, {
departmentId: formState.departmentId + 1 departmentId: SessionStore.getDepartments()[formState.departmentIndex].id
}) })
}).then(this.onTicketSuccess.bind(this)).catch(this.onTicketFail.bind(this)); }).then(this.onTicketSuccess.bind(this)).catch(this.onTicketFail.bind(this));
} }

View File

@ -2,6 +2,7 @@ import React from 'react';
import _ from 'lodash'; import _ from 'lodash';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import SessionActions from 'actions/session-actions';
import TicketViewer from 'app-components/ticket-viewer'; import TicketViewer from 'app-components/ticket-viewer';
class DashboardTicketPage extends React.Component { class DashboardTicketPage extends React.Component {
@ -13,7 +14,7 @@ class DashboardTicketPage extends React.Component {
render() { render() {
return ( return (
<div className="dashboard-ticket-page"> <div className="dashboard-ticket-page">
<TicketViewer ticket={this.getTicketData()} /> <TicketViewer ticket={this.getTicketData()} onChange={this.retrieveUserData.bind(this)}/>
</div> </div>
); );
} }
@ -21,6 +22,10 @@ class DashboardTicketPage extends React.Component {
getTicketData() { getTicketData() {
return _.find(this.props.tickets, {ticketNumber: this.props.params.ticketNumber}); return _.find(this.props.tickets, {ticketNumber: this.props.params.ticketNumber});
} }
retrieveUserData() {
this.props.dispatch(SessionActions.getUserData());
}
} }
export default connect((store) => { export default connect((store) => {

View File

@ -18,6 +18,7 @@ class Button extends React.Component {
static propTypes = { static propTypes = {
children: React.PropTypes.node, children: React.PropTypes.node,
size: React.PropTypes.oneOf([ size: React.PropTypes.oneOf([
'extra-small',
'small', 'small',
'medium', 'medium',
'large', 'large',
@ -76,6 +77,7 @@ class Button extends React.Component {
'button_clean': (this.props.type === 'clean'), 'button_clean': (this.props.type === 'clean'),
'button_link': (this.props.type === 'link'), 'button_link': (this.props.type === 'link'),
'button_extra-small': (this.props.size === 'extra-small'),
'button_small': (this.props.size === 'small'), 'button_small': (this.props.size === 'small'),
'button_medium': (this.props.size === 'medium'), 'button_medium': (this.props.size === 'medium'),
'button_large': (this.props.size === 'large'), 'button_large': (this.props.size === 'large'),

View File

@ -47,6 +47,12 @@
} }
} }
&_extra-small {
text-transform: none;
width: 130px;
height: 30px;
}
&_small { &_small {
width: 100px; width: 100px;
height: 47px; height: 47px;

View File

@ -27,7 +27,7 @@
&__list-container { &__list-container {
position: absolute; position: absolute;
width: 150px; width: 150px;
z-index: 5; z-index: 100;
} }
&_closed { &_closed {

View File

@ -9,9 +9,9 @@ module.exports = [
'language': 'en', 'language': 'en',
'reCaptchaKey': '6LfM5CYTAAAAAGLz6ctpf-hchX2_l0Ge-Bn-n8wS', 'reCaptchaKey': '6LfM5CYTAAAAAGLz6ctpf-hchX2_l0Ge-Bn-n8wS',
'departments': [ 'departments': [
'Sales Support', {id: 1, name: 'Sales Support'},
'Technical Issues', {id: 2, name: 'Technical Issues'},
'System and Administration' {id: 3, name: 'System and Administration'}
] ]
} }
}; };

View File

@ -39,11 +39,11 @@ module.exports = [
return { return {
status: 'success', status: 'success',
data: [ data: [
{name: 'Common issue #1', language: 'en', content: 'some content'}, {name: 'Common issue #1', language: 'en', content: 'some content 1'},
{name: 'Common issue #2', language: 'en', content: 'some content'}, {name: 'Common issue #2', language: 'en', content: 'some content 2'},
{name: 'Common issue #3', language: 'en', content: 'some content'}, {name: 'Common issue #3', language: 'en', content: 'some content 3'},
{name: 'Häufiges Problem #1', language: 'de', content: 'einige Inhalte'}, {name: 'Häufiges Problem #1', language: 'de', content: 'einige Inhalte 1'},
{name: 'Häufiges Problem #2', language: 'de', content: 'einige Inhalte'} {name: 'Häufiges Problem #2', language: 'de', content: 'einige Inhalte 2'}
] ]
}; };
} }
@ -77,5 +77,130 @@ module.exports = [
data: {} data: {}
}; };
} }
},
{
path: '/ticket/get',
time: 1000,
response: function () {
return {
status: 'success',
data: {
ticketNumber: '878552',
title: 'Lorem ipsum door',
content: 'I had a problem with the installation of the php server',
department: {
id: 1,
name: 'Sales Support'
},
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://i65.tinypic.com/9bep95.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://i65.tinypic.com/9bep95.jpg',
staff: true
}
},
{
type: 'UN_ASSIGN',
date: '20150410',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.jpg',
staff: true
}
},
{
type: 'DEPARTMENT_CHANGED',
date: '20150411',
content: 'System support',
author: {
name: 'Emilia Clarke',
email: 'jobs@steve.com',
profilePic: 'http://i65.tinypic.com/9bep95.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://i65.tinypic.com/9bep95.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://i65.tinypic.com/9bep95.jpg',
staff: true
}
},
{
type: 'RE_OPEN',
date: '20151018',
author: {
name: 'Haskell Curry',
email: 'haskell@lambda.com',
staff: false
}
}
]
}
};
}
} }
]; ];

View File

@ -140,7 +140,7 @@ module.exports = [
content: 'I had a problem with the installation of the php server', content: 'I had a problem with the installation of the php server',
department: { department: {
id: 2, id: 2,
name: 'Environment Setup' name: 'Technical Issues'
}, },
date: '20160416', date: '20160416',
file: 'http://www.opensupports.com/some_file.zip', file: 'http://www.opensupports.com/some_file.zip',
@ -158,7 +158,7 @@ module.exports = [
name: 'Steve Jobs', name: 'Steve Jobs',
email: 'steve@jobs.com' email: 'steve@jobs.com'
}, },
actions: [ events: [
{ {
type: 'ASSIGN', type: 'ASSIGN',
date: '20150409', date: '20150409',
@ -258,8 +258,8 @@ module.exports = [
title: 'Lorem ipsum door', title: 'Lorem ipsum door',
content: 'I had a problem with the installation of the php server', content: 'I had a problem with the installation of the php server',
department: { department: {
id: 2, id: 1,
name: 'Environment Setup' name: 'Sales Support'
}, },
date: '20160415', date: '20160415',
file: 'http://www.opensupports.com/some_file.zip', file: 'http://www.opensupports.com/some_file.zip',
@ -274,7 +274,7 @@ module.exports = [
owner: { owner: {
name: 'Steve Jobs' name: 'Steve Jobs'
}, },
actions: [ events: [
{ {
type: 'ASSIGN', type: 'ASSIGN',
date: '20150409', date: '20150409',
@ -374,8 +374,8 @@ module.exports = [
title: 'Lorem ipsum door', title: 'Lorem ipsum door',
content: 'I had a problem with the installation of the php server', content: 'I had a problem with the installation of the php server',
department: { department: {
id: 2, id: 1,
name: 'Environment Setup' name: 'Sales Support'
}, },
date: '20150409', date: '20150409',
file: 'http://www.opensupports.com/some_file.zip', file: 'http://www.opensupports.com/some_file.zip',
@ -390,7 +390,7 @@ module.exports = [
owner: { owner: {
name: 'Steve Jobs' name: 'Steve Jobs'
}, },
actions: [ events: [
{ {
type: 'ASSIGN', type: 'ASSIGN',
date: '20150409', date: '20150409',

View File

@ -60,6 +60,18 @@ export default {
'DISCARD_CHANGES': 'Discard changes', 'DISCARD_CHANGES': 'Discard changes',
'DELETE': 'Delete', 'DELETE': 'Delete',
'LANGUAGE': 'Language', 'LANGUAGE': 'Language',
'OWNER': 'Owner',
'OWNED': 'Owned',
'STATUS': 'Status',
'NONE': 'None',
'OPENED': 'Opened',
'CLOSED': 'Closed',
'CLOSE': 'Close',
'RE_OPEN': 'Re open',
'ASSIGN_TO_ME': 'Assign to me',
'UN_ASSIGN': 'Unassign',
'VIEW_TICKET': 'View Ticket',
'SELECT_CUSTOM_RESPONSE': 'Select a custom response...',
//VIEW DESCRIPTIONS //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.', '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.',
@ -70,6 +82,7 @@ export default {
'SUPPORT_CENTER_DESCRIPTION': 'Welcome to our support center. You can contact us through a tickets system. Your tickets will be answered by our staff.', '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', 'CUSTOM_RESPONSES_DESCRIPTION': 'Custom responses are automated responses for common problems',
'MY_TICKETS_DESCRIPTION': 'Here you can view the tickets you are responsible for.', 'MY_TICKETS_DESCRIPTION': 'Here you can view the tickets you are responsible for.',
'TICKET_VIEW_DESCRIPTION': 'This ticket has been sent by a customer. Here you can respond or assign the ticket',
//ERRORS //ERRORS
'EMAIL_OR_PASSWORD': 'Email or password invalid', 'EMAIL_OR_PASSWORD': 'Email or password invalid',
@ -83,6 +96,7 @@ export default {
'PASSWORD_NOT_MATCH': 'Password does not match', 'PASSWORD_NOT_MATCH': 'Password does not match',
'INVALID_RECOVER': 'Invalid recover data', 'INVALID_RECOVER': 'Invalid recover data',
'TICKET_SENT_ERROR': 'An error occurred while trying to create the ticket.', 'TICKET_SENT_ERROR': 'An error occurred while trying to create the ticket.',
'NO_PERMISSION': 'You\'ve no permission to access to this page.',
//MESSAGES //MESSAGES
'SIGNUP_SUCCESS': 'You have registered successfully in our support system.', 'SIGNUP_SUCCESS': 'You have registered successfully in our support system.',

View File

@ -1,6 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import Reducer from 'reducers/reducer'; import Reducer from 'reducers/reducer';
import sessionStore from 'lib-app/session-store';
class AdminDataReducer extends Reducer { class AdminDataReducer extends Reducer {
@ -16,18 +17,30 @@ class AdminDataReducer extends Reducer {
getTypeHandlers() { getTypeHandlers() {
return { return {
'CUSTOM_RESPONSES_FULFILLED': this.onCustomResponses, 'CUSTOM_RESPONSES_FULFILLED': this.onCustomResponses,
'SESSION_CHECKED': this.onSessionChecked
'MY_TICKETS_FULFILLED': this.onMyTicketsRetrieved, 'MY_TICKETS_FULFILLED': this.onMyTicketsRetrieved,
'MY_TICKETS_PENDING': this.onMyTicketsPending 'MY_TICKETS_PENDING': this.onMyTicketsPending
}; };
} }
onCustomResponses(state, payload) { onCustomResponses(state, payload) {
sessionStore.setItem('customResponses', JSON.stringify(payload.data));
return _.extend({}, state, { return _.extend({}, state, {
customResponses: payload.data, customResponses: payload.data,
customResponsesLoaded: true customResponsesLoaded: true
}); });
} }
onSessionChecked(state) {
const customResponses = sessionStore.getItem('customResponses');
return _.extend({}, state, {
customResponses: JSON.parse(customResponses),
customResponsesLoaded: true
});
}
onMyTicketsRetrieved(state, payload) { onMyTicketsRetrieved(state, payload) {
return _.extend({}, state, { return _.extend({}, state, {
myTickets: payload.data, myTickets: payload.data,

View File

@ -34,6 +34,7 @@ class AssignStaffController extends Controller {
} else { } else {
$this->user->sharedTicketList->add($this->ticket); $this->user->sharedTicketList->add($this->ticket);
$this->ticket->owner = $this->user; $this->ticket->owner = $this->user;
$this->ticket->unread = true;
$this->ticket->store(); $this->ticket->store();
$this->user->store(); $this->user->store();

View File

@ -26,6 +26,7 @@ class UnAssignStaffController extends Controller {
$user->sharedTicketList->remove($ticket); $user->sharedTicketList->remove($ticket);
$user->store(); $user->store();
$ticket->owner = null; $ticket->owner = null;
$ticket->unread = true;
$ticket->store(); $ticket->store();
Response::respondSuccess(); Response::respondSuccess();
} else { } else {

View File

@ -10,6 +10,7 @@ include 'ticket/change-department.php';
include 'ticket/close.php'; include 'ticket/close.php';
include 'ticket/re-open.php'; include 'ticket/re-open.php';
include 'ticket/change-priority.php'; include 'ticket/change-priority.php';
include 'ticket/seen.php';
$ticketControllers = new ControllerGroup(); $ticketControllers = new ControllerGroup();
$ticketControllers->setGroupPath('/ticket'); $ticketControllers->setGroupPath('/ticket');
@ -25,5 +26,6 @@ $ticketControllers->addController(new ChangeDepartmentController);
$ticketControllers->addController(new CloseController); $ticketControllers->addController(new CloseController);
$ticketControllers->addController(new ReOpenController); $ticketControllers->addController(new ReOpenController);
$ticketControllers->addController(new ChangePriorityController); $ticketControllers->addController(new ChangePriorityController);
$ticketControllers->addController(new SeenController);
$ticketControllers->finalize(); $ticketControllers->finalize();

View File

@ -34,6 +34,7 @@ class ChangeDepartmentController extends Controller {
} }
$ticket->department = $department; $ticket->department = $department;
$ticket->unread = true;
$ticket->store(); $ticket->store();
Response::respondSuccess(); Response::respondSuccess();
} }

View File

@ -28,6 +28,7 @@ class ChangePriorityController extends Controller {
if($ticket->owner && $user->id === $ticket->owner->id) { if($ticket->owner && $user->id === $ticket->owner->id) {
$ticket->priority = $priority; $ticket->priority = $priority;
$ticket->unread = true;
$ticket->store(); $ticket->store();
Response::respondSuccess(); Response::respondSuccess();
} else { } else {

View File

@ -31,7 +31,12 @@ class CloseController extends Controller {
Response::respondError(ERRORS::NO_PERMISSION); Response::respondError(ERRORS::NO_PERMISSION);
return; return;
} }
if(Controller::isStaffLogged()) {
$ticket->unread = true;
} else {
$ticket->unreadStaff = true;
}
$ticket->closed = true; $ticket->closed = true;
$ticket->store(); $ticket->store();
Response::respondSuccess(); Response::respondSuccess();

View File

@ -51,11 +51,11 @@ class CommentController extends Controller {
)); ));
if(Controller::isStaffLogged()) { if(Controller::isStaffLogged()) {
$comment->authorUser = Controller::getLoggedUser(); $this->ticket->unread = true;
} else { } else {
$comment->authorUser = Controller::getLoggedUser(); $this->ticket->unreadStaff = true;
} }
$comment->authorUser = Controller::getLoggedUser();
$this->ticket->addEvent($comment); $this->ticket->addEvent($comment);
$this->ticket->store(); $this->ticket->store();
} }

View File

@ -58,6 +58,7 @@ class CreateController extends Controller {
'file' => '', 'file' => '',
'date' => Date::getCurrentDate(), 'date' => Date::getCurrentDate(),
'unread' => false, 'unread' => false,
'unreadStaff' => true,
'closed' => false, 'closed' => false,
)); ));

View File

@ -30,7 +30,11 @@ class ReOpenController extends Controller {
Response::respondError(ERRORS::NO_PERMISSION); Response::respondError(ERRORS::NO_PERMISSION);
return; return;
} }
if(Controller::isStaffLogged()) {
$ticket->unread = true;
} else {
$ticket->unreadStaff = true;
}
$ticket->closed = false; $ticket->closed = false;
$ticket->store(); $ticket->store();
Response::respondSuccess(); Response::respondSuccess();

View File

@ -0,0 +1,39 @@
<?php
use Respect\Validation\Validator as DataValidator;
class SeenController extends Controller {
const PATH = '/seen';
public function validations() {
return [
'permission' => 'user',
'requestData' => [
'ticketNumber' => [
'validation' => DataValidator::validTicketNumber(),
'error' => ERRORS::INVALID_TICKET
]
]
];
}
public function handler() {
$ticketnumber = Controller::request('ticketNumber');
$user = Controller::getLoggedUser();
$ticket = Ticket::getByTicketNumber($ticketnumber);
if (Controller::isStaffLogged() && $ticket->owner && $ticket->owner->id === $user->id) {
$ticket->unreadStaff = false;
$ticket->store();
Response::respondSuccess();
return;
}
if (!Controller::isStaffLogged() && $ticket->author && $user->id === $ticket->author->id) {
$ticket->unread = false;
$ticket->store();
Response::respondSuccess();
return;
}
Response::respondError(ERRORS::NO_PERMISSION);
}
}

View File

@ -18,7 +18,8 @@ class Ticket extends DataStore {
'priority', 'priority',
'author', 'author',
'owner', 'owner',
'ownTicketeventList' 'ownTicketeventList',
'unreadStaff'
); );
} }
@ -33,6 +34,8 @@ class Ticket extends DataStore {
public function getDefaultProps() { public function getDefaultProps() {
return array( return array(
'priority' => 'low', 'priority' => 'low',
'unread' => false,
'unreadStaff' => true,
'ticketNumber' => $this->generateUniqueTicketNumber() 'ticketNumber' => $this->generateUniqueTicketNumber()
); );
} }

View File

@ -22,6 +22,8 @@ describe '/staff/assign-ticket' do
(ticket['owner_id']).should.equal('1') (ticket['owner_id']).should.equal('1')
(ticket['unread']).should.equal('1')
staff_ticket = $database.getRow('staff_ticket', 1 , 'id') staff_ticket = $database.getRow('staff_ticket', 1 , 'id')
(staff_ticket['staff_id']).should.equal('1') (staff_ticket['staff_id']).should.equal('1')

View File

@ -21,6 +21,7 @@ describe '/staff/un-assign-ticket' do
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['owner_id']).should.equal(nil) (ticket['owner_id']).should.equal(nil)
(ticket['unread']).should.equal('1')
staff_ticket = $database.getRow('staff_ticket', 1 , 'id') staff_ticket = $database.getRow('staff_ticket', 1 , 'id')

View File

@ -17,6 +17,7 @@ describe '/ticket/change-department' do
(result['status']).should.equal('success') (result['status']).should.equal('success')
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['unread']).should.equal('1')
(ticket['department_id']).should.equal('2') (ticket['department_id']).should.equal('2')
end end
end end

View File

@ -18,6 +18,7 @@ describe '/ticket/change-priority' do
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['priority']).should.equal('high') (ticket['priority']).should.equal('high')
(ticket['unread']).should.equal('1')
end end
it 'should change priority to medium if everything is okey' do it 'should change priority to medium if everything is okey' do
@ -34,6 +35,7 @@ describe '/ticket/change-priority' do
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['priority']).should.equal('medium') (ticket['priority']).should.equal('medium')
(ticket['unread']).should.equal('1')
end end
it 'should change priority to low if everything is okey' do it 'should change priority to low if everything is okey' do
@ -50,6 +52,7 @@ describe '/ticket/change-priority' do
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['priority']).should.equal('low') (ticket['priority']).should.equal('low')
(ticket['unread']).should.equal('1')
end end
end end

View File

@ -17,6 +17,7 @@ describe '/ticket/close' do
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['closed']).should.equal('1') (ticket['closed']).should.equal('1')
(ticket['unread']).should.equal('1')
end end
end end

View File

@ -72,6 +72,7 @@ describe '/ticket/comment/' do
(comment['content']).should.equal('some comment content') (comment['content']).should.equal('some comment content')
(comment['type']).should.equal('COMMENT') (comment['type']).should.equal('COMMENT')
(comment['author_user_id']).should.equal($csrf_userid) (comment['author_user_id']).should.equal($csrf_userid)
(ticket['unread_staff']).should.equal('1')
end end
it 'should fail if user is not the author nor owner' do it 'should fail if user is not the author nor owner' do

View File

@ -17,6 +17,7 @@ describe '/ticket/re-open' do
ticket = $database.getRow('ticket', 1 , 'id') ticket = $database.getRow('ticket', 1 , 'id')
(ticket['closed']).should.equal('0') (ticket['closed']).should.equal('0')
(ticket['unread']).should.equal('1')
end end
end end

39
tests/ticket/seen.rb Normal file
View File

@ -0,0 +1,39 @@
describe '/ticket/seen' do
describe 'when a staff is logged' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
it 'should change unread if everything is okey ' do
ticket = $database.getRow('ticket', 1, 'id')
result = request('/ticket/seen', {
ticketNumber: ticket['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
ticket = $database.getRow('ticket', 1, 'id')
(ticket['unreadStaff']).should.equal('0')
end
end
describe 'when a user is logged' do
request('/user/logout')
Scripts.login()
it 'should change unread if everything is okey ' do
ticket = $database.getRow('ticket', 1, 'id')
result = request('/ticket/seen', {
ticketNumber: ticket['ticket_number'],
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
ticket = $database.getRow('ticket', 1, 'id')
(ticket['unread']).should.equal('0')
end
end
end